|
| 1 | +import type { RemovableRef } from '@vueuse/core' |
| 2 | +import { useLocalStorage } from '@vueuse/core' |
1 | 3 | import { ACCENT_COLORS, type AccentColorId } from '#shared/utils/constants' |
2 | 4 | import type { LocaleObject } from '@nuxtjs/i18n' |
3 | 5 | import { BACKGROUND_THEMES } from '#shared/utils/constants' |
| 6 | +import { normalizeSearchParam } from '#shared/utils/url' |
4 | 7 |
|
5 | 8 | type BackgroundThemeId = keyof typeof BACKGROUND_THEMES |
6 | 9 |
|
@@ -92,86 +95,22 @@ const DEFAULT_SETTINGS: AppSettings = { |
92 | 95 |
|
93 | 96 | const STORAGE_KEY = 'npmx-settings' |
94 | 97 |
|
95 | | -/** |
96 | | - * Read settings from localStorage and merge with defaults. |
97 | | - */ |
98 | | -function normaliseSettings(input: AppSettings): AppSettings { |
99 | | - return { |
100 | | - ...input, |
101 | | - searchProvider: input.searchProvider === 'npm' ? 'npm' : 'algolia', |
102 | | - sidebar: { |
103 | | - ...input.sidebar, |
104 | | - collapsed: Array.isArray(input.sidebar?.collapsed) |
105 | | - ? input.sidebar.collapsed.filter((v): v is string => typeof v === 'string') |
106 | | - : [], |
107 | | - }, |
108 | | - } |
109 | | -} |
110 | | - |
111 | | -function readFromLocalStorage(): AppSettings { |
112 | | - try { |
113 | | - const raw = localStorage.getItem(STORAGE_KEY) |
114 | | - if (raw) { |
115 | | - const stored = JSON.parse(raw) |
116 | | - return normaliseSettings({ |
117 | | - ...DEFAULT_SETTINGS, |
118 | | - ...stored, |
119 | | - connector: { ...DEFAULT_SETTINGS.connector, ...stored.connector }, |
120 | | - sidebar: { ...DEFAULT_SETTINGS.sidebar, ...stored.sidebar }, |
121 | | - chartFilter: { ...DEFAULT_SETTINGS.chartFilter, ...stored.chartFilter }, |
122 | | - }) |
123 | | - } |
124 | | - } catch {} |
125 | | - return { ...DEFAULT_SETTINGS } |
126 | | -} |
127 | | - |
128 | | -let syncInitialized = false |
| 98 | +// Shared settings instance (singleton per app) |
| 99 | +let settingsRef: RemovableRef<AppSettings> | null = null |
129 | 100 |
|
130 | 101 | /** |
131 | | - * Composable for managing application settings. |
132 | | - * |
133 | | - * Uses useState for SSR-safe hydration (server and client agree on initial |
134 | | - * values during hydration) and syncs with localStorage on the client. |
135 | | - * The onPrehydrate script in prehydrate.ts handles DOM-level patches |
136 | | - * (accent color, bg theme, collapsed sections, etc.) to prevent visual |
137 | | - * flash before hydration. |
| 102 | + * Composable for managing application settings with localStorage persistence. |
| 103 | + * Settings are shared across all components that use this composable. |
138 | 104 | */ |
139 | 105 | export function useSettings() { |
140 | | - const settings = useState<AppSettings>(STORAGE_KEY, () => ({ ...DEFAULT_SETTINGS })) |
141 | | - |
142 | | - if (import.meta.client && !syncInitialized) { |
143 | | - syncInitialized = true |
144 | | - |
145 | | - // Read localStorage eagerly but apply after mount to prevent hydration |
146 | | - // mismatch. During hydration, useState provides server-matching defaults. |
147 | | - // After mount, we swap in the user's actual preferences from localStorage. |
148 | | - // Uses nuxtApp.hook('app:mounted') instead of onMounted so it works even |
149 | | - // when useSettings() is first called from a plugin (no component context). |
150 | | - const stored = readFromLocalStorage() |
151 | | - const nuxtApp = useNuxtApp() |
152 | | - |
153 | | - if (nuxtApp.isHydrating) { |
154 | | - nuxtApp.hook('app:mounted', () => { |
155 | | - settings.value = stored |
156 | | - }) |
157 | | - } else { |
158 | | - settings.value = stored |
159 | | - } |
160 | | - |
161 | | - // Persist future changes back to localStorage |
162 | | - watch( |
163 | | - settings, |
164 | | - value => { |
165 | | - try { |
166 | | - localStorage.setItem(STORAGE_KEY, JSON.stringify(value)) |
167 | | - } catch {} |
168 | | - }, |
169 | | - { deep: true }, |
170 | | - ) |
| 106 | + if (!settingsRef) { |
| 107 | + settingsRef = useLocalStorage<AppSettings>(STORAGE_KEY, DEFAULT_SETTINGS, { |
| 108 | + mergeDefaults: true, |
| 109 | + }) |
171 | 110 | } |
172 | 111 |
|
173 | 112 | return { |
174 | | - settings, |
| 113 | + settings: settingsRef, |
175 | 114 | } |
176 | 115 | } |
177 | 116 |
|
@@ -280,6 +219,31 @@ export function useSearchProvider() { |
280 | 219 | } |
281 | 220 | } |
282 | 221 |
|
| 222 | +/** |
| 223 | + * SSR/hydration-safe search provider for use in async-data cache keys. |
| 224 | + * Defers reading the localStorage-backed preference until after `app:mounted` |
| 225 | + * so the server-rendered key matches initial hydration. |
| 226 | + */ |
| 227 | +export function useEffectiveSearchProvider() { |
| 228 | + const route = useRoute() |
| 229 | + const { searchProvider } = useSearchProvider() |
| 230 | + |
| 231 | + const storedProviderReady = ref(import.meta.server ? false : !useNuxtApp().isHydrating) |
| 232 | + if (import.meta.client && !storedProviderReady.value) { |
| 233 | + useNuxtApp().hook('app:mounted', () => { |
| 234 | + storedProviderReady.value = true |
| 235 | + }) |
| 236 | + } |
| 237 | + |
| 238 | + return computed<SearchProvider>(() => { |
| 239 | + const p = normalizeSearchParam(route.query.p) |
| 240 | + if (p === 'npm') return 'npm' |
| 241 | + if (p === 'algolia') return 'algolia' |
| 242 | + if (storedProviderReady.value && searchProvider.value === 'npm') return 'npm' |
| 243 | + return 'algolia' |
| 244 | + }) |
| 245 | +} |
| 246 | + |
283 | 247 | export function useBackgroundTheme() { |
284 | 248 | const { t } = useI18n() |
285 | 249 |
|
|
0 commit comments