Back to Readest

View Settings

apps/readest-app/docs/view-settings.md

0.10.68.4 KB
Original Source

Adding a Config to ViewSettings

ViewSettings is the per-book view state (layout, fonts, colors, TTS, etc.) composed from several sub-interfaces defined in src/types/book.ts. A matching globalViewSettings lives on SystemSettings and acts as the default for every book. The per-book value is derived by merging the global defaults with any overrides stored on the book's BookConfig.

This doc covers how to plumb a new config through the three layers:

  1. Typessrc/types/book.ts
  2. Defaultssrc/services/constants.ts and src/services/settingsService.ts
  3. Read/write — components via saveViewSettings from src/helpers/settings.ts

Pick a Pattern

Pattern A — add a field to an existing sub-interface. Use when the new option belongs to an existing bundle (BookLayout, BookStyle, BookFont, ViewConfig, TTSConfig, etc.).

Pattern B — introduce a new sub-interface. Use when several related fields cluster together, or when a single field is semantically its own concept (e.g. ParagraphModeConfig, ViewSettingsConfig). Then extend ViewSettings with it.

Both patterns follow the same three-layer flow. The only difference is whether you reuse an existing DEFAULT_* constant or add a new one.

Step 1 — Declare the Type

Pattern A — add a required field to the sub-interface that owns this concern:

ts
// src/types/book.ts
export interface ViewConfig {
  // ...existing fields
  myNewToggle: boolean;
}

Pattern B — define a new interface and extend ViewSettings:

ts
// src/types/book.ts
export interface ViewSettingsConfig {
  isGlobal: boolean;
}

export interface ViewSettings
  extends
    BookLayout,
    BookStyle,
    // ...other bundles
    ViewSettingsConfig {}

Fields should be required, not optional. Optional fields make downstream code defensive. Provide a sensible default in Step 2 instead.

Step 2 — Provide a Default

Every field in ViewSettings must have a default, otherwise getDefaultViewSettings() produces an incomplete object.

Pattern A — add the value to the existing DEFAULT_* constant:

ts
// src/services/constants.ts
export const DEFAULT_VIEW_CONFIG: ViewConfig = {
  // ...existing defaults
  myNewToggle: false,
};

Pattern B — add a DEFAULT_*_CONFIG constant for your new bundle, then register it in getDefaultViewSettings:

ts
// src/services/constants.ts
export const DEFAULT_VIEW_SETTINGS_CONFIG: ViewSettingsConfig = {
  isGlobal: true,
};
ts
// src/services/settingsService.ts
export function getDefaultViewSettings(ctx: Context): ViewSettings {
  return {
    ...DEFAULT_BOOK_LAYOUT,
    ...DEFAULT_BOOK_STYLE,
    // ...other bundles
    ...DEFAULT_VIEW_SETTINGS_CONFIG,
    // platform overrides go last so they win
    ...(ctx.isMobile ? DEFAULT_MOBILE_VIEW_SETTINGS : {}),
    ...(ctx.isEink ? DEFAULT_EINK_VIEW_SETTINGS : {}),
    ...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}),
  };
}

Platform Overrides

To tweak the default on mobile, e-ink, or CJK locales, add the field to the matching Partial<ViewSettings> constant (DEFAULT_MOBILE_VIEW_SETTINGS, DEFAULT_EINK_VIEW_SETTINGS, DEFAULT_CJK_VIEW_SETTINGS). These are spread after the base defaults in getDefaultViewSettings, so they override them.

Migration

Old settings.json files on disk won't have your new field. loadSettings merges the stored blob over fresh defaults:

ts
settings.globalViewSettings = {
  ...getDefaultViewSettings(ctx),
  ...settings.globalViewSettings,
};

So existing users pick up your default automatically — no explicit migration is needed for adding a field. Only bump SYSTEM_SETTINGS_VERSION if you are reshaping existing data.

Step 3 — Read and Write from Components

Read the current value by preferring the per-book settings, falling back to the global:

tsx
const { settings } = useSettingsStore();
const { getViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings;

Write via saveViewSettings — never mutate the store directly. The helper handles the global-vs-per-book routing, persists to disk, and re-applies styles when needed.

tsx
import { saveViewSettings } from '@/helpers/settings';

const [myNewToggle, setMyNewToggle] = useState(viewSettings.myNewToggle);

useEffect(() => {
  saveViewSettings(envConfig, bookKey, 'myNewToggle', myNewToggle);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [myNewToggle]);

The useEffect-on-local-state pattern is the established convention in LayoutPanel, ControlPanel, ColorPanel, etc. It keeps the UI responsive and batches store updates until the user stops interacting.

Signature

ts
saveViewSettings<K extends keyof ViewSettings>(
  envConfig,
  bookKey,
  key: K,
  value: ViewSettings[K],
  skipGlobal = false,     // true → only update this book's settings
  applyStyles = true,     // false → don't re-run style recomputation
)

Global vs. per-book routing. saveViewSettings inspects viewSettings.isGlobal on the target book. When true (the default), it writes to globalViewSettings, loops through every open book, and saves to disk. When false, it writes only to the one book's config.

Skip global. Pass skipGlobal=true when the setting is meta — i.e. it describes the settings system itself, not book content. The canonical case is toggling isGlobal from DialogMenu: you want the scope flag to live on the specific book without propagating it to every other book.

tsx
saveViewSettings(envConfig, bookKey, 'isGlobal', !isSettingsGlobal, true, false);

Skip styles. Pass applyStyles=false for options that don't affect CSS rendering (toggles, flags, metadata). This avoids an unnecessary renderer.setStyles call.

Step 4 — Support Reset

If your field should be resettable from the panel menu, register a setter in the panel's handleReset via useResetViewSettings:

tsx
const resetToDefaults = useResetViewSettings();

const handleReset = () => {
  resetToDefaults({
    myNewToggle: setMyNewToggle,
    // ...other setters
  });
};

The hook resolves the default by reading from getDefaultViewSettings(ctx) and calls each provided setter with that value, which then fires your useEffect and persists the change.

Step 5 — Register in the Command Palette

If your setting has a visible row in a panel, register it in the matching *PanelItems array in src/services/commandRegistry.ts. This wires it into the command-palette fuzzy search so users can jump straight to it.

ts
// src/services/commandRegistry.ts
const layoutPanelItems = [
  // ...existing entries
  {
    id: 'settings.layout.myNewToggle',
    labelKey: _('My New Toggle'),
    keywords: ['search', 'terms', 'for', 'discoverability'],
    section: 'Paragraph',
  },
];
  • id must match the data-setting-id attribute on the panel row. The palette uses it to scroll/highlight the target control.
  • labelKey uses stubTranslation (imported as _) so the extractor picks it up — the same string that appears in the panel.
  • keywords broadens fuzzy-search hits beyond the label; include synonyms, related jargon, and the panel section name.
  • section groups the entry in the palette results (matches the panel's sub-header: Layout, Paragraph, Page, Header & Footer, etc.).

Skip this step only for settings that don't surface as a user-visible row (hidden toggles, flags used by other settings).

Don'ts

  • Don't make the field optional just to skip providing a default. Add a default in Step 2 instead.
  • Don't mutate settings.globalViewSettings directly in a component — saveViewSettings already handles global propagation when isGlobal is true.
  • Don't bump SYSTEM_SETTINGS_VERSION for a plain additive field. The load-time merge handles it.

Minimal Checklist

  • Field or new interface added in src/types/book.ts
  • Default value in src/services/constants.ts
  • New DEFAULT_*_CONFIG spread into getDefaultViewSettings (Pattern B only)
  • Optional mobile/eink/CJK override in the matching Partial<ViewSettings> constant
  • Read via getViewSettings(bookKey) || settings.globalViewSettings
  • Write via saveViewSettings(envConfig, bookKey, 'key', value)
  • Reset setter wired into useResetViewSettings if the panel has a reset menu
  • Command-palette entry added to the matching *PanelItems array in src/services/commandRegistry.ts, with an id that matches the panel row's data-setting-id