Skip to content

Commit d054c88

Browse files
committed
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: efstajas/svelte-stored-writable#5
1 parent bd02c85 commit d054c88

File tree

7 files changed

+127
-87
lines changed

7 files changed

+127
-87
lines changed

package-lock.json

Lines changed: 0 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
"wait-on": "^8.0.1"
5454
},
5555
"dependencies": {
56-
"@efstajas/svelte-stored-writable": "^0.3.0",
5756
"@radicle/gray-matter": "4.1.0",
5857
"@wooorm/starry-night": "^3.5.0",
5958
"async-mutex": "^0.5.0",

src/App.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as router from "@app/lib/router";
33
import { unreachable } from "@app/lib/utils";
44
5-
import { codeFont, theme } from "@app/lib/appearance";
5+
import { codeFont, followSystemTheme, theme } from "@app/lib/appearance";
66
77
import FullscreenModalPortal from "./App/FullscreenModalPortal.svelte";
88
import Hotkeys from "./App/Hotkeys.svelte";

src/App/Settings.svelte

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22
import {
33
codeFont,
44
codeFonts,
5-
storeCodeFont,
6-
storeTheme,
75
theme,
86
followSystemTheme,
97
} from "@app/lib/appearance";
108
119
import Button from "@app/components/Button.svelte";
1210
import Icon from "@app/components/Icon.svelte";
1311
import Radio from "@app/components/Radio.svelte";
14-
1512
</script>
1613

1714
<style>
@@ -48,7 +45,10 @@
4845
variant={!$followSystemTheme && $theme === "light"
4946
? "selected"
5047
: "not-selected"}
51-
on:click={() => storeTheme("light")}>
48+
on:click={() => {
49+
theme.set("light");
50+
followSystemTheme.set(false);
51+
}}>
5252
<Icon name="sun" />
5353
</Button>
5454
<div class="global-spacer" />
@@ -58,15 +58,18 @@
5858
variant={!$followSystemTheme && $theme === "dark"
5959
? "selected"
6060
: "not-selected"}
61-
on:click={() => storeTheme("dark")}>
61+
on:click={() => {
62+
theme.set("dark");
63+
followSystemTheme.set(false);
64+
}}>
6265
<Icon name="moon" />
6366
</Button>
6467
<div class="global-spacer" />
6568
<Button
6669
ariaLabel="System Theme"
6770
styleBorderRadius="0"
6871
variant={$followSystemTheme ? "selected" : "not-selected"}
69-
on:click={() => storeTheme("system")}>
72+
on:click={() => followSystemTheme.set(true)}>
7073
<Icon name="device" />
7174
</Button>
7275
</Radio>
@@ -80,7 +83,7 @@
8083
<Button
8184
styleBorderRadius="0"
8285
styleFontFamily={font.fontFamily}
83-
on:click={() => storeCodeFont(font.storedName)}
86+
on:click={() => codeFont.set(font.storedName)}
8487
variant={$codeFont === font.storedName
8588
? "selected"
8689
: "not-selected"}>

src/lib/appearance.ts

Lines changed: 32 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
1-
import { writable } from "svelte/store";
1+
import storedWritable from "@app/lib/localStore";
2+
import { boolean, literal, union, z } from "zod";
3+
4+
const themeSchema = union([literal("dark"), literal("light")]);
5+
type Theme = z.infer<typeof themeSchema>;
6+
7+
export const followSystemTheme = storedWritable<boolean | undefined>(
8+
"followSystemTheme",
9+
boolean(),
10+
!localStorage.getItem("theme"),
11+
!window.localStorage,
12+
);
13+
export const theme = storedWritable<Theme>(
14+
"theme",
15+
themeSchema,
16+
loadTheme(),
17+
!window.localStorage,
18+
);
219

3-
export type Theme = "dark" | "light";
4-
export const followSystemTheme = writable<boolean>(shouldFollowSystemTheme());
5-
export const theme = writable<Theme>(loadTheme());
20+
function loadTheme(): Theme {
21+
const { matches } = window.matchMedia("(prefers-color-scheme: dark)");
22+
23+
return matches ? "dark" : "light";
24+
}
25+
26+
const codeFontSchema = union([literal("jetbrains"), literal("system")]);
27+
type CodeFont = z.infer<typeof codeFontSchema>;
628

7-
export type CodeFont = "jetbrains" | "system";
8-
export const codeFont = writable<CodeFont>(loadCodeFont());
29+
export const codeFont = storedWritable(
30+
"codefont",
31+
codeFontSchema,
32+
"jetbrains",
33+
!window.localStorage,
34+
);
935

1036
export const codeFonts: {
1137
storedName: CodeFont;
@@ -19,64 +45,3 @@ export const codeFonts: {
1945
},
2046
{ storedName: "system", fontFamily: "monospace", displayName: "System" },
2147
];
22-
23-
function loadCodeFont(): CodeFont {
24-
const storedCodeFont = localStorage ? localStorage.getItem("codefont") : null;
25-
26-
if (storedCodeFont === null) {
27-
return "jetbrains";
28-
} else {
29-
return storedCodeFont as CodeFont;
30-
}
31-
}
32-
33-
function shouldFollowSystemTheme(): boolean {
34-
const storedTheme = localStorage ? localStorage.getItem("theme") : null;
35-
if (storedTheme === null) {
36-
return true; // default to following the system theme
37-
} else {
38-
return storedTheme === "system";
39-
}
40-
}
41-
42-
function loadTheme(): Theme {
43-
const { matches } = window.matchMedia("(prefers-color-scheme: dark)");
44-
const storedTheme = localStorage ? localStorage.getItem("theme") : null;
45-
46-
if (storedTheme === null || storedTheme === "system") {
47-
return matches ? "dark" : "light";
48-
} else {
49-
return storedTheme as Theme;
50-
}
51-
}
52-
53-
export function storeTheme(newTheme: Theme | "system"): void {
54-
followSystemTheme.set(newTheme === "system" ? true : false);
55-
if (localStorage) {
56-
localStorage.setItem("theme", newTheme);
57-
} else {
58-
console.warn(
59-
"localStorage isn't available, not able to persist the selected theme without it.",
60-
);
61-
}
62-
if (newTheme !== "system") {
63-
// update the theme to newTheme
64-
theme.set(newTheme);
65-
} else {
66-
// update the theme to the current system theme
67-
theme.set(
68-
window.matchMedia("(prefers-color-scheme: dark)") ? "dark" : "light",
69-
);
70-
}
71-
}
72-
73-
export function storeCodeFont(newCodeFont: CodeFont): void {
74-
codeFont.set(newCodeFont);
75-
if (localStorage) {
76-
localStorage.setItem("codefont", newCodeFont);
77-
} else {
78-
console.warn(
79-
"localStorage isn't available, not able to persist the selected code font without it.",
80-
);
81-
}
82-
}

src/lib/localStore.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { writable, type Writable, get } from "svelte/store";
2+
import { z } from "zod";
3+
4+
type Equals<X, Y> =
5+
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
6+
? true
7+
: false;
8+
9+
/**
10+
* An extension of Svelte's `writable` that also saves its state to localStorage and
11+
* automatically restores it.
12+
* @param key The localStorage key to use for saving the writable's contents.
13+
* @param schema A Zod schema describing the contents of the writable.
14+
* @param initialValue The initial value to use if no prior state has been saved in
15+
* localstorage.
16+
* @param disableLocalStorage Skip interaction with localStorage, for example during SSR.
17+
* @returns A stored writable.
18+
*/
19+
export default function storedWritable<
20+
S extends z.infer<T>,
21+
T extends z.ZodType = z.ZodType<S>,
22+
>(
23+
key: string,
24+
schema: T,
25+
initialValue: z.infer<typeof schema>,
26+
disableLocalStorage = false,
27+
): Writable<
28+
Equals<T, typeof schema> extends true ? S : z.infer<typeof schema>
29+
> & { clear: () => void } {
30+
const stored = !disableLocalStorage ? localStorage.getItem(key) : null;
31+
32+
// Subscribe to window storage event to keep changes from another tab in sync.
33+
if (!disableLocalStorage) {
34+
window?.addEventListener("storage", event => {
35+
if (event.key === key) {
36+
if (event.newValue === null) {
37+
w.set(initialValue);
38+
return;
39+
}
40+
41+
const { success, data } = schema.safeParse(event.newValue);
42+
w.set(success ? data : initialValue);
43+
}
44+
});
45+
}
46+
47+
const parsed = stored ? schema.safeParse(stored) : null;
48+
console.log(parsed);
49+
const w = writable<S>(parsed?.success ? parsed.data : initialValue);
50+
51+
/**
52+
* Set writable value and inform subscribers. Updates the writeable's stored data in
53+
* localstorage.
54+
* */
55+
function set(...args: Parameters<typeof w.set>) {
56+
w.set(...args);
57+
if (!disableLocalStorage) localStorage.setItem(key, JSON.stringify(get(w)));
58+
}
59+
60+
/**
61+
* Update writable value using a callback and inform subscribers. Updates the writeable's
62+
* stored data in localstorage.
63+
* */
64+
function update(...args: Parameters<typeof w.update>) {
65+
w.update(...args);
66+
if (!disableLocalStorage) localStorage.setItem(key, JSON.stringify(get(w)));
67+
}
68+
69+
/**
70+
* Delete any data saved for this StoredWritable in localstorage.
71+
*/
72+
function clear() {
73+
w.set(initialValue);
74+
localStorage.removeItem(key);
75+
}
76+
77+
return {
78+
subscribe: w.subscribe,
79+
set,
80+
update,
81+
clear,
82+
};
83+
}

src/views/nodes/SeedSelector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { BaseUrl } from "@http-client";
22

33
import isEqual from "lodash/isEqual";
4-
import storedWritable from "@efstajas/svelte-stored-writable";
4+
import storedWritable from "@app/lib/localStore";
55
import { array, number, string, object } from "zod";
66
import { get } from "svelte/store";
77

0 commit comments

Comments
 (0)