Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions web/src/components/NoteContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ type NoteSide = "left" | "right";
type NoteContextValue = ReturnType<typeof useSplitScreenNote> & {
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;
};
Expand All @@ -16,10 +18,12 @@ const NoteContext = createContext<NoteContextValue | null>(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);
};

Expand All @@ -30,6 +34,8 @@ export function NoteProvider({ children }: { children: ReactNode }) {
storageKey,
setStorageKey,
openNote,
noteTitle,
setNoteTitle,
defaultTitle,
setDefaultTitle,
}}
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/NoteSplitLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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}
Expand All @@ -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}
/>
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/StudyNotePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -59,13 +60,13 @@ export function StudyNoteLauncher({ storageKey, title, label, className = "" }:
<button
type="button"
className={`inline-flex h-9 items-center justify-center rounded-full border border-[#d8cdb8] px-3 text-xs font-medium text-[#5f7890] hover:bg-[#e9dfce] hover:text-[#25344a] ${className}`}
onClick={() => setOpen(true)}
onClick={() => (onOpen ? onOpen() : setOpen(true))}
title={t("studyNote.open", { label: resolvedLabel })}
>
<BookOpen size={15} />
{resolvedLabel && <span className="hidden sm:inline">{resolvedLabel}</span>}
</button>
{open && (
{!onOpen && open && (
<StudyNotePanel
open={open}
onClose={() => setOpen(false)}
Expand Down
21 changes: 21 additions & 0 deletions web/src/pages/FlashReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -67,6 +69,7 @@ function mapToFlashWord(w: Record<string, unknown>): FlashWord {
export default function FlashReview() {
const { t } = useTranslation();
const navigate = useNavigate();
const note = useNote();
const [words, setWords] = useState<FlashWord[]>([]);
const [showCompleteDialog, setShowCompleteDialog] = useState(false);
const [annotationOpen, setAnnotationOpen] = useState(false);
Expand Down Expand Up @@ -399,6 +402,15 @@ export default function FlashReview() {
</CloudButton>
</div>
<div className="flex items-center justify-center gap-3 border-t border-border/60 px-4 py-4">
<div onClick={(e) => e.stopPropagation()}>
<StudyNoteLauncher
storageKey={wordNoteKey(visibleWords[cardIndex].id)}
title={t("practice.note_title", { word: visibleWords[cardIndex].word })}
label={t("practice.note")}
className="h-9 px-2"
onOpen={() => note.openNote(wordNoteKey(visibleWords[cardIndex].id), t("practice.note_title", { word: visibleWords[cardIndex].word }))}
/>
</div>
<CloudButton
type="button"
variant="ghost"
Expand Down Expand Up @@ -470,6 +482,15 @@ export default function FlashReview() {
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3 -mr-1 sm:mr-0">
<div onClick={(e) => e.stopPropagation()}>
<StudyNoteLauncher
storageKey={wordNoteKey(word.id)}
title={t("practice.note_title", { word: word.word })}
label={t("practice.note")}
className="h-9 px-2"
onOpen={() => note.openNote(wordNoteKey(word.id), t("practice.note_title", { word: word.word }))}
/>
</div>
<CloudButton
type="button"
variant="ghost"
Expand Down
41 changes: 39 additions & 2 deletions web/src/pages/ListenIdentify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import {
markWordCardStyle,
type WordViewMode,
} from "../components/WordMarkView";
import { ArrowRight, Volume2, Shuffle, BookOpen, ChevronLeft, ChevronRight } from "lucide-react";
import { ArrowRight, Volume2, Shuffle, BookOpen, ChevronLeft, ChevronRight, PanelTop } from "lucide-react";
import { useNavigate } from "react-router";
import { useEffect, useMemo, useRef, useState } from "react";
import { playFirstWordAudio } from "../utils/audioPlayer";
import { displayTranslationFull, displayTranslationShort, pickPhoneticDisplay } from "../utils/wordFormat";
import { getReviewReturnPath } from "../utils/reviewPractice";
import { applyUserWordView } from "../components/WordEditControls";
import { StudyNoteLauncher } from "../components/StudyNotePanel";
import { NoteSplitLayout } from "../components/NoteSplitLayout";
import { useNote } from "../components/NoteContext";

import { useTranslation } from "react-i18next";

Expand All @@ -34,6 +37,7 @@ type ListenWord = {
export default function ListenIdentify() {
const { t } = useTranslation();
const navigate = useNavigate();
const note = useNote();
const [words, setWords] = useState<ListenWord[]>([]);
const [annotationOpen, setAnnotationOpen] = useState(false);
const [viewMode, setViewMode] = useState<WordViewMode>("list");
Expand Down Expand Up @@ -203,7 +207,15 @@ export default function ListenIdentify() {
)}
{showAnswer && renderRevealed(w)}
</div>

<div onClick={(e) => e.stopPropagation()}>
<StudyNoteLauncher
storageKey={wordNoteKey(w.id)}
title={t("practice.note_title", { word: w.word })}
label={t("practice.note")}
className="h-9 px-2"
onOpen={() => note.openNote(wordNoteKey(w.id), t("practice.note_title", { word: w.word }))}
/>
</div>
</div>
{detailMode && showAnswer && (
<div className="mt-3" onClick={(e) => e.stopPropagation()}>
Expand Down Expand Up @@ -256,6 +268,10 @@ export default function ListenIdentify() {
onOpenChange={setAnnotationOpen}
/>

<NoteSplitLayout
defaultStorageKey={`study-note:global:${wordBookId}`}
defaultTitle={t("practice.free_note")}
>
<div className="px-4 mt-6 max-w-5xl mx-auto w-full pb-28">
<div className="text-center text-sm text-[#718096] mb-6">{t("practice.batch_group", { current: batchIdx + 1, total: totalBatches })}</div>

Expand Down Expand Up @@ -324,6 +340,15 @@ export default function ListenIdentify() {
</CloudButton>
</div>
<div className="flex items-center justify-center gap-3 border-t border-border/60 px-4 py-4">
<div onClick={(e) => e.stopPropagation()}>
<StudyNoteLauncher
storageKey={wordNoteKey(cardWord.id)}
title={t("practice.note_title", { word: cardWord.word })}
label={t("practice.note")}
className="h-9 px-2"
onOpen={() => note.openNote(wordNoteKey(cardWord.id), t("practice.note_title", { word: cardWord.word }))}
/>
</div>
<CloudButton
type="button"
variant="ghost"
Expand Down Expand Up @@ -360,6 +385,7 @@ export default function ListenIdentify() {
</div>
)}
</div>
</NoteSplitLayout>

<div className="fixed bottom-0 left-0 right-0 z-20 bg-white border-t border-[#E2E8F0] px-3 sm:px-4 py-1.5 sm:py-2 shadow-lg">
<div className="max-w-5xl mx-auto w-full">
Expand All @@ -379,6 +405,17 @@ export default function ListenIdentify() {
<BookOpen size={15} />
<span className="hidden sm:inline">{t("practice.expand")}</span>
</CloudButton>
<CloudButton
type="button"
variant={note.open ? "brand" : "outline"}
size="pill"
onClick={() => (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"
>
<PanelTop size={15} className={note.open ? "text-white" : "text-[#c45c78]"} />
<span className="hidden sm:inline">{t("practice.free_note")}</span>
</CloudButton>
</div>
<div className="flex items-center gap-1.5 sm:gap-2 shrink-0">
<CloudButton
Expand Down
10 changes: 10 additions & 0 deletions web/src/pages/PreTrainingCheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type WordViewMode,
} from "../components/WordMarkView";
import { WordDetailPanel } from "../components/WordDetailPanel";
import { StudyNoteLauncher } from "../components/StudyNotePanel";

import { NoteSplitLayout } from "../components/NoteSplitLayout";
import { useNote } from "../components/NoteContext";
Expand Down Expand Up @@ -401,6 +402,15 @@ export default function PreTrainingCheck() {
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3 -mr-1 sm:mr-0">
<div onClick={(e) => e.stopPropagation()}>
<StudyNoteLauncher
storageKey={`study-note:word:${wordBookId}:${word.id}`}
title={t("practice.note_title", { word: word.word })}
label={t("practice.note")}
className="h-9 px-2"
onOpen={() => note.openNote(`study-note:word:${wordBookId}:${word.id}`, t("practice.note_title", { word: word.word }))}
/>
</div>
<CloudButton
type="button"
variant="ghost"
Expand Down
10 changes: 10 additions & 0 deletions web/src/pages/ReviewCheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type WordViewMode,
} from "../components/WordMarkView";
import { WordDetailPanel } from "../components/WordDetailPanel";
import { StudyNoteLauncher } from "../components/StudyNotePanel";

import { NoteSplitLayout } from "../components/NoteSplitLayout";
import { useNote } from "../components/NoteContext";
Expand Down Expand Up @@ -318,6 +319,15 @@ export default function ReviewCheck() {
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3 -mr-1 sm:mr-0">
<div onClick={(e) => e.stopPropagation()}>
<StudyNoteLauncher
storageKey={`study-note:word:${wordBookId}:${word.id}`}
title={t("practice.note_title", { word: word.word })}
label={t("practice.note")}
className="h-9 px-2"
onOpen={() => note.openNote(`study-note:word:${wordBookId}:${word.id}`, t("practice.note_title", { word: word.word }))}
/>
</div>
<CloudButton
type="button"
variant="ghost"
Expand Down
35 changes: 29 additions & 6 deletions web/src/pages/ReviewWordList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { WordDetailPanel } from "../components/WordDetailPanel";
import { nextWordTapState, syncDetailWordWithTap } from "../utils/wordReveal";
import { getReviewReturnPath } from "../utils/reviewPractice";
import { useSplitScreenNote } from "../hooks/useSplitScreenNote";
import { StudyNotePanel } from "../components/StudyNotePanel";
import { StudyNoteLauncher, StudyNotePanel } from "../components/StudyNotePanel";
import { useTranslation } from "react-i18next";
import { formatApiMessage } from "../utils/apiMessage";

Expand Down Expand Up @@ -75,6 +75,20 @@ export default function ReviewWordList() {
return Number(sessionStorage.getItem("lb_review_wordbook_id") || 0);
}, []);

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 reviewDate = useMemo(() => {
const url = new URL(window.location.href);
const qp = url.searchParams.get("date") || "";
Expand Down Expand Up @@ -360,6 +374,15 @@ export default function ReviewWordList() {
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3 -mr-1 sm:mr-0">
<div onClick={(e) => e.stopPropagation()}>
<StudyNoteLauncher
storageKey={`study-note:word:${wordBookId}:${item.id}`}
title={t("practice.note_title", { word: item.word })}
label={t("practice.note")}
className="h-9 px-2"
onOpen={() => openWordNote(`study-note:word:${wordBookId}:${item.id}`, t("practice.note_title", { word: item.word }))}
/>
</div>
<CloudButton
type="button"
variant="ghost"
Expand Down Expand Up @@ -430,8 +453,8 @@ export default function ReviewWordList() {
<StudyNotePanel
open={globalNoteOpen}
onClose={() => setGlobalNoteOpen(false)}
storageKey={`study-note:global:${wordBookId}`}
title={t("practice.free_note")}
storageKey={activeNoteKey}
title={activeNoteTitle}
side={noteSide}
split
onSideChange={setNoteSide}
Expand All @@ -446,8 +469,8 @@ export default function ReviewWordList() {
<StudyNotePanel
open={globalNoteOpen}
onClose={() => setGlobalNoteOpen(false)}
storageKey={`study-note:global:${wordBookId}`}
title={t("practice.free_note")}
storageKey={activeNoteKey}
title={activeNoteTitle}
side={noteSide}
onSideChange={setNoteSide}
/>
Expand Down Expand Up @@ -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"
>
Expand Down
Loading
Loading