minor UI fixes

This commit is contained in:
itsamejms
2026-07-13 10:39:51 +01:00
parent a24f3615e0
commit a6171a8158
31 changed files with 1528 additions and 508 deletions
+7
View File
@@ -43,6 +43,7 @@ export const bus = new EventBus();
export const Events = {
AddCombatants: "add-combatants",
PlayAmbient: "play-ambient",
} as const;
export type AddCombatantsPayload = {
@@ -50,3 +51,9 @@ export type AddCombatantsPayload = {
/** Source label, e.g. "Goblin Ambush (Easy)" — shown in the toast. */
source: string;
};
export type PlayAmbientPayload = {
/** Ambient sound id from the Soundboard (rain, wind, fire, …). */
id: string;
source: string;
};
+66
View File
@@ -0,0 +1,66 @@
import { useEffect, useRef, useState } from "react";
import { LazyStore } from "@tauri-apps/plugin-store";
// ponytail: one store file for small UI state. Generated content (NPCs,
// encounters, etc.) goes through the generations backend instead; this is
// only for things that live in component state today (initiative, dice
// history, calendar events). Debounced save so a fast dice session doesn't
// write on every roll.
const store = new LazyStore("dm-pal-state.json");
function debounce<T extends (...args: never[]) => void>(fn: T, ms: number): T {
let t: ReturnType<typeof setTimeout> | undefined;
return ((...args: never[]) => {
if (t) clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
}) as T;
}
const save = debounce((key: string, value: unknown) => {
void store.set(key, value);
void store.save();
}, 400);
/**
* useState that persists to tauri-plugin-store. Loads on mount (returns the
* fallback until the async load completes), saves on every change (debounced).
* Reload = no more data loss for initiative / dice / calendar.
*/
export function usePersistentState<T>(
key: string,
fallback: T,
): [T, React.Dispatch<React.SetStateAction<T>>, boolean] {
const [value, setValue] = useState<T>(fallback);
const [loaded, setLoaded] = useState(false);
const skipSave = useRef(true);
// Load once on mount.
useEffect(() => {
let alive = true;
(async () => {
try {
const stored = await store.get<T>(key);
if (alive && stored !== null && stored !== undefined) {
setValue(stored);
}
} catch {
// store unavailable — keep fallback.
}
if (alive) setLoaded(true);
})();
return () => {
alive = false;
};
}, [key]);
// Save on change (skip the initial load-induced render).
useEffect(() => {
if (skipSave.current) {
skipSave.current = false;
return;
}
save(key, value);
}, [key, value]);
return [value, setValue, loaded];
}