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:
Mohammed Efaz
2026-01-25 12:52:29 +06:00
committed by GitHub
parent aba9e87fc1
commit c31ece6742
7 changed files with 172 additions and 44 deletions
@@ -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) => (
<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] : 'transparent',
backgroundColor:
selectedColor !== color ? customColors[color] || color : 'transparent',
}}
className='rounded-full p-0'
>
{selectedColor === color && (
<FaCheckCircle
size={size16}
style={{ fill: isEink ? einkFgColor : customColors[color] }}
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,28 +1,55 @@
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='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) => {
@@ -36,7 +63,7 @@ const HighlightColorsEditor: React.FC<HighlightColorsEditorProps> = ({
/>
<ColorInput
label=''
value={customHighlightColors[color]}
value={customHighlightColors[color]!}
compact={true}
pickerPosition={position}
onChange={(value: string) => handleColorChange(color, value)}
@@ -46,6 +73,67 @@ const HighlightColorsEditor: React.FC<HighlightColorsEditorProps> = ({
},
)}
</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-6 w-6 rounded-full border-2 shadow-sm'
style={{ backgroundColor: newColor }}
/>
<ColorInput
label=''
value={newColor}
compact={true}
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>
</div>
);
@@ -125,6 +125,7 @@ export const DEFAULT_READSETTINGS: ReadSettings = {
squiggly: 'blue',
},
customHighlightColors: HIGHLIGHT_COLOR_HEX,
userHighlightColors: [],
customTtsHighlightColors: [],
};
+2 -1
View File
@@ -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']);
+1
View File
@@ -34,6 +34,7 @@ export interface ReadSettings {
highlightStyle: HighlightStyle;
highlightStyles: Record<HighlightStyle, HighlightColor>;
customHighlightColors: Record<HighlightColor, string>;
userHighlightColors: string[];
customTtsHighlightColors: string[];
customThemes: CustomTheme[];
}