Move provider normalization logic into a dedicated module to ensure consistent handling of provider defaults and user preference inheritance across client and server. Update layout components to mount ConfigProvider only at the shared layout level, preventing unnecessary remounts and hydration issues during navigation. Adjust app config defaults so user provider settings are empty by default, always inheriting the admin-configured provider unless explicitly set. Add tests for normalization and sync logic to ensure robust provider resolution.
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
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<string, string>;
|
|
|
|
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<AppConfigValues> = {
|
|
get(_target, prop) {
|
|
if (!cachedDefaults) cachedDefaults = getAppConfigDefaults();
|
|
return (cachedDefaults as unknown as Record<string | symbol, unknown>)[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<string | symbol, unknown>)[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;
|
|
}
|