From 7da5f8321311ffce2e0b826b58031e789da9cc7e Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Wed, 24 Jun 2026 21:48:03 +0800 Subject: [PATCH] fix(reader): make annotation toolbar customization apply to all books (#4760) The customized annotation toolbar only took effect in the book where it was changed, instead of applying to every book. Root cause: serializeConfig decided which per-book view settings to persist as overrides using a reference check (globalViewSettings[key] !== value). It deep-clones the config first, so array-valued settings like annotationToolbarItems are always a fresh reference and were stored as a per-book override on every save (each progress autosave). On reopen the per-book override shadowed the global value, so a global toolbar change never reached already-opened books. Compare view-setting values by content, not reference, so array/object settings equal to the global value are no longer persisted per-book. This also fixes the same latent issue for paragraphMode, proofreadRules, ttsHighlightOptions and noteExportConfig. Co-authored-by: Claude Opus 4.8 (1M context) --- .../src/__tests__/utils/serializer.test.ts | 37 +++++++++++++++++++ apps/readest-app/src/utils/serializer.ts | 11 +++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/apps/readest-app/src/__tests__/utils/serializer.test.ts b/apps/readest-app/src/__tests__/utils/serializer.test.ts index ca958664..4117ddb9 100644 --- a/apps/readest-app/src/__tests__/utils/serializer.test.ts +++ b/apps/readest-app/src/__tests__/utils/serializer.test.ts @@ -108,6 +108,43 @@ describe('BookConfig serialization', () => { expect(tombstone.deletedAt).toBeTruthy(); }); + it('does not persist an array view setting that equals the global value', () => { + // Array/object view settings must be compared by value, not reference — + // otherwise annotationToolbarItems (an array) is stored as a per-book override on + // every save, shadowing later global changes (the customize-toolbar bug). + const global = { + ...globalViewSettings, + annotationToolbarItems: ['highlight', 'annotate', 'copy'], + } as unknown as ViewSettings; + const config: BookConfig = { + updatedAt: 1, + // Same content as global but a distinct array reference, as produced by the + // load -> merge -> serialize round-trip. + viewSettings: { + annotationToolbarItems: ['highlight', 'annotate', 'copy'], + } as Partial, + }; + + const parsed = JSON.parse(serializeConfig(config, global, defaultSearchConfig)); + + expect(parsed.viewSettings.annotationToolbarItems).toBeUndefined(); + }); + + it('persists an array view setting that differs from the global value', () => { + const global = { + ...globalViewSettings, + annotationToolbarItems: ['highlight', 'annotate', 'copy'], + } as unknown as ViewSettings; + const config: BookConfig = { + updatedAt: 1, + viewSettings: { annotationToolbarItems: ['copy'] } as Partial, + }; + + const parsed = JSON.parse(serializeConfig(config, global, defaultSearchConfig)); + + expect(parsed.viewSettings.annotationToolbarItems).toEqual(['copy']); + }); + it('does not migrate annotations when schemaVersion is already 2', () => { const config = deserializeConfig( JSON.stringify({ diff --git a/apps/readest-app/src/utils/serializer.ts b/apps/readest-app/src/utils/serializer.ts index 615f0a24..e2531fc5 100644 --- a/apps/readest-app/src/utils/serializer.ts +++ b/apps/readest-app/src/utils/serializer.ts @@ -14,6 +14,15 @@ export const serializeRawConfig = (config: Partial): string => { return JSON.stringify(stampBookConfigSchema(config)); }; +// Compare a per-book view-setting value with the global one by value, not +// reference. serializeConfig deep-clones the config first, so array/object +// settings (e.g. annotationToolbarItems) are always fresh references; a reference +// check would persist them as per-book overrides even when identical to global, +// which then shadows later global changes on reopen. Primitives short-circuit; +// the JSON compare covers arrays and plain config objects (all JSON-serializable). +const isSameViewSettingValue = (a: unknown, b: unknown): boolean => + a === b || JSON.stringify(a) === JSON.stringify(b); + export const serializeConfig = ( config: BookConfig, globalViewSettings: ViewSettings, @@ -35,7 +44,7 @@ export const serializeConfig = ( const searchConfig = (config.searchConfig ?? {}) as Partial; config.viewSettings = Object.entries(viewSettings).reduce( (acc: Partial>, [key, value]) => { - if (globalViewSettings[key as keyof ViewSettings] !== value) { + if (!isSameViewSettingValue(globalViewSettings[key as keyof ViewSettings], value)) { acc[key as keyof ViewSettings] = value; } return acc;