mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
feat(settings): add device-specific settings storage
Implement per-device settings sync via mana-core-auth. Settings are now stored both locally (localStorage) and in the cloud, with each device (desktop, mobile, tablet) maintaining its own configuration. Changes: - Add deviceSettings JSONB column to user_settings table - Add device API endpoints (GET/PATCH/DELETE /settings/device/:id/:app) - Extend user-settings-store with device ID generation and detection - Integrate calendar settings with cloud sync per device - Remove todos from calendar header row (sidebar + grid only) - Add hours dropdown to CalendarHeader for time range configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5921cfd257
commit
c6f8b9f87c
11 changed files with 863 additions and 416 deletions
|
|
@ -302,12 +302,46 @@ export interface AppOverride {
|
|||
theme?: Partial<ThemeSettings>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Device type for device-specific settings
|
||||
*/
|
||||
export type DeviceType = 'desktop' | 'mobile' | 'tablet';
|
||||
|
||||
/**
|
||||
* Device-specific app settings
|
||||
*/
|
||||
export interface DeviceAppSettings {
|
||||
deviceName: string;
|
||||
deviceType: DeviceType;
|
||||
lastSeen: string;
|
||||
apps: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Device info for listing
|
||||
*/
|
||||
export interface DeviceInfo {
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
deviceType: DeviceType;
|
||||
lastSeen: string;
|
||||
appCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full user settings response from API
|
||||
*/
|
||||
export interface UserSettingsResponse {
|
||||
globalSettings: GlobalSettings;
|
||||
appOverrides: Record<string, AppOverride>;
|
||||
deviceSettings: Record<string, DeviceAppSettings>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Devices list response
|
||||
*/
|
||||
export interface DevicesListResponse {
|
||||
devices: DeviceInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -353,6 +387,12 @@ export interface UserSettingsStore {
|
|||
readonly syncing: boolean;
|
||||
/** Whether settings are loaded */
|
||||
readonly loaded: boolean;
|
||||
/** Current device ID */
|
||||
readonly deviceId: string;
|
||||
/** All device settings */
|
||||
readonly deviceSettings: Record<string, DeviceAppSettings>;
|
||||
/** Current device's app settings */
|
||||
readonly currentDeviceAppSettings: Record<string, unknown>;
|
||||
|
||||
/** Load settings from server */
|
||||
load: () => Promise<void>;
|
||||
|
|
@ -372,6 +412,14 @@ export interface UserSettingsStore {
|
|||
toggleNavItemVisibility: (appId: string, href: string) => Promise<void>;
|
||||
/** Set hidden nav items for an app */
|
||||
setHiddenNavItems: (appId: string, hiddenHrefs: string[]) => Promise<void>;
|
||||
/** Update device-specific app settings */
|
||||
updateDeviceAppSettings: (settings: Record<string, unknown>) => Promise<void>;
|
||||
/** Get device-specific app settings */
|
||||
getDeviceAppSettings: () => Record<string, unknown>;
|
||||
/** List all devices */
|
||||
getDevices: () => Promise<DeviceInfo[]>;
|
||||
/** Remove a device */
|
||||
removeDevice: (deviceId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -384,261 +432,8 @@ export interface UserSettingsStoreConfig {
|
|||
authUrl: string;
|
||||
/** Function to get current access token */
|
||||
getAccessToken: () => Promise<string | null>;
|
||||
/** Optional device name (auto-detected if not provided) */
|
||||
deviceName?: string;
|
||||
/** Optional device type (auto-detected if not provided) */
|
||||
deviceType?: DeviceType;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Custom & Community Themes Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Partial theme colors for API DTOs (some fields optional)
|
||||
*/
|
||||
export interface ThemeColorsInput {
|
||||
primary: HSLValue;
|
||||
primaryForeground?: HSLValue;
|
||||
background: HSLValue;
|
||||
foreground: HSLValue;
|
||||
surface: HSLValue;
|
||||
surfaceHover?: HSLValue;
|
||||
surfaceElevated?: HSLValue;
|
||||
muted?: HSLValue;
|
||||
mutedForeground?: HSLValue;
|
||||
border?: HSLValue;
|
||||
borderStrong?: HSLValue;
|
||||
secondary?: HSLValue;
|
||||
secondaryForeground?: HSLValue;
|
||||
input?: HSLValue;
|
||||
ring?: HSLValue;
|
||||
error: HSLValue;
|
||||
success: HSLValue;
|
||||
warning: HSLValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* User-created custom theme
|
||||
*/
|
||||
export interface CustomTheme {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
emoji: string;
|
||||
icon: string;
|
||||
lightColors: ThemeColors;
|
||||
darkColors: ThemeColors;
|
||||
baseVariant?: ThemeVariant;
|
||||
isPublished: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for creating a new custom theme
|
||||
*/
|
||||
export interface CreateCustomThemeInput {
|
||||
name: string;
|
||||
description?: string;
|
||||
emoji?: string;
|
||||
icon?: string;
|
||||
lightColors: ThemeColorsInput;
|
||||
darkColors: ThemeColorsInput;
|
||||
baseVariant?: ThemeVariant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for updating a custom theme
|
||||
*/
|
||||
export interface UpdateCustomThemeInput {
|
||||
name?: string;
|
||||
description?: string;
|
||||
emoji?: string;
|
||||
icon?: string;
|
||||
lightColors?: ThemeColorsInput;
|
||||
darkColors?: ThemeColorsInput;
|
||||
baseVariant?: ThemeVariant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Community theme shared publicly
|
||||
*/
|
||||
export interface CommunityTheme {
|
||||
id: string;
|
||||
authorId?: string;
|
||||
authorName?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
emoji: string;
|
||||
icon: string;
|
||||
lightColors: ThemeColors;
|
||||
darkColors: ThemeColors;
|
||||
baseVariant?: ThemeVariant;
|
||||
downloadCount: number;
|
||||
averageRating: number;
|
||||
ratingCount: number;
|
||||
status: 'pending' | 'approved' | 'rejected' | 'featured';
|
||||
isFeatured: boolean;
|
||||
tags: string[];
|
||||
createdAt: Date;
|
||||
publishedAt?: Date;
|
||||
/** User-specific fields (when authenticated) */
|
||||
isFavorited?: boolean;
|
||||
isDownloaded?: boolean;
|
||||
userRating?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query parameters for browsing community themes
|
||||
*/
|
||||
export interface CommunityThemeQuery {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sort?: 'popular' | 'recent' | 'rating' | 'downloads';
|
||||
search?: string;
|
||||
tags?: string[];
|
||||
authorId?: string;
|
||||
featuredOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated response for community themes
|
||||
*/
|
||||
export interface PaginatedCommunityThemes {
|
||||
themes: CommunityTheme[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for publishing a theme to the community
|
||||
*/
|
||||
export interface PublishThemeInput {
|
||||
tags?: string[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme editor state for UI
|
||||
*/
|
||||
export interface ThemeEditorState {
|
||||
/** Theme being edited */
|
||||
theme: Partial<CreateCustomThemeInput>;
|
||||
/** Currently editing light or dark colors */
|
||||
editingMode: EffectiveMode;
|
||||
/** Currently selected color key */
|
||||
selectedColorKey: keyof ThemeColors | null;
|
||||
/** Is preview mode active */
|
||||
isPreviewing: boolean;
|
||||
/** Has unsaved changes */
|
||||
isDirty: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom themes store interface
|
||||
*/
|
||||
export interface CustomThemesStore {
|
||||
/** User's custom themes */
|
||||
readonly customThemes: CustomTheme[];
|
||||
/** Community themes (from current query) */
|
||||
readonly communityThemes: CommunityTheme[];
|
||||
/** User's favorited themes */
|
||||
readonly favorites: CommunityTheme[];
|
||||
/** User's downloaded themes */
|
||||
readonly downloaded: CommunityTheme[];
|
||||
/** Pagination info */
|
||||
readonly pagination: { page: number; totalPages: number; total: number };
|
||||
/** Loading state */
|
||||
readonly loading: boolean;
|
||||
/** Error state */
|
||||
readonly error: string | null;
|
||||
|
||||
// Custom theme operations
|
||||
loadCustomThemes: () => Promise<void>;
|
||||
createTheme: (input: CreateCustomThemeInput) => Promise<CustomTheme>;
|
||||
updateTheme: (id: string, input: UpdateCustomThemeInput) => Promise<CustomTheme>;
|
||||
deleteTheme: (id: string) => Promise<void>;
|
||||
publishTheme: (id: string, input?: PublishThemeInput) => Promise<CommunityTheme>;
|
||||
|
||||
// Community theme operations
|
||||
browseCommunity: (query?: CommunityThemeQuery) => Promise<void>;
|
||||
downloadTheme: (id: string) => Promise<CommunityTheme>;
|
||||
rateTheme: (
|
||||
id: string,
|
||||
rating: number
|
||||
) => Promise<{ averageRating: number; ratingCount: number }>;
|
||||
toggleFavorite: (id: string) => Promise<{ isFavorited: boolean }>;
|
||||
loadFavorites: () => Promise<void>;
|
||||
loadDownloaded: () => Promise<void>;
|
||||
|
||||
// Apply theme
|
||||
applyCustomTheme: (theme: CustomTheme | CommunityTheme) => void;
|
||||
clearCustomTheme: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom themes store configuration
|
||||
*/
|
||||
export interface CustomThemesStoreConfig {
|
||||
/** Auth service base URL */
|
||||
authUrl: string;
|
||||
/** Function to get current access token */
|
||||
getAccessToken: () => Promise<string | null>;
|
||||
/** Theme store to apply custom themes to */
|
||||
themeStore?: ThemeStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main colors for the simplified editor view
|
||||
* These are the 7 most important colors users typically want to customize
|
||||
*/
|
||||
export const MAIN_THEME_COLORS: (keyof ThemeColors)[] = [
|
||||
'primary',
|
||||
'background',
|
||||
'surface',
|
||||
'foreground',
|
||||
'error',
|
||||
'success',
|
||||
'warning',
|
||||
];
|
||||
|
||||
/**
|
||||
* Extended/advanced colors (collapsed by default in editor)
|
||||
*/
|
||||
export const EXTENDED_THEME_COLORS: (keyof ThemeColors)[] = [
|
||||
'primaryForeground',
|
||||
'secondary',
|
||||
'secondaryForeground',
|
||||
'surfaceHover',
|
||||
'surfaceElevated',
|
||||
'muted',
|
||||
'mutedForeground',
|
||||
'border',
|
||||
'borderStrong',
|
||||
'input',
|
||||
'ring',
|
||||
];
|
||||
|
||||
/**
|
||||
* Color labels for the editor UI
|
||||
*/
|
||||
export const THEME_COLOR_LABELS: Record<keyof ThemeColors, string> = {
|
||||
primary: 'Primary',
|
||||
primaryForeground: 'Primary Text',
|
||||
secondary: 'Secondary',
|
||||
secondaryForeground: 'Secondary Text',
|
||||
background: 'Background',
|
||||
foreground: 'Text',
|
||||
surface: 'Surface',
|
||||
surfaceHover: 'Surface Hover',
|
||||
surfaceElevated: 'Elevated Surface',
|
||||
muted: 'Muted',
|
||||
mutedForeground: 'Muted Text',
|
||||
border: 'Border',
|
||||
borderStrong: 'Border Strong',
|
||||
error: 'Error',
|
||||
success: 'Success',
|
||||
warning: 'Warning',
|
||||
input: 'Input',
|
||||
ring: 'Focus Ring',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,12 +7,74 @@ import type {
|
|||
ThemeSettings,
|
||||
UserSettingsResponse,
|
||||
GeneralSettings,
|
||||
DeviceAppSettings,
|
||||
DeviceInfo,
|
||||
DeviceType,
|
||||
DevicesListResponse,
|
||||
} from './types';
|
||||
import { DEFAULT_GLOBAL_SETTINGS, DEFAULT_GENERAL_SETTINGS } from './types';
|
||||
import { isBrowser } from './utils';
|
||||
import { getStartPage as getStartPageFromConfig } from './app-routes';
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'manacore-user-settings';
|
||||
const DEVICE_ID_KEY = 'manacore-device-id';
|
||||
|
||||
/**
|
||||
* Generate a unique device ID
|
||||
*/
|
||||
function generateDeviceId(): string {
|
||||
return 'dev_' + crypto.randomUUID().replace(/-/g, '').substring(0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create device ID from localStorage
|
||||
*/
|
||||
function getOrCreateDeviceId(): string {
|
||||
if (!isBrowser()) return 'server';
|
||||
try {
|
||||
let deviceId = localStorage.getItem(DEVICE_ID_KEY);
|
||||
if (!deviceId) {
|
||||
deviceId = generateDeviceId();
|
||||
localStorage.setItem(DEVICE_ID_KEY, deviceId);
|
||||
}
|
||||
return deviceId;
|
||||
} catch {
|
||||
return generateDeviceId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect device type based on user agent and screen size
|
||||
*/
|
||||
function detectDeviceType(): DeviceType {
|
||||
if (!isBrowser()) return 'desktop';
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isMobile = /mobile|iphone|ipod|android.*mobile|windows phone/i.test(ua);
|
||||
const isTablet = /tablet|ipad|android(?!.*mobile)/i.test(ua);
|
||||
if (isTablet) return 'tablet';
|
||||
if (isMobile) return 'mobile';
|
||||
return 'desktop';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect device name based on user agent
|
||||
*/
|
||||
function detectDeviceName(): string {
|
||||
if (!isBrowser()) return 'Server';
|
||||
const ua = navigator.userAgent;
|
||||
// Try to extract device/browser info
|
||||
if (/iPhone/.test(ua)) return 'iPhone';
|
||||
if (/iPad/.test(ua)) return 'iPad';
|
||||
if (/Android/.test(ua)) {
|
||||
const match = ua.match(/Android.*;\s*([^;)]+)/);
|
||||
if (match) return match[1].trim();
|
||||
return 'Android Gerät';
|
||||
}
|
||||
if (/Mac/.test(ua)) return 'Mac';
|
||||
if (/Windows/.test(ua)) return 'Windows PC';
|
||||
if (/Linux/.test(ua)) return 'Linux PC';
|
||||
return 'Unbekanntes Gerät';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a User Settings store for your app
|
||||
|
|
@ -41,12 +103,18 @@ const STORAGE_KEY_PREFIX = 'manacore-user-settings';
|
|||
* ```
|
||||
*/
|
||||
export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSettingsStore {
|
||||
const { appId, authUrl, getAccessToken } = config;
|
||||
const { appId, authUrl, getAccessToken, deviceName, deviceType } = config;
|
||||
const storageKey = `${STORAGE_KEY_PREFIX}-${appId}`;
|
||||
|
||||
// Device info (initialized once)
|
||||
const deviceId = getOrCreateDeviceId();
|
||||
const detectedDeviceType = deviceType || detectDeviceType();
|
||||
const detectedDeviceName = deviceName || detectDeviceName();
|
||||
|
||||
// State
|
||||
let globalSettings = $state<GlobalSettings>({ ...DEFAULT_GLOBAL_SETTINGS });
|
||||
let appOverrides = $state<Record<string, AppOverride>>({});
|
||||
let deviceSettings = $state<Record<string, DeviceAppSettings>>({});
|
||||
let syncing = $state(false);
|
||||
let loaded = $state(false);
|
||||
|
||||
|
|
@ -88,6 +156,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
JSON.stringify({
|
||||
globalSettings,
|
||||
appOverrides,
|
||||
deviceSettings,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
);
|
||||
|
|
@ -111,6 +180,9 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
if (data.appOverrides) {
|
||||
appOverrides = data.appOverrides;
|
||||
}
|
||||
if (data.deviceSettings) {
|
||||
deviceSettings = data.deviceSettings;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -165,6 +237,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
if (data?.success) {
|
||||
globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings };
|
||||
appOverrides = data.appOverrides || {};
|
||||
deviceSettings = data.deviceSettings || {};
|
||||
saveToStorage();
|
||||
loaded = true;
|
||||
}
|
||||
|
|
@ -205,6 +278,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
if (data?.success) {
|
||||
globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings };
|
||||
appOverrides = data.appOverrides || {};
|
||||
deviceSettings = data.deviceSettings || {};
|
||||
saveToStorage();
|
||||
} else {
|
||||
// Rollback on failure
|
||||
|
|
@ -242,6 +316,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
if (data?.success) {
|
||||
globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings };
|
||||
appOverrides = data.appOverrides || {};
|
||||
deviceSettings = data.deviceSettings || {};
|
||||
saveToStorage();
|
||||
} else {
|
||||
// Rollback on failure
|
||||
|
|
@ -303,6 +378,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
if (data?.success) {
|
||||
globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings };
|
||||
appOverrides = data.appOverrides || {};
|
||||
deviceSettings = data.deviceSettings || {};
|
||||
saveToStorage();
|
||||
} else {
|
||||
// Rollback on failure
|
||||
|
|
@ -354,6 +430,108 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
} as Partial<GlobalSettings>);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Device Settings Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Update device-specific app settings for current device
|
||||
*/
|
||||
async function updateDeviceAppSettings(settings: Record<string, unknown>): Promise<void> {
|
||||
// Optimistic update
|
||||
const previousDeviceSettings = { ...deviceSettings };
|
||||
const existingDevice = deviceSettings[deviceId] || {
|
||||
deviceName: detectedDeviceName,
|
||||
deviceType: detectedDeviceType,
|
||||
lastSeen: new Date().toISOString(),
|
||||
apps: {},
|
||||
};
|
||||
|
||||
deviceSettings = {
|
||||
...deviceSettings,
|
||||
[deviceId]: {
|
||||
...existingDevice,
|
||||
lastSeen: new Date().toISOString(),
|
||||
apps: {
|
||||
...existingDevice.apps,
|
||||
[appId]: {
|
||||
...(existingDevice.apps?.[appId] || {}),
|
||||
...settings,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
saveToStorage();
|
||||
|
||||
syncing = true;
|
||||
try {
|
||||
const data = await apiRequest<UserSettingsResponse & { success: boolean }>(
|
||||
'PATCH',
|
||||
`/device/${deviceId}/${appId}`,
|
||||
{
|
||||
deviceName: detectedDeviceName,
|
||||
deviceType: detectedDeviceType,
|
||||
settings,
|
||||
}
|
||||
);
|
||||
|
||||
if (data?.success) {
|
||||
globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings };
|
||||
appOverrides = data.appOverrides || {};
|
||||
deviceSettings = data.deviceSettings || {};
|
||||
saveToStorage();
|
||||
} else {
|
||||
// Rollback on failure
|
||||
deviceSettings = previousDeviceSettings;
|
||||
saveToStorage();
|
||||
}
|
||||
} finally {
|
||||
syncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device-specific app settings for current device
|
||||
*/
|
||||
function getDeviceAppSettings(): Record<string, unknown> {
|
||||
const device = deviceSettings[deviceId];
|
||||
if (!device?.apps?.[appId]) return {};
|
||||
return device.apps[appId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of all devices
|
||||
*/
|
||||
async function getDevices(): Promise<DeviceInfo[]> {
|
||||
const data = await apiRequest<DevicesListResponse & { success: boolean }>('GET', '/devices');
|
||||
if (data?.success) {
|
||||
return data.devices;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a device
|
||||
*/
|
||||
async function removeDevice(targetDeviceId: string): Promise<void> {
|
||||
syncing = true;
|
||||
try {
|
||||
const data = await apiRequest<UserSettingsResponse & { success: boolean }>(
|
||||
'DELETE',
|
||||
`/device/${targetDeviceId}`
|
||||
);
|
||||
|
||||
if (data?.success) {
|
||||
globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings };
|
||||
appOverrides = data.appOverrides || {};
|
||||
deviceSettings = data.deviceSettings || {};
|
||||
saveToStorage();
|
||||
}
|
||||
} finally {
|
||||
syncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get nav() {
|
||||
return nav;
|
||||
|
|
@ -382,6 +560,17 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
get loaded() {
|
||||
return loaded;
|
||||
},
|
||||
get deviceId() {
|
||||
return deviceId;
|
||||
},
|
||||
get deviceSettings() {
|
||||
return deviceSettings;
|
||||
},
|
||||
get currentDeviceAppSettings() {
|
||||
const device = deviceSettings[deviceId];
|
||||
if (!device?.apps?.[appId]) return {};
|
||||
return device.apps[appId];
|
||||
},
|
||||
|
||||
load,
|
||||
updateGlobal,
|
||||
|
|
@ -392,5 +581,9 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
|
|||
getHiddenNavItemsForApp,
|
||||
toggleNavItemVisibility,
|
||||
setHiddenNavItems,
|
||||
updateDeviceAppSettings,
|
||||
getDeviceAppSettings,
|
||||
getDevices,
|
||||
removeDevice,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue