feat(memoir): persist chapter reading prefs globally

Share font size, font family, and background across all memoir chapters via MemoirReadingSettingsProvider and SecureStore (same app-settings pattern). Add parse/merge helpers and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-15 17:23:02 +08:00
parent c4d2a38b09
commit 6f41574bda
7 changed files with 322 additions and 24 deletions

View File

@@ -10,11 +10,18 @@ import { supportedLanguages, type AppLanguage } from '@/i18n/resources';
import { THEME_NAMES, type ThemeName } from '@/constants/theme-bridge';
import {
mergeMemoirReadingPreferences,
parseMemoirReadingPreferences,
type MemoirReadingPreferences,
} from './memoir-reading-settings';
const KEY_LANGUAGE = 'app_settings_language';
const KEY_LARGE_TEXT = 'app_settings_large_text';
const KEY_DARK_MODE = 'app_settings_dark_mode';
const KEY_THEME_NAME = 'app_settings_theme_name';
const KEY_TTS_SPEAK_DEFAULT = 'app_settings_tts_speak_default';
const KEY_MEMOIR_READING = 'app_settings_memoir_reading';
const webFallback: Record<string, string> = {};
@@ -95,5 +102,20 @@ export async function setTtsSpeakDefault(value: boolean): Promise<void> {
await setStored(KEY_TTS_SPEAK_DEFAULT, value ? 'true' : 'false');
}
export async function getMemoirReadingPreferences(): Promise<MemoirReadingPreferences> {
const raw = await getStored(KEY_MEMOIR_READING);
return parseMemoirReadingPreferences(raw);
}
export async function setMemoirReadingPreferences(
patch: Partial<MemoirReadingPreferences>,
): Promise<void> {
const raw = await getStored(KEY_MEMOIR_READING);
const current = parseMemoirReadingPreferences(raw);
const next = mergeMemoirReadingPreferences(current, patch);
await setStored(KEY_MEMOIR_READING, JSON.stringify(next));
}
export { supportedLanguages, THEME_NAMES };
export type { AppLanguage, ThemeName };
export type { MemoirReadingPreferences } from './memoir-reading-settings';

View File

@@ -0,0 +1,70 @@
export type MemoirReadingFontSize = 'small' | 'default' | 'large';
export type MemoirReadingFontFamily = 'serif' | 'sans';
export type MemoirReadingBackground = 'white' | 'sepia';
export interface MemoirReadingPreferences {
fontSize: MemoirReadingFontSize;
fontFamily: MemoirReadingFontFamily;
background: MemoirReadingBackground;
}
export const DEFAULT_MEMOIR_READING_PREFERENCES: MemoirReadingPreferences = {
fontSize: 'default',
fontFamily: 'serif',
background: 'white',
};
const FONT_SIZES: MemoirReadingFontSize[] = ['small', 'default', 'large'];
const FONT_FAMS: MemoirReadingFontFamily[] = ['serif', 'sans'];
const BACKGROUNDS: MemoirReadingBackground[] = ['white', 'sepia'];
function isFontSize(v: unknown): v is MemoirReadingFontSize {
return typeof v === 'string' && FONT_SIZES.includes(v as MemoirReadingFontSize);
}
function isFontFamily(v: unknown): v is MemoirReadingFontFamily {
return typeof v === 'string' && FONT_FAMS.includes(v as MemoirReadingFontFamily);
}
function isBackground(v: unknown): v is MemoirReadingBackground {
return typeof v === 'string' && BACKGROUNDS.includes(v as MemoirReadingBackground);
}
export function parseMemoirReadingPreferences(
raw: string | null,
): MemoirReadingPreferences {
if (raw == null || raw.trim() === '') {
return { ...DEFAULT_MEMOIR_READING_PREFERENCES };
}
try {
const parsed: unknown = JSON.parse(raw);
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
return { ...DEFAULT_MEMOIR_READING_PREFERENCES };
}
const o = parsed as Record<string, unknown>;
const d = DEFAULT_MEMOIR_READING_PREFERENCES;
return {
fontSize: isFontSize(o.fontSize) ? o.fontSize : d.fontSize,
fontFamily: isFontFamily(o.fontFamily) ? o.fontFamily : d.fontFamily,
background: isBackground(o.background) ? o.background : d.background,
};
} catch {
return { ...DEFAULT_MEMOIR_READING_PREFERENCES };
}
}
export function mergeMemoirReadingPreferences(
base: MemoirReadingPreferences,
patch: Partial<MemoirReadingPreferences>,
): MemoirReadingPreferences {
return {
fontSize:
patch.fontSize !== undefined ? patch.fontSize : base.fontSize,
fontFamily:
patch.fontFamily !== undefined ? patch.fontFamily : base.fontFamily,
background:
patch.background !== undefined ? patch.background : base.background,
};
}