flow / src /store /useLibraryStore.ts
mrfakename's picture
Require sign-in, bucket-only storage, sign in via new tab when embedded
0ac416b verified
Raw History Blame Contribute Delete
3.06 kB
import { create } from "zustand";
import { prefs } from "../lib/prefs";
const KEY = "gr-canvas-library";
interface LibraryState {
/** Def types pinned to the top of the library, in display order. */
pinned: string[];
/** Def types hidden from the library. */
hidden: string[];
/** Collapsed section keys. */
collapsed: string[];
showHidden: boolean;
/** Whether the full node tree is unfolded under the favourites in the sidebar. */
allOpen: boolean;
setAllOpen: (v: boolean) => void;
pin: (type: string) => void;
unpin: (type: string) => void;
movePinned: (type: string, toIndex: number) => void;
hide: (type: string) => void;
unhide: (type: string) => void;
toggleSection: (key: string) => void;
setShowHidden: (v: boolean) => void;
/** Drops prefs for defs that no longer exist (e.g. a removed Space). */
forget: (predicate: (type: string) => boolean) => void;
reset: () => void;
}
/** First visit: a couple of favourites so the sidebar is not empty. */
const DEFAULT_PINNED = ["in:text", "in:image"];
function load(): Pick<LibraryState, "pinned" | "hidden" | "collapsed" | "allOpen"> {
try {
const raw = prefs.get(KEY);
if (raw) {
const p = JSON.parse(raw);
return { pinned: p.pinned ?? [], hidden: p.hidden ?? [], collapsed: p.collapsed ?? [], allOpen: !!p.allOpen };
}
} catch {
/* ignore */
}
return { pinned: DEFAULT_PINNED, hidden: [], collapsed: [], allOpen: false };
}
const without = (xs: string[], x: string) => xs.filter((y) => y !== x);
export const useLibraryStore = create<LibraryState>((set) => ({
...load(),
showHidden: false,
setAllOpen: (allOpen) => set({ allOpen }),
pin: (t) => set((s) => ({ pinned: [...without(s.pinned, t), t], hidden: without(s.hidden, t) })),
unpin: (t) => set((s) => ({ pinned: without(s.pinned, t) })),
movePinned: (t, to) =>
set((s) => {
const from = s.pinned.indexOf(t);
if (from < 0) return {};
const next = without(s.pinned, t);
next.splice(Math.max(0, Math.min(to > from ? to - 1 : to, next.length)), 0, t);
return { pinned: next };
}),
hide: (t) => set((s) => ({ hidden: [...without(s.hidden, t), t], pinned: without(s.pinned, t) })),
unhide: (t) => set((s) => ({ hidden: without(s.hidden, t) })),
toggleSection: (k) => set((s) => ({ collapsed: s.collapsed.includes(k) ? without(s.collapsed, k) : [...s.collapsed, k] })),
setShowHidden: (v) => set({ showHidden: v }),
forget: (pred) => set((s) => ({ pinned: s.pinned.filter((t) => !pred(t)), hidden: s.hidden.filter((t) => !pred(t)) })),
reset: () => set({ pinned: [], hidden: [], collapsed: [] }),
}));
/** Re-reads prefs (after state.json was loaded). */
export const rehydrateLibrary = () => useLibraryStore.setState(load());
useLibraryStore.subscribe((s) => {
try {
prefs.set(KEY, JSON.stringify({ pinned: s.pinned, hidden: s.hidden, collapsed: s.collapsed, allOpen: s.allOpen }));
} catch {
/* ignore */
}
});
export const isPinned = (t: string) => useLibraryStore.getState().pinned.includes(t);