forked from akai/readest
feat: add named highlight colors with sync and picker ux fixes (#3741)
* fix: add highlight color label fields
* fix: add default highlight label sync fields
* fix: add highlight prefs sync helpers
* fix: add highlight color name inputs
* fix: persist and sync highlight color names
* fix: add highlight color label helpers
* fix: add long press highlight label preview
* fix: pull highlight color prefs during library sync
* test: cover highlight color label helpers
* fix: widen highlight color name inputs
* fix: show highlight names on hover and touch hold
* fix: prevent highlight name input overlap
* fix: improve highlight name input responsiveness
* fix: support drag scrolling for highlight colors
* fix: batch custom color and label updates
* fix: serialize highlight prefs saves
* fix: align color strip drag and restore color clicks
* fix: translate default highlight color labels
* refactor: remove highlight preference sync wiring
* fix: align highlight option i18n with existing pattern
* fix: remove redundant english highlight keys
* fix: support raw and normalized highlight label keys
* fix: use underscore translator in highlight options
* fix: translate custom highlight color labels in editor
* refactor: simplify highlight settings persistence
* fix: maintianer review
* refactor: simplify highlight prefs save and clean up editor
- Drop the skipUserColors/skipLabels options from handleHighlightPrefsChange
and always persist both arrays; the flags only masked a no-op caller.
- Type handleHighlightColorsChange with Record<HighlightColor, string>
instead of typeof so the signature reads clearly.
- Remove the always-true `|| true` guard around the custom colors section.
- Stop wrapping user-typed custom color labels with _(), matching the
built-in color inputs and keeping the input value equal to what the
user typed.
* refactor: couple highlight labels to their colors
Replace the parallel `highlightColorLabels: Record<string, string>` map
with label storage that lives next to each color. This removes the hex
key normalization layer and its whole class of orphan/case-drift bugs.
Data model:
userHighlightColors: string[] -> UserHighlightColor[]
({ hex, label? })
highlightColorLabels: Record<string, string> -> (removed)
defaultHighlightLabels:
Partial<Record<DefaultHighlightColor, string>>
A `migrateHighlightColorPrefs` helper runs during `loadSettings` and
handles both the shipped `string[]` layout and the draft-build
`highlightColorLabels` layout: hex-keyed labels attach to matching user
colors, name-keyed labels move into `defaultHighlightLabels`. Malformed
entries are dropped.
Editor:
- Label inputs commit on blur (Enter also commits), so typing a long
label no longer fires `setSettings`/`saveSettings` on every
keystroke. A small `LabelInput` component owns the draft state and
syncs if the prop changes externally.
- Three explicit callbacks (`onCustomHighlightColorsChange`,
`onUserHighlightColorsChange`, `onDefaultHighlightLabelsChange`)
replace the previous single callback with opaque skip flags.
Picker:
- Extracted a reusable `useDragScroll` hook (mouse only, 6px
threshold, 120ms click suppression) from the inline state machine
in `HighlightOptions`. The picker drops to ~280 lines.
- Long-press preview stays touch/pen only. It now reads labels via
`getHighlightColorLabel` (which returns `undefined` when no user
label is set), letting the component layer decide whether to fall
back to a translated default name.
i18n:
- Default color names ('red' | 'yellow' | 'green' | 'blue' | 'violet')
are registered once at module scope via `stubTranslation` and
translated at the picker layer through `useTranslation`. User-typed
labels are never run through `_()`, so what the user types is what
the editor shows.
Tests:
- `annotator-util.test.ts`: rewritten around the new helper contract
(user label -> undefined fallback) and case-insensitive hex matching.
- `settings-highlight-migration.test.ts`: new, covers legacy
`string[]`, already-migrated entries, malformed hex filtering, and
the two draft-label fold paths.
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
This commit is contained in:
@@ -157,6 +157,7 @@ export const DEFAULT_READSETTINGS: ReadSettings = {
|
||||
},
|
||||
customHighlightColors: HIGHLIGHT_COLOR_HEX,
|
||||
userHighlightColors: [],
|
||||
defaultHighlightLabels: {},
|
||||
customTtsHighlightColors: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FileSystem } from '@/types/system';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { ViewSettings } from '@/types/book';
|
||||
import { ReadSettings, SystemSettings } from '@/types/settings';
|
||||
import { DEFAULT_HIGHLIGHT_COLORS, UserHighlightColor, ViewSettings } from '@/types/book';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
DEFAULT_BOOK_LAYOUT,
|
||||
@@ -50,6 +50,55 @@ export function getDefaultViewSettings(ctx: Context): ViewSettings {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize highlight color prefs into the current shape:
|
||||
* - `userHighlightColors` becomes `UserHighlightColor[]`. Legacy `string[]` entries
|
||||
* are lifted into `{ hex }`. A legacy `highlightColorLabels` map (shipped only in
|
||||
* draft builds of this feature) is folded in: hex entries attach to matching user
|
||||
* colors, named entries move into `defaultHighlightLabels`.
|
||||
*/
|
||||
export function migrateHighlightColorPrefs(read: ReadSettings): void {
|
||||
const rawUser = (read.userHighlightColors ?? []) as unknown[];
|
||||
const userColors: UserHighlightColor[] = rawUser
|
||||
.map((entry) => {
|
||||
if (typeof entry === 'string') {
|
||||
return { hex: entry.trim().toLowerCase() };
|
||||
}
|
||||
if (entry && typeof entry === 'object' && 'hex' in entry) {
|
||||
const { hex, label } = entry as UserHighlightColor;
|
||||
return {
|
||||
hex: typeof hex === 'string' ? hex.trim().toLowerCase() : '',
|
||||
...(label?.trim() ? { label: label.trim() } : {}),
|
||||
};
|
||||
}
|
||||
return { hex: '' };
|
||||
})
|
||||
.filter((entry) => entry.hex.startsWith('#'));
|
||||
|
||||
read.defaultHighlightLabels = { ...(read.defaultHighlightLabels ?? {}) };
|
||||
|
||||
const legacyLabels = (read as unknown as { highlightColorLabels?: unknown }).highlightColorLabels;
|
||||
if (legacyLabels && typeof legacyLabels === 'object') {
|
||||
const labels = legacyLabels as Record<string, unknown>;
|
||||
for (const name of DEFAULT_HIGHLIGHT_COLORS) {
|
||||
const value = labels[name];
|
||||
if (typeof value === 'string' && value.trim() && !read.defaultHighlightLabels[name]) {
|
||||
read.defaultHighlightLabels[name] = value.trim();
|
||||
}
|
||||
}
|
||||
for (const entry of userColors) {
|
||||
if (entry.label) continue;
|
||||
const value = labels[entry.hex];
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
entry.label = value.trim();
|
||||
}
|
||||
}
|
||||
delete (read as unknown as { highlightColorLabels?: unknown }).highlightColorLabels;
|
||||
}
|
||||
|
||||
read.userHighlightColors = userColors;
|
||||
}
|
||||
|
||||
export async function loadSettings(ctx: Context): Promise<SystemSettings> {
|
||||
const defaultSettings: SystemSettings = {
|
||||
...DEFAULT_SYSTEM_SETTINGS,
|
||||
@@ -85,6 +134,7 @@ export async function loadSettings(ctx: Context): Promise<SystemSettings> {
|
||||
...(ctx.isMobile ? DEFAULT_MOBILE_READSETTINGS : {}),
|
||||
...settings.globalReadSettings,
|
||||
};
|
||||
migrateHighlightColorPrefs(settings.globalReadSettings);
|
||||
settings.globalViewSettings = {
|
||||
...getDefaultViewSettings(ctx),
|
||||
...settings.globalViewSettings,
|
||||
|
||||
Reference in New Issue
Block a user