forked from akai/readest
feat: more highlight colours (#3062)
* feat(highlight): extend types and constants for custom hex colours * feat(highlight): update colour to support hex strings * refactor(annotator): update component to accept hex colours * feat(highlight): add colour picker with max 4 custom colors * feat(settings): add custom colour editor with limit * refactor: custom highlight colors can only be modified in settings --------- Co-authored-by: Huang Xin <chrox.huang@gmail.com>
This commit is contained in:
@@ -9,7 +9,15 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { saveSysSettings } from '@/helpers/settings';
|
||||
|
||||
const styles = ['highlight', 'underline', 'squiggly'] as HighlightStyle[];
|
||||
const colors = ['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[];
|
||||
const defaultColors = ['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[];
|
||||
|
||||
const getColorHex = (
|
||||
customColors: Record<HighlightColor, string>,
|
||||
color: HighlightColor,
|
||||
): string => {
|
||||
if (color.startsWith('#')) return color;
|
||||
return customColors[color as HighlightColor] ?? color;
|
||||
};
|
||||
|
||||
interface HighlightOptionsProps {
|
||||
isVertical: boolean;
|
||||
@@ -41,6 +49,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
const einkBgColor = isDarkMode ? '#000000' : '#ffffff';
|
||||
const einkFgColor = isDarkMode ? '#ffffff' : '#000000';
|
||||
const customColors = globalReadSettings.customHighlightColors;
|
||||
const userColors = globalReadSettings.userHighlightColors ?? [];
|
||||
const [selectedStyle, setSelectedStyle] = useState<HighlightStyle>(_selectedStyle);
|
||||
const [selectedColor, setSelectedColor] = useState<HighlightColor>(_selectedColor);
|
||||
const size16 = useResponsiveSize(16);
|
||||
@@ -55,6 +64,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
setSelectedColor(globalReadSettings.highlightStyles[style]);
|
||||
onHandleHighlight(true);
|
||||
};
|
||||
|
||||
const handleSelectColor = (color: HighlightColor) => {
|
||||
const newGlobalReadSettings = {
|
||||
...globalReadSettings,
|
||||
@@ -65,10 +75,11 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
setSelectedColor(color);
|
||||
onHandleHighlight(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'highlight-options absolute flex items-center justify-between',
|
||||
'highlight-options absolute flex items-center justify-between gap-4',
|
||||
isVertical ? 'flex-col' : 'flex-row',
|
||||
)}
|
||||
style={{
|
||||
@@ -106,7 +117,9 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
height: size16,
|
||||
...(style === 'highlight' &&
|
||||
selectedStyle === 'highlight' && {
|
||||
backgroundColor: isEink ? einkFgColor : customColors[selectedColor],
|
||||
backgroundColor: isEink
|
||||
? einkFgColor
|
||||
: getColorHex(customColors, selectedColor),
|
||||
color: isEink ? einkBgColor : '#d1d5db',
|
||||
paddingTop: '2px',
|
||||
}),
|
||||
@@ -123,7 +136,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
selectedStyle === style
|
||||
? isEink
|
||||
? einkFgColor
|
||||
: customColors[selectedColor]
|
||||
: getColorHex(customColors, selectedColor)
|
||||
: '#d1d5db',
|
||||
...(style === 'squiggly' && { textDecorationStyle: 'wavy' }),
|
||||
}),
|
||||
@@ -138,31 +151,39 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'not-eink:bg-gray-700 eink-bordered flex items-center justify-center gap-2 rounded-3xl',
|
||||
isVertical ? 'flex-col py-2' : 'flex-row px-2',
|
||||
'not-eink:bg-gray-700 eink-bordered flex items-center gap-2 rounded-3xl',
|
||||
isVertical ? 'flex-col overflow-y-auto py-2' : 'flex-row overflow-x-auto px-2',
|
||||
)}
|
||||
style={isVertical ? { width: size28 } : { height: size28 }}
|
||||
style={{
|
||||
...(isVertical ? { width: size28 } : { height: size28 }),
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none',
|
||||
}}
|
||||
>
|
||||
{colors
|
||||
{defaultColors
|
||||
.concat(userColors)
|
||||
.filter((c) => (isEink ? selectedColor === c : true))
|
||||
.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
onClick={() => handleSelectColor(color)}
|
||||
style={{
|
||||
width: size16,
|
||||
height: size16,
|
||||
backgroundColor: selectedColor !== color ? customColors[color] : 'transparent',
|
||||
}}
|
||||
className='rounded-full p-0'
|
||||
>
|
||||
{selectedColor === color && (
|
||||
<FaCheckCircle
|
||||
size={size16}
|
||||
style={{ fill: isEink ? einkFgColor : customColors[color] }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<div key={color} className='flex items-center justify-center'>
|
||||
<button
|
||||
key={color}
|
||||
onClick={() => handleSelectColor(color)}
|
||||
style={{
|
||||
width: size16,
|
||||
height: size16,
|
||||
backgroundColor:
|
||||
selectedColor !== color ? customColors[color] || color : 'transparent',
|
||||
}}
|
||||
className='rounded-full p-0'
|
||||
>
|
||||
{selectedColor === color && (
|
||||
<FaCheckCircle
|
||||
size={size16}
|
||||
style={{ fill: isEink ? einkFgColor : customColors[color] || color }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,12 @@ import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
|
||||
import { HighlightColor } from '@/types/book';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
|
||||
export const getHighlightColorHex = (settings: SystemSettings, color?: HighlightColor) => {
|
||||
if (!color) return color;
|
||||
|
||||
export const getHighlightColorHex = (
|
||||
settings: SystemSettings,
|
||||
color?: HighlightColor,
|
||||
): string | undefined => {
|
||||
if (!color) return undefined;
|
||||
if (color.startsWith('#')) return color;
|
||||
const customColors = settings.globalReadSettings.customHighlightColors;
|
||||
return customColors?.[color] ?? HIGHLIGHT_COLOR_HEX[color];
|
||||
};
|
||||
|
||||
@@ -61,6 +61,9 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
const [customTtsHighlightColors, setCustomTtsHighlightColors] = useState(
|
||||
settings.globalReadSettings.customTtsHighlightColors || [],
|
||||
);
|
||||
const [userHighlightColors, setUserHighlightColors] = useState(
|
||||
settings.globalReadSettings.userHighlightColors || [],
|
||||
);
|
||||
|
||||
const {
|
||||
textures: customTextures,
|
||||
@@ -87,6 +90,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
setBackgroundOpacity(0.6);
|
||||
setBackgroundSize('cover');
|
||||
setCustomHighlightColors(HIGHLIGHT_COLOR_HEX);
|
||||
setUserHighlightColors([]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -253,6 +257,13 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
saveSettings(envConfig, settings);
|
||||
};
|
||||
|
||||
const handleUserHighlightColorsChange = (colors: string[]) => {
|
||||
setUserHighlightColors(colors);
|
||||
settings.globalReadSettings.userHighlightColors = colors;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
{showCustomThemeEditor ? (
|
||||
@@ -311,7 +322,9 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
|
||||
<HighlightColorsEditor
|
||||
customHighlightColors={customHighlightColors}
|
||||
userHighlightColors={userHighlightColors}
|
||||
onChange={handleHighlightColorsChange}
|
||||
onUserColorsChange={handleUserHighlightColorsChange}
|
||||
/>
|
||||
|
||||
<TTSHighlightStyleEditor
|
||||
|
||||
@@ -1,49 +1,137 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { MdClose } from 'react-icons/md';
|
||||
import { HighlightColor } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import ColorInput from './ColorInput';
|
||||
|
||||
const MAX_USER_HIGHLIGHT_COLORS = 10;
|
||||
|
||||
interface HighlightColorsEditorProps {
|
||||
customHighlightColors: Record<HighlightColor, string>;
|
||||
userHighlightColors: string[];
|
||||
onChange: (colors: Record<HighlightColor, string>) => void;
|
||||
onUserColorsChange: (colors: string[]) => void;
|
||||
}
|
||||
|
||||
const HighlightColorsEditor: React.FC<HighlightColorsEditorProps> = ({
|
||||
customHighlightColors,
|
||||
userHighlightColors,
|
||||
onChange,
|
||||
onUserColorsChange,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const [newColor, setNewColor] = useState('#808080');
|
||||
|
||||
const handleColorChange = (color: HighlightColor, value: string) => {
|
||||
const updated = { ...customHighlightColors, [color]: value };
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
const handleAddUserColor = () => {
|
||||
if (userHighlightColors.length >= MAX_USER_HIGHLIGHT_COLORS) return;
|
||||
if (!userHighlightColors.includes(newColor)) {
|
||||
const updatedColors = [...userHighlightColors, newColor];
|
||||
onUserColorsChange(updatedColors);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUserColor = (hex: string) => {
|
||||
const updatedColors = userHighlightColors.filter((c) => c !== hex);
|
||||
onUserColorsChange(updatedColors);
|
||||
};
|
||||
|
||||
const handleUserColorChange = (oldHex: string, newHex: string) => {
|
||||
const updatedColors = userHighlightColors.map((c) => (c === oldHex ? newHex : c));
|
||||
onUserColorsChange(updatedColors);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className='mb-2 font-medium'>{_('Highlight Colors')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 overflow-visible border p-4 shadow'>
|
||||
<div className='grid grid-cols-3 gap-3 sm:grid-cols-5'>
|
||||
{(['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[]).map(
|
||||
(color, index, array) => {
|
||||
const position =
|
||||
index === 0 ? 'left' : index === array.length - 1 ? 'right' : 'center';
|
||||
return (
|
||||
<div key={color} className='flex flex-col items-center gap-2'>
|
||||
<div className='space-y-4'>
|
||||
<div className='grid grid-cols-3 gap-3 sm:grid-cols-5'>
|
||||
{(['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[]).map(
|
||||
(color, index, array) => {
|
||||
const position =
|
||||
index === 0 ? 'left' : index === array.length - 1 ? 'right' : 'center';
|
||||
return (
|
||||
<div key={color} className='flex flex-col items-center gap-2'>
|
||||
<div
|
||||
className='border-base-300 h-8 w-8 rounded-full border-2 shadow-sm'
|
||||
style={{ backgroundColor: customHighlightColors[color] }}
|
||||
/>
|
||||
<ColorInput
|
||||
label=''
|
||||
value={customHighlightColors[color]!}
|
||||
compact={true}
|
||||
pickerPosition={position}
|
||||
onChange={(value: string) => handleColorChange(color, value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(userHighlightColors.length > 0 || true) && (
|
||||
<div className='border-base-200 border-t pt-4'>
|
||||
<div className='mb-3 flex items-center justify-between'>
|
||||
<span className='text-sm font-medium'>
|
||||
{_('Custom Colors')} ({userHighlightColors.length}/{MAX_USER_HIGHLIGHT_COLORS})
|
||||
</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div
|
||||
className='border-base-300 h-8 w-8 rounded-full border-2 shadow-sm'
|
||||
style={{ backgroundColor: customHighlightColors[color] }}
|
||||
className='border-base-300 h-6 w-6 rounded-full border-2 shadow-sm'
|
||||
style={{ backgroundColor: newColor }}
|
||||
/>
|
||||
<ColorInput
|
||||
label=''
|
||||
value={customHighlightColors[color]}
|
||||
value={newColor}
|
||||
compact={true}
|
||||
pickerPosition={position}
|
||||
onChange={(value: string) => handleColorChange(color, value)}
|
||||
pickerPosition='right'
|
||||
onChange={setNewColor}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddUserColor}
|
||||
disabled={
|
||||
userHighlightColors.includes(newColor) ||
|
||||
userHighlightColors.length >= MAX_USER_HIGHLIGHT_COLORS
|
||||
}
|
||||
className='btn btn-ghost btn-sm gap-1 bg-transparent disabled:bg-transparent disabled:opacity-40'
|
||||
>
|
||||
<span className='text-xs'>{_('Add')}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
</div>
|
||||
|
||||
{userHighlightColors.length > 0 && (
|
||||
<div className='grid grid-cols-3 gap-3 sm:grid-cols-5'>
|
||||
{userHighlightColors.map((hex, index) => (
|
||||
<div key={hex} className='group relative flex flex-col items-center gap-2'>
|
||||
<div
|
||||
className='border-base-300 h-8 w-8 rounded-full border-2 shadow-sm'
|
||||
style={{ backgroundColor: hex }}
|
||||
/>
|
||||
<ColorInput
|
||||
label=''
|
||||
value={hex}
|
||||
compact={true}
|
||||
pickerPosition={index === 0 ? 'left' : 'center'}
|
||||
onChange={(value: string) => handleUserColorChange(hex, value)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleDeleteUserColor(hex)}
|
||||
className='absolute -right-1 -top-1 rounded-full bg-red-500 p-0.5 text-white opacity-0 transition-opacity hover:opacity-100 group-hover:opacity-100'
|
||||
title={_('Delete')}
|
||||
>
|
||||
<MdClose size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -125,6 +125,7 @@ export const DEFAULT_READSETTINGS: ReadSettings = {
|
||||
squiggly: 'blue',
|
||||
},
|
||||
customHighlightColors: HIGHLIGHT_COLOR_HEX,
|
||||
userHighlightColors: [],
|
||||
customTtsHighlightColors: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ export type BookFormat =
|
||||
| 'MD';
|
||||
export type BookNoteType = 'bookmark' | 'annotation' | 'excerpt';
|
||||
export type HighlightStyle = 'highlight' | 'underline' | 'squiggly';
|
||||
export type HighlightColor = 'red' | 'yellow' | 'green' | 'blue' | 'violet';
|
||||
// Predefined highlight colors, can be extended with custom hex colors
|
||||
export type HighlightColor = 'red' | 'yellow' | 'green' | 'blue' | 'violet' | string;
|
||||
|
||||
export const FIXED_LAYOUT_FORMATS: Set<BookFormat> = new Set(['PDF', 'CBZ']);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface ReadSettings {
|
||||
highlightStyle: HighlightStyle;
|
||||
highlightStyles: Record<HighlightStyle, HighlightColor>;
|
||||
customHighlightColors: Record<HighlightColor, string>;
|
||||
userHighlightColors: string[];
|
||||
customTtsHighlightColors: string[];
|
||||
customThemes: CustomTheme[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user