mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-18 19:30:27 +08:00
Improve handling of localStorage and svelte stores
Also moving the svelte-stored-writable package into the repo. So we have it closer for maintenance, we can eventually rewrite it in svelte 5 runes later. Reference: https://github.com/efstajas/svelte-stored-writable/pull/5
This commit is contained in:
Generated
-10
@@ -9,7 +9,6 @@
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@efstajas/svelte-stored-writable": "^0.3.0",
|
||||
"@radicle/gray-matter": "4.1.0",
|
||||
"@wooorm/starry-night": "^3.5.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
@@ -82,15 +81,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@efstajas/svelte-stored-writable": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@efstajas/svelte-stored-writable/-/svelte-stored-writable-0.3.0.tgz",
|
||||
"integrity": "sha512-TIhJ5gpivhCsN+ijt/bAWp20uoeRZHJCOCTLqfzN4n5fk6uMUVtPeqnmC5eCe1Tn23moBuKOC6wgK4APuyuT6A==",
|
||||
"dependencies": {
|
||||
"svelte": "^4.2.19",
|
||||
"zod": "^3.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"wait-on": "^8.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@efstajas/svelte-stored-writable": "^0.3.0",
|
||||
"@radicle/gray-matter": "4.1.0",
|
||||
"@wooorm/starry-night": "^3.5.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
|
||||
+3
-2
@@ -2,7 +2,7 @@
|
||||
import * as router from "@app/lib/router";
|
||||
import { unreachable } from "@app/lib/utils";
|
||||
|
||||
import { codeFont, theme } from "@app/lib/appearance";
|
||||
import { codeFont, followSystemTheme, theme } from "@app/lib/appearance";
|
||||
|
||||
import FullscreenModalPortal from "./App/FullscreenModalPortal.svelte";
|
||||
import Hotkeys from "./App/Hotkeys.svelte";
|
||||
@@ -27,8 +27,9 @@
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", ({ matches }) => {
|
||||
if ($followSystemTheme) {
|
||||
theme.set(matches ? "dark" : "light");
|
||||
followSystemTheme.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
void router.loadFromLocation();
|
||||
|
||||
+14
-6
@@ -2,10 +2,9 @@
|
||||
import {
|
||||
codeFont,
|
||||
codeFonts,
|
||||
storeCodeFont,
|
||||
storeTheme,
|
||||
theme,
|
||||
followSystemTheme,
|
||||
loadTheme,
|
||||
} from "@app/lib/appearance";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
@@ -47,7 +46,10 @@
|
||||
variant={!$followSystemTheme && $theme === "light"
|
||||
? "selected"
|
||||
: "not-selected"}
|
||||
on:click={() => storeTheme("light")}>
|
||||
on:click={() => {
|
||||
theme.set("light");
|
||||
followSystemTheme.set(false);
|
||||
}}>
|
||||
<Icon name="sun" />
|
||||
</Button>
|
||||
<div class="global-spacer" />
|
||||
@@ -57,7 +59,10 @@
|
||||
variant={!$followSystemTheme && $theme === "dark"
|
||||
? "selected"
|
||||
: "not-selected"}
|
||||
on:click={() => storeTheme("dark")}>
|
||||
on:click={() => {
|
||||
theme.set("dark");
|
||||
followSystemTheme.set(false);
|
||||
}}>
|
||||
<Icon name="moon" />
|
||||
</Button>
|
||||
<div class="global-spacer" />
|
||||
@@ -65,7 +70,10 @@
|
||||
ariaLabel="System Theme"
|
||||
styleBorderRadius="0"
|
||||
variant={$followSystemTheme ? "selected" : "not-selected"}
|
||||
on:click={() => storeTheme("system")}>
|
||||
on:click={() => {
|
||||
theme.set(loadTheme());
|
||||
followSystemTheme.set(true);
|
||||
}}>
|
||||
<Icon name="device" />
|
||||
</Button>
|
||||
</Radio>
|
||||
@@ -79,7 +87,7 @@
|
||||
<Button
|
||||
styleBorderRadius="0"
|
||||
styleFontFamily={font.fontFamily}
|
||||
on:click={() => storeCodeFont(font.storedName)}
|
||||
on:click={() => codeFont.set(font.storedName)}
|
||||
variant={$codeFont === font.storedName
|
||||
? "selected"
|
||||
: "not-selected"}>
|
||||
|
||||
+32
-67
@@ -1,11 +1,37 @@
|
||||
import { writable } from "svelte/store";
|
||||
import storedWritable from "@app/lib/localStore";
|
||||
import { boolean, literal, union, z } from "zod";
|
||||
|
||||
export type Theme = "dark" | "light";
|
||||
export const followSystemTheme = writable<boolean>(shouldFollowSystemTheme());
|
||||
export const theme = writable<Theme>(loadTheme());
|
||||
const themeSchema = union([literal("dark"), literal("light")]);
|
||||
type Theme = z.infer<typeof themeSchema>;
|
||||
|
||||
export type CodeFont = "jetbrains" | "system";
|
||||
export const codeFont = writable<CodeFont>(loadCodeFont());
|
||||
export const followSystemTheme = storedWritable<boolean | undefined>(
|
||||
"followSystemTheme",
|
||||
boolean(),
|
||||
!localStorage.getItem("theme"),
|
||||
!window.localStorage,
|
||||
);
|
||||
export const theme = storedWritable<Theme>(
|
||||
"theme",
|
||||
themeSchema,
|
||||
loadTheme(),
|
||||
!window.localStorage,
|
||||
);
|
||||
|
||||
export function loadTheme(): Theme {
|
||||
const { matches } = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
return matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
const codeFontSchema = union([literal("jetbrains"), literal("system")]);
|
||||
type CodeFont = z.infer<typeof codeFontSchema>;
|
||||
|
||||
export const codeFont = storedWritable(
|
||||
"codefont",
|
||||
codeFontSchema,
|
||||
"jetbrains",
|
||||
!window.localStorage,
|
||||
);
|
||||
|
||||
export const codeFonts: {
|
||||
storedName: CodeFont;
|
||||
@@ -19,64 +45,3 @@ export const codeFonts: {
|
||||
},
|
||||
{ storedName: "system", fontFamily: "monospace", displayName: "System" },
|
||||
];
|
||||
|
||||
function loadCodeFont(): CodeFont {
|
||||
const storedCodeFont = localStorage ? localStorage.getItem("codefont") : null;
|
||||
|
||||
if (storedCodeFont === null) {
|
||||
return "jetbrains";
|
||||
} else {
|
||||
return storedCodeFont as CodeFont;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldFollowSystemTheme(): boolean {
|
||||
const storedTheme = localStorage ? localStorage.getItem("theme") : null;
|
||||
if (storedTheme === null) {
|
||||
return true; // default to following the system theme
|
||||
} else {
|
||||
return storedTheme === "system";
|
||||
}
|
||||
}
|
||||
|
||||
function loadTheme(): Theme {
|
||||
const { matches } = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const storedTheme = localStorage ? localStorage.getItem("theme") : null;
|
||||
|
||||
if (storedTheme === null || storedTheme === "system") {
|
||||
return matches ? "dark" : "light";
|
||||
} else {
|
||||
return storedTheme as Theme;
|
||||
}
|
||||
}
|
||||
|
||||
export function storeTheme(newTheme: Theme | "system"): void {
|
||||
followSystemTheme.set(newTheme === "system" ? true : false);
|
||||
if (localStorage) {
|
||||
localStorage.setItem("theme", newTheme);
|
||||
} else {
|
||||
console.warn(
|
||||
"localStorage isn't available, not able to persist the selected theme without it.",
|
||||
);
|
||||
}
|
||||
if (newTheme !== "system") {
|
||||
// update the theme to newTheme
|
||||
theme.set(newTheme);
|
||||
} else {
|
||||
// update the theme to the current system theme
|
||||
theme.set(
|
||||
window.matchMedia("(prefers-color-scheme: dark)") ? "dark" : "light",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function storeCodeFont(newCodeFont: CodeFont): void {
|
||||
codeFont.set(newCodeFont);
|
||||
if (localStorage) {
|
||||
localStorage.setItem("codefont", newCodeFont);
|
||||
} else {
|
||||
console.warn(
|
||||
"localStorage isn't available, not able to persist the selected code font without it.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { SafeParseReturnType } from "zod";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
import { writable, get } from "svelte/store";
|
||||
import { z } from "zod";
|
||||
|
||||
type Equals<X, Y> =
|
||||
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
|
||||
? true
|
||||
: false;
|
||||
|
||||
/**
|
||||
* An extension of Svelte's `writable` that also saves its state to localStorage and
|
||||
* automatically restores it.
|
||||
* @param key The localStorage key to use for saving the writable's contents.
|
||||
* @param schema A Zod schema describing the contents of the writable.
|
||||
* @param initialValue The initial value to use if no prior state has been saved in
|
||||
* localstorage.
|
||||
* @param disableLocalStorage Skip interaction with localStorage, for example during SSR.
|
||||
* @returns A stored writable.
|
||||
*/
|
||||
export default function storedWritable<
|
||||
S extends z.infer<T>,
|
||||
T extends z.ZodType = z.ZodType<S>,
|
||||
>(
|
||||
key: string,
|
||||
schema: T,
|
||||
initialValue: z.infer<typeof schema>,
|
||||
disableLocalStorage = false,
|
||||
): Writable<
|
||||
Equals<T, typeof schema> extends true ? S : z.infer<typeof schema>
|
||||
> & { clear: () => void } {
|
||||
const stored = !disableLocalStorage ? localStorage.getItem(key) : null;
|
||||
|
||||
const parseFromJson = (
|
||||
content: string,
|
||||
): SafeParseReturnType<string, T["_output"]> => {
|
||||
return z
|
||||
.string()
|
||||
.transform((_, ctx) => {
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "invalid json",
|
||||
});
|
||||
return z.never;
|
||||
}
|
||||
})
|
||||
.pipe(schema)
|
||||
.safeParse(content);
|
||||
};
|
||||
|
||||
// Subscribe to window storage event to keep changes from another tab in sync.
|
||||
if (!disableLocalStorage) {
|
||||
window?.addEventListener("storage", event => {
|
||||
if (event.key === key) {
|
||||
if (event.newValue === null) {
|
||||
w.set(initialValue);
|
||||
return;
|
||||
}
|
||||
|
||||
const { success, data } = parseFromJson(event.newValue);
|
||||
w.set(success ? data : initialValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
const parsed = parseFromJson(stored || initialValue);
|
||||
const w = writable<S>(parsed?.success ? parsed.data : initialValue);
|
||||
|
||||
/**
|
||||
* Set writable value and inform subscribers. Updates the writeable's stored data in
|
||||
* localstorage.
|
||||
* */
|
||||
function set(...args: Parameters<typeof w.set>) {
|
||||
w.set(...args);
|
||||
if (!disableLocalStorage) localStorage.setItem(key, JSON.stringify(get(w)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update writable value using a callback and inform subscribers. Updates the writeable's
|
||||
* stored data in localstorage.
|
||||
* */
|
||||
function update(...args: Parameters<typeof w.update>) {
|
||||
w.update(...args);
|
||||
if (!disableLocalStorage) localStorage.setItem(key, JSON.stringify(get(w)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete any data saved for this StoredWritable in localstorage.
|
||||
*/
|
||||
function clear() {
|
||||
w.set(initialValue);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: w.subscribe,
|
||||
set,
|
||||
update,
|
||||
clear,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BaseUrl } from "@http-client";
|
||||
|
||||
import isEqual from "lodash/isEqual";
|
||||
import storedWritable from "@efstajas/svelte-stored-writable";
|
||||
import storedWritable from "@app/lib/localStore";
|
||||
import { array, number, string, object } from "zod";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user