Replace the boolean large-text flag with three global typography tiers, defaulting new installs to the smallest tier when English is in effect while preserving legacy storage and Chinese defaults. Add a profile sub-screen to pick the tier and unit tests for storage resolution. Co-authored-by: Cursor <cursoragent@cursor.com>
187 lines
5.7 KiB
TypeScript
187 lines
5.7 KiB
TypeScript
import { getLocales } from 'expo-localization';
|
|
import { Platform } from 'react-native';
|
|
|
|
import {
|
|
deleteSecureItem,
|
|
getSecureItem,
|
|
setSecureItem,
|
|
} from '@/core/storage/secure';
|
|
|
|
import {
|
|
fallbackLanguage,
|
|
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_LARGE_TEXT_LEVEL = 'app_settings_large_text_level';
|
|
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> = {};
|
|
|
|
async function getStored(key: string): Promise<string | null> {
|
|
if (Platform.OS === 'web') return webFallback[key] ?? null;
|
|
return getSecureItem(key);
|
|
}
|
|
|
|
async function setStored(key: string, value: string): Promise<void> {
|
|
if (Platform.OS === 'web') {
|
|
webFallback[key] = value;
|
|
return;
|
|
}
|
|
await setSecureItem(key, value);
|
|
}
|
|
|
|
async function deleteStored(key: string): Promise<void> {
|
|
if (Platform.OS === 'web') {
|
|
delete webFallback[key];
|
|
return;
|
|
}
|
|
await deleteSecureItem(key);
|
|
}
|
|
|
|
export async function getAppLanguage(): Promise<AppLanguage | null> {
|
|
const v = await getStored(KEY_LANGUAGE);
|
|
if (!v) return null;
|
|
return supportedLanguages.includes(v as AppLanguage)
|
|
? (v as AppLanguage)
|
|
: null;
|
|
}
|
|
|
|
export async function setAppLanguage(lang: AppLanguage): Promise<void> {
|
|
await setStored(KEY_LANGUAGE, lang);
|
|
}
|
|
|
|
export async function clearAppLanguage(): Promise<void> {
|
|
await deleteStored(KEY_LANGUAGE);
|
|
}
|
|
|
|
export type LargeTextLevel = 0 | 1 | 2;
|
|
|
|
function parseStoredLargeTextLevel(raw: string | null): LargeTextLevel | null {
|
|
if (raw === '0' || raw === '1' || raw === '2') {
|
|
return Number(raw) as LargeTextLevel;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function deviceLanguageFromLocales(): AppLanguage {
|
|
const locale = getLocales()[0];
|
|
const tag = locale?.languageCode ?? locale?.languageTag;
|
|
if (!tag) return fallbackLanguage;
|
|
return tag.toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
}
|
|
|
|
async function effectiveAppLanguage(): Promise<AppLanguage> {
|
|
const override = await getAppLanguage();
|
|
return override ?? deviceLanguageFromLocales();
|
|
}
|
|
|
|
/** 无存储时的默认档位:英文关大字、中文沿用偏大默认 */
|
|
export function defaultLargeTextLevelForLanguage(lang: AppLanguage): LargeTextLevel {
|
|
return lang === 'en' ? 0 : 1;
|
|
}
|
|
|
|
/**
|
|
* 纯函数:由原始存储值与当前生效语言解析档位(供单测与实现共用)。
|
|
*/
|
|
export function computeLargeTextLevelFromStorage(
|
|
levelRaw: string | null,
|
|
legacyLargeTextRaw: string | null,
|
|
effectiveLanguage: AppLanguage,
|
|
): LargeTextLevel {
|
|
const parsed = parseStoredLargeTextLevel(levelRaw);
|
|
if (parsed !== null) return parsed;
|
|
if (legacyLargeTextRaw === 'true') return 1;
|
|
if (legacyLargeTextRaw === 'false') return 0;
|
|
return defaultLargeTextLevelForLanguage(effectiveLanguage);
|
|
}
|
|
|
|
export async function getLargeTextLevel(): Promise<LargeTextLevel> {
|
|
const [levelRaw, legacyRaw, lang] = await Promise.all([
|
|
getStored(KEY_LARGE_TEXT_LEVEL),
|
|
getStored(KEY_LARGE_TEXT),
|
|
effectiveAppLanguage(),
|
|
]);
|
|
const level = computeLargeTextLevelFromStorage(levelRaw, legacyRaw, lang);
|
|
|
|
const hasValidLevel = parseStoredLargeTextLevel(levelRaw) !== null;
|
|
if (hasValidLevel && legacyRaw != null && legacyRaw !== '') {
|
|
await deleteStored(KEY_LARGE_TEXT);
|
|
} else if (
|
|
!hasValidLevel &&
|
|
legacyRaw != null &&
|
|
legacyRaw !== ''
|
|
) {
|
|
await setStored(KEY_LARGE_TEXT_LEVEL, String(level));
|
|
await deleteStored(KEY_LARGE_TEXT);
|
|
}
|
|
|
|
return level;
|
|
}
|
|
|
|
export async function setLargeTextLevel(level: LargeTextLevel): Promise<void> {
|
|
await setStored(KEY_LARGE_TEXT_LEVEL, String(level));
|
|
await deleteStored(KEY_LARGE_TEXT);
|
|
}
|
|
|
|
export async function getDarkMode(): Promise<boolean> {
|
|
const v = await getStored(KEY_DARK_MODE);
|
|
return v === 'true';
|
|
}
|
|
|
|
export async function setDarkMode(value: boolean): Promise<void> {
|
|
await setStored(KEY_DARK_MODE, value ? 'true' : 'false');
|
|
}
|
|
|
|
export async function getThemeName(): Promise<ThemeName> {
|
|
const v = await getStored(KEY_THEME_NAME);
|
|
if (!v || !THEME_NAMES.includes(v as ThemeName)) return 'default';
|
|
return v as ThemeName;
|
|
}
|
|
|
|
export async function setThemeName(value: ThemeName): Promise<void> {
|
|
await setStored(KEY_THEME_NAME, value);
|
|
}
|
|
|
|
/** 会话页「Speak / 本轮朗读」开关是否默认开启(跨会话记忆) */
|
|
export async function getTtsSpeakDefault(): Promise<boolean> {
|
|
const v = await getStored(KEY_TTS_SPEAK_DEFAULT);
|
|
if (v == null || v === '') return false;
|
|
return v === 'true';
|
|
}
|
|
|
|
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';
|