From 1859ad41c05b0cc0bf125a9de8aeb46d467220aa Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 20 Aug 2026 13:54:14 +0200 Subject: [PATCH] ui : extract settings localStorage persistence into SettingsService Stateless load/save of the settings config and user-override keys, plus the legacy theme key migration. Business logic (default merging, mobile sendOnEnter default, applying the migrated theme) stays in the store. --- tools/ui/src/lib/services/index.ts | 10 +++ tools/ui/src/lib/services/settings.service.ts | 76 +++++++++++++++++++ .../src/lib/stores/settings/index.svelte.ts | 76 +++++++------------ 3 files changed, 113 insertions(+), 49 deletions(-) create mode 100644 tools/ui/src/lib/services/settings.service.ts diff --git a/tools/ui/src/lib/services/index.ts b/tools/ui/src/lib/services/index.ts index 7ae9e23d48..88e79446ec 100644 --- a/tools/ui/src/lib/services/index.ts +++ b/tools/ui/src/lib/services/index.ts @@ -340,3 +340,13 @@ export { RouterService } from './router.service'; * @see migration.service.ts — full implementation (non-destructive) */ export { MigrationService } from './migration.service'; + +/** + * **SettingsService** - localStorage persistence layer for settings + * + * Stateless read/write of the settings config and user-override keys. Business + * logic (default merging, mobile defaults, theme migration) stays in the store. + * + * @see settingsStore in stores/settings/index.svelte.ts - reactive state + business logic + */ +export { SettingsService } from './settings.service'; diff --git a/tools/ui/src/lib/services/settings.service.ts b/tools/ui/src/lib/services/settings.service.ts new file mode 100644 index 0000000000..639fb063e4 --- /dev/null +++ b/tools/ui/src/lib/services/settings.service.ts @@ -0,0 +1,76 @@ +import { browser } from '$app/environment'; +import { CONFIG_LOCALSTORAGE_KEY, USER_OVERRIDES_LOCALSTORAGE_KEY } from '$lib/constants'; + +/** + * SettingsService - localStorage persistence layer for settings + * + * Stateless read/write of the settings config and user-override keys. Business + * logic (default merging, mobile defaults, theme migration) stays in the store. + * + * **Architecture & Relationships:** + * - **settingsStore**: Primary consumer - loads config on init and persists on change + * + * @see settingsStore in stores/settings/index.svelte.ts - reactive state + business logic + */ +export class SettingsService { + /** + * Read the raw config and user overrides from localStorage. + * @returns Parsed values, or empty defaults when nothing is stored or parsing fails. + */ + static loadConfig(): { + config: Record; + userOverrides: string[]; + isFirstVisit: boolean; + } { + if (!browser) { + return { config: {}, isFirstVisit: false, userOverrides: [] }; + } + + try { + const storedConfigRaw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY); + const isFirstVisit = storedConfigRaw === null; + const config = JSON.parse(storedConfigRaw || '{}') as Record; + const userOverrides = JSON.parse( + localStorage.getItem(USER_OVERRIDES_LOCALSTORAGE_KEY) || '[]' + ) as string[]; + + return { config, isFirstVisit, userOverrides }; + } catch (error) { + console.warn('Failed to parse config from localStorage, using defaults:', error); + + return { config: {}, isFirstVisit: false, userOverrides: [] }; + } + } + + /** + * Migrate the legacy un-namespaced "theme" localStorage key. + * Returns the legacy theme value (and removes the key) when present, else null. + */ + static migrateLegacyTheme(): string | null { + if (!browser) return null; + + const legacyTheme = localStorage.getItem('theme'); + + if (legacyTheme) { + localStorage.removeItem('theme'); + + return legacyTheme; + } + + return null; + } + + /** + * Persist the config and user overrides to localStorage. + */ + static saveConfig(config: Record, userOverrides: string[]): void { + if (!browser) return; + + try { + localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(config)); + localStorage.setItem(USER_OVERRIDES_LOCALSTORAGE_KEY, JSON.stringify(userOverrides)); + } catch (error) { + console.error('Failed to save config to localStorage:', error); + } + } +} diff --git a/tools/ui/src/lib/stores/settings/index.svelte.ts b/tools/ui/src/lib/stores/settings/index.svelte.ts index 0373ade420..a583a1423f 100644 --- a/tools/ui/src/lib/stores/settings/index.svelte.ts +++ b/tools/ui/src/lib/stores/settings/index.svelte.ts @@ -8,14 +8,10 @@ */ import { browser } from '$app/environment'; -import { - CONFIG_LOCALSTORAGE_KEY, - SETTING_CONFIG_DEFAULT, - SETTINGS_KEYS, - USER_OVERRIDES_LOCALSTORAGE_KEY -} from '$lib/constants'; +import { SETTING_CONFIG_DEFAULT, SETTINGS_KEYS } from '$lib/constants'; import { ColorMode } from '$lib/enums'; import { ParameterSyncService } from '$lib/services/parameter-sync.service'; +import { SettingsService } from '$lib/services/settings.service'; import { deviceStore } from '$lib/stores/device.svelte'; // direct imports between stores, not via the barrel, to avoid circular deps import { serverStore } from '$lib/stores/server.svelte'; @@ -428,45 +424,37 @@ class SettingsStore { } /** - * Load configuration from localStorage - * Returns default values for missing keys to prevent breaking changes + * Load configuration from localStorage via the persistence service. + * Returns default values for missing keys to prevent breaking changes. */ private loadConfig() { if (!browser) return; - try { - const storedConfigRaw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY); + const { + config: savedVal, + isFirstVisit, + userOverrides: savedOverrides + } = SettingsService.loadConfig(); - // First visit: no stored config yet. Server ui_settings apply once in - // this state, then the user's config diverges freely. - this.isFirstVisit = storedConfigRaw === null; + // First visit: no stored config yet. Server ui_settings apply once in + // this state, then the user's config diverges freely. + this.isFirstVisit = isFirstVisit; - const savedVal = JSON.parse(storedConfigRaw || '{}'); + // Merge with defaults to prevent breaking changes + this.config = { + ...SETTING_CONFIG_DEFAULT, + ...savedVal + }; - // Merge with defaults to prevent breaking changes - this.config = { - ...SETTING_CONFIG_DEFAULT, - ...savedVal - }; - - // Default sendOnEnter to false on mobile when the user has no saved preference - if (!(SETTINGS_KEYS.SEND_ON_ENTER in savedVal)) { - if (deviceStore.isMobile) { - this.config[SETTINGS_KEYS.SEND_ON_ENTER] = false; - } + // Default sendOnEnter to false on mobile when the user has no saved preference + if (!(SETTINGS_KEYS.SEND_ON_ENTER in savedVal)) { + if (deviceStore.isMobile) { + this.config[SETTINGS_KEYS.SEND_ON_ENTER] = false; } - - // Load user overrides - const savedOverrides = JSON.parse( - localStorage.getItem(USER_OVERRIDES_LOCALSTORAGE_KEY) || '[]' - ); - - this.userOverrides = new Set(savedOverrides); - } catch (error) { - console.warn('Failed to parse config from localStorage, using defaults:', error); - this.config = { ...SETTING_CONFIG_DEFAULT }; - this.userOverrides = new Set(); } + + // Load user overrides + this.userOverrides = new Set(savedOverrides); } /** @@ -478,32 +466,22 @@ class SettingsStore { private migrateLegacyTheme() { if (!browser) return; - const legacyTheme = localStorage.getItem('theme'); + const legacyTheme = SettingsService.migrateLegacyTheme(); if (legacyTheme) { this.config[SETTINGS_KEYS.THEME] = legacyTheme; - localStorage.removeItem('theme'); this.saveConfig(); setMode(legacyTheme as ColorMode); } } /** - * Save the current configuration to localStorage + * Save the current configuration to localStorage via the persistence service. */ private saveConfig() { if (!browser) return; - try { - localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(this.config)); - - localStorage.setItem( - USER_OVERRIDES_LOCALSTORAGE_KEY, - JSON.stringify(Array.from(this.userOverrides)) - ); - } catch (error) { - console.error('Failed to save config to localStorage:', error); - } + SettingsService.saveConfig(this.config, Array.from(this.userOverrides)); } }