Experiment to rebuild Diffuse using web applets.
0

Configure Feed

Select the types of activity you want to include in your feed.

1import { type Signal, computed, effect, signal } from "spellcaster/spellcaster.js"; 2import { type ElementConfigurator, h, repeat, text } from "spellcaster/hyperscript.js"; 3 4import { applet, hs, reactive } from "@scripts/applet/common"; 5import { CUSTOM_KEY } from "./constants"; 6import { active, setActive } from "./signals"; 7import { connection } from "./connections"; 8import { context } from "./context"; 9import type { List, ListItem, Method } from "./types"; 10import { setContextData } from "./events"; 11 12// const h = ( 13// tag: string, 14// props?: Record<string, any> | Signal<Record<string, any>>, 15// configure?: ElementConfigurator, 16// ) => hs(tag, scope, props, configure); 17 18//////////////////////////////////////////// 19// EFFECTS 20//////////////////////////////////////////// 21reactive( 22 context.scope, 23 (data) => data.tracks.cacheId, 24 () => { 25 // Export data URI 26 const dl = document.querySelector("#download"); 27 if (dl) { 28 const json = JSON.stringify(context.data.tracks.collection, null, 2); 29 const href = URL.createObjectURL(new Blob([json], { type: "application/json" })); 30 dl.setAttribute("href", href); 31 } 32 }, 33); 34 35// Mount + Unmount 36async function mountStorageMethod(method: Method) { 37 switch (method) { 38 case "custom": 39 setModalIsOpen(true); 40 break; 41 default: 42 const conn = await connection(method); 43 try { 44 await conn.sendAction("mount", undefined, { timeoutDuration: 60000 }); 45 setActive(method); 46 } catch (err) { 47 const msg: string = 48 err && typeof err === "object" && "message" in err ? `${err.message}` : `${err}`; 49 if (msg.startsWith("[user] ")) alert(msg.slice(7)); 50 } 51 break; 52 } 53} 54 55async function unmountStorageMethod(method: Method) { 56 const conn = await connection(method); 57 conn.removeEventListener("data", setContextData); 58 await conn.sendAction("unmount", undefined, { timeoutDuration: 60000 }); 59} 60 61//////////////////////////////////////////// 62// LIST 63//////////////////////////////////////////// 64const list = computed<List>(() => { 65 const a = active(); 66 67 return new Map([ 68 [ 69 `browser-${a === "browser"}`, 70 { 71 title: "Browser storage", 72 icon: "iconoir-app-window", 73 method: "browser", 74 activated: a === "browser", 75 }, 76 ], 77 [ 78 `device-${a === "device"}`, 79 { 80 title: "Device storage", 81 icon: "iconoir-laptop", 82 method: "device", 83 activated: a === "device", 84 }, 85 ], 86 [ 87 `custom-${a === "custom"}`, 88 { 89 title: "Custom applet", 90 icon: "iconoir-globe", 91 method: "custom", 92 activated: a === "custom", 93 }, 94 ], 95 ]); 96}); 97 98const Item = (signal: Signal<ListItem<Method>>) => { 99 const item = signal(); 100 101 const colorClass = item.activated ? "pico-color-jade-500" : "pico-color-grey-500"; 102 const icon = item.activated ? "iconoir-check-circle-solid" : "iconoir-check-circle"; 103 104 return h( 105 "p", 106 { 107 onclick: clickHandler(item.method), 108 style: "cursor: pointer", 109 }, 110 [ 111 h("span", { className: "with-icon" }, [ 112 h("i", { className: item.icon }), 113 h("strong", {}, text(item.title)), 114 ]), 115 h("br"), 116 h("span", { className: `with-icon ${colorClass}` }, [ 117 h("i", { className: icon }), 118 h("span", {}, text(item.activated ? "Active" : "Select")), 119 ]), 120 ], 121 ); 122}; 123 124function clickHandler(method: Method) { 125 return async () => { 126 const currentlyActive = active(); 127 if (currentlyActive === method && currentlyActive !== "custom") return; 128 if (currentlyActive) unmountStorageMethod(currentlyActive); 129 await mountStorageMethod(method); 130 }; 131} 132 133const Options = computed(() => { 134 return h("div", { id: "options" }, repeat(list, Item)); 135}); 136 137// Add to DOM 138document.getElementById("options")?.replaceWith(Options()); 139 140//////////////////////////////////////////// 141// CUSTOM APPLET 142//////////////////////////////////////////// 143type CustomAppletState = "waiting" | "connecting" | { error: string } | "connected"; 144 145const [modalIsOpen, setModalIsOpen] = signal(false); 146const [customState, setCustomState] = signal<CustomAppletState>("waiting"); 147 148const Modal = () => { 149 const Header = h("header", {}, [ 150 h("button", { 151 attrs: { rel: "prev" }, 152 ariaLabel: "Close", 153 onclick: close, 154 }), 155 h("p", {}, [ 156 h("strong", {}, [ 157 h("span", { className: "with-icon" }, [ 158 h("i", { className: "iconoir-globe" }), 159 h("span", {}, text("Load a custom applet")), 160 ]), 161 ]), 162 ]), 163 ]); 164 165 const Content = h("form", { onsubmit: submit }, [ 166 h("fieldset", { role: "group" }, [ 167 h("input", { 168 type: "url", 169 name: "url", 170 placeholder: "https://applets.diffuse.sh/storage/output/indexed-db/", 171 required: true, 172 value: localStorage.getItem(CUSTOM_KEY) || "", 173 }), 174 h("input", { type: "submit", value: "Connect" }), 175 ]), 176 h("p", {}, [ 177 h("small", { className: "with-icon" }, (element) => { 178 const comp = computed(() => { 179 const s = customState(); 180 181 if (s === "connecting") { 182 return [ 183 h("i", { className: "iconoir-ev-plug-charging" }), 184 h("span", {}, text("Connecting ...")), 185 ]; 186 } else if (typeof s !== "string") { 187 return [ 188 h("i", { className: "iconoir-warning-circle" }), 189 h("span", {}, text(`Error: ${s.error}`)), 190 ]; 191 } 192 193 return [h("span", {}, text("Enter the URL to the applet."))]; 194 }); 195 196 effect(() => { 197 element.replaceChildren(...comp()); 198 }); 199 }), 200 ]), 201 ]); 202 203 return h( 204 "dialog", 205 computed(() => ({ open: modalIsOpen() })), 206 [h("article", {}, [Header, Content])], 207 ); 208}; 209 210// Events 211function close() { 212 setModalIsOpen(false); 213} 214 215async function submit(event: SubmitEvent) { 216 event.preventDefault(); 217 218 const input: HTMLInputElement | null = (event.target as HTMLFormElement).querySelector( 219 `input[type="url"]`, 220 ); 221 222 if (!input) return; 223 224 const url = input.value; 225 setCustomState("connecting"); 226 227 const apl = await applet(url).catch((err) => { 228 setCustomState({ error: "Failed to connect" }); 229 throw err; 230 }); 231 232 let missingAction; 233 234 ["tracks", "mount", "unmount"].forEach((method) => { 235 if (!apl.manifest.actions?.[method]) missingAction = method; 236 }); 237 238 if (missingAction) { 239 setCustomState({ error: `Applet is missing a required action: "${missingAction}"` }); 240 return; 241 } 242 243 localStorage.setItem(CUSTOM_KEY, url); 244 await apl.sendAction("mount", undefined, { timeoutDuration: 60000 }); 245 246 setActive("custom"); 247 setModalIsOpen(false); 248 setCustomState("waiting"); 249} 250 251// Add to DOM 252document.querySelector("main")?.appendChild(Modal());