feat: add settings synchronization feature with server, including UI toggle and localization

This commit is contained in:
Linus Rath
2026-03-11 16:46:43 +01:00
parent a8a1ad1d54
commit 723aff38f2
4 changed files with 74 additions and 7 deletions
+3
View File
@@ -43,3 +43,6 @@ next-env.d.ts
# claude code
.claude/
# settings sync data
/data/
+10 -1
View File
@@ -3,14 +3,16 @@
import { useState, useRef } from 'react';
import { useTranslations } from 'next-intl';
import { useSettingsStore } from '@/stores/settings-store';
import { useConfig } from '@/hooks/use-config';
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
import { Button } from '@/components/ui/button';
export function AdvancedSettings() {
const t = useTranslations('settings.advanced');
const tCommon = useTranslations('common');
const { debugMode, updateSetting, resetToDefaults, exportSettings, importSettings } =
const { debugMode, settingsSyncDisabled, updateSetting, resetToDefaults, exportSettings, importSettings } =
useSettingsStore();
const { settingsSyncEnabled } = useConfig();
const [showResetConfirm, setShowResetConfirm] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -66,6 +68,13 @@ export function AdvancedSettings() {
<ToggleSwitch checked={debugMode} onChange={(checked) => updateSetting('debugMode', checked)} />
</SettingItem>
{/* Settings Sync */}
{settingsSyncEnabled && (
<SettingItem label={t('settings_sync.label')} description={t('settings_sync.description')}>
<ToggleSwitch checked={!settingsSyncDisabled} onChange={(checked) => updateSetting('settingsSyncDisabled', !checked)} />
</SettingItem>
)}
{/* Export Settings */}
<SettingItem label={t('export_settings.label')} description={t('export_settings.description')}>
<Button variant="outline" size="sm" onClick={handleExport}>
+4
View File
@@ -746,6 +746,10 @@
"label": "Debug Mode",
"description": "Enable detailed logging for troubleshooting"
},
"settings_sync": {
"label": "Settings Sync",
"description": "Sync your settings across browsers and devices"
},
"keyboard_shortcuts": {
"label": "Keyboard Shortcuts",
"description": "View available keyboard shortcuts",
+57 -6
View File
@@ -1,5 +1,13 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useThemeStore } from './theme-store';
import { useLocaleStore } from './locale-store';
// Use console directly to avoid circular dependency with lib/debug.ts
// (debug.ts imports useSettingsStore for debugMode check)
const syncLog = (...args: unknown[]) => console.log('[SETTINGS_SYNC]', ...args);
const syncWarn = (...args: unknown[]) => console.warn('[SETTINGS_SYNC]', ...args);
const syncError = (...args: unknown[]) => console.error('[SETTINGS_SYNC]', ...args);
// Settings sync state (module-level, not persisted)
let syncEnabled = false;
@@ -58,6 +66,7 @@ interface SettingsState {
// Advanced
debugMode: boolean;
settingsSyncDisabled: boolean;
// Actions
updateSetting: <K extends keyof SettingsState>(
@@ -122,6 +131,7 @@ const DEFAULT_SETTINGS = {
// Advanced
debugMode: false,
settingsSyncDisabled: false,
};
export const useSettingsStore = create<SettingsState>()(
@@ -179,6 +189,10 @@ export const useSettingsStore = create<SettingsState>()(
senderFavicons: state.senderFavicons,
folderIcons: state.folderIcons,
debugMode: state.debugMode,
settingsSyncDisabled: state.settingsSyncDisabled,
// Cross-store settings
theme: useThemeStore.getState().theme,
locale: useLocaleStore.getState().locale,
};
return JSON.stringify(settings, null, 2);
},
@@ -204,6 +218,14 @@ export const useSettingsStore = create<SettingsState>()(
applyListDensity(get().listDensity);
applyAnimations(get().animationsEnabled);
// Apply cross-store settings
if (settings.theme) {
useThemeStore.getState().setTheme(settings.theme);
}
if (settings.locale) {
useLocaleStore.getState().setLocale(settings.locale);
}
return true;
} catch (error) {
console.error('Failed to import settings:', error);
@@ -247,6 +269,7 @@ export const useSettingsStore = create<SettingsState>()(
syncUsername = username;
syncServerUrl = serverUrl;
syncEnabled = true;
syncLog('Settings sync enabled for', username);
},
disableSync: () => {
@@ -257,26 +280,33 @@ export const useSettingsStore = create<SettingsState>()(
clearTimeout(syncTimeout);
syncTimeout = null;
}
syncLog('Settings sync disabled');
},
loadFromServer: async (username: string, serverUrl: string) => {
try {
syncLog('Loading settings from server for', username);
const res = await fetch('/api/settings', {
headers: {
'x-settings-username': username,
'x-settings-server': serverUrl,
},
});
if (!res.ok) return false;
if (!res.ok) {
syncLog('No server settings found (status', res.status + ')');
return false;
}
const { settings } = await res.json();
if (settings && typeof settings === 'object') {
isLoadingFromServer = true;
get().importSettings(JSON.stringify(settings));
isLoadingFromServer = false;
syncLog('Settings loaded from server successfully');
return true;
}
return false;
} catch {
} catch (error) {
syncError('Failed to load settings from server:', error);
isLoadingFromServer = false;
return false;
}
@@ -332,24 +362,45 @@ if (typeof window !== 'undefined') {
applyListDensity(store.listDensity);
applyAnimations(store.animationsEnabled);
// Auto-sync settings to server on any state change
useSettingsStore.subscribe(() => {
// Shared sync function used by all store subscribers
const triggerSync = () => {
if (!syncEnabled || !syncUsername || !syncServerUrl || isLoadingFromServer) return;
if (syncTimeout) clearTimeout(syncTimeout);
syncTimeout = setTimeout(async () => {
try {
const settings = JSON.parse(useSettingsStore.getState().exportSettings());
syncLog('Syncing settings to server...');
const res = await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: syncUsername, serverUrl: syncServerUrl, settings }),
});
if (res.status === 404) {
syncWarn('Settings sync endpoint returned 404, disabling sync');
syncEnabled = false;
} else if (!res.ok) {
syncError('Settings sync failed with status', res.status);
} else {
syncLog('Settings synced to server successfully');
}
} catch {
// Silently ignore sync failures
} catch (error) {
syncError('Settings sync error:', error);
}
}, SYNC_DEBOUNCE_MS);
};
// Auto-sync settings to server on any state change
let prevSyncDisabled = useSettingsStore.getState().settingsSyncDisabled;
useSettingsStore.subscribe(() => {
const currentSyncDisabled = useSettingsStore.getState().settingsSyncDisabled;
const syncToggleChanged = currentSyncDisabled !== prevSyncDisabled;
prevSyncDisabled = currentSyncDisabled;
// Skip sync if disabled, unless the toggle itself just changed
if (currentSyncDisabled && !syncToggleChanged) return;
triggerSync();
});
// Also sync when theme or locale changes
useThemeStore.subscribe(triggerSync);
useLocaleStore.subscribe(triggerSync);
}