diff --git a/src/app/router/index.ts b/src/app/router/index.ts index 97e966a..c4c6523 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -44,6 +44,8 @@ const routes: RouteRecordRaw[] = [ }, { path: "activity/user/:handle", component: () => import("@/features/profile/UserProfilePage.vue") }, { path: "profile", component: () => import("@/features/profile/ProfilePage.vue") }, + { path: "profile/settings", redirect: "/tabs/settings" }, + { path: "settings", component: () => import("@/features/profile/SettingsPage.vue") }, ], }, ]; diff --git a/src/components/repo/MarkdownRenderer.vue b/src/components/repo/MarkdownRenderer.vue index a2aee3f..887b41e 100644 --- a/src/components/repo/MarkdownRenderer.vue +++ b/src/components/repo/MarkdownRenderer.vue @@ -3,239 +3,263 @@ diff --git a/src/core/query/cache.ts b/src/core/query/cache.ts new file mode 100644 index 0000000..6d47866 --- /dev/null +++ b/src/core/query/cache.ts @@ -0,0 +1,8 @@ +import { queryClient } from "./client.js"; +import { createIdbPersister } from "./persister.js"; + +export async function clearAppCache() { + await queryClient.cancelQueries(); + queryClient.clear(); + await createIdbPersister().removeClient(); +} diff --git a/src/core/theme/preferences.ts b/src/core/theme/preferences.ts new file mode 100644 index 0000000..3c54288 --- /dev/null +++ b/src/core/theme/preferences.ts @@ -0,0 +1,87 @@ +import { computed, readonly, ref } from "vue"; + +export type ThemePreference = "system" | "light" | "dark"; + +type ResolvedTheme = "light" | "dark"; + +const STORAGE_KEY = "twisted-theme-preference"; +const DARK_CLASS = "ion-palette-dark"; +const SYSTEM_DARK_QUERY = "(prefers-color-scheme: dark)"; + +const themePreference = ref(readStoredThemePreference()); +const resolvedTheme = ref(resolveThemePreference(themePreference.value)); + +let hasRegisteredMediaListener = false; + +export function useThemePreference() { + return { + themePreference: readonly(themePreference), + resolvedTheme: readonly(resolvedTheme), + isSystemTheme: computed(() => themePreference.value === "system"), + setThemePreference, + }; +} + +export function initializeThemePreference() { + applyResolvedTheme(resolveThemePreference(themePreference.value)); + + if (hasRegisteredMediaListener || typeof window === "undefined" || typeof window.matchMedia !== "function") { + return; + } + + const mediaQuery = window.matchMedia(SYSTEM_DARK_QUERY); + const handleChange = () => { + if (themePreference.value !== "system") return; + applyResolvedTheme(mediaQuery.matches ? "dark" : "light"); + }; + + if (typeof mediaQuery.addEventListener === "function") { + mediaQuery.addEventListener("change", handleChange); + } else if (typeof (mediaQuery as MediaQueryList & { addListener?: typeof handleChange }).addListener === "function") { + (mediaQuery as MediaQueryList & { addListener?: typeof handleChange }).addListener?.(handleChange); + } + + hasRegisteredMediaListener = true; +} + +export function setThemePreference(value: ThemePreference) { + themePreference.value = value; + resolvedTheme.value = resolveThemePreference(value); + applyResolvedTheme(resolvedTheme.value); + + if (typeof window !== "undefined") { + window.localStorage.setItem(STORAGE_KEY, value); + } +} + +function readStoredThemePreference(): ThemePreference { + if (typeof window === "undefined") return "system"; + + const storedValue = window.localStorage.getItem(STORAGE_KEY); + return isThemePreference(storedValue) ? storedValue : "system"; +} + +function resolveThemePreference(value: ThemePreference): ResolvedTheme { + if (value === "dark" || value === "light") return value; + + if (typeof window !== "undefined" && typeof window.matchMedia === "function") { + return window.matchMedia(SYSTEM_DARK_QUERY).matches ? "dark" : "light"; + } + + return "light"; +} + +function applyResolvedTheme(value: ResolvedTheme) { + resolvedTheme.value = value; + + if (typeof document === "undefined") return; + + const root = document.documentElement; + root.classList.toggle(DARK_CLASS, value === "dark"); + root.dataset.theme = value; + root.style.colorScheme = value; +} + +function isThemePreference(value: string | null): value is ThemePreference { + return value === "system" || value === "light" || value === "dark"; +} diff --git a/src/features/profile/SettingsPage.vue b/src/features/profile/SettingsPage.vue new file mode 100644 index 0000000..5e339bd --- /dev/null +++ b/src/features/profile/SettingsPage.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/src/lib/syntax.ts b/src/lib/syntax.ts index 5fb4f48..c361554 100644 --- a/src/lib/syntax.ts +++ b/src/lib/syntax.ts @@ -5,10 +5,7 @@ let promise: Promise | null = null; export function getHighlighter(): Promise { if (!promise) { - promise = createHighlighter({ - themes: ["github-light", "github-dark"], - langs: [], - }); + promise = createHighlighter({ themes: ["github-light", "github-dark"], langs: [] }); } return promise; } @@ -107,8 +104,7 @@ export async function highlightCode(code: string, filename: string): Promise Profile + + + Settings +