BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1import type { ExplorerState, ExplorerTargetKind, ExplorerViewState } from "$/lib/api/types/explorer";
2import { createStore, produce } from "solid-js/store";
3
4export type Crumb = { label: string; level: ExplorerTargetKind; active: boolean };
5
6export function createExplorerState() {
7 const [state, setState] = createStore<ExplorerState>({
8 inputValue: "",
9 current: null,
10 history: [],
11 historyIndex: -1,
12 lexiconIcons: {},
13 });
14
15 function setInputValue(value: string) {
16 setState("inputValue", value);
17 }
18
19 function pushView(viewState: ExplorerViewState) {
20 setState(produce((draft) => {
21 draft.history = draft.history.slice(0, draft.historyIndex + 1);
22 draft.history.push(viewState);
23 draft.historyIndex = draft.history.length - 1;
24 draft.current = viewState;
25 }));
26 }
27
28 function goBack(): boolean {
29 if (state.historyIndex > 0) {
30 setState(produce((draft) => {
31 draft.historyIndex -= 1;
32 draft.current = draft.history[draft.historyIndex];
33 }));
34 return true;
35 }
36 return false;
37 }
38
39 function goForward(): boolean {
40 if (state.historyIndex < state.history.length - 1) {
41 setState(produce((draft) => {
42 draft.historyIndex += 1;
43 draft.current = draft.history[draft.historyIndex];
44 }));
45 return true;
46 }
47 return false;
48 }
49
50 function goUp(): boolean {
51 const current = state.current;
52 if (!current || !current.resolved) return false;
53
54 const resolved = current.resolved;
55 if (resolved.targetKind === "record" && resolved.collection) {
56 return true;
57 } else if (resolved.targetKind === "collection") {
58 return true;
59 } else if (resolved.targetKind === "repo") {
60 return true;
61 }
62 return false;
63 }
64
65 function canGoBack() {
66 return state.historyIndex > 0;
67 }
68
69 function canGoForward() {
70 return state.historyIndex < state.history.length - 1;
71 }
72
73 function mergeLexiconIcons(icons: Record<string, string | null>) {
74 setState("lexiconIcons", (current) => ({ ...current, ...icons }));
75 }
76
77 function resetLexiconIcons() {
78 setState("lexiconIcons", {});
79 }
80
81 function getBreadcrumb(): Crumb[] {
82 const current = state.current;
83 if (!current || !current.resolved) return [];
84
85 const resolved = current.resolved;
86 const crumbs: Crumb[] = [];
87
88 if (resolved.pdsUrl) {
89 crumbs.push({ label: "PDS", level: "pds", active: resolved.targetKind === "pds" });
90 }
91
92 if (resolved.did) {
93 const handle = resolved.handle || resolved.did.slice(0, 20) + "...";
94 crumbs.push({ label: handle, level: "repo", active: resolved.targetKind === "repo" });
95 }
96
97 if (resolved.collection) {
98 const nsidParts = resolved.collection.split(".");
99 const shortName = nsidParts.at(-1) || resolved.collection;
100 crumbs.push({ label: shortName, level: "collection", active: resolved.targetKind === "collection" });
101 }
102
103 if (resolved.rkey) {
104 crumbs.push({
105 label: resolved.rkey.slice(0, 12) + "...",
106 level: "record",
107 active: resolved.targetKind === "record",
108 });
109 }
110
111 if (crumbs.length > 0) {
112 crumbs.at(-1)!.active = true;
113 }
114
115 return crumbs;
116 }
117
118 return {
119 state,
120 setState,
121 setInputValue,
122 pushView,
123 goBack,
124 goForward,
125 goUp,
126 canGoBack,
127 canGoForward,
128 mergeLexiconIcons,
129 resetLexiconIcons,
130 getBreadcrumb,
131 };
132}