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.
This commit is contained in:
Aleksander Grygier
2026-08-20 13:54:14 +02:00
parent de40f3e81b
commit 1859ad41c0
3 changed files with 113 additions and 49 deletions
+10
View File
@@ -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';
@@ -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<string, unknown>;
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<string, unknown>;
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<string, unknown>, 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);
}
}
}
@@ -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));
}
}