import type { DocumentListState } from '@/types/documents'; import type { TtsProviderType } from '@/lib/shared/tts-provider-catalog'; export type ViewType = 'single' | 'dual' | 'scroll'; export type SavedVoices = Record; export const SEGMENT_PRELOAD_DEPTH_MIN = 1; export const SEGMENT_PRELOAD_DEPTH_MAX = 5; export const SEGMENT_PRELOAD_SENTENCE_LOOKAHEAD_MIN = 1; export const SEGMENT_PRELOAD_SENTENCE_LOOKAHEAD_MAX = 10; export const TTS_SEGMENT_MAX_BLOCK_LENGTH_MIN = 150; export const TTS_SEGMENT_MAX_BLOCK_LENGTH_MAX = 1200; export const TTS_SEGMENT_MAX_BLOCK_LENGTH_STEP = 25; export function clampSegmentPreloadDepth(value: number | undefined | null): number { const candidate = Math.floor(Number(value) || SEGMENT_PRELOAD_DEPTH_MIN); return Math.max(SEGMENT_PRELOAD_DEPTH_MIN, Math.min(SEGMENT_PRELOAD_DEPTH_MAX, candidate)); } export function clampSegmentPreloadSentenceLookahead(value: number | undefined | null): number { const candidate = Math.floor(Number(value) || SEGMENT_PRELOAD_SENTENCE_LOOKAHEAD_MIN); return Math.max(SEGMENT_PRELOAD_SENTENCE_LOOKAHEAD_MIN, Math.min(SEGMENT_PRELOAD_SENTENCE_LOOKAHEAD_MAX, candidate)); } export function clampTtsSegmentMaxBlockLength(value: number | undefined | null): number { const candidate = Math.floor(Number(value) || TTS_SEGMENT_MAX_BLOCK_LENGTH_MIN); return Math.max(TTS_SEGMENT_MAX_BLOCK_LENGTH_MIN, Math.min(TTS_SEGMENT_MAX_BLOCK_LENGTH_MAX, candidate)); } export interface AppConfigValues { apiKey: string; baseUrl: string; viewType: ViewType; voiceSpeed: number; audioPlayerSpeed: number; voice: string; skipBlank: boolean; epubTheme: boolean; headerMargin: number; footerMargin: number; leftMargin: number; rightMargin: number; providerRef: string; providerType: TtsProviderType; ttsModel: string; ttsInstructions: string; savedVoices: SavedVoices; segmentPreloadDepthPages: number; segmentPreloadSentenceLookahead: number; ttsSegmentMaxBlockLength: number; pdfHighlightEnabled: boolean; pdfWordHighlightEnabled: boolean; epubHighlightEnabled: boolean; epubWordHighlightEnabled: boolean; htmlHighlightEnabled: boolean; htmlWordHighlightEnabled: boolean; firstVisit: boolean; documentListState: DocumentListState; privacyAccepted: boolean; documentsMigrationPrompted: boolean; } /** * Build the static app-config defaults. These no longer read any SSR/admin * runtime config: the user's TTS provider defaults to empty ("inherit the * admin default"), which is resolved to a concrete provider where it is used * (ConfigContext on the client, credential resolution on the server). */ export function getAppConfigDefaults(): AppConfigValues { // The user's TTS provider is intentionally left empty by default. An empty // providerRef means "inherit the instance/admin default" and is resolved to a // concrete provider at read time (see ConfigContext) and at generation time // (server-side credential resolution). We no longer bake a placeholder // provider id (the old 'custom-openai') into every user's config, since that // value isn't actually selectable in shared-provider mode. return { apiKey: '', baseUrl: '', viewType: 'single', voiceSpeed: 1, audioPlayerSpeed: 1, voice: '', skipBlank: true, epubTheme: false, headerMargin: 0, footerMargin: 0, leftMargin: 0, rightMargin: 0, providerRef: '', providerType: 'unknown', ttsModel: '', ttsInstructions: '', savedVoices: {}, segmentPreloadDepthPages: 1, segmentPreloadSentenceLookahead: 3, ttsSegmentMaxBlockLength: 450, pdfHighlightEnabled: true, pdfWordHighlightEnabled: true, epubHighlightEnabled: true, epubWordHighlightEnabled: true, htmlHighlightEnabled: true, htmlWordHighlightEnabled: true, firstVisit: false, documentListState: { sortBy: 'name', sortDirection: 'asc', folders: [], collapsedFolders: [], showHint: true, viewMode: 'grid', }, privacyAccepted: false, documentsMigrationPrompted: false, }; } /** * Static defaults snapshot, resolved once at first access. The values are * constant (no SSR/admin overrides are read here anymore), so the lazy Proxy * just avoids building the object until something first reads it. */ let cachedDefaults: AppConfigValues | null = null; export const APP_CONFIG_DEFAULTS: AppConfigValues = (() => { // Getter-backed object that builds the defaults lazily on first access. const handler: ProxyHandler = { get(_target, prop) { if (!cachedDefaults) cachedDefaults = getAppConfigDefaults(); return (cachedDefaults as unknown as Record)[prop as string]; }, has(_target, prop) { if (!cachedDefaults) cachedDefaults = getAppConfigDefaults(); return prop in (cachedDefaults as object); }, ownKeys() { if (!cachedDefaults) cachedDefaults = getAppConfigDefaults(); return Reflect.ownKeys(cachedDefaults as object); }, getOwnPropertyDescriptor(_target, prop) { if (!cachedDefaults) cachedDefaults = getAppConfigDefaults(); const value = (cachedDefaults as unknown as Record)[prop as string]; if (value === undefined && !(prop in (cachedDefaults as object))) return undefined; return { value, writable: true, enumerable: true, configurable: true, }; }, }; return new Proxy({} as AppConfigValues, handler); })(); export interface AppConfigRow extends AppConfigValues { id: string; }