minor UI fixes
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
Reference in New Issue
Block a user