Editor: Find & Replace, read-aloud, backup, typography, phonetic, org niceties
Writer power-ups (Phase 11), plus the selection-bubble vs copy/paste fix. - Find & Replace (Ctrl/Cmd+F): SearchHighlight decoration extension + FindReplace bar (match-case, replace-all back-to-front, scroll without popping the selection bubble). - Read-aloud (Web Speech, offline) on the word card and selection bubble. - Keyboard/touch access to the ESL helpers: Ctrl/Cmd+D look up word at caret, Ctrl/Cmd+J rewrite selection, touch long-press lookup. Refactored the right-click handler into a shared openWordLookup(pos). - Whole-corpus backup: GET /api/docs/export-all zips every doc (md/docx), de-dupes filenames, dated name; sidebar download links. TestExportAll. - Smart typography input rules (curly quotes/em-dash/ellipsis), ASCII-only so CJK is untouched. - Duplicate doc, sidebar sort (Recent/Title/Longest), toolbar outline popover. - English phonetic (chosen over pinyin for an English learner): ECDICT-built phonetic.json.gz (46,579 words) + Result.Phonetic + WordCard IPA line; scripts/build_phonetic.py (full build + --seed fallback). - Selection bubble no longer blocks copy/paste: deferred to pointer-up and made click-through except on its buttons. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -192,6 +192,38 @@ export default function App() {
|
||||
setDocText('')
|
||||
}, [saveNow, isBlankDraft])
|
||||
|
||||
// Duplicate a document: copy its body/tone into a fresh doc titled "… (副本)",
|
||||
// then open the copy. Tags aren't carried over (a fresh start for the copy).
|
||||
const handleDuplicate = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await saveNow() // flush in case we're duplicating the open doc
|
||||
const src = await api.getDoc(id)
|
||||
const fresh = await api.createDoc()
|
||||
const dupTitle = `${src.title?.trim() || 'Untitled'} (副本)`
|
||||
const updated = await api.updateDoc(fresh.id, {
|
||||
title: dupTitle,
|
||||
content: src.content,
|
||||
content_text: src.content_text,
|
||||
tone: src.tone,
|
||||
word_count: src.word_count,
|
||||
})
|
||||
setDocs((prev) => [
|
||||
{ id: updated.id, title: updated.title, word_count: updated.word_count, updated_at: updated.updated_at, tags: [] },
|
||||
...prev,
|
||||
])
|
||||
setCurrentDoc(updated)
|
||||
setTitle(updated.title)
|
||||
setWordCount(updated.word_count)
|
||||
setTone(updated.tone || 'general')
|
||||
setDocText(updated.content_text)
|
||||
} catch (err) {
|
||||
console.error('duplicate failed', err)
|
||||
}
|
||||
},
|
||||
[saveNow],
|
||||
)
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (id: string) => {
|
||||
await api.deleteDoc(id)
|
||||
@@ -371,6 +403,7 @@ export default function App() {
|
||||
onSelect={openDoc}
|
||||
onCreate={handleCreate}
|
||||
onDelete={handleDelete}
|
||||
onDuplicate={handleDuplicate}
|
||||
onToggleTag={handleToggleTag}
|
||||
onCreateTag={handleCreateTag}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user