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: initial working constituent loader

+262 -64
+1
_config.ts
··· 123 123 124 124 // MISC 125 125 126 + site.add("/themes/loader/constituent/examples/"); 126 127 site.use(sourceMaps()); 127 128 128 129 site.script("copy-type-defs", () => {
+2 -2
deno.jsonc
··· 5 5 "imports": { 6 6 "98.css": "npm:98.css@^0.1.21", 7 7 "@atcute/cbor": "npm:@atcute/cbor@^2.3.0", 8 - "@atcute/cid": "npm:@atcute/cid@^2.4.0", 8 + "@atcute/cid": "https://esm.sh/@atcute/cid@2.4.0", 9 9 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.7", 10 10 "@automerge/automerge": "npm:@automerge/automerge@^3.2.3", 11 11 "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.9.4", ··· 51 51 "@dotenv-run/esbuild": "npm:@dotenv-run/esbuild@^1.5.1", 52 52 "@std/fs": "jsr:@std/fs@^1.0.19", 53 53 "@std/path": "jsr:@std/path@^1.1.2", 54 - "esbuild-plugins-node-modules-polyfill": "npm:esbuild-plugins-node-modules-polyfill@^1.7.1", 54 + "esbuild-plugins-node-modules-polyfill": "npm:esbuild-plugins-node-modules-polyfill@^1.8.1", 55 55 "esbuild-plugin-wasm": "npm:esbuild-plugin-wasm@^1.1.0", 56 56 "lume/": "https://cdn.jsdelivr.net/gh/lumeland/lume@3.1.4/", 57 57 "lume/jsx-runtime": "https://cdn.jsdelivr.net/gh/oscarotero/ssx@0.1.14/jsx-runtime.ts",
+20 -27
src/common/constituents/foundation.js
··· 110 110 const a = new AudioEngine(); 111 111 a.setAttribute("group", GROUP); 112 112 113 - addToBodyIfNeeded(a); 114 - return a; 113 + return findExistingOrAdd(a) 115 114 } 116 115 117 116 function queue() { 118 117 const q = new Queue(); 119 118 q.setAttribute("group", GROUP); 120 119 121 - addToBodyIfNeeded(q); 122 - return q; 120 + return findExistingOrAdd(q); 123 121 } 124 122 125 123 // Processors ··· 127 125 const a = new ArtworkProcessor(); 128 126 a.setAttribute("group", GROUP); 129 127 130 - addToBodyIfNeeded(a); 131 - return a; 128 + return findExistingOrAdd(a); 132 129 } 133 130 134 131 function metadata() { 135 132 const m = new MetadataProcessor(); 136 133 m.setAttribute("group", GROUP); 137 134 138 - addToBodyIfNeeded(m); 139 - return m; 135 + return findExistingOrAdd(m); 140 136 } 141 137 142 138 function search() { 143 139 const s = new SearchProcessor(); 144 140 s.setAttribute("group", GROUP); 145 141 146 - addToBodyIfNeeded(s); 147 - return s; 142 + return findExistingOrAdd(s); 148 143 } 149 144 150 145 // Orchestrators ··· 153 148 i.setAttribute("group", GROUP); 154 149 i.setAttribute("id", "input"); 155 150 156 - addToBodyIfNeeded(i); 157 - return i; 151 + return findExistingOrAdd(i); 158 152 } 159 153 160 154 function output() { ··· 162 156 o.setAttribute("group", GROUP); 163 157 o.setAttribute("id", "output"); 164 158 165 - addToBodyIfNeeded(o); 166 - return o; 159 + return findExistingOrAdd(o); 167 160 } 168 161 169 162 function processTracks() { ··· 178 171 opt.setAttribute("metadata-processor-selector", m.selector); 179 172 opt.toggleAttribute("process-when-ready"); 180 173 181 - document.body.append(opt); 174 + return findExistingOrAdd(opt); 182 175 } 183 176 184 177 function queueAudio() { ··· 192 185 oqa.setAttribute("input-selector", i.selector); 193 186 oqa.setAttribute("queue-engine-selector", q.selector); 194 187 195 - addToBodyIfNeeded(oqa); 196 - return oqa; 188 + return findExistingOrAdd(oqa); 197 189 } 198 190 199 191 function queueTracks() { ··· 207 199 oqt.setAttribute("output-selector", o.selector); 208 200 oqt.setAttribute("queue-engine-selector", q.selector); 209 201 210 - addToBodyIfNeeded(oqt); 211 - return oqt; 202 + return findExistingOrAdd(oqt); 212 203 } 213 204 214 205 function repeatShuffle() { ··· 218 209 ors.setAttribute("group", GROUP); 219 210 ors.setAttribute("queue-engine-selector", q.selector); 220 211 221 - addToBodyIfNeeded(ors); 222 - return ors; 212 + return findExistingOrAdd(ors); 223 213 } 224 214 225 215 function searchTracks() { ··· 233 223 ost.setAttribute("output-selector", o.selector); 234 224 ost.setAttribute("search-processor-selector", s.selector); 235 225 236 - addToBodyIfNeeded(ost); 237 - return ost; 226 + return findExistingOrAdd(ost); 238 227 } 239 228 240 229 function sources() { ··· 245 234 so.setAttribute("input-selector", i.selector); 246 235 so.setAttribute("output-selector", o.selector); 247 236 248 - addToBodyIfNeeded(so); 249 - return so; 237 + return findExistingOrAdd(so); 250 238 } 251 239 252 240 // 🛠️ ··· 254 242 /** 255 243 * @param {DiffuseElement} element 256 244 */ 257 - export function addToBodyIfNeeded(element) { 245 + export function findExistingOrAdd(element) { 258 246 const alreadyAdded = document.body.querySelector(element.selector); 259 - if (!alreadyAdded) document.body.append(element); 247 + if (!alreadyAdded) { 248 + document.body.append(element); 249 + return element 250 + } 251 + 252 + return alreadyAdded 260 253 }
+5 -2
src/components/orchestrator/output/element.js
··· 9 9 10 10 /** 11 11 * @import {RenderArg} from "@common/element.d.ts" 12 - * @import {Track} from "@definitions/types.d.ts" 13 12 * @import {OutputElement} from "@components/output/types.d.ts" 14 13 */ 15 14 ··· 28 27 */ 29 28 get output() { 30 29 /** @type {OutputElement | null} */ 31 - const output = this.querySelector("#do-output__output"); 30 + const output = this.root().querySelector("#do-output__output"); 32 31 33 32 if (!output) throw new Error("Output orchestrator did not render yet."); 34 33 return output; 35 34 } 36 35 37 36 // PROXY OUTPUT ACTIONS 37 + 38 + get constituents() { 39 + return this.output.constituents; 40 + } 38 41 39 42 get tracks() { 40 43 return this.output.tracks;
+3 -3
src/definitions/output/constituent.json
··· 6 6 "type": "record", 7 7 "record": { 8 8 "type": "object", 9 - "required": ["cid", "html", "label"], 9 + "required": ["cid", "html", "name"], 10 10 "properties": { 11 11 "cid": { 12 12 "type": "string", 13 - "description": "A DASL CID representing the DRISL-encoded HTML" 13 + "description": "A DASL CID representing the DRISL-encoded HTML (raw 0x55 codec)" 14 14 }, 15 15 "description": { "type": "string" }, 16 16 "html": { "type": "string", "description": "The constituent HTML" }, 17 - "label": { "type": "string" } 17 + "name": { "type": "string" } 18 18 } 19 19 } 20 20 }
-1
src/index.vto
··· 38 38 title: "Loader" 39 39 desc: > 40 40 **Bring in other constituents!** Load a constituent from a URL, text snippet or from your user data output. 41 - todo: true 42 41 - url: "themes/webamp/browser/" 43 42 title: "Webamp / Browser" 44 43 desc: >
+6 -1
src/styles/diffuse/page.css
··· 111 111 * Forms 112 112 */ 113 113 114 + input, 114 115 textarea { 115 116 background: transparent; 116 117 border: 3px solid var(--form-color); 117 118 color: inherit; 118 119 font-size: var(--fs-sm); 120 + padding: var(--space-2xs); 121 + width: 100%; 122 + } 123 + 124 + textarea { 119 125 height: var(--container-xs); 120 126 padding: var(--space-xs); 121 127 resize: none; 122 - width: 100%; 123 128 } 124 129 125 130 /**
+29
src/themes/loader/constituent/examples/now-playing.txt
··· 1 + <div id="now-playing">Loading ...</div> 2 + <button>⏭</button> 3 + 4 + <script type="module"> 5 + import foundation from "./common/constituents/foundation.js"; 6 + import { effect } from "./common/signal.js"; 7 + 8 + const components = foundation.assemblage.queueManagement(); 9 + const queue = components.engine.queue; 10 + 11 + effect(() => { 12 + const currentlyPlaying = queue.now(); 13 + const tags = currentlyPlaying?.tags; 14 + 15 + const element = document.querySelector("#now-playing"); 16 + if (!element) return; 17 + 18 + if (currentlyPlaying) { 19 + element.innerText = `${tags.artist} - ${tags.title}`; 20 + } else { 21 + element.innerText = "Nothing is playing yet"; 22 + } 23 + }); 24 + 25 + document.body.querySelector("button").onclick = () => { 26 + if (components.orchestrator.output.tracks.state() !== "loaded") return; 27 + queue.shift(); 28 + }; 29 + </script>
+109
src/themes/loader/constituent/index.js
··· 1 + import * as CID from "@atcute/cid"; 2 + import { html, render } from "lit-html"; 3 + 4 + import foundation from "@common/constituents/foundation.js"; 5 + import { effect } from "@common/signal.js"; 6 + 7 + /** 8 + * @import {Constituent} from "@definitions/types.d.ts" 9 + */ 10 + 11 + //////////////////////////////////////////// 12 + // LIST 13 + //////////////////////////////////////////// 14 + 15 + /** @type {HTMLElement | null} */ 16 + const listEl = document.querySelector("#list"); 17 + if (!listEl) throw new Error("List element not found"); 18 + 19 + const output = foundation.orchestrator.output(); 20 + 21 + effect(() => { 22 + const col = output.constituents.collection(); 23 + 24 + const h = col.length 25 + ? html` 26 + <ul> 27 + ${col.map((c) => 28 + html` 29 + <li> 30 + <a href="themes/loader/constituent/s/?cid=${c.cid}"> 31 + ${c.name} 32 + </a> 33 + </li> 34 + ` 35 + )} 36 + </ul> 37 + ` 38 + : output.constituents.state() === "loaded" 39 + ? emptyConstituentsList 40 + : html` 41 + <i class="ph-bold ph-spinner-gap"></i> 42 + `; 43 + 44 + render(h, listEl); 45 + }); 46 + 47 + const emptyConstituentsList = html` 48 + <p style="margin-bottom: 0;"> 49 + <i class="ph-fill ph-info"></i> You have not added any constituents yet. Add 50 + or create some using the tools below. 51 + </p> 52 + `; 53 + 54 + //////////////////////////////////////////// 55 + // BUILD 56 + //////////////////////////////////////////// 57 + 58 + document.querySelector("#build-form")?.addEventListener( 59 + "submit", 60 + onBuildSubmit, 61 + ); 62 + 63 + /** 64 + * @param {Event} event 65 + */ 66 + async function onBuildSubmit(event) { 67 + event.preventDefault(); 68 + 69 + const htmlEl = 70 + /** @type {HTMLTextAreaElement | null} */ (document.querySelector( 71 + "#html-input", 72 + )); 73 + const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 74 + "#name-input", 75 + )); 76 + 77 + const html = htmlEl?.value ?? ""; 78 + const cid = await CID.create(0x55, new TextEncoder().encode(html)); 79 + const name = nameEl?.value ?? "nameless"; 80 + 81 + /** @type {Constituent} */ 82 + const constituent = { 83 + $type: "sh.diffuse.output.constituent", 84 + cid: CID.toString(cid), 85 + html, 86 + name, 87 + }; 88 + 89 + switch (/** @type {any} */ (event).submitter.name) { 90 + case "load-example": { 91 + /** @type {HTMLSelectElement | null} */ 92 + const selected = document.body.querySelector("#example-select"); 93 + 94 + if (htmlEl && selected?.value) { 95 + htmlEl.value = await fetch( 96 + `themes/loader/constituent/examples/${selected.value}`, 97 + ).then((r) => r.text()); 98 + } 99 + break; 100 + } 101 + case "save": 102 + await output.constituents.save([constituent]); 103 + break; 104 + case "save+open": 105 + await output.constituents.save([constituent]); 106 + window.open(`${location.href}s/?cid=${constituent.cid}`, "blank"); 107 + break; 108 + } 109 + }
+19 -23
src/themes/loader/constituent/index.vto
··· 5 5 styles: 6 6 - styles/base.css 7 7 - styles/diffuse/page.css 8 + - styles/vendor/phosphor/bold/style.css 8 9 - styles/vendor/phosphor/fill/style.css 9 10 10 11 scripts: 11 - - index.js 12 + - themes/loader/constituent/index.js 12 13 --- 13 14 14 15 <header> ··· 34 35 35 36 <section class="flex"> 36 37 <h2 id="yours" style="margin-top: 0;">Yours</h2> 37 - 38 - <p style="margin-bottom: 0;"> 39 - <i class="ph-fill ph-info"></i> You have not added any constituents yet. Add or create some using the tools below. 40 - </p> 38 + <div id="list"></div> 41 39 </section> 42 40 </div> 43 41 ··· 61 59 </section> 62 60 </div> 63 61 64 - <!-- CONSTRUCT --> 62 + <!-- BUILD --> 65 63 <section> 66 64 <h2 id="build">Build</h2> 67 65 68 - <div class="columns"> 66 + <form id="build-form" class="columns"> 69 67 <div class="flex"> 70 68 <p style="margin-top: 0"> 71 69 If you know a bit of HTML & Javascript, you can write your own or plug in some code you found elsewhere: 72 70 </p> 73 71 74 - <form> 75 - <textarea class="monospace-font" placeholder="<code>goes here</code>"></textarea> 76 - </form> 72 + <div> 73 + <textarea id="html-input" class="monospace-font" placeholder="<div>goes here</div>"></textarea> 74 + </div> 77 75 </div> 78 76 79 77 <div class="flex"> 80 78 <p style="margin-top: 0"> 81 - Your code here builds on the <a href="themes/loader/constituent/#foundation">foundation</a> listed below, it'll be injected into a <code>&lt;body&gt;</code> element. 79 + Your code here builds on the <a href="themes/loader/constituent/#foundation">foundation</a> listed below, it'll be injected into a <code>&lt;div id="container"&gt;</code> element in the body. 82 80 </p> 81 + <input id="name-input" type="text" placeholder="Unique name" name="name" value="Unique name" required /> 83 82 <p> 84 83 <span class="button-row"> 85 - <button disabled>Save</button> 86 - <button disabled>Save &amp; Open</button> 87 - <button disabled>Preview</button> 84 + <button name="save">Save</button> 85 + <button name="save+open">Save &amp; Open</button> 88 86 </span> 89 87 </p> 90 88 <p> 91 - Add element assemblage: 89 + Browse examples: 92 90 </p> 93 - <form> 94 - <select> 95 - <option>Play audio from queue</option> 96 - <option>Queue management</option> 97 - <option>Search through collection</option> 91 + <div> 92 + <select id="example-select"> 93 + <option value="now-playing.txt" selected>Now playing + Next Queue Item</option> 98 94 </select> 99 - </form> 95 + </div> 100 96 <p> 101 97 <span class="button-row"> 102 - <button disabled>Add code</button> 98 + <button name="load-example">Load example</button> 103 99 </span> 104 100 </p> 105 101 </div> 106 - </div> 102 + </form> 107 103 </section> 108 104 109 105 <!-- FOUNDATION -->
+62
src/themes/loader/constituent/s/index.js
··· 1 + import foundation from "@common/constituents/foundation.js"; 2 + import { effect } from "@common/signal.js"; 3 + 4 + /** 5 + * @import {Constituent} from "@definitions/types.d.ts" 6 + */ 7 + 8 + //////////////////////////////////////////// 9 + // OUTPUT 10 + //////////////////////////////////////////// 11 + 12 + const output = foundation.orchestrator.output(); 13 + 14 + //////////////////////////////////////////// 15 + // URL PARAMS 16 + //////////////////////////////////////////// 17 + 18 + const url = new URL(document.location.href); 19 + 20 + const cid = url.searchParams.get("cid"); 21 + const name = url.searchParams.get("name"); 22 + 23 + //////////////////////////////////////////// 24 + // LOAD 25 + //////////////////////////////////////////// 26 + 27 + const containerNull = document.querySelector("#container"); 28 + if (!containerNull) throw new Error("Container not found"); 29 + 30 + const container = /** @type {HTMLDivElement} */ (containerNull); 31 + 32 + effect(() => { 33 + const collection = output.constituents.collection(); 34 + if (output.constituents.state() !== "loaded") return; 35 + 36 + let constituent; 37 + 38 + if (cid) { 39 + constituent = collection.find((c) => c.cid === cid); 40 + } else if (name) { 41 + constituent = collection.find((c) => c.name === name); 42 + } 43 + 44 + // TODO: Message that constituent was not found 45 + if (!constituent) return; 46 + 47 + loadIntoContainer(constituent); 48 + }); 49 + 50 + /** 51 + * @param {Constituent} constituent 52 + */ 53 + function loadIntoContainer(constituent) { 54 + // TODO: Validate if CID matches HTML 55 + 56 + const range = document.createRange(); 57 + range.selectNode(container); 58 + const documentFragment = range.createContextualFragment(constituent.html); 59 + 60 + container.innerHTML = ""; 61 + container.append(documentFragment); 62 + }
+5
src/themes/loader/constituent/s/index.vto
··· 4 4 5 5 styles: 6 6 - styles/base.css 7 + 8 + scripts: 9 + - themes/loader/constituent/s/index.js 7 10 --- 11 + 12 + <div id="container"></div>
+1 -5
src/themes/webamp/configurators/output.js
··· 1 - import { DiffuseElement, query, whenElementsDefined } from "@common/element.js"; 2 - import { signal } from "@common/signal.js"; 1 + import { DiffuseElement } from "@common/element.js"; 3 2 4 3 /** 5 4 * @import {RenderArg} from "@common/element.d.ts" 6 - * @import {Track} from "@definitions/types.d.ts" 7 - * @import {InputElement} from "@components/input/types.d.ts" 8 - * @import {OutputElement} from "@components/output/types.d.ts" 9 5 */ 10 6 11 7 class OutputConfig extends DiffuseElement {