diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index 79544f0..f09825e 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -88,6 +88,7 @@ // Apply persisted settings once on mount (server-loaded data). // Use a derived to react to future invalidateAll() re-loads too. let settingsApplied = false; + let settingsDirty = false; // true only after the first apply completes $effect(() => { if (data.settings) { if (!settingsApplied) { @@ -100,6 +101,9 @@ currentTheme = data.settings.theme ?? 'amber'; currentFontFamily = data.settings.fontFamily ?? 'system'; currentFontSize = data.settings.fontSize ?? 1.0; + // Mark dirty only after the synchronous apply is done so the save + // effect doesn't fire for this initial load. + setTimeout(() => { settingsDirty = true; }, 0); } }); @@ -114,8 +118,9 @@ const fontFamily = currentFontFamily; const fontSize = currentFontSize; - // Skip saving until settings have been applied from the server - if (!settingsApplied) return; + // Skip saving until settings have been applied from the server AND + // at least one user-driven change has occurred after that. + if (!settingsDirty) return; clearTimeout(settingsSaveTimer); settingsSaveTimer = setTimeout(() => { diff --git a/ui/src/routes/api/settings/+server.ts b/ui/src/routes/api/settings/+server.ts index b39f653..552c15a 100644 --- a/ui/src/routes/api/settings/+server.ts +++ b/ui/src/routes/api/settings/+server.ts @@ -43,27 +43,27 @@ export const PUT: RequestHandler = async ({ request, locals }) => { error(400, 'Invalid body — expected { autoNext, voice, speed }'); } - // theme is optional — if provided it must be a known value + // theme is optional — if provided (and non-empty) it must be a known value const validThemes = ['amber', 'slate', 'rose', 'light', 'light-slate', 'light-rose']; - if (body.theme !== undefined && !validThemes.includes(body.theme)) { + if (body.theme !== undefined && body.theme !== '' && !validThemes.includes(body.theme)) { error(400, `Invalid theme — must be one of: ${validThemes.join(', ')}`); } - // locale is optional — if provided it must be a known value + // locale is optional — if provided (and non-empty) it must be a known value const validLocales = ['en', 'ru', 'id', 'pt', 'fr']; - if (body.locale !== undefined && !validLocales.includes(body.locale)) { + if (body.locale !== undefined && body.locale !== '' && !validLocales.includes(body.locale)) { error(400, `Invalid locale — must be one of: ${validLocales.join(', ')}`); } - // fontFamily is optional — if provided it must be a known value + // fontFamily is optional — if provided (and non-empty) it must be a known value const validFontFamilies = ['system', 'serif', 'mono']; - if (body.fontFamily !== undefined && !validFontFamilies.includes(body.fontFamily)) { + if (body.fontFamily !== undefined && body.fontFamily !== '' && !validFontFamilies.includes(body.fontFamily)) { error(400, `Invalid fontFamily — must be one of: ${validFontFamilies.join(', ')}`); } - // fontSize is optional — if provided it must be one of the valid steps + // fontSize is optional — if provided (and non-zero) it must be one of the valid steps const validFontSizes = [0.9, 1.0, 1.15, 1.3]; - if (body.fontSize !== undefined && !validFontSizes.includes(body.fontSize)) { + if (body.fontSize !== undefined && body.fontSize !== 0 && !validFontSizes.includes(body.fontSize)) { error(400, `Invalid fontSize — must be one of: ${validFontSizes.join(', ')}`); }