diff --git a/apps/readest-app/src/app/reader/components/BooksGrid.tsx b/apps/readest-app/src/app/reader/components/BooksGrid.tsx index c548e2ed..0419e8f8 100644 --- a/apps/readest-app/src/app/reader/components/BooksGrid.tsx +++ b/apps/readest-app/src/app/reader/components/BooksGrid.tsx @@ -189,6 +189,7 @@ const BooksGrid: React.FC = ({ bookKeys, onCloseBook }) => { {viewSettings.readingRulerEnabled && !viewState?.loading && ( = ({ bookKey, + isVertical, lines, position, opacity, @@ -44,15 +47,37 @@ const ReadingRuler: React.FC = ({ const { envConfig } = useEnv(); const containerRef = useRef(null); const [currentPosition, setCurrentPosition] = useState(position); + const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); + const isDragging = useRef(false); - const height = calculateRulerHeight(lines, viewSettings, bookFormat); + const rulerSize = calculateRulerSize(lines, viewSettings, bookFormat); const baseColor = READING_RULER_COLORS[color] || READING_RULER_COLORS['yellow']; useEffect(() => { setCurrentPosition(position); }, [position]); + useEffect(() => { + if (!containerRef.current) return; + + const updateSize = () => { + if (containerRef.current) { + setContainerSize({ + width: containerRef.current.clientWidth, + height: containerRef.current.clientHeight, + }); + } + }; + + updateSize(); + + const resizeObserver = new ResizeObserver(updateSize); + resizeObserver.observe(containerRef.current); + + return () => resizeObserver.disconnect(); + }, []); + const handlePointerDown = useCallback((e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); @@ -60,16 +85,26 @@ const ReadingRuler: React.FC = ({ (e.target as HTMLElement).setPointerCapture(e.pointerId); }, []); - const handlePointerMove = useCallback((e: React.PointerEvent) => { - if (!isDragging.current || !containerRef.current) return; - e.preventDefault(); - e.stopPropagation(); + const handlePointerMove = useCallback( + (e: React.PointerEvent) => { + if (!isDragging.current || !containerRef.current) return; + e.preventDefault(); + e.stopPropagation(); - const rect = containerRef.current.getBoundingClientRect(); - const relativeY = e.clientY - rect.top; - const newPosition = Math.max(0, Math.min(100, (relativeY / rect.height) * 100)); - setCurrentPosition(newPosition); - }, []); + const rect = containerRef.current.getBoundingClientRect(); + let newPosition: number; + + if (isVertical) { + const relativeX = e.clientX - rect.left; + newPosition = Math.max(0, Math.min(100, (relativeX / rect.width) * 100)); + } else { + const relativeY = e.clientY - rect.top; + newPosition = Math.max(0, Math.min(100, (relativeY / rect.height) * 100)); + } + setCurrentPosition(newPosition); + }, + [isVertical], + ); const handlePointerUp = useCallback( (e: React.PointerEvent) => { @@ -81,33 +116,136 @@ const ReadingRuler: React.FC = ({ [envConfig, bookKey, currentPosition], ); - const topOffset = gridInsets.top; - const bottomOffset = gridInsets.bottom; + const fadeOpacity = Math.min(0.9, opacity); + // Calculate dimensions based on orientation + const containerDimension = isVertical ? containerSize.width : containerSize.height; + const rulerCenterPx = (currentPosition / 100) * containerDimension; + const rulerStartPx = Math.max(0, rulerCenterPx - rulerSize / 2); + const rulerEndPx = Math.min(containerDimension, rulerCenterPx + rulerSize / 2); + + // Map color names to CSS filter values (compatible with iOS Safari) + // Uses sepia as base, then hue-rotate to target color + const colorToFilter: Record = { + yellow: `sepia(${opacity}) saturate(2) hue-rotate(0deg) brightness(1)`, + green: `sepia(${opacity}) saturate(2) hue-rotate(70deg) brightness(1)`, + blue: `sepia(${opacity}) saturate(2) hue-rotate(135deg) brightness(1)`, + rose: `sepia(${opacity}) saturate(2) hue-rotate(225deg) brightness(1)`, + }; + + const cssFilter = colorToFilter[color] || colorToFilter['yellow']; + + // Insets based on orientation + const containerStyle = isVertical + ? { left: `${gridInsets.left}px`, right: `${gridInsets.right}px` } + : { top: `${gridInsets.top}px`, bottom: `${gridInsets.bottom}px` }; + + const backdropFilterStyle = { + backdropFilter: cssFilter, + WebkitBackdropFilter: cssFilter, + }; + + if (isVertical) { + // Vertical ruler (for vertical writing mode - moves left/right) + return ( +
+ {/* Left overlay */} +
+ + {/* Right overlay */} +
+ + {/* Vertical ruler */} +
+ {/* extended touch area */} +
+
+
+ ); + } + + // Horizontal ruler (default - moves up/down) return (
+ {/* Top overlay */}
+ + {/* Bottom overlay */} +
+ + {/* Horizontal ruler */} +
- {/* extended touch area */} + {/* Extended touch area */}
diff --git a/apps/readest-app/src/components/settings/ColorPanel.tsx b/apps/readest-app/src/components/settings/ColorPanel.tsx index 072b90eb..7b47214e 100644 --- a/apps/readest-app/src/components/settings/ColorPanel.tsx +++ b/apps/readest-app/src/components/settings/ColorPanel.tsx @@ -27,6 +27,7 @@ import BackgroundTextureSelector from './color/BackgroundTextureSelector'; import HighlightColorsEditor from './color/HighlightColorsEditor'; import TTSHighlightStyleEditor, { TTSHighlightStyle } from './color/TTSHighlightStyleEditor'; import CodeHighlightingSettings from './color/CodeHighlightingSettings'; +import ReadingRulerSettings from './color/ReadingRulerSettings'; const ColorPanel: React.FC = ({ bookKey, onRegisterReset }) => { const _ = useTranslation(); @@ -65,6 +66,11 @@ const ColorPanel: React.FC = ({ bookKey, onRegisterReset settings.globalReadSettings.userHighlightColors || [], ); + const [readingRulerEnabled, setReadingRulerEnabled] = useState(viewSettings.readingRulerEnabled); + const [readingRulerLines, setReadingRulerLines] = useState(viewSettings.readingRulerLines); + const [readingRulerOpacity, setReadingRulerOpacity] = useState(viewSettings.readingRulerOpacity); + const [readingRulerColor, setReadingRulerColor] = useState(viewSettings.readingRulerColor); + const { textures: customTextures, addTexture, @@ -83,6 +89,9 @@ const ColorPanel: React.FC = ({ bookKey, onRegisterReset invertImgColorInDark: setInvertImgColorInDark, codeHighlighting: setcodeHighlighting, codeLanguage: setCodeLanguage, + readingRulerEnabled: setReadingRulerEnabled, + readingRulerLines: setReadingRulerLines, + readingRulerOpacity: setReadingRulerOpacity, }); setThemeColor('default'); setThemeMode('auto'); @@ -153,6 +162,26 @@ const ColorPanel: React.FC = ({ bookKey, onRegisterReset // eslint-disable-next-line react-hooks/exhaustive-deps }, [backgroundSize]); + useEffect(() => { + saveViewSettings(envConfig, bookKey, 'readingRulerEnabled', readingRulerEnabled, false, false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [readingRulerEnabled]); + + useEffect(() => { + saveViewSettings(envConfig, bookKey, 'readingRulerLines', readingRulerLines, false, false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [readingRulerLines]); + + useEffect(() => { + saveViewSettings(envConfig, bookKey, 'readingRulerOpacity', readingRulerOpacity, false, false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [readingRulerOpacity]); + + useEffect(() => { + saveViewSettings(envConfig, bookKey, 'readingRulerColor', readingRulerColor, false, false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [readingRulerColor]); + const applyBackgroundTexture = () => { applyTexture(envConfig, selectedTextureId); document.documentElement.style.setProperty('--bg-texture-opacity', `${backgroundOpacity}`); @@ -336,6 +365,17 @@ const ColorPanel: React.FC = ({ bookKey, onRegisterReset onCustomColorsChange={handleCustomTtsColorsChange} /> + + = ({ bookKey, onRegisterRese const [progressStyle, setProgressStyle] = useState(viewSettings.progressStyle); const [screenOrientation, setScreenOrientation] = useState(viewSettings.screenOrientation); - const [readingRulerEnabled, setReadingRulerEnabled] = useState(viewSettings.readingRulerEnabled); - const [readingRulerLines, setReadingRulerLines] = useState(viewSettings.readingRulerLines); - const [readingRulerOpacity, setReadingRulerOpacity] = useState(viewSettings.readingRulerOpacity); - const [readingRulerColor, setReadingRulerColor] = useState(viewSettings.readingRulerColor); - const resetToDefaults = useResetViewSettings(); const handleReset = () => { @@ -113,9 +108,6 @@ const LayoutPanel: React.FC = ({ bookKey, onRegisterRese showProgressInfo: setShowProgressInfo, tapToToggleFooter: setTapToToggleFooter, showMarginsOnScroll: setShowMarginsOnScroll, - readingRulerEnabled: setReadingRulerEnabled, - readingRulerLines: setReadingRulerLines, - readingRulerOpacity: setReadingRulerOpacity, }); }; @@ -391,26 +383,6 @@ const LayoutPanel: React.FC = ({ bookKey, onRegisterRese // eslint-disable-next-line react-hooks/exhaustive-deps }, [screenOrientation]); - useEffect(() => { - saveViewSettings(envConfig, bookKey, 'readingRulerEnabled', readingRulerEnabled, false, false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [readingRulerEnabled]); - - useEffect(() => { - saveViewSettings(envConfig, bookKey, 'readingRulerLines', readingRulerLines, false, false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [readingRulerLines]); - - useEffect(() => { - saveViewSettings(envConfig, bookKey, 'readingRulerOpacity', readingRulerOpacity, false, false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [readingRulerOpacity]); - - useEffect(() => { - saveViewSettings(envConfig, bookKey, 'readingRulerColor', readingRulerColor, false, false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [readingRulerColor]); - const langCode = getBookLangCode(bookData?.bookDoc?.metadata?.language); const mightBeRTLBook = MIGHT_BE_RTL_LANGS.includes(langCode) || isCJKEnv(); const isVertical = viewSettings.vertical || writingMode.includes('vertical'); @@ -757,71 +729,6 @@ const LayoutPanel: React.FC = ({ bookKey, onRegisterRese
-
-

{_('Reading Ruler')}

-
-
-
- {_('Enable Reading Ruler')} - setReadingRulerEnabled(!readingRulerEnabled)} - /> -
- -
- {_('Ruler Color')} -
-
-
- -
-
-
- {appService?.hasOrientationLock && (

{_('Screen')}

diff --git a/apps/readest-app/src/components/settings/color/ReadingRulerSettings.tsx b/apps/readest-app/src/components/settings/color/ReadingRulerSettings.tsx new file mode 100644 index 00000000..5193e485 --- /dev/null +++ b/apps/readest-app/src/components/settings/color/ReadingRulerSettings.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { useTranslation } from '@/hooks/useTranslation'; +import { ReadingRulerColor } from '@/types/book'; +import NumberInput from '../NumberInput'; + +interface ReadingRulerSettingsProps { + enabled: boolean; + lines: number; + opacity: number; + color: ReadingRulerColor; + onEnabledChange: (enabled: boolean) => void; + onLinesChange: (lines: number) => void; + onOpacityChange: (opacity: number) => void; + onColorChange: (color: ReadingRulerColor) => void; +} + +const RULER_COLORS: { value: ReadingRulerColor; className: string; hoverClassName: string }[] = [ + { value: 'transparent', className: 'bg-transparent', hoverClassName: 'hover:bg-transparent' }, + { value: 'yellow', className: 'bg-yellow-400', hoverClassName: 'hover:bg-yellow-500' }, + { value: 'green', className: 'bg-green-400', hoverClassName: 'hover:bg-green-500' }, + { value: 'blue', className: 'bg-blue-400', hoverClassName: 'hover:bg-blue-500' }, + { value: 'rose', className: 'bg-rose-400', hoverClassName: 'hover:bg-rose-500' }, +]; + +const ReadingRulerSettings: React.FC = ({ + enabled, + lines, + opacity, + color, + onEnabledChange, + onLinesChange, + onOpacityChange, + onColorChange, +}) => { + const _ = useTranslation(); + + return ( +
+

{_('Reading Ruler')}

+
+
+
+ {_('Enable Reading Ruler')} + onEnabledChange(!enabled)} + /> +
+ +
+ {_('Ruler Color')} +
+ {RULER_COLORS.map(({ value, className, hoverClassName }) => ( +
+
+ +
+
+
+ ); +}; + +export default ReadingRulerSettings; diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index da2a1955..cb1bee61 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -7,6 +7,7 @@ import { BookStyle, HighlightColor, NoteExportConfig, + ReadingRulerColor, ScreenConfig, TranslatorConfig, TTSConfig, @@ -99,11 +100,11 @@ export const HIGHLIGHT_COLOR_HEX: Record = { violet: '#a78bfa', // violet-400 }; -export const READING_RULER_COLORS: Record = { +export const READING_RULER_COLORS: Record = { + transparent: '#00000000', yellow: '#facc15', green: '#4ade80', blue: '#60a5fa', - violet: '#a78bfa', rose: '#fb7185', }; @@ -256,8 +257,8 @@ export const DEFAULT_VIEW_CONFIG: ViewConfig = { readingRulerEnabled: false, readingRulerLines: 2, readingRulerPosition: 33, - readingRulerOpacity: 0.3, - readingRulerColor: 'yellow', + readingRulerOpacity: 0.5, + readingRulerColor: 'transparent', }; export const DEFAULT_TTS_CONFIG: TTSConfig = { diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index c47425b3..8a299d98 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -17,6 +17,7 @@ export type BookNoteType = 'bookmark' | 'annotation' | 'excerpt'; export type HighlightStyle = 'highlight' | 'underline' | 'squiggly'; // Predefined highlight colors, can be extended with custom hex colors export type HighlightColor = 'red' | 'yellow' | 'green' | 'blue' | 'violet' | string; +export type ReadingRulerColor = 'transparent' | 'yellow' | 'green' | 'blue' | 'rose'; export const FIXED_LAYOUT_FORMATS: Set = new Set(['PDF', 'CBZ']); @@ -208,7 +209,7 @@ export interface ViewConfig { readingRulerLines: number; readingRulerPosition: number; readingRulerOpacity: number; - readingRulerColor: 'yellow' | 'green' | 'blue' | 'violet' | 'rose'; + readingRulerColor: ReadingRulerColor; } export interface TTSConfig {