feat: add withPersistence state, change theme state
This commit is contained in:
+21
-8
@@ -1,11 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
const saved = localStorage.getItem("theme");
|
||||
if (saved) {
|
||||
const theme = JSON.parse(saved).theme;
|
||||
if (theme === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<div class="header-content">
|
||||
<h1>Twitch Panels</h1>
|
||||
<button class="theme-toggle" aria-label="Toggle theme" onclick={toggleTheme}>
|
||||
<Sun />
|
||||
<Moon />
|
||||
<Sun class="sun-icon" />
|
||||
<Moon class="moon-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -66,4 +66,11 @@ export const TextAlign = {
|
||||
export type TextAlignType = (typeof TextAlign)[keyof typeof TextAlign];
|
||||
export const DEFAULT_TEXT_ALIGN: TextAlignType = TextAlign.CENTER;
|
||||
|
||||
export const Theme = {
|
||||
DARK: "dark",
|
||||
LIGHT: "light",
|
||||
} as const;
|
||||
|
||||
export type ThemeType = (typeof Theme)[keyof typeof Theme];
|
||||
|
||||
export const TRANSITION_DURATION = import.meta.env.MODE === "test" ? 0 : 300;
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const newTheme = themeState.theme;
|
||||
const newTheme = themeState.current;
|
||||
document.documentElement.setAttribute("data-theme", newTheme);
|
||||
localStorage.setItem("theme", newTheme);
|
||||
});
|
||||
|
||||
let { children }: Props = $props();
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export const STATE_DATA = Symbol("state-data");
|
||||
export const DEBOUNCE_DURATION = import.meta.env.MODE === "test" ? 0 : 500;
|
||||
|
||||
type OnlyData<T> = {
|
||||
[K in keyof T as T[K] extends Function ? never : K]: T[K];
|
||||
};
|
||||
|
||||
export interface Persistable<D> {
|
||||
[STATE_DATA]: D;
|
||||
}
|
||||
|
||||
export function withPersistence<D extends object, T extends Persistable<D>>(
|
||||
key: string,
|
||||
state: T,
|
||||
debounceMs = DEBOUNCE_DURATION,
|
||||
): T {
|
||||
if (!browser) return state;
|
||||
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
|
||||
if (parsed) {
|
||||
state[STATE_DATA] = parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error repairing state for ${key}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
const data = JSON.stringify($state.snapshot(state[STATE_DATA]));
|
||||
|
||||
if (debounceMs > 0) {
|
||||
const timeout = setTimeout(() => localStorage.setItem(key, data), debounceMs);
|
||||
return () => clearTimeout(timeout);
|
||||
} else {
|
||||
localStorage.setItem(key, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TextAlignType } from "$lib/constants";
|
||||
import { type TextAlignType } from "$lib/constants";
|
||||
import type { HexColor } from "$lib/types";
|
||||
import { STATE_DATA, withPersistence } from "./persisted.svelte";
|
||||
|
||||
export type TextConfig = {
|
||||
fontSize: number;
|
||||
@@ -58,7 +59,25 @@ function createState() {
|
||||
set offsetY(offsetY: number) {
|
||||
state.offsetY = offsetY;
|
||||
},
|
||||
get [STATE_DATA]() {
|
||||
return {
|
||||
fontSize: state.fontSize,
|
||||
fontFamily: state.fontFamily,
|
||||
color: state.color,
|
||||
align: state.align,
|
||||
paddingX: state.paddingX,
|
||||
offsetY: state.offsetY,
|
||||
};
|
||||
},
|
||||
set [STATE_DATA](newConfig: TextConfig) {
|
||||
state.fontSize = newConfig.fontSize;
|
||||
state.fontFamily = newConfig.fontFamily;
|
||||
state.color = newConfig.color;
|
||||
state.align = newConfig.align;
|
||||
state.paddingX = newConfig.paddingX;
|
||||
state.offsetY = newConfig.offsetY;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const textConfigState = createState();
|
||||
export const textConfigState = withPersistence("text-config", createState());
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { STATE_DATA, withPersistence } from "./persisted.svelte";
|
||||
|
||||
export interface TextItem {
|
||||
text: string;
|
||||
id: number;
|
||||
@@ -25,7 +27,14 @@ export function createState() {
|
||||
texts = [];
|
||||
nextId = 0;
|
||||
},
|
||||
get [STATE_DATA]() {
|
||||
return texts.map(({ text }) => text);
|
||||
},
|
||||
set [STATE_DATA](newTexts: Array<string>) {
|
||||
texts = newTexts.map((text, idx) => ({ text, id: idx }));
|
||||
nextId = newTexts.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const textsState = createState();
|
||||
export const textsState = withPersistence("texts", createState());
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
export type Theme = "dark" | "light";
|
||||
import { Theme, type ThemeType } from "$lib/constants";
|
||||
import { STATE_DATA, withPersistence } from "./persisted.svelte";
|
||||
|
||||
function createState() {
|
||||
let current: Theme = $state("dark");
|
||||
let current: ThemeType = $state(Theme.LIGHT);
|
||||
|
||||
return {
|
||||
get theme() {
|
||||
get current() {
|
||||
return current;
|
||||
},
|
||||
set theme(value: Theme) {
|
||||
set current(value: ThemeType) {
|
||||
current = value;
|
||||
},
|
||||
toggle() {
|
||||
current = current === "dark" ? "light" : "dark";
|
||||
current = current === Theme.DARK ? Theme.LIGHT : Theme.DARK;
|
||||
},
|
||||
get [STATE_DATA]() {
|
||||
return {
|
||||
current,
|
||||
};
|
||||
},
|
||||
set [STATE_DATA](data: { current: ThemeType }) {
|
||||
current = data.current;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const themeState = createState();
|
||||
export const themeState = withPersistence("theme", createState());
|
||||
|
||||
Reference in New Issue
Block a user