diff --git a/web/src/components/NoteContext.tsx b/web/src/components/NoteContext.tsx index 797f7bb..0cf7471 100644 --- a/web/src/components/NoteContext.tsx +++ b/web/src/components/NoteContext.tsx @@ -6,7 +6,9 @@ type NoteSide = "left" | "right"; type NoteContextValue = ReturnType & { storageKey: string; setStorageKey: (key: string) => void; - openNote: (key: string) => void; + openNote: (key: string, title?: string) => void; + noteTitle: string; + setNoteTitle: (title: string) => void; defaultTitle: string; setDefaultTitle: (title: string) => void; }; @@ -16,10 +18,12 @@ const NoteContext = createContext(null); export function NoteProvider({ children }: { children: ReactNode }) { const note = useSplitScreenNote("lb_global_note_width"); const [storageKey, setStorageKey] = useState(""); + const [noteTitle, setNoteTitle] = useState("随心记"); const [defaultTitle, setDefaultTitle] = useState("随心记"); - const openNote = (key: string) => { + const openNote = (key: string, title?: string) => { setStorageKey(key); + setNoteTitle(title ?? defaultTitle); note.setOpen(true); }; @@ -30,6 +34,8 @@ export function NoteProvider({ children }: { children: ReactNode }) { storageKey, setStorageKey, openNote, + noteTitle, + setNoteTitle, defaultTitle, setDefaultTitle, }} diff --git a/web/src/components/NoteSplitLayout.tsx b/web/src/components/NoteSplitLayout.tsx index c25e1de..aa205b7 100644 --- a/web/src/components/NoteSplitLayout.tsx +++ b/web/src/components/NoteSplitLayout.tsx @@ -9,9 +9,10 @@ type Props = { }; export function NoteSplitLayout({ children, defaultStorageKey = "", defaultTitle = "随心记" }: Props) { - const { open, setOpen, side, setSide, width, isDesktop, startResize, storageKey } = useNote(); + const { open, setOpen, side, setSide, width, isDesktop, startResize, storageKey, noteTitle } = useNote(); const key = storageKey || defaultStorageKey; + const title = noteTitle || defaultTitle; const split = open && isDesktop; return ( @@ -52,7 +53,7 @@ export function NoteSplitLayout({ children, defaultStorageKey = "", defaultTitle open={open} onClose={() => setOpen(false)} storageKey={key} - title={defaultTitle} + title={title} side={side} split onSideChange={setSide} @@ -66,7 +67,7 @@ export function NoteSplitLayout({ children, defaultStorageKey = "", defaultTitle open={open} onClose={() => setOpen(false)} storageKey={key} - title={defaultTitle} + title={title} side={side} onSideChange={setSide} /> diff --git a/web/src/components/StudyNotePanel.tsx b/web/src/components/StudyNotePanel.tsx index 3c7419f..cffb035 100644 --- a/web/src/components/StudyNotePanel.tsx +++ b/web/src/components/StudyNotePanel.tsx @@ -46,9 +46,10 @@ type StudyNoteLauncherProps = { title?: string; label?: string; className?: string; + onOpen?: () => void; }; -export function StudyNoteLauncher({ storageKey, title, label, className = "" }: StudyNoteLauncherProps) { +export function StudyNoteLauncher({ storageKey, title, label, className = "", onOpen }: StudyNoteLauncherProps) { const { t } = useTranslation(); const resolvedTitle = title ?? t("studyNote.title"); const resolvedLabel = label ?? t("studyNote.title"); @@ -59,13 +60,13 @@ export function StudyNoteLauncher({ storageKey, title, label, className = "" }: - {open && ( + {!onOpen && open && ( setOpen(false)} diff --git a/web/src/pages/FlashReview.tsx b/web/src/pages/FlashReview.tsx index f133a79..bb13024 100644 --- a/web/src/pages/FlashReview.tsx +++ b/web/src/pages/FlashReview.tsx @@ -14,6 +14,8 @@ import { displayTranslationFull, displayTranslationShort, pickPhoneticDisplay } import { nextWordTapState, syncDetailWordWithTap } from "../utils/wordReveal"; import { applyUserWordView } from "../components/WordEditControls"; import { NoteSplitLayout } from "../components/NoteSplitLayout"; +import { useNote } from "../components/NoteContext"; +import { StudyNoteLauncher } from "../components/StudyNotePanel"; import { clearStudyRetryFlash, @@ -67,6 +69,7 @@ function mapToFlashWord(w: Record): FlashWord { export default function FlashReview() { const { t } = useTranslation(); const navigate = useNavigate(); + const note = useNote(); const [words, setWords] = useState([]); const [showCompleteDialog, setShowCompleteDialog] = useState(false); const [annotationOpen, setAnnotationOpen] = useState(false); @@ -399,6 +402,15 @@ export default function FlashReview() {
+
e.stopPropagation()}> + note.openNote(wordNoteKey(visibleWords[cardIndex].id), t("practice.note_title", { word: visibleWords[cardIndex].word }))} + /> +
+
e.stopPropagation()}> + note.openNote(wordNoteKey(word.id), t("practice.note_title", { word: word.word }))} + /> +
([]); const [annotationOpen, setAnnotationOpen] = useState(false); const [viewMode, setViewMode] = useState("list"); @@ -203,7 +207,15 @@ export default function ListenIdentify() { )} {showAnswer && renderRevealed(w)}
- +
e.stopPropagation()}> + note.openNote(wordNoteKey(w.id), t("practice.note_title", { word: w.word }))} + /> +
{detailMode && showAnswer && (
e.stopPropagation()}> @@ -256,6 +268,10 @@ export default function ListenIdentify() { onOpenChange={setAnnotationOpen} /> +
{t("practice.batch_group", { current: batchIdx + 1, total: totalBatches })}
@@ -324,6 +340,15 @@ export default function ListenIdentify() {
+
e.stopPropagation()}> + note.openNote(wordNoteKey(cardWord.id), t("practice.note_title", { word: cardWord.word }))} + /> +
)}
+
@@ -379,6 +405,17 @@ export default function ListenIdentify() { {t("practice.expand")} + (note.open ? note.setOpen(false) : note.openNote(`study-note:global:${wordBookId}`, t("practice.free_note")))} + aria-label={t("practice.open_free_note")} + className="max-sm:px-2 max-sm:text-xs" + > + + {t("practice.free_note")} +
+
e.stopPropagation()}> + note.openNote(`study-note:word:${wordBookId}:${word.id}`, t("practice.note_title", { word: word.word }))} + /> +
+
e.stopPropagation()}> + note.openNote(`study-note:word:${wordBookId}:${word.id}`, t("practice.note_title", { word: word.word }))} + /> +
{ + setActiveNoteKey(`study-note:global:${wordBookId}`); + setActiveNoteTitle(t("practice.free_note")); + setGlobalNoteOpen(true); + }; + const openWordNote = (key: string, title: string) => { + setActiveNoteKey(key); + setActiveNoteTitle(title); + setGlobalNoteOpen(true); + }; + const reviewDate = useMemo(() => { const url = new URL(window.location.href); const qp = url.searchParams.get("date") || ""; @@ -360,6 +374,15 @@ export default function ReviewWordList() {
+
e.stopPropagation()}> + openWordNote(`study-note:word:${wordBookId}:${item.id}`, t("practice.note_title", { word: item.word }))} + /> +
setGlobalNoteOpen(false)} - storageKey={`study-note:global:${wordBookId}`} - title={t("practice.free_note")} + storageKey={activeNoteKey} + title={activeNoteTitle} side={noteSide} split onSideChange={setNoteSide} @@ -446,8 +469,8 @@ export default function ReviewWordList() { setGlobalNoteOpen(false)} - storageKey={`study-note:global:${wordBookId}`} - title={t("practice.free_note")} + storageKey={activeNoteKey} + title={activeNoteTitle} side={noteSide} onSideChange={setNoteSide} /> @@ -477,7 +500,7 @@ export default function ReviewWordList() { type="button" variant={globalNoteOpen ? "brand" : "outline"} size="pill" - onClick={() => setGlobalNoteOpen(true)} + onClick={() => (globalNoteOpen ? setGlobalNoteOpen(false) : openGlobalNote())} aria-label={t("practice.open_free_note")} className="max-sm:px-2 max-sm:text-xs" > diff --git a/web/src/pages/WordPractice.tsx b/web/src/pages/WordPractice.tsx index 0bef1be..a9326f7 100644 --- a/web/src/pages/WordPractice.tsx +++ b/web/src/pages/WordPractice.tsx @@ -9,7 +9,7 @@ import { FlowPageShell } from "../components/PageTransition"; import { TopBar } from "../components/TopBar"; import { SequenceNextMark } from "../components/SequenceNextMark"; import { WordDetailPanel } from "../components/WordDetailPanel"; -import { StudyNotePanel } from "../components/StudyNotePanel"; +import { StudyNoteLauncher, StudyNotePanel } from "../components/StudyNotePanel"; import { WordViewModeToggle, type WordViewMode } from "../components/WordMarkView"; import { playFirstWordAudio, playWordAudio, playAudioAtIndex, parseAudioUrls, WORD_AUDIO_SLOT_COUNT } from "../utils/audioPlayer"; import { displayTranslationFull, displayTranslationShort, pickPhoneticDisplay } from "../utils/wordFormat"; @@ -63,6 +63,24 @@ export default function WordPractice() { startResize: startNoteResize, } = useSplitScreenNote("lb_practice_note_width"); + const mode = useMemo(() => sessionStorage.getItem("lb_mode") || "study", []); + const wordBookId = useMemo(() => Number(sessionStorage.getItem("lb_wordbook_id") || 0), []); + const wordNoteKey = (wordId: number) => `study-note:word:${wordBookId}:${wordId}`; + + const [activeNoteKey, setActiveNoteKey] = useState(`study-note:global:${wordBookId}`); + const [activeNoteTitle, setActiveNoteTitle] = useState(t("practice.free_note")); + + const openGlobalNote = () => { + setActiveNoteKey(`study-note:global:${wordBookId}`); + setActiveNoteTitle(t("practice.free_note")); + setGlobalNoteOpen(true); + }; + const openWordNote = (key: string, title: string) => { + setActiveNoteKey(key); + setActiveNoteTitle(title); + setGlobalNoteOpen(true); + }; + const handlePlayNextAudio = (word: PracticeWord) => { if (!word.audioUrl) return; const urls = parseAudioUrls(word.audioUrl); @@ -79,10 +97,6 @@ export default function WordPractice() { setAudioIndexMap(new Map(audioIndexMap).set(word.id, next)); }; - const mode = useMemo(() => sessionStorage.getItem("lb_mode") || "study", []); - const wordBookId = useMemo(() => Number(sessionStorage.getItem("lb_wordbook_id") || 0), []); - const wordNoteKey = (wordId: number) => `study-note:word:${wordBookId}:${wordId}`; - const applyPatchedWord = (view: UserWordView) => { const e = view.effective; setWords((prev) => @@ -365,6 +379,15 @@ export default function WordPractice() {
+
e.stopPropagation()}> + openWordNote(wordNoteKey(cardWord.id), t("practice.note_title", { word: cardWord.word }))} + /> +
{!manualReadMode && parseAudioUrls(cardWord.audioUrl).length > 0 && (
+
e.stopPropagation()}> + openWordNote(wordNoteKey(word.id), t("practice.note_title", { word: word.word }))} + /> +
{!manualReadMode && parseAudioUrls(word.audioUrl).length > 0 && ( setGlobalNoteOpen(false)} - storageKey={`study-note:global:${wordBookId}`} - title={t("practice.free_note")} + storageKey={activeNoteKey} + title={activeNoteTitle} side={noteSide} split onSideChange={setNoteSide} @@ -484,8 +516,8 @@ export default function WordPractice() { setGlobalNoteOpen(false)} - storageKey={`study-note:global:${wordBookId}`} - title={t("practice.free_note")} + storageKey={activeNoteKey} + title={activeNoteTitle} side={noteSide} onSideChange={setNoteSide} /> @@ -541,7 +573,7 @@ export default function WordPractice() { type="button" variant={globalNoteOpen ? "brand" : "outline"} size="pill" - onClick={() => setGlobalNoteOpen((v) => !v)} + onClick={() => (globalNoteOpen ? setGlobalNoteOpen(false) : openGlobalNote())} aria-label={t("practice.open_free_note")} className="max-sm:px-2 max-sm:text-xs" >