A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

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

feat: generate standalone split-view facets

+157 -116
+28
src/common/output.js
··· 1 + import { effect } from "~/common/signal.js"; 2 + 3 + /** 4 + * @import {SignalReader} from "~/common/signal.d.ts"; 5 + */ 6 + 7 + /** 8 + * @param {{ collection: SignalReader<any>; state: SignalReader<"loading" | "loaded" | "sleeping"> }} output 9 + */ 10 + export async function waitUntilLoaded(output) { 11 + return await new Promise((resolve) => { 12 + let resolved = false; 13 + 14 + const stop = effect(() => { 15 + if (resolved) { 16 + stop(); 17 + return; 18 + } 19 + 20 + if (output.state() === "loaded") { 21 + resolved = true; 22 + resolve(void 0); 23 + } else if (output.state() === "sleeping") { 24 + output.collection(); 25 + } 26 + }); 27 + }); 28 + }
-36
src/components/configurator/output/element.js
··· 40 40 41 41 return this.#memory.facets.value; 42 42 }), 43 - loaded: () => { 44 - const out = this.#selected.value; 45 - if (out) return out.facets.loaded(); 46 - 47 - const def = this.#defaultOutput.value; 48 - if (def) return def.facets.loaded(); 49 - 50 - return Promise.resolve(); 51 - }, 52 43 reload: () => { 53 44 const def = this.#defaultOutput.value; 54 45 if (def) def.facets.reload(); ··· 87 78 88 79 return this.#memory.playlistItems.value; 89 80 }), 90 - loaded: () => { 91 - const out = this.#selected.value; 92 - if (out) return out.playlistItems.loaded(); 93 - 94 - const def = this.#defaultOutput.value; 95 - if (def) return def.playlistItems.loaded(); 96 - 97 - return Promise.resolve(); 98 - }, 99 81 reload: () => { 100 82 const def = this.#defaultOutput.value; 101 83 if (def) def.playlistItems.reload(); ··· 134 116 135 117 return this.#memory.themes.value; 136 118 }), 137 - loaded: () => { 138 - const out = this.#selected.value; 139 - if (out) return out.themes.loaded(); 140 - 141 - const def = this.#defaultOutput.value; 142 - if (def) return def.themes.loaded(); 143 - 144 - return Promise.resolve(); 145 - }, 146 119 reload: () => { 147 120 const def = this.#defaultOutput.value; 148 121 if (def) def.themes.reload(); ··· 181 154 182 155 return this.#memory.tracks.value; 183 156 }), 184 - loaded: () => { 185 - const out = this.#selected.value; 186 - if (out) return out.tracks.loaded(); 187 - 188 - const def = this.#defaultOutput.value; 189 - if (def) return def.tracks.loaded(); 190 - 191 - return Promise.resolve(); 192 - }, 193 157 reload: () => { 194 158 const def = this.#defaultOutput.value; 195 159 if (def) def.tracks.reload();
+2 -40
src/components/output/common.js
··· 1 1 import { BroadcastableDiffuseElement } from "~/common/element.js"; 2 - import { batch, computed, effect, signal, untracked } from "~/common/signal.js"; 2 + import { batch, computed, signal, untracked } from "~/common/signal.js"; 3 3 import { strictEquality } from "~/common/compare.js"; 4 4 5 5 /** 6 6 * @import {Facet, PlaylistItem, Theme, Track} from "~/definitions/types.d.ts" 7 - * @import {SignalReader, SignalWriter} from "~/common/signal.d.ts"; 7 + * @import {SignalWriter} from "~/common/signal.d.ts"; 8 8 * @import {OutputManager, OutputManagerProperties} from "./types.d.ts" 9 9 */ 10 10 ··· 68 68 } 69 69 70 70 /** 71 - * @param {() => void} loader 72 - * @param {SignalReader<"loading" | "loaded" | "sleeping">} state 73 - */ 74 - export function promiseLoadedState(loader, state) { 75 - return () => 76 - new Promise((resolve) => { 77 - let resolved = false; 78 - 79 - const stop = effect(() => { 80 - if (resolved) { 81 - try { 82 - stop(); 83 - } catch {} 84 - return; 85 - } 86 - 87 - if (state() === "loaded") { 88 - try { 89 - stop(); 90 - } catch { 91 - resolved = true; 92 - } 93 - 94 - resolve(void 0); 95 - } 96 - }); 97 - 98 - if (state() === "sleeping") { 99 - loader(); 100 - } 101 - }); 102 - } 103 - 104 - /** 105 71 * @template [Encoding=null] 106 72 * @param {OutputManagerProperties<Encoding>} _ 107 73 * @returns {OutputManager<Encoding>} ··· 177 143 if (untracked(() => cs.value === "sleeping")) loadFacets(); 178 144 return c.value; 179 145 }), 180 - loaded: promiseLoadedState(loadFacets, cs.get), 181 146 reload: loadFacets, 182 147 save: async (newFacets) => { 183 148 batch(() => { ··· 193 158 if (untracked(() => pls.value === "sleeping")) loadPlaylistItems(); 194 159 return pl.value; 195 160 }), 196 - loaded: promiseLoadedState(loadPlaylistItems, pls.get), 197 161 reload: loadPlaylistItems, 198 162 save: async (newPlaylistItems) => { 199 163 batch(() => { ··· 209 173 if (untracked(() => ths.value === "sleeping")) loadThemes(); 210 174 return th.value; 211 175 }), 212 - loaded: promiseLoadedState(loadThemes, ths.get), 213 176 reload: loadThemes, 214 177 save: async (newThemes) => { 215 178 batch(() => { ··· 225 188 if (untracked(() => ts.value === "sleeping")) loadTracks(); 226 189 return t.value; 227 190 }), 228 - loaded: promiseLoadedState(loadTracks, ts.get), 229 191 reload: loadTracks, 230 192 save: async (newTracks) => { 231 193 batch(() => {
-4
src/components/output/types.d.ts
··· 18 18 export type OutputManager<Encoding = null> = { 19 19 facets: { 20 20 collection: SignalReader<Encoding extends null ? Facet[] : Encoding>; 21 - loaded: () => Promise<void>; 22 21 reload: () => Promise<void>; 23 22 save: ( 24 23 facets: Encoding extends null ? Facet[] : Encoding, ··· 27 26 }; 28 27 playlistItems: { 29 28 collection: SignalReader<Encoding extends null ? PlaylistItem[] : Encoding>; 30 - loaded: () => Promise<void>; 31 29 reload: () => Promise<void>; 32 30 save: ( 33 31 playlistItems: Encoding extends null ? PlaylistItem[] : Encoding, ··· 49 47 }; 50 48 themes: { 51 49 collection: SignalReader<Encoding extends null ? Theme[] : Encoding>; 52 - loaded: () => Promise<void>; 53 50 reload: () => Promise<void>; 54 51 save: ( 55 52 themes: Encoding extends null ? Theme[] : Encoding, ··· 58 55 }; 59 56 tracks: { 60 57 collection: SignalReader<Encoding extends null ? Track[] : Encoding>; 61 - loaded: () => Promise<void>; 62 58 reload: () => Promise<void>; 63 59 save: (tracks: Encoding extends null ? Track[] : Encoding) => Promise<void>; 64 60 state: SignalReader<"loading" | "loaded" | "sleeping">;
-13
src/components/transformer/output/base.js
··· 46 46 collection: computed(() => { 47 47 return this.output.signal()?.facets?.collection(); 48 48 }), 49 - loaded: () => { 50 - return this.output.signal()?.facets?.loaded() ?? Promise.resolve(); 51 - }, 52 49 reload: () => { 53 50 return this.output.signal()?.facets?.reload() ?? 54 51 Promise.resolve(); ··· 66 63 collection: computed(() => { 67 64 return this.output.signal()?.playlistItems?.collection(); 68 65 }), 69 - loaded: () => { 70 - return this.output.signal()?.playlistItems?.loaded() ?? 71 - Promise.resolve(); 72 - }, 73 66 reload: () => { 74 67 return this.output.signal()?.playlistItems?.reload() ?? 75 68 Promise.resolve(); ··· 87 80 collection: computed(() => { 88 81 return this.output.signal()?.themes?.collection(); 89 82 }), 90 - loaded: () => { 91 - return this.output.signal()?.themes?.loaded() ?? Promise.resolve(); 92 - }, 93 83 reload: () => { 94 84 return this.output.signal()?.themes?.reload() ?? 95 85 Promise.resolve(); ··· 107 97 collection: computed(() => { 108 98 return this.output.signal()?.tracks?.collection(); 109 99 }), 110 - loaded: () => { 111 - return this.output.signal()?.tracks?.loaded() ?? Promise.resolve(); 112 - }, 113 100 reload: () => { 114 101 return this.output.signal()?.tracks?.reload() ?? Promise.resolve(); 115 102 },
+1 -9
src/components/transformer/output/bytes/dasl-sync/element.js
··· 9 9 import { computed, signal } from "~/common/signal.js"; 10 10 import { compareTimestamps } from "~/common/utils.js"; 11 11 import { OutputTransformer } from "../../base.js"; 12 - import { promiseLoadedState } from "~/components/output/common.js"; 13 12 14 13 /** 15 14 * @import { SignalReader } from "~/common/signal.d.ts"; ··· 411 410 * @param {{ save: (bytes: Uint8Array) => Promise<void> | void }} local 412 411 * @param {{ collection: SignalReader<Uint8Array | undefined>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} remote 413 412 * @param {SignalReader<Container<T>>} container 414 - * @returns {{ collection: SignalReader<T[]>, loaded: () => Promise<void>; reload: () => Promise<void>, save: (items: T[]) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} 413 + * @returns {{ collection: SignalReader<T[]>, reload: () => Promise<void>, save: (items: T[]) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} 415 414 */ 416 415 managerProp(local, remote, container) { 417 416 return { 418 417 collection: computed(() => { 419 418 return container().data; 420 419 }), 421 - loaded: promiseLoadedState( 422 - computed(() => container()), 423 - computed(() => { 424 - if (container().cid) return "loaded"; 425 - return "loading"; 426 - }), 427 - ), 428 420 reload: remote.reload, 429 421 save: async (/** @type {T[]} */ newItems) => { 430 422 const adjustedContainer = await this.updateContainer({
-11
src/components/transformer/output/raw/atproto-sync/element.js
··· 4 4 5 5 import { computed, signal } from "~/common/signal.js"; 6 6 import { OutputTransformer } from "../../base.js"; 7 - import { promiseLoadedState } from "@toko/diffuse/components/output/common.js"; 8 7 9 8 /** 10 9 * @import { RenderArg } from "~/common/element.d.ts" ··· 56 55 const data = l[name].collection(); 57 56 return Array.isArray(data) ? data : []; 58 57 }), 59 - loaded: promiseLoadedState( 60 - computed(() => { 61 - const l = local(); 62 - if (!l) return []; 63 - l[name].collection(); 64 - }), 65 - computed(() => { 66 - return local()?.[name]?.state() ?? "sleeping"; 67 - }), 68 - ), 69 58 reload: async () => { 70 59 await this.#sync(); 71 60 },
+14
src/facets/tools/split-view.html
··· 16 16 > 17 17 <wa-icon id="edit-icon" name="border-all"></wa-icon> 18 18 </wa-button> 19 + <wa-button 20 + id="save-copy-btn" 21 + appearance="filled" 22 + variant="neutral" 23 + size="small" 24 + pill 25 + aria-label="Save simplified copy" 26 + style="display: none" 27 + > 28 + <wa-icon id="save-copy-icon" name="floppy-disk"></wa-icon> 29 + </wa-button> 19 30 </div> 20 31 21 32 <wa-dialog id="facet-picker" label="Choose a facet" style="--width: 360px"> ··· 126 137 top: var(--wa-space-xs); 127 138 left: var(--wa-space-xs); 128 139 z-index: 100; 140 + display: flex; 141 + flex-direction: column; 142 + gap: var(--wa-space-2xs); 129 143 } 130 144 131 145 #facet-select {
+110
src/facets/tools/split-view.inline.js
··· 7 7 import "@awesome.me/webawesome/dist/components/option/option.js"; 8 8 9 9 import "~/common/webawesome/detect-dark.js"; 10 + import foundation from "~/common/facets/foundation.js"; 11 + import * as Output from "~/common/output.js"; 10 12 11 13 /** 12 14 * @import { default as WaSplitPanel } from "@awesome.me/webawesome/dist/components/split-panel/split-panel.js" ··· 275 277 const editToggle = 276 278 /** @type {HTMLElement} */ (document.querySelector("#edit-toggle")); 277 279 const editIcon = /** @type {WaIcon} */ (document.querySelector("#edit-icon")); 280 + const saveCopyBtn = 281 + /** @type {HTMLElement} */ (document.querySelector("#save-copy-btn")); 282 + const saveCopyIcon = 283 + /** @type {WaIcon} */ (document.querySelector("#save-copy-icon")); 278 284 279 285 editToggle.addEventListener("click", () => { 280 286 editMode = !editMode; 281 287 editToggle.setAttribute("aria-label", editMode ? "Done" : "Edit layout"); 282 288 editIcon.name = editMode ? "xmark" : "border-all"; 283 289 layout.classList.toggle("edit-mode", editMode); 290 + saveCopyBtn.style.display = editMode ? "" : "none"; 291 + }); 292 + 293 + saveCopyBtn.addEventListener("click", async () => { 294 + await saveSimplifiedCopy(); 295 + saveCopyIcon.name = "check"; 296 + setTimeout(() => { 297 + saveCopyIcon.name = "floppy-disk"; 298 + }, 2000); 284 299 }); 285 300 286 301 // ─── Facet picker ───────────────────────────────────────────────────────────── ··· 312 327 facetSelect.value = null; 313 328 customPath.value = ""; 314 329 pickerDialog.open = true; 330 + } 331 + 332 + // ─── Save simplified copy ───────────────────────────────────────────────────── 333 + 334 + /** 335 + * @param {Node} node 336 + * @param {string} indent 337 + * @returns {string} 338 + */ 339 + function generateNodeHTML(node, indent = "") { 340 + const inner = indent + " "; 341 + 342 + if (node.type === "split") { 343 + const orientationAttr = node.orientation === "vertical" 344 + ? ' orientation="vertical"' 345 + : ""; 346 + return `${indent}<wa-split-panel position="${node.position}"${orientationAttr}> 347 + ${inner}<div slot="start"> 348 + ${generateNodeHTML(node.start, inner + " ")} 349 + ${inner}</div> 350 + ${inner}<div slot="end"> 351 + ${generateNodeHTML(node.end, inner + " ")} 352 + ${inner}</div> 353 + ${indent}</wa-split-panel>`; 354 + } 355 + 356 + if (node.facet) { 357 + const uri = node.facet.includes("://") 358 + ? node.facet 359 + : `diffuse://${node.facet}`; 360 + const src = "facets/l/?uri=" + encodeURIComponent(uri); 361 + return `${indent}<div class="pane"> 362 + ${inner}<iframe src="${src}" allow="autoplay"></iframe> 363 + ${indent}</div>`; 364 + } 365 + 366 + return `${indent}<div class="pane"></div>`; 367 + } 368 + 369 + function generateSimplifiedHTML() { 370 + const scriptClose = "</" + "script>"; 371 + return `\ 372 + <link rel="stylesheet" href="vendor/@awesome.me/webawesome/styles/themes/default.css" /> 373 + 374 + <style> 375 + body { margin: 0; height: 100dvh; overflow: hidden; } 376 + #layout, #layout > * { height: 100%; } 377 + wa-split-panel { height: 100%; } 378 + [slot="start"], [slot="end"] { height: 100%; } 379 + .pane { height: 100%; } 380 + .pane iframe { border: none; width: 100%; height: 100%; } 381 + .dragging iframe { pointer-events: none; } 382 + </style> 383 + 384 + <div id="layout"> 385 + ${generateNodeHTML(state, " ")} 386 + </div> 387 + 388 + <script type="module"> 389 + import "@awesome.me/webawesome/dist/components/split-panel/split-panel.js"; 390 + import "~/common/webawesome/detect-dark.js"; 391 + 392 + const layout = document.querySelector("#layout"); 393 + 394 + document.addEventListener("mousedown", (e) => { 395 + const isDivider = e.composedPath().some( 396 + (el) => el instanceof Element && el.getAttribute("part") === "divider", 397 + ); 398 + if (isDivider) layout.classList.add("dragging"); 399 + }, { capture: true }); 400 + 401 + document.addEventListener("mouseup", () => { 402 + layout.classList.remove("dragging"); 403 + }); 404 + ${scriptClose}`; 405 + } 406 + 407 + async function saveSimplifiedCopy() { 408 + const output = foundation.orchestrator.output(); 409 + await Output.waitUntilLoaded(output.facets); 410 + 411 + const html = generateSimplifiedHTML(); 412 + const now = new Date().toISOString(); 413 + 414 + await output.facets.save([ 415 + ...output.facets.collection(), 416 + { 417 + $type: "sh.diffuse.output.facet", 418 + id: crypto.randomUUID(), 419 + name: "Split View", 420 + html, 421 + createdAt: now, 422 + updatedAt: now, 423 + }, 424 + ]); 315 425 } 316 426 317 427 // ─── Init ─────────────────────────────────────────────────────────────────────
+2 -3
src/index.js
··· 1 1 import { GROUP } from "~/common/facets/foundation.js"; 2 - 3 - import { effect } from "~/common/signal.js"; 2 + import * as Output from "~/common/output.js"; 4 3 5 4 import InputConfigurator from "~/components/configurator/input/element.js"; 6 5 import MetadataProcessor from "~/components/processor/metadata/element.js"; ··· 43 42 </span>`; 44 43 45 44 const demo = await s3.demo(); 46 - await output.tracks.loaded(); 45 + await Output.waitUntilLoaded(output.tracks); 47 46 48 47 addDemoBtn.innerHTML = `<span> 49 48 <i class="ph-fill ph-hourglass-medium"></i>