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

feat: facet kinds + load facet preludes

+154 -22
+23 -1
src/_components/facets/grid.vto
··· 1 <ul class="grid" style="margin-top: var(--space-lg);"> 2 {{ for index, item of items }} 3 - <li class="grid-item" data-uri="{{ item.url |> facetOrThemeURI }}" data-name="{{item.title}}"> 4 <div class="grid-item__contents"> 5 <div class="grid-item__title"> 6 <a href="{{ item.url |> facetLoaderURL }}" style="padding: var(--space-3xs) 0"> 7 {{item.title}} 8 </a> 9 </div> 10 <div class="list-description"> 11 {{ item.desc |> md(true) }}
··· 1 <ul class="grid" style="margin-top: var(--space-lg);"> 2 {{ for index, item of items }} 3 + {{ set color = (() => { 4 + switch (item.kind) { 5 + case "prelude": return "var(--accent-twist-4)"; 6 + default: return "var(--accent-twist-2)"; 7 + } 8 + })() }} 9 + 10 + {{ set kind = (() => { 11 + // return item.kind ?? "interactive" 12 + switch (item.kind) { 13 + case "prelude": return "feature"; 14 + default: return "interface"; 15 + } 16 + })() }} 17 + 18 + <li 19 + class="grid-item" 20 + data-active-color="{{color}}" 21 + data-name="{{item.title}}" 22 + data-uri="{{ item.url |> facetOrThemeURI }}" 23 + > 24 <div class="grid-item__contents"> 25 <div class="grid-item__title"> 26 <a href="{{ item.url |> facetLoaderURL }}" style="padding: var(--space-3xs) 0"> 27 {{item.title}} 28 </a> 29 + <span style="flex: 1"></span> 30 + <span class="grid-item__kind" style="color: {{ color }};">{{ kind }}</span> 31 </div> 32 <div class="list-description"> 33 {{ item.desc |> md(true) }}
+30
src/common/facets/category.js
···
··· 1 + /** 2 + * @import { Facet } from "~/definitions/types.d.ts"; 3 + */ 4 + 5 + /** 6 + * @param {Facet} facet 7 + * @returns {string} 8 + */ 9 + export function color(facet) { 10 + switch (facet.kind) { 11 + case "prelude": 12 + return "var(--accent-twist-4)"; 13 + default: 14 + return "var(--accent-twist-2)"; 15 + } 16 + } 17 + 18 + /** 19 + * @param {Facet} facet 20 + * @returns {string} 21 + */ 22 + export function name(facet) { 23 + // return facet.kind ?? "interactive"; 24 + switch (facet.kind) { 25 + case "prelude": 26 + return "feature"; 27 + default: 28 + return "interface"; 29 + } 30 + }
+1 -7
src/common/facets/foundation.js
··· 82 } 83 84 async function playAudioFromQueue() { 85 - const [sc, a, q, ms, qa, sca] = await Promise.all([ 86 - // configurator 87 - scrobbles(), 88 - 89 // engine 90 audio(), 91 queue(), ··· 97 ]); 98 99 return { 100 - configurator: { 101 - scrobbles: sc, 102 - }, 103 engine: { 104 audio: a, 105 queue: q,
··· 82 } 83 84 async function playAudioFromQueue() { 85 + const [a, q, ms, qa, sca] = await Promise.all([ 86 // engine 87 audio(), 88 queue(), ··· 94 ]); 95 96 return { 97 engine: { 98 audio: a, 99 queue: q,
+6
src/definitions/output/facet.json
··· 22 "type": "string", 23 "description": "The UTF8 HTML string that makes up the facet" 24 }, 25 "name": { "type": "string" }, 26 "updatedAt": { "type": "string", "format": "datetime" }, 27 "uri": {
··· 22 "type": "string", 23 "description": "The UTF8 HTML string that makes up the facet" 24 }, 25 + "kind": { 26 + "type": "string", 27 + "default": "interactive", 28 + "enum": ["interactive", "prelude"], 29 + "description": "A facet is by default interactive, but headless 'prelude' facets may also be created, these run before any main interactive facet is loaded." 30 + }, 31 "name": { "type": "string" }, 32 "updatedAt": { "type": "string", "format": "datetime" }, 33 "uri": {
+10
src/facets/build.vto
··· 33 <input id="name-input" type="text" placeholder="Name" name="name" required /> 34 </p> 35 <p> 36 <textarea id="description-input" placeholder="Description" name="description" rows="5"></textarea> 37 </p> 38 <p> ··· 61 {{- echo -}} 62 import foundation from "common/facets/foundation.js" 63 {{ /echo }} 64 {{ echo -}}await foundation.engine.audio(){{- /echo }} 65 {{ echo -}}await foundation.engine.queue(){{- /echo }} 66 {{ echo -}}await foundation.engine.repeatShuffle(){{- /echo }} ··· 69 {{ echo -}}await foundation.orchestrator.autoQueue(){{- /echo }} 70 {{ echo -}}await foundation.orchestrator.favourites(){{- /echo }} 71 {{ echo -}}await foundation.orchestrator.input(){{- /echo }} 72 {{ echo -}}await foundation.orchestrator.output(){{- /echo }} 73 {{ echo -}}await foundation.orchestrator.queueAudio(){{- /echo }} 74 {{ echo -}}await foundation.orchestrator.processTracks(){{- /echo }} 75 {{ echo -}}await foundation.orchestrator.scopedTracks(){{- /echo }} 76 {{ echo -}}await foundation.orchestrator.sources(){{- /echo }} 77 78 {{ echo -}}await foundation.processor.artwork(){{- /echo }}
··· 33 <input id="name-input" type="text" placeholder="Name" name="name" required /> 34 </p> 35 <p> 36 + <select id="kind-input" name="kind"> 37 + <option value="interactive" selected>Interactive</option> 38 + <option value="prelude">Prelude</option> 39 + </select> 40 + </p> 41 + <p> 42 <textarea id="description-input" placeholder="Description" name="description" rows="5"></textarea> 43 </p> 44 <p> ··· 67 {{- echo -}} 68 import foundation from "common/facets/foundation.js" 69 {{ /echo }} 70 + {{ echo -}}await foundation.configurator.scrobbles(){{- /echo }} 71 + 72 {{ echo -}}await foundation.engine.audio(){{- /echo }} 73 {{ echo -}}await foundation.engine.queue(){{- /echo }} 74 {{ echo -}}await foundation.engine.repeatShuffle(){{- /echo }} ··· 77 {{ echo -}}await foundation.orchestrator.autoQueue(){{- /echo }} 78 {{ echo -}}await foundation.orchestrator.favourites(){{- /echo }} 79 {{ echo -}}await foundation.orchestrator.input(){{- /echo }} 80 + {{ echo -}}await foundation.orchestrator.mediaSession(){{- /echo }} 81 {{ echo -}}await foundation.orchestrator.output(){{- /echo }} 82 {{ echo -}}await foundation.orchestrator.queueAudio(){{- /echo }} 83 {{ echo -}}await foundation.orchestrator.processTracks(){{- /echo }} 84 {{ echo -}}await foundation.orchestrator.scopedTracks(){{- /echo }} 85 + {{ echo -}}await foundation.orchestrator.scrobbleAudio(){{- /echo }} 86 {{ echo -}}await foundation.orchestrator.sources(){{- /echo }} 87 88 {{ echo -}}await foundation.processor.artwork(){{- /echo }}
+15
src/facets/common/build.js
··· 97 document.querySelector("#description-input") 98 ); 99 100 const html = editor.state.doc.toString(); 101 const cid = await CID.create(0x55, new TextEncoder().encode(html)); 102 const name = nameEl?.value ?? "nameless"; 103 const description = descriptionEl?.value ?? ""; 104 105 /** @type {Facet} */ 106 const facet = $editingFacet.value ··· 109 cid, 110 description, 111 html, 112 name, 113 } 114 : { ··· 117 cid, 118 description, 119 html, 120 name, 121 }; 122 ··· 144 document.querySelector("#description-input") 145 ); 146 147 if (!nameEl) return; 148 149 // Reset url — remove `id` param if not matching the facet ··· 169 170 $editingFacet.value = facet; 171 nameEl.value = facet.name; 172 173 if (descriptionEl) { 174 descriptionEl.value = facet.description ?? "";
··· 97 document.querySelector("#description-input") 98 ); 99 100 + const kindEl = /** @type {HTMLSelectElement | null} */ ( 101 + document.querySelector("#kind-input") 102 + ); 103 + 104 const html = editor.state.doc.toString(); 105 const cid = await CID.create(0x55, new TextEncoder().encode(html)); 106 const name = nameEl?.value ?? "nameless"; 107 const description = descriptionEl?.value ?? ""; 108 + const kind = /** @type {"interactive" | "prelude"} */ (kindEl?.value ?? "interactive"); 109 110 /** @type {Facet} */ 111 const facet = $editingFacet.value ··· 114 cid, 115 description, 116 html, 117 + kind, 118 name, 119 } 120 : { ··· 123 cid, 124 description, 125 html, 126 + kind, 127 name, 128 }; 129 ··· 151 document.querySelector("#description-input") 152 ); 153 154 + const kindEl = /** @type {HTMLSelectElement | null} */ ( 155 + document.querySelector("#kind-input") 156 + ); 157 + 158 if (!nameEl) return; 159 160 // Reset url — remove `id` param if not matching the facet ··· 180 181 $editingFacet.value = facet; 182 nameEl.value = facet.name; 183 + 184 + if (kindEl) { 185 + kindEl.value = facet.kind ?? "interactive"; 186 + } 187 188 if (descriptionEl) { 189 descriptionEl.value = facet.description ?? "";
+1 -1
src/facets/common/grid.js
··· 82 ? "ph-fill ph-toggle-right" 83 : "ph-fill ph-toggle-left"; 84 /** @type {HTMLElement} */ (icon).style.color = isActive 85 - ? "var(--accent-twist-2)" 86 : ""; 87 } 88 });
··· 82 ? "ph-fill ph-toggle-right" 83 : "ph-fill ph-toggle-left"; 84 /** @type {HTMLElement} */ (icon).style.color = isActive 85 + ? li.getAttribute("data-active-color") ?? "var(--accent-twist-2)" 86 : ""; 87 } 88 });
+11 -5
src/facets/common/you.js
··· 3 import { marked } from "marked"; 4 import { unsafeHTML } from "lit-html/directives/unsafe-html.js"; 5 6 import foundation from "~/common/facets/foundation.js"; 7 import { effect } from "~/common/signal.js"; 8 import { facetFromURI } from "~/common/facets/utils.js"; ··· 202 const h = col.length 203 ? html` 204 <ul class="grid" style="margin: 0"> 205 - ${col.map((c, index) => 206 - keyed( 207 c.id, 208 html` 209 <li class="grid-item"> 210 <div class="grid-item__contents"> 211 - <div> 212 <a 213 href="facets/l/?id=${c 214 .id}" ··· 216 > 217 ${c.name} 218 </a> 219 </div> 220 <div class="list-description"> 221 <div> ··· 263 </div> 264 </li> 265 `, 266 - ) 267 - )} ${ADD_FROM_URI_ITEM} 268 </ul> 269 ` 270 : html`
··· 3 import { marked } from "marked"; 4 import { unsafeHTML } from "lit-html/directives/unsafe-html.js"; 5 6 + import * as FacetCategory from "~/common/facets/category.js"; 7 import foundation from "~/common/facets/foundation.js"; 8 import { effect } from "~/common/signal.js"; 9 import { facetFromURI } from "~/common/facets/utils.js"; ··· 203 const h = col.length 204 ? html` 205 <ul class="grid" style="margin: 0"> 206 + ${col.map((c, index) => { 207 + const color = FacetCategory.color(c); 208 + const kind = FacetCategory.name(c); 209 + 210 + return keyed( 211 c.id, 212 html` 213 <li class="grid-item"> 214 <div class="grid-item__contents"> 215 + <div class="grid-item__title"> 216 <a 217 href="facets/l/?id=${c 218 .id}" ··· 220 > 221 ${c.name} 222 </a> 223 + <span class="grid-item__kind" style="color: ${color};" 224 + >${kind}</span> 225 </div> 226 <div class="list-description"> 227 <div> ··· 269 </div> 270 </li> 271 `, 272 + ); 273 + })} ${ADD_FROM_URI_ITEM} 274 </ul> 275 ` 276 : html`
+26 -6
src/facets/l/index.js
··· 1 import foundation from "~/common/facets/foundation.js"; 2 import * as CID from "~/common/cid.js"; 3 - import { createLoader, renderError } from "~/common/loader.js"; 4 5 const output = await foundation.orchestrator.output(); 6 7 createLoader({ 8 $type: "sh.diffuse.output.facet", 9 label: "Facet", 10 source: () => output.facets, 11 async render(facet) { 12 - const container = /** @type {HTMLDivElement} */ ( 13 - document.querySelector("#container") 14 - ); 15 - 16 if (facet.cid) { 17 const valid = await CID.verify( 18 new TextEncoder().encode(facet.html ?? ""), ··· 28 } 29 } 30 31 const range = document.createRange(); 32 range.selectNode(container); 33 const documentFragment = range.createContextualFragment(facet.html ?? ""); 34 35 - container.innerHTML = ""; 36 container.append(documentFragment); 37 }, 38 });
··· 1 import foundation from "~/common/facets/foundation.js"; 2 import * as CID from "~/common/cid.js"; 3 + import * as Output from "~/common/output.js"; 4 + import { createLoader, loadURI, renderError } from "~/common/loader.js"; 5 6 + // Output element 7 const output = await foundation.orchestrator.output(); 8 9 + // Contaienr 10 + const container = /** @type {HTMLDivElement} */ ( 11 + document.querySelector("#container") 12 + ); 13 + 14 + // Preludes 15 + const facets = await Output.data(output.facets); 16 + 17 + // Load 18 createLoader({ 19 $type: "sh.diffuse.output.facet", 20 label: "Facet", 21 source: () => output.facets, 22 async render(facet) { 23 if (facet.cid) { 24 const valid = await CID.verify( 25 new TextEncoder().encode(facet.html ?? ""), ··· 35 } 36 } 37 38 + container.innerHTML = ""; 39 + 40 const range = document.createRange(); 41 range.selectNode(container); 42 const documentFragment = range.createContextualFragment(facet.html ?? ""); 43 44 + const preludes = facets 45 + .filter((f) => f.kind === "prelude") 46 + .sort((a, b) => a.name.localeCompare(b.name)); 47 + 48 + for (const prelude of preludes) { 49 + const html = prelude.html ?? 50 + (prelude.uri ? await loadURI(prelude.uri) : ""); 51 + if (!html) continue; 52 + const preludeFragment = range.createContextualFragment(html); 53 + container.append(preludeFragment); 54 + } 55 + 56 container.append(documentFragment); 57 }, 58 });
+31 -2
src/styles/diffuse/page.css
··· 222 width: 100%; 223 } 224 225 textarea { 226 padding: var(--space-xs); 227 resize: none; ··· 252 .grid-item__contents { 253 flex: 1; 254 padding: var(--space-md); 255 } 256 257 .grid-item__menu { ··· 279 justify-content: center; 280 } 281 282 .grid-item__title { 283 align-items: center; 284 display: flex; 285 - gap: var(--space-xs); 286 - justify-content: space-between; 287 } 288 } 289
··· 222 width: 100%; 223 } 224 225 + select { 226 + appearance: none; 227 + background: transparent; 228 + border: 2px solid var(--form-color); 229 + border-radius: var(--radius-md); 230 + color: inherit; 231 + font-family: inherit; 232 + font-size: var(--fs-sm); 233 + padding: var(--space-2xs) var(--space-xs); 234 + transition-duration: 250ms; 235 + transition-property: border-color; 236 + width: 100%; 237 + 238 + option, 239 + optgroup { 240 + color: black; 241 + } 242 + } 243 + 244 textarea { 245 padding: var(--space-xs); 246 resize: none; ··· 271 .grid-item__contents { 272 flex: 1; 273 padding: var(--space-md); 274 + position: relative; 275 } 276 277 .grid-item__menu { ··· 299 justify-content: center; 300 } 301 302 + .grid-item__kind { 303 + border: 1.5px solid currentColor; 304 + border-radius: 9999px; 305 + display: inline-block; 306 + font-size: var(--fs-2xs); 307 + font-weight: 500; 308 + padding: 1px var(--space-3xs); 309 + vertical-align: middle; 310 + } 311 + 312 .grid-item__title { 313 align-items: center; 314 display: flex; 315 + gap: var(--space-2xs); 316 } 317 } 318