import { schema } from '#triplit/schema'; import { type Entity } from '@triplit/client'; import { computed, watch, ref, type Ref } from 'vue'; type UserSettings = Entity; interface UserSettingsState { accent: Ref; neutral: Ref; hinting: Ref; colorSchemePreference: Ref<'light' | 'dark' | 'system'>; colorSchemeClass: Ref<'light' | 'dark' | undefined>; remoteSettings: Ref; initPromise: Promise | null; } export const useUserSettings = () => { const nuxtApp = useNuxtApp(); const triplit = useTriplitClient(); const { user, loggedIn } = useAuth(); if (!nuxtApp._userSettingsState) { nuxtApp._userSettingsState = { accent: ref('violet'), neutral: ref('zinc'), hinting: ref('0'), colorSchemePreference: ref<'light' | 'dark' | 'system'>('system'), colorSchemeClass: ref(undefined), remoteSettings: ref(null), initPromise: null, } as UserSettingsState; } const state = nuxtApp._userSettingsState as UserSettingsState; const init = (): Promise => { if (state.initPromise) return state.initPromise; state.initPromise = (async () => { const { results } = await useQuery('settings', triplit, triplit.query('settings')); watch(results, (val) => { if (val && val.length > 0) { state.remoteSettings.value = val[0] as UserSettings; } else { state.remoteSettings.value = null; } }, { immediate: true, deep: true }); watch(state.remoteSettings, (newSettings) => { if (!newSettings?.appearance) return; const { appearance } = newSettings; if (appearance.colorScheme) { state.colorSchemePreference.value = appearance.colorScheme as 'light' | 'dark' | 'system'; } if (appearance.accent) { state.accent.value = appearance.accent; } if (appearance.neutral) { state.neutral.value = appearance.neutral; } if (appearance.hinting !== undefined) { state.hinting.value = String(appearance.hinting); } }, { immediate: true, deep: true }); if (import.meta.client) { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const update = () => { if (state.colorSchemePreference.value === 'system') { state.colorSchemeClass.value = mediaQuery.matches ? 'dark' : 'light'; } else { state.colorSchemeClass.value = state.colorSchemePreference.value as 'dark' | 'light'; } }; mediaQuery.addEventListener('change', update); watch(state.colorSchemePreference, update, { immediate: true }); } else { watch(state.colorSchemePreference, (pref) => { state.colorSchemeClass.value = pref === 'system' ? 'dark' : (pref as 'dark' | 'light'); }, { immediate: true }); } })(); return state.initPromise; }; const colorSchemeValue = computed(() => { if (state.colorSchemePreference.value === 'system') { if (import.meta.client) { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } return 'dark'; } return state.colorSchemePreference.value as 'dark' | 'light'; }); const settings = computed(() => { const remote = state.remoteSettings.value; return { appearance: { colorScheme: remote?.appearance?.colorScheme ?? state.colorSchemePreference.value, accent: remote?.appearance?.accent ?? state.accent.value, neutral: remote?.appearance?.neutral ?? state.neutral.value, hinting: remote?.appearance?.hinting ?? Number(state.hinting.value), fontSize: remote?.appearance?.fontSize ?? 'medium', }, systemAssistants: { rename: { enabled: remote?.systemAssistants?.rename?.enabled ?? false, prompt: remote?.systemAssistants?.rename?.prompt ?? null, modelId: remote?.systemAssistants?.rename?.modelId ?? null, } } }; }); const updateSettings = async (updates: { appearance?: { colorScheme?: 'light' | 'dark' | 'system'; accent?: string; neutral?: string; hinting?: number; fontSize?: string; }; systemAssistants?: { rename?: { enabled?: boolean; prompt?: string | null; modelId?: string | null; }; }; }) => { if (updates.appearance) { if (updates.appearance.colorScheme) state.colorSchemePreference.value = updates.appearance.colorScheme; if (updates.appearance.accent) state.accent.value = updates.appearance.accent; if (updates.appearance.neutral) state.neutral.value = updates.appearance.neutral; if (updates.appearance.hinting !== undefined) state.hinting.value = String(updates.appearance.hinting); } if (!loggedIn.value || !user.value?.id) return; const current = state.remoteSettings.value; if (!current) { await triplit.insert('settings', { userId: user.value.id, appearance: { colorScheme: state.colorSchemePreference.value, accent: state.accent.value, neutral: state.neutral.value, hinting: Number(state.hinting.value), ...(updates.appearance || {}) }, systemAssistants: { rename: { enabled: updates.systemAssistants?.rename?.enabled ?? true, prompt: updates.systemAssistants?.rename?.prompt ?? null, modelId: updates.systemAssistants?.rename?.modelId ?? null, } } }); return; } await triplit.update('settings', current.id, (s) => { if (updates.appearance) { s.appearance = { ...(s.appearance || {}), ...updates.appearance }; } if (updates.systemAssistants) { // @ts-expect-error s.systemAssistants = { ...(s.systemAssistants || {}), ...updates.systemAssistants }; } }); }; return { init, settings, remoteSettings: state.remoteSettings, updateSettings, accent: state.accent, neutral: state.neutral, hinting: state.hinting, colorScheme: { preference: state.colorSchemePreference, value: colorSchemeValue, class: state.colorSchemeClass } }; };