forked from akai/readest
feat(ruler): support vertical ruler and transparent ruler (#3076)
This commit is contained in:
@@ -189,6 +189,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
{viewSettings.readingRulerEnabled && !viewState?.loading && (
|
||||
<ReadingRuler
|
||||
bookKey={bookKey}
|
||||
isVertical={viewSettings.vertical}
|
||||
lines={viewSettings.readingRulerLines}
|
||||
position={viewSettings.readingRulerPosition}
|
||||
opacity={viewSettings.readingRulerOpacity}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import { Insets } from '@/types/misc';
|
||||
import { BookFormat, FIXED_LAYOUT_FORMATS, ViewSettings } from '@/types/book';
|
||||
@@ -7,6 +8,7 @@ import { READING_RULER_COLORS } from '@/services/constants';
|
||||
|
||||
interface ReadingRulerProps {
|
||||
bookKey: string;
|
||||
isVertical: boolean;
|
||||
lines: number;
|
||||
position: number;
|
||||
opacity: number;
|
||||
@@ -18,7 +20,7 @@ interface ReadingRulerProps {
|
||||
|
||||
const FIXED_LAYOUT_LINE_HEIGHT = 28;
|
||||
|
||||
const calculateRulerHeight = (
|
||||
const calculateRulerSize = (
|
||||
lines: number,
|
||||
viewSettings: ViewSettings,
|
||||
bookFormat: BookFormat,
|
||||
@@ -33,6 +35,7 @@ const calculateRulerHeight = (
|
||||
|
||||
const ReadingRuler: React.FC<ReadingRulerProps> = ({
|
||||
bookKey,
|
||||
isVertical,
|
||||
lines,
|
||||
position,
|
||||
opacity,
|
||||
@@ -44,15 +47,37 @@ const ReadingRuler: React.FC<ReadingRulerProps> = ({
|
||||
const { envConfig } = useEnv();
|
||||
const containerRef = useRef<HTMLDivElement>(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<ReadingRulerProps> = ({
|
||||
(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<ReadingRulerProps> = ({
|
||||
[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<string, string> = {
|
||||
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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='pointer-events-none absolute inset-0 z-[5]'
|
||||
style={containerStyle}
|
||||
>
|
||||
{/* Left overlay */}
|
||||
<div
|
||||
className='bg-base-100 pointer-events-none absolute bottom-0 left-0 top-0'
|
||||
style={{
|
||||
width: `${rulerStartPx}px`,
|
||||
opacity: fadeOpacity,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Right overlay */}
|
||||
<div
|
||||
className='bg-base-100 pointer-events-none absolute bottom-0 right-0 top-0'
|
||||
style={{
|
||||
width: `${containerSize.width - rulerEndPx}px`,
|
||||
opacity: fadeOpacity,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Vertical ruler */}
|
||||
<div
|
||||
className={clsx(
|
||||
'ruler pointer-events-auto absolute bottom-0 top-0 my-2 cursor-col-resize rounded-2xl',
|
||||
color === 'transparent' ? 'border-base-content/55 border' : '',
|
||||
)}
|
||||
style={{
|
||||
left: `${currentPosition}%`,
|
||||
width: `${rulerSize}px`,
|
||||
transform: 'translateX(-50%)',
|
||||
...(color === 'transparent'
|
||||
? {
|
||||
backgroundColor: baseColor,
|
||||
}
|
||||
: backdropFilterStyle),
|
||||
}}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
>
|
||||
{/* extended touch area */}
|
||||
<div className='absolute inset-y-0 -left-2 -right-2' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Horizontal ruler (default - moves up/down)
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='pointer-events-none absolute inset-0 z-[5]'
|
||||
style={{
|
||||
top: `${topOffset}px`,
|
||||
bottom: `${bottomOffset}px`,
|
||||
}}
|
||||
style={containerStyle}
|
||||
>
|
||||
{/* Top overlay */}
|
||||
<div
|
||||
className='pointer-events-auto absolute left-0 right-0 cursor-row-resize transition-shadow active:shadow-lg'
|
||||
className='bg-base-100 pointer-events-none absolute left-0 right-0 top-0'
|
||||
style={{
|
||||
height: `${rulerStartPx}px`,
|
||||
opacity: fadeOpacity,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Bottom overlay */}
|
||||
<div
|
||||
className='bg-base-100 pointer-events-none absolute bottom-0 left-0 right-0'
|
||||
style={{
|
||||
height: `${containerSize.height - rulerEndPx}px`,
|
||||
opacity: fadeOpacity,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Horizontal ruler */}
|
||||
<div
|
||||
className={clsx(
|
||||
'ruler pointer-events-auto absolute left-0 right-0 mx-2 cursor-row-resize rounded-2xl',
|
||||
color === 'transparent' ? 'border-base-content/55 border' : '',
|
||||
)}
|
||||
style={{
|
||||
top: `${currentPosition}%`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: baseColor,
|
||||
opacity: opacity,
|
||||
height: `${rulerSize}px`,
|
||||
transform: 'translateY(-50%)',
|
||||
...(color === 'transparent'
|
||||
? {
|
||||
backgroundColor: baseColor,
|
||||
}
|
||||
: backdropFilterStyle),
|
||||
}}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
>
|
||||
{/* extended touch area */}
|
||||
{/* Extended touch area */}
|
||||
<div className='absolute inset-x-0 -bottom-2 -top-2' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
|
||||
const _ = useTranslation();
|
||||
@@ -65,6 +66,11 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ 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<SettingsPanelPanelProp> = ({ 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<SettingsPanelPanelProp> = ({ 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<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
onCustomColorsChange={handleCustomTtsColorsChange}
|
||||
/>
|
||||
|
||||
<ReadingRulerSettings
|
||||
enabled={readingRulerEnabled}
|
||||
lines={readingRulerLines}
|
||||
opacity={readingRulerOpacity}
|
||||
color={readingRulerColor}
|
||||
onEnabledChange={setReadingRulerEnabled}
|
||||
onLinesChange={setReadingRulerLines}
|
||||
onOpacityChange={setReadingRulerOpacity}
|
||||
onColorChange={setReadingRulerColor}
|
||||
/>
|
||||
|
||||
<CodeHighlightingSettings
|
||||
codeHighlighting={codeHighlighting}
|
||||
codeLanguage={codeLanguage}
|
||||
|
||||
@@ -74,11 +74,6 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ 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<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
showProgressInfo: setShowProgressInfo,
|
||||
tapToToggleFooter: setTapToToggleFooter,
|
||||
showMarginsOnScroll: setShowMarginsOnScroll,
|
||||
readingRulerEnabled: setReadingRulerEnabled,
|
||||
readingRulerLines: setReadingRulerLines,
|
||||
readingRulerOpacity: setReadingRulerOpacity,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -391,26 +383,6 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ 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<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Reading Ruler')}</h2>
|
||||
<div className='card bg-base-100 border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Enable Reading Ruler')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={readingRulerEnabled}
|
||||
onChange={() => setReadingRulerEnabled(!readingRulerEnabled)}
|
||||
/>
|
||||
</div>
|
||||
<NumberInput
|
||||
label={_('Lines to Highlight')}
|
||||
value={readingRulerLines}
|
||||
onChange={setReadingRulerLines}
|
||||
disabled={!readingRulerEnabled}
|
||||
min={1}
|
||||
max={6}
|
||||
step={1}
|
||||
/>
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Ruler Color')}</span>
|
||||
<div className='flex gap-2'>
|
||||
<button
|
||||
disabled={!readingRulerEnabled}
|
||||
className={`btn btn-circle btn-sm bg-yellow-400 hover:bg-yellow-500 ${readingRulerColor === 'yellow' ? 'ring-base-content ring-2 ring-offset-1' : ''} ${!readingRulerEnabled ? 'btn-disabled opacity-50' : ''}`}
|
||||
onClick={() => setReadingRulerColor('yellow')}
|
||||
/>
|
||||
<button
|
||||
disabled={!readingRulerEnabled}
|
||||
className={`btn btn-circle btn-sm bg-green-400 hover:bg-green-500 ${readingRulerColor === 'green' ? 'ring-base-content ring-2 ring-offset-1' : ''} ${!readingRulerEnabled ? 'btn-disabled opacity-50' : ''}`}
|
||||
onClick={() => setReadingRulerColor('green')}
|
||||
/>
|
||||
<button
|
||||
disabled={!readingRulerEnabled}
|
||||
className={`btn btn-circle btn-sm bg-blue-400 hover:bg-blue-500 ${readingRulerColor === 'blue' ? 'ring-base-content ring-2 ring-offset-1' : ''} ${!readingRulerEnabled ? 'btn-disabled opacity-50' : ''}`}
|
||||
onClick={() => setReadingRulerColor('blue')}
|
||||
/>
|
||||
<button
|
||||
disabled={!readingRulerEnabled}
|
||||
className={`btn btn-circle btn-sm bg-violet-400 hover:bg-violet-500 ${readingRulerColor === 'violet' ? 'ring-base-content ring-2 ring-offset-1' : ''} ${!readingRulerEnabled ? 'btn-disabled opacity-50' : ''}`}
|
||||
onClick={() => setReadingRulerColor('violet')}
|
||||
/>
|
||||
<button
|
||||
disabled={!readingRulerEnabled}
|
||||
className={`btn btn-circle btn-sm bg-rose-400 hover:bg-rose-500 ${readingRulerColor === 'rose' ? 'ring-base-content ring-2 ring-offset-1' : ''} ${!readingRulerEnabled ? 'btn-disabled opacity-50' : ''}`}
|
||||
onClick={() => setReadingRulerColor('rose')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NumberInput
|
||||
label={_('Opacity')}
|
||||
value={readingRulerOpacity}
|
||||
onChange={setReadingRulerOpacity}
|
||||
disabled={!readingRulerEnabled}
|
||||
min={0.1}
|
||||
max={0.5}
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{appService?.hasOrientationLock && (
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Screen')}</h2>
|
||||
|
||||
@@ -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<ReadingRulerSettingsProps> = ({
|
||||
enabled,
|
||||
lines,
|
||||
opacity,
|
||||
color,
|
||||
onEnabledChange,
|
||||
onLinesChange,
|
||||
onOpacityChange,
|
||||
onColorChange,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Reading Ruler')}</h2>
|
||||
<div className='card bg-base-100 border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<div className='config-item'>
|
||||
<span>{_('Enable Reading Ruler')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={enabled}
|
||||
onChange={() => onEnabledChange(!enabled)}
|
||||
/>
|
||||
</div>
|
||||
<NumberInput
|
||||
label={_('Lines to Highlight')}
|
||||
value={lines}
|
||||
onChange={onLinesChange}
|
||||
disabled={!enabled}
|
||||
min={1}
|
||||
max={6}
|
||||
step={1}
|
||||
/>
|
||||
<div className='config-item'>
|
||||
<span>{_('Ruler Color')}</span>
|
||||
<div className='flex gap-2'>
|
||||
{RULER_COLORS.map(({ value, className, hoverClassName }) => (
|
||||
<button
|
||||
key={value}
|
||||
disabled={!enabled}
|
||||
className={`btn btn-circle btn-sm ${className} ${hoverClassName} ${
|
||||
color === value ? 'ring-base-content ring-2 ring-offset-1' : ''
|
||||
} ${!enabled ? 'btn-disabled opacity-50' : ''}`}
|
||||
onClick={() => onColorChange(value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<NumberInput
|
||||
label={_('Opacity')}
|
||||
value={opacity}
|
||||
onChange={onOpacityChange}
|
||||
disabled={!enabled}
|
||||
min={0.1}
|
||||
max={0.9}
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadingRulerSettings;
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
BookStyle,
|
||||
HighlightColor,
|
||||
NoteExportConfig,
|
||||
ReadingRulerColor,
|
||||
ScreenConfig,
|
||||
TranslatorConfig,
|
||||
TTSConfig,
|
||||
@@ -99,11 +100,11 @@ export const HIGHLIGHT_COLOR_HEX: Record<HighlightColor, string> = {
|
||||
violet: '#a78bfa', // violet-400
|
||||
};
|
||||
|
||||
export const READING_RULER_COLORS: Record<string, string> = {
|
||||
export const READING_RULER_COLORS: Record<ReadingRulerColor, string> = {
|
||||
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 = {
|
||||
|
||||
@@ -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<BookFormat> = 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 {
|
||||
|
||||
Reference in New Issue
Block a user