From d522898159f91118095ca8ebc2021e632c5642dd Mon Sep 17 00:00:00 2001 From: codedogQBY <1369175442@qq.com> Date: Sun, 31 May 2026 11:40:55 +0800 Subject: [PATCH] feat: remember default highlight color --- .../components/reader/SelectionPopover.tsx | 59 +++++++++--------- .../app-expo/src/screens/ReaderScreen.tsx | 50 ++++++++++++--- .../app-expo/src/stores/settings-store.ts | 1 + .../src/components/reader/FoliateViewer.tsx | 1 + .../app/src/components/reader/ReaderView.tsx | 62 +++++++++++++++---- .../components/reader/SelectionPopover.tsx | 12 ++-- packages/core/src/stores/settings-store.ts | 1 + packages/core/src/types/book.ts | 2 + 8 files changed, 135 insertions(+), 53 deletions(-) diff --git a/packages/app-expo/src/components/reader/SelectionPopover.tsx b/packages/app-expo/src/components/reader/SelectionPopover.tsx index aeda5b2b..e0b73bd9 100644 --- a/packages/app-expo/src/components/reader/SelectionPopover.tsx +++ b/packages/app-expo/src/components/reader/SelectionPopover.tsx @@ -12,6 +12,8 @@ import { RichTextEditor } from "@/components/ui/RichTextEditor"; import type { SelectionEvent } from "@/hooks/use-reader-bridge"; import { radius, spacing, useColors } from "@/styles/theme"; import type { ThemeColors } from "@/styles/theme"; +import { HIGHLIGHT_COLORS, HIGHLIGHT_COLOR_HEX } from "@readany/core/types"; +import type { HighlightColor } from "@readany/core/types"; import * as Clipboard from "expo-clipboard"; /** * SelectionPopover — floating action bar shown when text is selected in the reader. @@ -31,15 +33,6 @@ import { View, } from "react-native"; -const HIGHLIGHT_COLORS = [ - { key: "yellow", hex: "#facc15" }, - { key: "red", hex: "#f87171" }, - { key: "green", hex: "#4ade80" }, - { key: "blue", hex: "#60a5fa" }, - { key: "violet", hex: "#a78bfa" }, - { key: "pink", hex: "#f472b6" }, -] as const; - const SCREEN_WIDTH = Dimensions.get("window").width; const SCREEN_HEIGHT = Dimensions.get("window").height; const POPOVER_MARGIN = 8; @@ -53,7 +46,7 @@ const SELECTION_POPOVER_BELOW_OFFSET = 6; interface Props { selection: SelectionEvent; - onHighlight: (color: string) => void; + onHighlight: (color: HighlightColor) => void; onDismiss: () => void; onCopy: () => void; onAIChat: () => void; @@ -61,7 +54,8 @@ interface Props { onNote?: (text: string, cfi: string) => void; onTranslate?: (text: string) => void; onRemoveHighlight?: () => void; - existingHighlight?: { id: string; color: string; note?: string } | null; + existingHighlight?: { id: string; color: HighlightColor; note?: string } | null; + defaultColor?: HighlightColor; } export function SelectionPopover({ @@ -75,6 +69,7 @@ export function SelectionPopover({ onTranslate, onRemoveHighlight, existingHighlight, + defaultColor = "yellow", }: Props) { const { t } = useTranslation(); const colors = useColors(); @@ -82,17 +77,23 @@ export function SelectionPopover({ const [showNoteModal, setShowNoteModal] = useState(false); const [showColors, setShowColors] = useState(!!existingHighlight); const [noteContent, setNoteContent] = useState(existingHighlight?.note || ""); + const existingHighlightNote = existingHighlight?.note || ""; + const hasExistingHighlight = !!existingHighlight; + + useEffect(() => { + setNoteContent(existingHighlightNote); + }, [existingHighlightNote]); useEffect(() => { - setNoteContent(existingHighlight?.note || ""); - }, [existingHighlight?.id, existingHighlight?.note, selection.cfi]); + setShowColors(hasExistingHighlight); + }, [hasExistingHighlight]); const buttonCount = 4 + (onNote ? 1 : 0) + (onTranslate ? 1 : 0) + (onSpeak ? 1 : 0) + - (existingHighlight && onRemoveHighlight ? 1 : 0); + (hasExistingHighlight && onRemoveHighlight ? 1 : 0); const colorRowHeight = showColors ? 40 : 0; const popoverHeight = 44 + colorRowHeight + POPOVER_PADDING * 2 + GAP; const popoverWidth = Math.min( @@ -114,18 +115,14 @@ export function SelectionPopover({ const yAbove = selTop - popoverHeight + SELECTION_POPOVER_ABOVE_OFFSET; const yBelow = selBottom + SELECTION_POPOVER_BELOW_OFFSET; const aboveValid = yAbove >= SAFE_TOP; - const belowValid = - yBelow + popoverHeight + POPOVER_MARGIN <= SCREEN_HEIGHT - SAFE_BOTTOM; + const belowValid = yBelow + popoverHeight + POPOVER_MARGIN <= SCREEN_HEIGHT - SAFE_BOTTOM; if (aboveValid) { y = yAbove; } else if (belowValid) { y = yBelow; } else { - y = Math.max( - SAFE_TOP, - Math.min(yBelow, SCREEN_HEIGHT - popoverHeight - POPOVER_MARGIN), - ); + y = Math.max(SAFE_TOP, Math.min(yBelow, SCREEN_HEIGHT - popoverHeight - POPOVER_MARGIN)); } return { x, y }; @@ -170,9 +167,13 @@ export function SelectionPopover({ onDismiss(); }, [onRemoveHighlight, onDismiss]); - const toggleColors = useCallback(() => { - setShowColors((prev) => !prev); - }, []); + const handleHighlightPress = useCallback(() => { + if (hasExistingHighlight) { + setShowColors((prev) => !prev); + return; + } + onHighlight(defaultColor); + }, [defaultColor, hasExistingHighlight, onHighlight]); return ( @@ -180,15 +181,15 @@ export function SelectionPopover({ {showColors && ( - {HIGHLIGHT_COLORS.map((c) => ( + {HIGHLIGHT_COLORS.map((color) => ( onHighlight(c.key)} + onPress={() => onHighlight(color)} /> ))} @@ -197,7 +198,7 @@ export function SelectionPopover({ diff --git a/packages/app-expo/src/screens/ReaderScreen.tsx b/packages/app-expo/src/screens/ReaderScreen.tsx index afac9df9..9a9793c0 100644 --- a/packages/app-expo/src/screens/ReaderScreen.tsx +++ b/packages/app-expo/src/screens/ReaderScreen.tsx @@ -41,7 +41,7 @@ import { useReadingSession } from "@readany/core/hooks/use-reading-session"; import { createSelectionNoteMutation } from "@readany/core/reader"; import { getPlatformService } from "@readany/core/services"; import { getCSSFontFace, useFontStore } from "@readany/core/stores"; -import type { ReadSettings, TOCItem } from "@readany/core/types"; +import type { HighlightColor, ReadSettings, TOCItem } from "@readany/core/types"; import { eventBus } from "@readany/core/utils/event-bus"; import { throttle } from "@readany/core/utils/throttle"; import { Asset } from "expo-asset"; @@ -977,14 +977,36 @@ export function ReaderScreen({ route, navigation }: Props) { // Selection popover handlers const handleHighlight = useCallback( - (color: string) => { + (color: HighlightColor = readSettings.defaultHighlightColor ?? "yellow") => { if (!selection) return; + updateReadSettings({ defaultHighlightColor: color }); + + const existingHighlight = highlights.find( + (h) => h.bookId === bookId && h.cfi === selection.cfi, + ); + + if (existingHighlight) { + updateHighlight(existingHighlight.id, { + color, + updatedAt: Date.now(), + }); + bridge.removeAnnotation({ value: existingHighlight.cfi }); + bridge.addAnnotation({ + value: existingHighlight.cfi, + type: "highlight", + color, + note: existingHighlight.note, + }); + setSelection(null); + return; + } + const highlight = { id: `hl-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, bookId, cfi: selection.cfi, text: selection.text, - color: color as any, + color, chapterTitle: currentChapter, createdAt: Date.now(), updatedAt: Date.now(), @@ -993,7 +1015,17 @@ export function ReaderScreen({ route, navigation }: Props) { bridge.addAnnotation({ value: selection.cfi, type: "highlight", color }); setSelection(null); }, - [selection, bookId, currentChapter, addHighlight, bridge], + [ + selection, + readSettings.defaultHighlightColor, + updateReadSettings, + highlights, + bookId, + currentChapter, + addHighlight, + updateHighlight, + bridge, + ], ); const handleDismissSelection = useCallback(() => { @@ -1371,7 +1403,8 @@ export function ReaderScreen({ route, navigation }: Props) { const isPanelOpen = showTOC || showSettings || showSearch || showNotebook || showTranslation; const existingSelectionHighlight = selection - ? (highlights.find((highlight) => highlight.cfi === selection.cfi) ?? null) + ? (highlights.find((highlight) => highlight.bookId === bookId && highlight.cfi === selection.cfi) ?? + null) : null; const readerTopMargin = !showSearch ? showTopTitleProgress @@ -1570,7 +1603,7 @@ export function ReaderScreen({ route, navigation }: Props) { note: text, chapterTitle: currentChapter, existingHighlight: existingSelectionHighlight, - defaultColor: "yellow", + defaultColor: readSettings.defaultHighlightColor ?? "yellow", }); if (mutation.kind === "create") { @@ -1605,8 +1638,11 @@ export function ReaderScreen({ route, navigation }: Props) { } : null } + defaultColor={readSettings.defaultHighlightColor ?? "yellow"} onRemoveHighlight={() => { - const existing = highlights.find((h) => h.cfi === selectionPopoverSelection.cfi); + const existing = highlights.find( + (h) => h.bookId === bookId && h.cfi === selectionPopoverSelection.cfi, + ); if (existing) { removeHighlight(existing.id); bridge.removeAnnotation({ value: existing.cfi }); diff --git a/packages/app-expo/src/stores/settings-store.ts b/packages/app-expo/src/stores/settings-store.ts index fbcb8886..31a5b699 100644 --- a/packages/app-expo/src/stores/settings-store.ts +++ b/packages/app-expo/src/stores/settings-store.ts @@ -49,6 +49,7 @@ const defaultReadSettings: ReadSettings = { showTopTitleProgress: true, showBottomTimeBattery: true, volumeButtonsPageTurn: false, + defaultHighlightColor: "yellow", }; const defaultTranslationConfig: TranslationConfig = { diff --git a/packages/app/src/components/reader/FoliateViewer.tsx b/packages/app/src/components/reader/FoliateViewer.tsx index ab3eb701..0f6f81d1 100644 --- a/packages/app/src/components/reader/FoliateViewer.tsx +++ b/packages/app/src/components/reader/FoliateViewer.tsx @@ -1543,6 +1543,7 @@ export const FoliateViewer = forwardRef green: "rgba(74, 222, 128, 0.4)", // green-400 blue: "rgba(96, 165, 250, 0.4)", // blue-400 pink: "rgba(236, 72, 153, 0.4)", // pink-400 - ADDED + purple: "rgba(192, 132, 252, 0.4)", // purple-400 violet: "rgba(167, 139, 250, 0.4)", // violet-400 }; diff --git a/packages/app/src/components/reader/ReaderView.tsx b/packages/app/src/components/reader/ReaderView.tsx index 04d30011..a78ebc36 100644 --- a/packages/app/src/components/reader/ReaderView.tsx +++ b/packages/app/src/components/reader/ReaderView.tsx @@ -480,8 +480,10 @@ export function ReaderView({ bookId, tabId }: ReaderViewProps) { goToCfi: (cfi) => foliateRef.current?.goToCFI(cfi), }); - // Track which highlights have been rendered (id -> {cfi, note}) to detect changes - const renderedHighlightsRef = useRef>(new Map()); + // Track which highlights have been rendered (id -> {cfi, note, color}) to detect changes + const renderedHighlightsRef = useRef< + Map + >(new Map()); // Reset rendered highlights tracking when book changes useEffect(() => { @@ -599,15 +601,16 @@ export function ReaderView({ bookId, tabId }: ReaderViewProps) { } } - // Add new highlights or update existing ones if note status changed + // Add new highlights or update existing ones if note status or color changed for (const h of bookHighlights) { if (!h.cfi) continue; const existing = renderedHighlightsRef.current.get(h.id); const hasNote = !!h.note; + const color = h.color || "yellow"; - // Check if we need to re-render (new highlight or note status changed) - const needsRender = !existing || existing.hasNote !== hasNote; + // Check if we need to re-render (new highlight, note status changed, or color changed) + const needsRender = !existing || existing.hasNote !== hasNote || existing.color !== color; if (needsRender) { // Remove old annotation if exists @@ -619,10 +622,10 @@ export function ReaderView({ bookId, tabId }: ReaderViewProps) { foliateRef.current.addAnnotation({ value: h.cfi, type: "highlight", - color: h.color || "yellow", + color, note: h.note, // Pass note for wavy underline + tooltip }); - renderedHighlightsRef.current.set(h.id, { cfi: h.cfi, hasNote }); + renderedHighlightsRef.current.set(h.id, { cfi: h.cfi, hasNote, color }); } } }, 100); @@ -1212,7 +1215,11 @@ export function ReaderView({ bookId, tabId }: ReaderViewProps) { color: h.color || "yellow", note: h.note, // Pass note for wavy underline + tooltip }); - renderedHighlightsRef.current.set(h.id, { cfi: h.cfi, hasNote: !!h.note }); + renderedHighlightsRef.current.set(h.id, { + cfi: h.cfi, + hasNote: !!h.note, + color: h.color || "yellow", + }); } } @@ -1367,8 +1374,29 @@ export function ReaderView({ bookId, tabId }: ReaderViewProps) { // --- Selection actions --- const handleHighlight = useCallback( - (color: HighlightColor = "yellow") => { + (color: HighlightColor = viewSettings.defaultHighlightColor ?? "yellow") => { if (selection && selection.cfi) { + updateReadSettings({ defaultHighlightColor: color }); + const existingHighlight = selection.highlightId + ? highlights.find((h) => h.id === selection.highlightId) + : highlights.find((h) => h.bookId === bookId && h.cfi === selection.cfi); + + if (existingHighlight) { + useAnnotationStore.getState().updateHighlight(existingHighlight.id, { + color, + updatedAt: Date.now(), + }); + foliateRef.current?.deleteAnnotation({ value: existingHighlight.cfi }); + foliateRef.current?.addAnnotation({ + value: existingHighlight.cfi, + type: "highlight", + color, + note: existingHighlight.note, + }); + setSelection(null); + return; + } + const highlightId = crypto.randomUUID(); // Add to store (for persistence) @@ -1391,11 +1419,22 @@ export function ReaderView({ bookId, tabId }: ReaderViewProps) { }); // Track as rendered - renderedHighlightsRef.current.set(highlightId, { cfi: selection.cfi, hasNote: false }); + renderedHighlightsRef.current.set(highlightId, { + cfi: selection.cfi, + hasNote: false, + color, + }); } setSelection(null); }, - [selection, bookId, readerTab?.chapterTitle], + [ + selection, + bookId, + readerTab?.chapterTitle, + highlights, + updateReadSettings, + viewSettings.defaultHighlightColor, + ], ); // Handle note button - open notebook panel with pending note @@ -2728,6 +2767,7 @@ export function ReaderView({ bookId, tabId }: ReaderViewProps) { selectedText={selection.text} annotated={selection.annotated} currentColor={selection.color as HighlightColor | undefined} + defaultColor={viewSettings.defaultHighlightColor ?? "yellow"} isPdf={bookFormat === "PDF"} onHighlight={handleHighlight} onRemoveHighlight={handleRemoveHighlight} diff --git a/packages/app/src/components/reader/SelectionPopover.tsx b/packages/app/src/components/reader/SelectionPopover.tsx index a967dcaf..5b6cfebd 100644 --- a/packages/app/src/components/reader/SelectionPopover.tsx +++ b/packages/app/src/components/reader/SelectionPopover.tsx @@ -22,6 +22,7 @@ interface SelectionPopoverProps { selectedText: string; annotated?: boolean; // true if this is an existing annotation currentColor?: HighlightColor; // current highlight color (for existing annotations) + defaultColor?: HighlightColor; isPdf?: boolean; // true if viewing a PDF (highlight disabled) onHighlight: (color: HighlightColor) => void; onRemoveHighlight: () => void; @@ -38,6 +39,7 @@ export function SelectionPopover({ selectedText: _selectedText, annotated = false, currentColor, + defaultColor = "yellow", isPdf = false, onHighlight, onRemoveHighlight, @@ -50,7 +52,7 @@ export function SelectionPopover({ }: SelectionPopoverProps) { const { t } = useTranslation(); const [showColors, setShowColors] = useState(annotated); // Show colors immediately for existing annotations - const [selectedColor, setSelectedColor] = useState(currentColor || "yellow"); + const [selectedColor, setSelectedColor] = useState(currentColor || defaultColor); const handleHighlightClick = () => { // PDF doesn't support highlighting @@ -59,12 +61,8 @@ export function SelectionPopover({ if (annotated) { // For existing annotation, toggle color picker setShowColors(!showColors); - } else if (showColors) { - // If colors are already shown, apply highlight with selected color - onHighlight(selectedColor); } else { - // Show color picker - setShowColors(true); + onHighlight(selectedColor); } }; @@ -109,6 +107,7 @@ export function SelectionPopover({
{HIGHLIGHT_COLORS.map((color) => (