Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Improve handling of localStorage and svelte stores
Sebastian Martinez committed 1 year ago
commit 691e0255fbf1671571bd2f39c9cb5c0f0a1df0e0
parent 032b269
7 files changed +155 -88
modified package-lock.json
@@ -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",
modified package.json
@@ -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",
modified src/App.svelte
@@ -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 }) => {
-
      theme.set(matches ? "dark" : "light");
-
      followSystemTheme.set(true);
+
      if ($followSystemTheme) {
+
        theme.set(matches ? "dark" : "light");
+
      }
    });

  void router.loadFromLocation();
modified src/App/Settings.svelte
@@ -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"}>
modified src/lib/appearance.ts
@@ -1,11 +1,37 @@
-
import { writable } from "svelte/store";
+
import storedWritable from "@app/lib/localStore";
+
import { boolean, literal, union, z } from "zod";
+

+
const themeSchema = union([literal("dark"), literal("light")]);
+
type Theme = z.infer<typeof themeSchema>;
+

+
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";
+
}

-
export type Theme = "dark" | "light";
-
export const followSystemTheme = writable<boolean>(shouldFollowSystemTheme());
-
export const theme = writable<Theme>(loadTheme());
+
const codeFontSchema = union([literal("jetbrains"), literal("system")]);
+
type CodeFont = z.infer<typeof codeFontSchema>;

-
export type CodeFont = "jetbrains" | "system";
-
export const codeFont = writable<CodeFont>(loadCodeFont());
+
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.",
-
    );
-
  }
-
}
added src/lib/localStore.ts
@@ -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,
+
  };
+
}
modified src/views/nodes/SeedSelector.ts
@@ -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";