mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 18:19:39 +02:00
♻️ refactor: create createAppSettingsStore factory and migrate 3 apps
- Add createAppSettingsStore<T>() factory to @manacore/shared-stores - Migrate todo, contacts, calendar settings stores to use factory - Factory provides: localStorage persistence, type-safe set/update/reset - Optional onSettingsChange callback for cloud sync (used by calendar) - Reduces boilerplate by ~323 LOC across 3 apps Savings: - todo: 259 → 159 LOC (100 LOC) - contacts: 278 → 173 LOC (105 LOC) - calendar: 433 → 315 LOC (118 LOC)
This commit is contained in:
parent
9f4713117c
commit
4681ba8c36
9 changed files with 361 additions and 544 deletions
|
|
@ -35,6 +35,7 @@
|
|||
"@manacore/shared-api-client": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-splitscreen": "workspace:*",
|
||||
"@manacore/shared-stores": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-feedback-service": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
/**
|
||||
* Settings Store - Manages user preferences for the calendar app
|
||||
* Uses Svelte 5 runes with:
|
||||
* - localStorage for immediate persistence
|
||||
* - userSettings store for cloud sync (device-specific)
|
||||
* Uses @manacore/shared-stores createAppSettingsStore factory with cloud sync
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import type { CalendarViewType } from '@calendar/shared';
|
||||
import { createAppSettingsStore } from '@manacore/shared-stores';
|
||||
import { userSettings } from './user-settings.svelte';
|
||||
|
||||
// Settings types
|
||||
export type WeekStartDay = 0 | 1; // 0 = Sunday, 1 = Monday
|
||||
export type WeekStartDay = 0 | 1;
|
||||
export type TimeFormat = '24h' | '12h';
|
||||
export type AllDayDisplayMode = 'header' | 'block'; // header = separate row, block = full day block in grid
|
||||
export type AllDayDisplayMode = 'header' | 'block';
|
||||
export type WeekdayFormat = 'full' | 'short' | 'hidden';
|
||||
export type SttLanguage = 'de' | 'auto'; // Speech-to-text language setting
|
||||
export type SttLanguage = 'de' | 'auto';
|
||||
|
||||
export interface CalendarAppSettings {
|
||||
// View settings
|
||||
|
|
@ -23,56 +22,56 @@ export interface CalendarAppSettings {
|
|||
showOnlyWeekdays: boolean;
|
||||
showWeekNumbers: boolean;
|
||||
timeFormat: TimeFormat;
|
||||
filterHoursEnabled: boolean; // Filter visible hours
|
||||
dayStartHour: number; // First visible hour (0-23)
|
||||
dayEndHour: number; // Last visible hour (0-23)
|
||||
allDayDisplayMode: AllDayDisplayMode; // How to display all-day events
|
||||
filterHoursEnabled: boolean;
|
||||
dayStartHour: number;
|
||||
dayEndHour: number;
|
||||
allDayDisplayMode: AllDayDisplayMode;
|
||||
|
||||
// Header settings
|
||||
headerCompact: boolean; // Compact header display
|
||||
headerWeekdayFormat: WeekdayFormat; // Weekday display format
|
||||
headerShowDate: boolean; // Show date in header
|
||||
headerAlwaysShowMonth: boolean; // Always show month (e.g., "13.12.")
|
||||
headerCompact: boolean;
|
||||
headerWeekdayFormat: WeekdayFormat;
|
||||
headerShowDate: boolean;
|
||||
headerAlwaysShowMonth: boolean;
|
||||
|
||||
// DateStrip settings
|
||||
dateStripShowMoonPhases: boolean; // Show moon phase indicators
|
||||
dateStripShowEventIndicators: boolean; // Show event dot indicators
|
||||
dateStripShowWeekday: boolean; // Show weekday names (Mo, Di, Mi...)
|
||||
dateStripHighlightWeekends: boolean; // Visually highlight weekend days
|
||||
dateStripShowMonthDividers: boolean; // Show vertical dividers between months
|
||||
dateStripCompact: boolean; // Use compact/smaller DateStrip
|
||||
dateStripShowWeekNumbers: boolean; // Show week numbers at start of week
|
||||
dateStripCollapsed: boolean; // Whether DateStrip is minimized to FAB
|
||||
dateStripShowMoonPhases: boolean;
|
||||
dateStripShowEventIndicators: boolean;
|
||||
dateStripShowWeekday: boolean;
|
||||
dateStripHighlightWeekends: boolean;
|
||||
dateStripShowMonthDividers: boolean;
|
||||
dateStripCompact: boolean;
|
||||
dateStripShowWeekNumbers: boolean;
|
||||
dateStripCollapsed: boolean;
|
||||
|
||||
// TagStrip settings
|
||||
tagStripCollapsed: boolean; // Whether TagStrip is hidden
|
||||
selectedTagIds: string[]; // Tags selected for filtering calendar view
|
||||
tagStripCollapsed: boolean;
|
||||
selectedTagIds: string[];
|
||||
|
||||
// Immersive Mode settings
|
||||
immersiveModeEnabled: boolean; // Fullscreen mode - hides all UI elements
|
||||
immersiveModeEnabled: boolean;
|
||||
|
||||
// Birthday settings (cross-app integration with Contacts)
|
||||
showBirthdays: boolean; // Show contact birthdays in calendar
|
||||
showBirthdayAge: boolean; // Show age in birthday events
|
||||
// Birthday settings
|
||||
showBirthdays: boolean;
|
||||
showBirthdayAge: boolean;
|
||||
|
||||
// UI settings
|
||||
sidebarCollapsed: boolean;
|
||||
|
||||
// Quick View Pill settings
|
||||
quickViewPillViews: CalendarViewType[]; // Views shown in quick switcher
|
||||
customDayCount: number; // Custom day count for 'custom' view type (1-365)
|
||||
quickViewPillViews: CalendarViewType[];
|
||||
customDayCount: number;
|
||||
|
||||
// Event defaults
|
||||
defaultEventDuration: number; // in minutes
|
||||
defaultReminder: number; // in minutes before event
|
||||
defaultEventDuration: number;
|
||||
defaultReminder: number;
|
||||
|
||||
// Voice input settings
|
||||
sttLanguage: SttLanguage; // Speech-to-text language ('de' or 'auto')
|
||||
sttLanguage: SttLanguage;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: CalendarAppSettings = {
|
||||
defaultView: 'week',
|
||||
weekStartsOn: 1, // Monday
|
||||
weekStartsOn: 1,
|
||||
showOnlyWeekdays: false,
|
||||
showWeekNumbers: false,
|
||||
timeFormat: '24h',
|
||||
|
|
@ -80,12 +79,10 @@ const DEFAULT_SETTINGS: CalendarAppSettings = {
|
|||
dayStartHour: 6,
|
||||
dayEndHour: 20,
|
||||
allDayDisplayMode: 'header',
|
||||
// Header defaults
|
||||
headerCompact: false,
|
||||
headerWeekdayFormat: 'full',
|
||||
headerShowDate: true,
|
||||
headerAlwaysShowMonth: false,
|
||||
// DateStrip defaults
|
||||
dateStripShowMoonPhases: true,
|
||||
dateStripShowEventIndicators: true,
|
||||
dateStripShowWeekday: true,
|
||||
|
|
@ -94,67 +91,26 @@ const DEFAULT_SETTINGS: CalendarAppSettings = {
|
|||
dateStripCompact: false,
|
||||
dateStripShowWeekNumbers: false,
|
||||
dateStripCollapsed: false,
|
||||
// TagStrip defaults
|
||||
tagStripCollapsed: true, // Hidden by default
|
||||
selectedTagIds: [], // No tags selected by default
|
||||
// Immersive Mode defaults
|
||||
tagStripCollapsed: true,
|
||||
selectedTagIds: [],
|
||||
immersiveModeEnabled: false,
|
||||
// Birthday defaults
|
||||
showBirthdays: true,
|
||||
showBirthdayAge: true,
|
||||
// UI defaults
|
||||
sidebarCollapsed: false,
|
||||
// Quick View Pill defaults
|
||||
quickViewPillViews: ['week', 'month', 'agenda'],
|
||||
customDayCount: 30, // Default: 30 days (1 month)
|
||||
// Event defaults
|
||||
customDayCount: 30,
|
||||
defaultEventDuration: 60,
|
||||
defaultReminder: 15,
|
||||
// Voice input defaults
|
||||
sttLanguage: 'de',
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'calendar-settings';
|
||||
|
||||
// Load settings from localStorage
|
||||
function loadSettings(): CalendarAppSettings {
|
||||
if (!browser) return DEFAULT_SETTINGS;
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
return { ...DEFAULT_SETTINGS, ...parsed };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load calendar settings:', e);
|
||||
}
|
||||
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
// Save settings to localStorage
|
||||
function saveSettings(settings: CalendarAppSettings) {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch (e) {
|
||||
console.error('Failed to save calendar settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// State
|
||||
let settings = $state<CalendarAppSettings>(loadSettings());
|
||||
// Cloud sync state
|
||||
let cloudSyncEnabled = $state(false);
|
||||
let initialSyncDone = $state(false);
|
||||
|
||||
/**
|
||||
* Sync settings to cloud (device-specific)
|
||||
*/
|
||||
async function syncToCloud() {
|
||||
// Sync to cloud callback
|
||||
async function syncToCloud(settings: CalendarAppSettings) {
|
||||
if (!cloudSyncEnabled || !browser) return;
|
||||
|
||||
try {
|
||||
await userSettings.updateDeviceAppSettings(settings as unknown as Record<string, unknown>);
|
||||
} catch (e) {
|
||||
|
|
@ -162,12 +118,18 @@ async function syncToCloud() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings from cloud (device-specific)
|
||||
*/
|
||||
// Create base store with cloud sync callback
|
||||
const baseStore = createAppSettingsStore<CalendarAppSettings>(
|
||||
'calendar-settings',
|
||||
DEFAULT_SETTINGS,
|
||||
{
|
||||
onSettingsChange: syncToCloud,
|
||||
}
|
||||
);
|
||||
|
||||
// Load settings from cloud
|
||||
function loadFromCloud(): Partial<CalendarAppSettings> | null {
|
||||
if (!userSettings.loaded) return null;
|
||||
|
||||
const cloudSettings = userSettings.currentDeviceAppSettings;
|
||||
if (cloudSettings && Object.keys(cloudSettings).length > 0) {
|
||||
return cloudSettings as unknown as Partial<CalendarAppSettings>;
|
||||
|
|
@ -176,240 +138,166 @@ function loadFromCloud(): Partial<CalendarAppSettings> | null {
|
|||
}
|
||||
|
||||
export const settingsStore = {
|
||||
// Getters
|
||||
// Base store methods
|
||||
get settings() {
|
||||
return settings;
|
||||
return baseStore.settings;
|
||||
},
|
||||
initialize: baseStore.initialize,
|
||||
set: baseStore.set,
|
||||
update: baseStore.update,
|
||||
reset: baseStore.reset,
|
||||
getDefaults: baseStore.getDefaults,
|
||||
toggleImmersiveMode: baseStore.toggleImmersiveMode,
|
||||
|
||||
// Convenience getters
|
||||
get defaultView() {
|
||||
return settings.defaultView;
|
||||
return baseStore.settings.defaultView;
|
||||
},
|
||||
get weekStartsOn() {
|
||||
return settings.weekStartsOn;
|
||||
return baseStore.settings.weekStartsOn;
|
||||
},
|
||||
get showOnlyWeekdays() {
|
||||
return settings.showOnlyWeekdays;
|
||||
return baseStore.settings.showOnlyWeekdays;
|
||||
},
|
||||
get showWeekNumbers() {
|
||||
return settings.showWeekNumbers;
|
||||
return baseStore.settings.showWeekNumbers;
|
||||
},
|
||||
get timeFormat() {
|
||||
return settings.timeFormat;
|
||||
return baseStore.settings.timeFormat;
|
||||
},
|
||||
get filterHoursEnabled() {
|
||||
return settings.filterHoursEnabled;
|
||||
return baseStore.settings.filterHoursEnabled;
|
||||
},
|
||||
get dayStartHour() {
|
||||
return settings.dayStartHour;
|
||||
return baseStore.settings.dayStartHour;
|
||||
},
|
||||
get dayEndHour() {
|
||||
return settings.dayEndHour;
|
||||
return baseStore.settings.dayEndHour;
|
||||
},
|
||||
get allDayDisplayMode() {
|
||||
return settings.allDayDisplayMode;
|
||||
return baseStore.settings.allDayDisplayMode;
|
||||
},
|
||||
// Header settings
|
||||
get headerCompact() {
|
||||
return settings.headerCompact;
|
||||
return baseStore.settings.headerCompact;
|
||||
},
|
||||
get headerWeekdayFormat() {
|
||||
return settings.headerWeekdayFormat;
|
||||
return baseStore.settings.headerWeekdayFormat;
|
||||
},
|
||||
get headerShowDate() {
|
||||
return settings.headerShowDate;
|
||||
return baseStore.settings.headerShowDate;
|
||||
},
|
||||
get headerAlwaysShowMonth() {
|
||||
return settings.headerAlwaysShowMonth;
|
||||
return baseStore.settings.headerAlwaysShowMonth;
|
||||
},
|
||||
// DateStrip settings
|
||||
get dateStripShowMoonPhases() {
|
||||
return settings.dateStripShowMoonPhases;
|
||||
return baseStore.settings.dateStripShowMoonPhases;
|
||||
},
|
||||
get dateStripShowEventIndicators() {
|
||||
return settings.dateStripShowEventIndicators;
|
||||
return baseStore.settings.dateStripShowEventIndicators;
|
||||
},
|
||||
get dateStripShowWeekday() {
|
||||
return settings.dateStripShowWeekday;
|
||||
return baseStore.settings.dateStripShowWeekday;
|
||||
},
|
||||
get dateStripHighlightWeekends() {
|
||||
return settings.dateStripHighlightWeekends;
|
||||
return baseStore.settings.dateStripHighlightWeekends;
|
||||
},
|
||||
get dateStripShowMonthDividers() {
|
||||
return settings.dateStripShowMonthDividers;
|
||||
return baseStore.settings.dateStripShowMonthDividers;
|
||||
},
|
||||
get dateStripCompact() {
|
||||
return settings.dateStripCompact;
|
||||
return baseStore.settings.dateStripCompact;
|
||||
},
|
||||
get dateStripShowWeekNumbers() {
|
||||
return settings.dateStripShowWeekNumbers;
|
||||
return baseStore.settings.dateStripShowWeekNumbers;
|
||||
},
|
||||
get dateStripCollapsed() {
|
||||
return settings.dateStripCollapsed;
|
||||
return baseStore.settings.dateStripCollapsed;
|
||||
},
|
||||
// TagStrip settings
|
||||
get tagStripCollapsed() {
|
||||
return settings.tagStripCollapsed;
|
||||
return baseStore.settings.tagStripCollapsed;
|
||||
},
|
||||
get selectedTagIds() {
|
||||
return settings.selectedTagIds;
|
||||
return baseStore.settings.selectedTagIds;
|
||||
},
|
||||
get hasSelectedTags() {
|
||||
return settings.selectedTagIds.length > 0;
|
||||
return baseStore.settings.selectedTagIds.length > 0;
|
||||
},
|
||||
// Immersive Mode settings
|
||||
get immersiveModeEnabled() {
|
||||
return settings.immersiveModeEnabled;
|
||||
return baseStore.settings.immersiveModeEnabled;
|
||||
},
|
||||
// Birthday settings
|
||||
get showBirthdays() {
|
||||
return settings.showBirthdays;
|
||||
return baseStore.settings.showBirthdays;
|
||||
},
|
||||
get showBirthdayAge() {
|
||||
return settings.showBirthdayAge;
|
||||
return baseStore.settings.showBirthdayAge;
|
||||
},
|
||||
get defaultEventDuration() {
|
||||
return settings.defaultEventDuration;
|
||||
return baseStore.settings.defaultEventDuration;
|
||||
},
|
||||
get defaultReminder() {
|
||||
return settings.defaultReminder;
|
||||
return baseStore.settings.defaultReminder;
|
||||
},
|
||||
get sidebarCollapsed() {
|
||||
return settings.sidebarCollapsed;
|
||||
return baseStore.settings.sidebarCollapsed;
|
||||
},
|
||||
get quickViewPillViews() {
|
||||
return settings.quickViewPillViews;
|
||||
return baseStore.settings.quickViewPillViews;
|
||||
},
|
||||
get customDayCount() {
|
||||
return settings.customDayCount;
|
||||
return baseStore.settings.customDayCount;
|
||||
},
|
||||
get sttLanguage() {
|
||||
return settings.sttLanguage;
|
||||
return baseStore.settings.sttLanguage;
|
||||
},
|
||||
get cloudSyncEnabled() {
|
||||
return cloudSyncEnabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable cloud sync and load settings from cloud
|
||||
*/
|
||||
// Cloud sync methods
|
||||
enableCloudSync() {
|
||||
cloudSyncEnabled = true;
|
||||
|
||||
// On first sync, prefer cloud settings over local if they exist
|
||||
if (!initialSyncDone) {
|
||||
const cloudSettings = loadFromCloud();
|
||||
if (cloudSettings && Object.keys(cloudSettings).length > 0) {
|
||||
settings = { ...DEFAULT_SETTINGS, ...settings, ...cloudSettings };
|
||||
saveSettings(settings);
|
||||
baseStore.update(cloudSettings);
|
||||
} else {
|
||||
// No cloud settings yet, push local settings to cloud
|
||||
syncToCloud();
|
||||
syncToCloud(baseStore.settings);
|
||||
}
|
||||
initialSyncDone = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable cloud sync
|
||||
*/
|
||||
disableCloudSync() {
|
||||
cloudSyncEnabled = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle sidebar collapsed state
|
||||
*/
|
||||
// Calendar-specific toggle methods
|
||||
toggleSidebar() {
|
||||
settings = { ...settings, sidebarCollapsed: !settings.sidebarCollapsed };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
baseStore.set('sidebarCollapsed', !baseStore.settings.sidebarCollapsed);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle TagStrip visibility
|
||||
*/
|
||||
toggleTagStrip() {
|
||||
settings = { ...settings, tagStripCollapsed: !settings.tagStripCollapsed };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
baseStore.set('tagStripCollapsed', !baseStore.settings.tagStripCollapsed);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle a tag selection for filtering
|
||||
*/
|
||||
toggleTagSelection(tagId: string) {
|
||||
const currentIds = settings.selectedTagIds;
|
||||
const currentIds = baseStore.settings.selectedTagIds;
|
||||
const isSelected = currentIds.includes(tagId);
|
||||
const newIds = isSelected ? currentIds.filter((id) => id !== tagId) : [...currentIds, tagId];
|
||||
settings = { ...settings, selectedTagIds: newIds };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
baseStore.set('selectedTagIds', newIds);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a tag is selected
|
||||
*/
|
||||
isTagSelected(tagId: string): boolean {
|
||||
return settings.selectedTagIds.includes(tagId);
|
||||
return baseStore.settings.selectedTagIds.includes(tagId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all tag selections
|
||||
*/
|
||||
clearTagSelection() {
|
||||
settings = { ...settings, selectedTagIds: [] };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
baseStore.set('selectedTagIds', []);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle Immersive Mode (fullscreen, hide all UI)
|
||||
*/
|
||||
toggleImmersiveMode() {
|
||||
settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize settings from localStorage
|
||||
*/
|
||||
initialize() {
|
||||
if (!browser) return;
|
||||
settings = loadSettings();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a single setting
|
||||
*/
|
||||
set<K extends keyof CalendarAppSettings>(key: K, value: CalendarAppSettings[K]) {
|
||||
settings = { ...settings, [key]: value };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update multiple settings at once
|
||||
*/
|
||||
update(updates: Partial<CalendarAppSettings>) {
|
||||
settings = { ...settings, ...updates };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset all settings to defaults
|
||||
*/
|
||||
reset() {
|
||||
settings = { ...DEFAULT_SETTINGS };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
},
|
||||
|
||||
/**
|
||||
* Format time according to user preference
|
||||
*/
|
||||
// Time formatting helpers
|
||||
formatTime(date: Date): string {
|
||||
if (settings.timeFormat === '12h') {
|
||||
if (baseStore.settings.timeFormat === '12h') {
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
|
|
@ -419,11 +307,8 @@ export const settingsStore = {
|
|||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format hour label according to user preference
|
||||
*/
|
||||
formatHour(hour: number): string {
|
||||
if (settings.timeFormat === '12h') {
|
||||
if (baseStore.settings.timeFormat === '12h') {
|
||||
const ampm = hour >= 12 ? 'PM' : 'AM';
|
||||
const displayHour = hour % 12 || 12;
|
||||
return `${displayHour} ${ampm}`;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
"@manacore/shared-icons": "workspace:*",
|
||||
"@manacore/shared-profile-ui": "workspace:*",
|
||||
"@manacore/shared-splitscreen": "workspace:*",
|
||||
"@manacore/shared-stores": "workspace:*",
|
||||
"@manacore/shared-subscription-ui": "workspace:*",
|
||||
"@manacore/shared-tags": "workspace:*",
|
||||
"@manacore/shared-tailwind": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* Settings Store - Manages user preferences for the Contacts app
|
||||
* Uses Svelte 5 runes and localStorage for persistence
|
||||
* Uses @manacore/shared-stores createAppSettingsStore factory
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createAppSettingsStore } from '@manacore/shared-stores';
|
||||
|
||||
// Settings types
|
||||
export type ContactSortBy = 'name' | 'company' | 'created' | 'updated';
|
||||
|
|
@ -13,61 +13,39 @@ export type DateFormat = 'dd.MM.yyyy' | 'MM/dd/yyyy' | 'yyyy-MM-dd';
|
|||
|
||||
export interface ContactsAppSettings {
|
||||
// Display Settings
|
||||
/** Default view mode for contacts list */
|
||||
defaultView: ContactView;
|
||||
/** Default sort field */
|
||||
sortBy: ContactSortBy;
|
||||
/** Default sort order */
|
||||
sortOrder: ContactSortOrder;
|
||||
/** Show contact photos in list */
|
||||
showPhotos: boolean;
|
||||
/** Show company name in list */
|
||||
showCompany: boolean;
|
||||
/** Contacts per page in list view */
|
||||
contactsPerPage: number;
|
||||
|
||||
// Contact Display
|
||||
/** Display name format: 'first-last' or 'last-first' */
|
||||
nameFormat: 'first-last' | 'last-first';
|
||||
/** Date format for birthdays etc. */
|
||||
dateFormat: DateFormat;
|
||||
/** Show birthday reminders */
|
||||
showBirthdayReminders: boolean;
|
||||
/** Days before birthday to remind */
|
||||
birthdayReminderDays: number;
|
||||
|
||||
// Import/Export
|
||||
/** Default export format */
|
||||
defaultExportFormat: 'vcf' | 'csv' | 'json';
|
||||
/** Include notes in export */
|
||||
includeNotesInExport: boolean;
|
||||
/** Include photos in export */
|
||||
includePhotosInExport: boolean;
|
||||
|
||||
// Duplicates
|
||||
/** Auto-detect duplicates on import */
|
||||
autoDetectDuplicates: boolean;
|
||||
/** Duplicate detection sensitivity: 'strict' | 'normal' | 'loose' */
|
||||
duplicateSensitivity: 'strict' | 'normal' | 'loose';
|
||||
|
||||
// Privacy
|
||||
/** Blur contact photos by default (privacy mode) */
|
||||
privacyMode: boolean;
|
||||
/** Require confirmation before sharing contact */
|
||||
confirmBeforeSharing: boolean;
|
||||
|
||||
// Alphabet Navigation Settings
|
||||
/** Hide letters that have no contacts */
|
||||
alphabetNavHideInactive: boolean;
|
||||
/** Use compact/smaller alphabet buttons */
|
||||
alphabetNavCompact: boolean;
|
||||
/** Reverse letter order (Z-A instead of A-Z) */
|
||||
alphabetNavReverseOrder: boolean;
|
||||
/** Show # symbol for non-letter names */
|
||||
alphabetNavShowHash: boolean;
|
||||
|
||||
// Immersive Mode
|
||||
/** Fullscreen mode - hides all UI elements */
|
||||
immersiveModeEnabled: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -109,170 +87,90 @@ const DEFAULT_SETTINGS: ContactsAppSettings = {
|
|||
immersiveModeEnabled: false,
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'contacts-settings';
|
||||
|
||||
// Load settings from localStorage
|
||||
function loadSettings(): ContactsAppSettings {
|
||||
if (!browser) return DEFAULT_SETTINGS;
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
// Merge with defaults to handle new settings added in updates
|
||||
return { ...DEFAULT_SETTINGS, ...parsed };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load contacts settings:', e);
|
||||
}
|
||||
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
// Save settings to localStorage
|
||||
function saveSettings(settings: ContactsAppSettings) {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch (e) {
|
||||
console.error('Failed to save contacts settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// State
|
||||
let settings = $state<ContactsAppSettings>(loadSettings());
|
||||
// Create base store using factory
|
||||
const baseStore = createAppSettingsStore<ContactsAppSettings>(
|
||||
'contacts-settings',
|
||||
DEFAULT_SETTINGS
|
||||
);
|
||||
|
||||
// Export with convenience getters for backwards compatibility
|
||||
export const contactsSettings = {
|
||||
// Full settings object
|
||||
// Base store methods
|
||||
get settings() {
|
||||
return settings;
|
||||
return baseStore.settings;
|
||||
},
|
||||
initialize: baseStore.initialize,
|
||||
set: baseStore.set,
|
||||
update: baseStore.update,
|
||||
reset: baseStore.reset,
|
||||
getDefaults: baseStore.getDefaults,
|
||||
toggleImmersiveMode: baseStore.toggleImmersiveMode,
|
||||
|
||||
// Display Settings
|
||||
// Convenience getters (backwards compatible)
|
||||
get defaultView() {
|
||||
return settings.defaultView;
|
||||
return baseStore.settings.defaultView;
|
||||
},
|
||||
get sortBy() {
|
||||
return settings.sortBy;
|
||||
return baseStore.settings.sortBy;
|
||||
},
|
||||
get sortOrder() {
|
||||
return settings.sortOrder;
|
||||
return baseStore.settings.sortOrder;
|
||||
},
|
||||
get showPhotos() {
|
||||
return settings.showPhotos;
|
||||
return baseStore.settings.showPhotos;
|
||||
},
|
||||
get showCompany() {
|
||||
return settings.showCompany;
|
||||
return baseStore.settings.showCompany;
|
||||
},
|
||||
get contactsPerPage() {
|
||||
return settings.contactsPerPage;
|
||||
return baseStore.settings.contactsPerPage;
|
||||
},
|
||||
|
||||
// Contact Display
|
||||
get nameFormat() {
|
||||
return settings.nameFormat;
|
||||
return baseStore.settings.nameFormat;
|
||||
},
|
||||
get dateFormat() {
|
||||
return settings.dateFormat;
|
||||
return baseStore.settings.dateFormat;
|
||||
},
|
||||
get showBirthdayReminders() {
|
||||
return settings.showBirthdayReminders;
|
||||
return baseStore.settings.showBirthdayReminders;
|
||||
},
|
||||
get birthdayReminderDays() {
|
||||
return settings.birthdayReminderDays;
|
||||
return baseStore.settings.birthdayReminderDays;
|
||||
},
|
||||
|
||||
// Import/Export
|
||||
get defaultExportFormat() {
|
||||
return settings.defaultExportFormat;
|
||||
return baseStore.settings.defaultExportFormat;
|
||||
},
|
||||
get includeNotesInExport() {
|
||||
return settings.includeNotesInExport;
|
||||
return baseStore.settings.includeNotesInExport;
|
||||
},
|
||||
get includePhotosInExport() {
|
||||
return settings.includePhotosInExport;
|
||||
return baseStore.settings.includePhotosInExport;
|
||||
},
|
||||
|
||||
// Duplicates
|
||||
get autoDetectDuplicates() {
|
||||
return settings.autoDetectDuplicates;
|
||||
return baseStore.settings.autoDetectDuplicates;
|
||||
},
|
||||
get duplicateSensitivity() {
|
||||
return settings.duplicateSensitivity;
|
||||
return baseStore.settings.duplicateSensitivity;
|
||||
},
|
||||
|
||||
// Privacy
|
||||
get privacyMode() {
|
||||
return settings.privacyMode;
|
||||
return baseStore.settings.privacyMode;
|
||||
},
|
||||
get confirmBeforeSharing() {
|
||||
return settings.confirmBeforeSharing;
|
||||
return baseStore.settings.confirmBeforeSharing;
|
||||
},
|
||||
|
||||
// Alphabet Navigation
|
||||
get alphabetNavHideInactive() {
|
||||
return settings.alphabetNavHideInactive;
|
||||
return baseStore.settings.alphabetNavHideInactive;
|
||||
},
|
||||
get alphabetNavCompact() {
|
||||
return settings.alphabetNavCompact;
|
||||
return baseStore.settings.alphabetNavCompact;
|
||||
},
|
||||
get alphabetNavReverseOrder() {
|
||||
return settings.alphabetNavReverseOrder;
|
||||
return baseStore.settings.alphabetNavReverseOrder;
|
||||
},
|
||||
get alphabetNavShowHash() {
|
||||
return settings.alphabetNavShowHash;
|
||||
return baseStore.settings.alphabetNavShowHash;
|
||||
},
|
||||
|
||||
// Immersive Mode
|
||||
get immersiveModeEnabled() {
|
||||
return settings.immersiveModeEnabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle Immersive Mode (fullscreen, hide all UI)
|
||||
*/
|
||||
toggleImmersiveMode() {
|
||||
settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize settings from localStorage
|
||||
*/
|
||||
initialize() {
|
||||
if (!browser) return;
|
||||
settings = loadSettings();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a single setting
|
||||
*/
|
||||
set<K extends keyof ContactsAppSettings>(key: K, value: ContactsAppSettings[K]) {
|
||||
settings = { ...settings, [key]: value };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update multiple settings at once
|
||||
*/
|
||||
update(updates: Partial<ContactsAppSettings>) {
|
||||
settings = { ...settings, ...updates };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset all settings to defaults
|
||||
*/
|
||||
reset() {
|
||||
settings = { ...DEFAULT_SETTINGS };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get default settings (for reference)
|
||||
*/
|
||||
getDefaults() {
|
||||
return DEFAULT_SETTINGS;
|
||||
return baseStore.settings.immersiveModeEnabled;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
"@manacore/shared-api-client": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-splitscreen": "workspace:*",
|
||||
"@manacore/shared-stores": "workspace:*",
|
||||
"@manacore/shared-types": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
"@manacore/shared-tags": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* Settings Store - Manages user preferences for the Todo app
|
||||
* Uses Svelte 5 runes and localStorage for persistence
|
||||
* Uses @manacore/shared-stores createAppSettingsStore factory
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createAppSettingsStore } from '@manacore/shared-stores';
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
|
||||
// Settings types
|
||||
|
|
@ -12,55 +12,35 @@ export type KanbanCardSize = 'compact' | 'normal' | 'large';
|
|||
|
||||
export interface TodoAppSettings {
|
||||
// Task Behavior
|
||||
/** Default priority for new tasks */
|
||||
defaultPriority: TaskPriority;
|
||||
/** Default due time for tasks (HH:mm format, null = no default) */
|
||||
defaultDueTime: string | null;
|
||||
/** Auto-archive completed tasks after X days (null = disabled) */
|
||||
autoArchiveCompletedDays: number | null;
|
||||
/** Default project for quick add (null = inbox) */
|
||||
quickAddProject: string | null;
|
||||
|
||||
// View & Display
|
||||
/** Default view when opening the app */
|
||||
defaultView: TodoView;
|
||||
/** Show task counts as badges in navigation */
|
||||
showTaskCounts: boolean;
|
||||
/** Compact mode with reduced padding */
|
||||
compactMode: boolean;
|
||||
/** Show progress bar for subtasks */
|
||||
showSubtaskProgress: boolean;
|
||||
/** Group tasks by project in list views */
|
||||
groupByProject: boolean;
|
||||
|
||||
// Kanban Board
|
||||
/** Kanban card size */
|
||||
kanbanCardSize: KanbanCardSize;
|
||||
/** Show labels on kanban cards */
|
||||
showLabelsOnCards: boolean;
|
||||
/** Work-in-progress limit per column (null = unlimited) */
|
||||
wipLimitPerColumn: number | null;
|
||||
|
||||
// Notifications & Reminders
|
||||
/** Default reminder time in minutes before due (null = no default) */
|
||||
defaultReminderMinutes: number | null;
|
||||
/** Enable daily digest email/notification */
|
||||
dailyDigestEnabled: boolean;
|
||||
/** Notify about overdue tasks */
|
||||
overdueNotifications: boolean;
|
||||
|
||||
// Productivity
|
||||
/** Focus mode - show only current task */
|
||||
focusMode: boolean;
|
||||
/** Enable pomodoro timer */
|
||||
pomodoroEnabled: boolean;
|
||||
/** Daily task completion goal (null = no goal) */
|
||||
dailyGoal: number | null;
|
||||
/** Show productivity streak */
|
||||
showStreak: boolean;
|
||||
|
||||
// Immersive Mode
|
||||
/** Fullscreen mode - hides all UI elements */
|
||||
immersiveModeEnabled: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -98,162 +78,81 @@ const DEFAULT_SETTINGS: TodoAppSettings = {
|
|||
immersiveModeEnabled: false,
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'todo-settings';
|
||||
|
||||
// Load settings from localStorage
|
||||
function loadSettings(): TodoAppSettings {
|
||||
if (!browser) return DEFAULT_SETTINGS;
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
// Merge with defaults to handle new settings added in updates
|
||||
return { ...DEFAULT_SETTINGS, ...parsed };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load todo settings:', e);
|
||||
}
|
||||
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
// Save settings to localStorage
|
||||
function saveSettings(settings: TodoAppSettings) {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch (e) {
|
||||
console.error('Failed to save todo settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// State
|
||||
let settings = $state<TodoAppSettings>(loadSettings());
|
||||
// Create base store using factory
|
||||
const baseStore = createAppSettingsStore<TodoAppSettings>('todo-settings', DEFAULT_SETTINGS);
|
||||
|
||||
// Export with convenience getters for backwards compatibility
|
||||
export const todoSettings = {
|
||||
// Full settings object
|
||||
// Base store methods
|
||||
get settings() {
|
||||
return settings;
|
||||
return baseStore.settings;
|
||||
},
|
||||
initialize: baseStore.initialize,
|
||||
set: baseStore.set,
|
||||
update: baseStore.update,
|
||||
reset: baseStore.reset,
|
||||
getDefaults: baseStore.getDefaults,
|
||||
toggleImmersiveMode: baseStore.toggleImmersiveMode,
|
||||
|
||||
// Task Behavior
|
||||
// Convenience getters (backwards compatible)
|
||||
get defaultPriority() {
|
||||
return settings.defaultPriority;
|
||||
return baseStore.settings.defaultPriority;
|
||||
},
|
||||
get defaultDueTime() {
|
||||
return settings.defaultDueTime;
|
||||
return baseStore.settings.defaultDueTime;
|
||||
},
|
||||
get autoArchiveCompletedDays() {
|
||||
return settings.autoArchiveCompletedDays;
|
||||
return baseStore.settings.autoArchiveCompletedDays;
|
||||
},
|
||||
get quickAddProject() {
|
||||
return settings.quickAddProject;
|
||||
return baseStore.settings.quickAddProject;
|
||||
},
|
||||
|
||||
// View & Display
|
||||
get defaultView() {
|
||||
return settings.defaultView;
|
||||
return baseStore.settings.defaultView;
|
||||
},
|
||||
get showTaskCounts() {
|
||||
return settings.showTaskCounts;
|
||||
return baseStore.settings.showTaskCounts;
|
||||
},
|
||||
get compactMode() {
|
||||
return settings.compactMode;
|
||||
return baseStore.settings.compactMode;
|
||||
},
|
||||
get showSubtaskProgress() {
|
||||
return settings.showSubtaskProgress;
|
||||
return baseStore.settings.showSubtaskProgress;
|
||||
},
|
||||
get groupByProject() {
|
||||
return settings.groupByProject;
|
||||
return baseStore.settings.groupByProject;
|
||||
},
|
||||
|
||||
// Kanban Board
|
||||
get kanbanCardSize() {
|
||||
return settings.kanbanCardSize;
|
||||
return baseStore.settings.kanbanCardSize;
|
||||
},
|
||||
get showLabelsOnCards() {
|
||||
return settings.showLabelsOnCards;
|
||||
return baseStore.settings.showLabelsOnCards;
|
||||
},
|
||||
get wipLimitPerColumn() {
|
||||
return settings.wipLimitPerColumn;
|
||||
return baseStore.settings.wipLimitPerColumn;
|
||||
},
|
||||
|
||||
// Notifications & Reminders
|
||||
get defaultReminderMinutes() {
|
||||
return settings.defaultReminderMinutes;
|
||||
return baseStore.settings.defaultReminderMinutes;
|
||||
},
|
||||
get dailyDigestEnabled() {
|
||||
return settings.dailyDigestEnabled;
|
||||
return baseStore.settings.dailyDigestEnabled;
|
||||
},
|
||||
get overdueNotifications() {
|
||||
return settings.overdueNotifications;
|
||||
return baseStore.settings.overdueNotifications;
|
||||
},
|
||||
|
||||
// Productivity
|
||||
get focusMode() {
|
||||
return settings.focusMode;
|
||||
return baseStore.settings.focusMode;
|
||||
},
|
||||
get pomodoroEnabled() {
|
||||
return settings.pomodoroEnabled;
|
||||
return baseStore.settings.pomodoroEnabled;
|
||||
},
|
||||
get dailyGoal() {
|
||||
return settings.dailyGoal;
|
||||
return baseStore.settings.dailyGoal;
|
||||
},
|
||||
get showStreak() {
|
||||
return settings.showStreak;
|
||||
return baseStore.settings.showStreak;
|
||||
},
|
||||
|
||||
// Immersive Mode
|
||||
get immersiveModeEnabled() {
|
||||
return settings.immersiveModeEnabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle Immersive Mode (fullscreen, hide all UI)
|
||||
*/
|
||||
toggleImmersiveMode() {
|
||||
settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize settings from localStorage
|
||||
*/
|
||||
initialize() {
|
||||
if (!browser) return;
|
||||
settings = loadSettings();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a single setting
|
||||
*/
|
||||
set<K extends keyof TodoAppSettings>(key: K, value: TodoAppSettings[K]) {
|
||||
settings = { ...settings, [key]: value };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update multiple settings at once
|
||||
*/
|
||||
update(updates: Partial<TodoAppSettings>) {
|
||||
settings = { ...settings, ...updates };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset all settings to defaults
|
||||
*/
|
||||
reset() {
|
||||
settings = { ...DEFAULT_SETTINGS };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get default settings (for reference)
|
||||
*/
|
||||
getDefaults() {
|
||||
return DEFAULT_SETTINGS;
|
||||
return baseStore.settings.immersiveModeEnabled;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|-----------|---------|----------------------|---------|
|
||||
| ~~**KRITISCH**~~ | ~~Backend Metrics Migration~~ | ~~350 LOC~~ ✅ **709 LOC entfernt** | ~~Niedrig~~ |
|
||||
| **HOCH** | Skeleton Components | 800-1.000 LOC | Mittel |
|
||||
| **HOCH** | App Settings Stores | 600-700 LOC | Mittel |
|
||||
| ~~**HOCH**~~ | ~~App Settings Stores~~ | ~~600-700 LOC~~ ✅ **323 LOC entfernt** | ~~Mittel~~ |
|
||||
| **HOCH** | Main.ts/CORS Patterns | 1.800 LOC | Mittel |
|
||||
| **MITTEL** | TypeScript Configs | 400 LOC | Niedrig |
|
||||
| **MITTEL** | UI Component Cleanup | 400 LOC | Niedrig |
|
||||
|
|
@ -146,54 +146,32 @@ import { HealthModule } from '@manacore/shared-nestjs-health';
|
|||
|
||||
## 2. Frontend Stores (Svelte 5)
|
||||
|
||||
### 2.1 HOCH: App Settings Stores (600-700 LOC)
|
||||
### ~~2.1 HOCH: App Settings Stores~~ ✅ ERLEDIGT (323 LOC gespart)
|
||||
|
||||
**Problem:** 3 Apps (todo, calendar, contacts) haben fast identische Settings-Store Implementierungen mit localStorage Persistenz.
|
||||
**Status:** `createAppSettingsStore<T>()` Factory erstellt und 3 Apps migriert (29.01.2026)
|
||||
|
||||
**Betroffene Dateien:**
|
||||
- `apps/todo/apps/web/src/lib/stores/settings.svelte.ts` (259 LOC)
|
||||
- `apps/calendar/apps/web/src/lib/stores/settings.svelte.ts` (433 LOC)
|
||||
- `apps/contacts/apps/web/src/lib/stores/settings.svelte.ts` (278 LOC)
|
||||
**Erstellte Factory:** `packages/shared-stores/src/settings.svelte.ts`
|
||||
- Type-safe Settings Store mit localStorage Persistenz
|
||||
- Optional: `onSettingsChange` Callback für Cloud-Sync
|
||||
- Reduziert Boilerplate von ~100 LOC pro App auf ~20 LOC
|
||||
|
||||
**Dupliziertes Pattern (100% identisch):**
|
||||
```typescript
|
||||
// Boilerplate in jedem (80-100 LOC):
|
||||
- TypeScript Interface für Settings
|
||||
- DEFAULT_SETTINGS Konstante
|
||||
- STORAGE_KEY
|
||||
- loadSettings() - localStorage laden + merge mit defaults
|
||||
- saveSettings() - localStorage speichern
|
||||
- let settings = $state(...)
|
||||
- toggleImmersiveMode(), initialize(), set(), update(), reset(), getDefaults()
|
||||
```
|
||||
|
||||
**Empfehlung:** Erstelle `createAppSettingsStore<T>()` Factory in `@manacore/shared-stores`
|
||||
**Migrierte Apps:**
|
||||
- ~~`apps/todo/apps/web/src/lib/stores/settings.svelte.ts`~~ ✅ (259 → 159 LOC = 100 LOC)
|
||||
- ~~`apps/contacts/apps/web/src/lib/stores/settings.svelte.ts`~~ ✅ (278 → 173 LOC = 105 LOC)
|
||||
- ~~`apps/calendar/apps/web/src/lib/stores/settings.svelte.ts`~~ ✅ (433 → 315 LOC = 118 LOC)
|
||||
|
||||
```typescript
|
||||
// packages/shared-stores/src/createAppSettingsStore.ts
|
||||
export function createAppSettingsStore<T extends Record<string, any>>(
|
||||
storageKey: string,
|
||||
defaultSettings: T,
|
||||
options?: { cloudSync?: boolean }
|
||||
) {
|
||||
let settings = $state<T>(defaultSettings);
|
||||
|
||||
function loadSettings(): T { /* localStorage logic */ }
|
||||
function saveSettings(newSettings: T): void { /* localStorage logic */ }
|
||||
|
||||
return {
|
||||
get value() { return settings; },
|
||||
initialize() { settings = loadSettings(); },
|
||||
set<K extends keyof T>(key: K, value: T[K]) { /* ... */ },
|
||||
update(updates: Partial<T>) { /* ... */ },
|
||||
reset() { settings = defaultSettings; saveSettings(settings); },
|
||||
getDefaults() { return defaultSettings; },
|
||||
};
|
||||
}
|
||||
// Nachher (Beispiel Todo)
|
||||
import { createAppSettingsStore } from '@manacore/shared-stores';
|
||||
const baseStore = createAppSettingsStore<TodoAppSettings>('todo-settings', DEFAULT_SETTINGS);
|
||||
export const todoSettings = {
|
||||
get settings() { return baseStore.settings; },
|
||||
initialize: baseStore.initialize,
|
||||
set: baseStore.set,
|
||||
// ... convenience getters
|
||||
};
|
||||
```
|
||||
|
||||
**Einsparung:** ~200 LOC Boilerplate pro App = 600 LOC
|
||||
|
||||
---
|
||||
|
||||
### 2.2 MITTEL: Navigation Stores (50 LOC)
|
||||
|
|
@ -409,12 +387,12 @@ export default createDrizzleConfig('chat');
|
|||
|
||||
### Phase 2: Stores & Configs (3-5 Tage, ~1.500 LOC)
|
||||
|
||||
| Aufgabe | LOC | Aufwand |
|
||||
|---------|-----|---------|
|
||||
| `createAppSettingsStore()` Factory erstellen | 600 | Mittel |
|
||||
| `@manacore/shared-tsconfig` Package erstellen | 400 | Niedrig |
|
||||
| `@manacore/shared-vite-config` Factory erstellen | 300 | Niedrig |
|
||||
| Navigation Store Factory erstellen | 50 | Niedrig |
|
||||
| Aufgabe | LOC | Aufwand | Status |
|
||||
|---------|-----|---------|--------|
|
||||
| ~~`createAppSettingsStore()` Factory erstellen~~ | ~~600~~ → **323** | ~~Mittel~~ | ✅ Erledigt |
|
||||
| `@manacore/shared-tsconfig` Package erstellen | 400 | Niedrig | Offen |
|
||||
| `@manacore/shared-vite-config` Factory erstellen | 300 | Niedrig | Offen |
|
||||
| Navigation Store Factory erstellen | 50 | Niedrig | Offen |
|
||||
|
||||
### Phase 3: Backend Setup (5-7 Tage, ~2.000 LOC)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,3 +10,8 @@ export {
|
|||
type NavigationStore,
|
||||
} from './navigation.svelte';
|
||||
export { createThemeStore, type ThemeStore, type ThemeMode } from './theme.svelte';
|
||||
export {
|
||||
createAppSettingsStore,
|
||||
type AppSettingsStore,
|
||||
type AppSettingsStoreOptions,
|
||||
} from './settings.svelte';
|
||||
|
|
|
|||
149
packages/shared-stores/src/settings.svelte.ts
Normal file
149
packages/shared-stores/src/settings.svelte.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* App Settings Store Factory
|
||||
*
|
||||
* Creates a type-safe settings store with localStorage persistence.
|
||||
* Reduces ~200 LOC boilerplate per app to ~20 LOC.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* interface MyAppSettings {
|
||||
* theme: 'light' | 'dark';
|
||||
* sidebarCollapsed: boolean;
|
||||
* immersiveModeEnabled: boolean;
|
||||
* }
|
||||
*
|
||||
* const DEFAULT_SETTINGS: MyAppSettings = {
|
||||
* theme: 'light',
|
||||
* sidebarCollapsed: false,
|
||||
* immersiveModeEnabled: false,
|
||||
* };
|
||||
*
|
||||
* export const settingsStore = createAppSettingsStore('my-app-settings', DEFAULT_SETTINGS);
|
||||
*
|
||||
* // Usage:
|
||||
* settingsStore.settings.theme // 'light'
|
||||
* settingsStore.set('theme', 'dark');
|
||||
* settingsStore.update({ sidebarCollapsed: true });
|
||||
* settingsStore.toggleImmersiveMode(); // If immersiveModeEnabled exists
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export interface AppSettingsStoreOptions<T> {
|
||||
/**
|
||||
* Callback invoked after each settings change.
|
||||
* Use for cloud sync or other side effects.
|
||||
*/
|
||||
onSettingsChange?: (settings: T) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface AppSettingsStore<T extends Record<string, unknown>> {
|
||||
/** Current settings state (reactive) */
|
||||
readonly settings: T;
|
||||
|
||||
/** Get the default settings */
|
||||
getDefaults(): T;
|
||||
|
||||
/** Initialize settings from localStorage (call on mount) */
|
||||
initialize(): void;
|
||||
|
||||
/** Set a single setting value */
|
||||
set<K extends keyof T>(key: K, value: T[K]): void;
|
||||
|
||||
/** Update multiple settings at once */
|
||||
update(updates: Partial<T>): void;
|
||||
|
||||
/** Reset all settings to defaults */
|
||||
reset(): void;
|
||||
|
||||
/** Toggle immersive mode (if immersiveModeEnabled exists in settings) */
|
||||
toggleImmersiveMode(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a settings store with localStorage persistence.
|
||||
*
|
||||
* @param storageKey - localStorage key for persistence
|
||||
* @param defaultSettings - Default settings object
|
||||
* @param options - Optional configuration (onSettingsChange callback)
|
||||
* @returns AppSettingsStore instance
|
||||
*/
|
||||
export function createAppSettingsStore<T extends Record<string, unknown>>(
|
||||
storageKey: string,
|
||||
defaultSettings: T,
|
||||
options?: AppSettingsStoreOptions<T>
|
||||
): AppSettingsStore<T> {
|
||||
// Load settings from localStorage
|
||||
function loadSettings(): T {
|
||||
if (!browser) return { ...defaultSettings };
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
// Merge with defaults to handle new settings added after initial save
|
||||
return { ...defaultSettings, ...parsed };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to load settings from ${storageKey}:`, e);
|
||||
}
|
||||
|
||||
return { ...defaultSettings };
|
||||
}
|
||||
|
||||
// Save settings to localStorage
|
||||
function saveSettings(newSettings: T): void {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(newSettings));
|
||||
} catch (e) {
|
||||
console.error(`Failed to save settings to ${storageKey}:`, e);
|
||||
}
|
||||
|
||||
// Invoke callback if provided
|
||||
options?.onSettingsChange?.(newSettings);
|
||||
}
|
||||
|
||||
// Reactive state using Svelte 5 runes
|
||||
let settings = $state<T>(loadSettings());
|
||||
|
||||
return {
|
||||
get settings() {
|
||||
return settings;
|
||||
},
|
||||
|
||||
getDefaults() {
|
||||
return { ...defaultSettings };
|
||||
},
|
||||
|
||||
initialize() {
|
||||
if (!browser) return;
|
||||
settings = loadSettings();
|
||||
},
|
||||
|
||||
set<K extends keyof T>(key: K, value: T[K]) {
|
||||
settings = { ...settings, [key]: value };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
update(updates: Partial<T>) {
|
||||
settings = { ...settings, ...updates };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
reset() {
|
||||
settings = { ...defaultSettings };
|
||||
saveSettings(settings);
|
||||
},
|
||||
|
||||
toggleImmersiveMode() {
|
||||
if ('immersiveModeEnabled' in settings) {
|
||||
const current = settings.immersiveModeEnabled as boolean;
|
||||
settings = { ...settings, immersiveModeEnabled: !current };
|
||||
saveSettings(settings);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue