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 1 <ul class="grid" style="margin-top: var(--space-lg);"> 2 2 {{ for index, item of items }} 3 - <li class="grid-item" data-uri="{{ item.url |> facetOrThemeURI }}" data-name="{{item.title}}"> 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 + > 4 24 <div class="grid-item__contents"> 5 25 <div class="grid-item__title"> 6 26 <a href="{{ item.url |> facetLoaderURL }}" style="padding: var(--space-3xs) 0"> 7 27 {{item.title}} 8 28 </a> 29 + <span style="flex: 1"></span> 30 + <span class="grid-item__kind" style="color: {{ color }};">{{ kind }}</span> 9 31 </div> 10 32 <div class="list-description"> 11 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 82 } 83 83 84 84 async function playAudioFromQueue() { 85 - const [sc, a, q, ms, qa, sca] = await Promise.all([ 86 - // configurator 87 - scrobbles(), 88 - 85 + const [a, q, ms, qa, sca] = await Promise.all([ 89 86 // engine 90 87 audio(), 91 88 queue(), ··· 97 94 ]); 98 95 99 96 return { 100 - configurator: { 101 - scrobbles: sc, 102 - }, 103 97 engine: { 104 98 audio: a, 105 99 queue: q,
+6
src/definitions/output/facet.json
··· 22 22 "type": "string", 23 23 "description": "The UTF8 HTML string that makes up the facet" 24 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 + }, 25 31 "name": { "type": "string" }, 26 32 "updatedAt": { "type": "string", "format": "datetime" }, 27 33 "uri": {
+10
src/facets/build.vto
··· 33 33 <input id="name-input" type="text" placeholder="Name" name="name" required /> 34 34 </p> 35 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> 36 42 <textarea id="description-input" placeholder="Description" name="description" rows="5"></textarea> 37 43 </p> 38 44 <p> ··· 61 67 {{- echo -}} 62 68 import foundation from "common/facets/foundation.js" 63 69 {{ /echo }} 70 + {{ echo -}}await foundation.configurator.scrobbles(){{- /echo }} 71 + 64 72 {{ echo -}}await foundation.engine.audio(){{- /echo }} 65 73 {{ echo -}}await foundation.engine.queue(){{- /echo }} 66 74 {{ echo -}}await foundation.engine.repeatShuffle(){{- /echo }} ··· 69 77 {{ echo -}}await foundation.orchestrator.autoQueue(){{- /echo }} 70 78 {{ echo -}}await foundation.orchestrator.favourites(){{- /echo }} 71 79 {{ echo -}}await foundation.orchestrator.input(){{- /echo }} 80 + {{ echo -}}await foundation.orchestrator.mediaSession(){{- /echo }} 72 81 {{ echo -}}await foundation.orchestrator.output(){{- /echo }} 73 82 {{ echo -}}await foundation.orchestrator.queueAudio(){{- /echo }} 74 83 {{ echo -}}await foundation.orchestrator.processTracks(){{- /echo }} 75 84 {{ echo -}}await foundation.orchestrator.scopedTracks(){{- /echo }} 85 + {{ echo -}}await foundation.orchestrator.scrobbleAudio(){{- /echo }} 76 86 {{ echo -}}await foundation.orchestrator.sources(){{- /echo }} 77 87 78 88 {{ echo -}}await foundation.processor.artwork(){{- /echo }}
+15
src/facets/common/build.js
··· 97 97 document.querySelector("#description-input") 98 98 ); 99 99 100 + const kindEl = /** @type {HTMLSelectElement | null} */ ( 101 + document.querySelector("#kind-input") 102 + ); 103 + 100 104 const html = editor.state.doc.toString(); 101 105 const cid = await CID.create(0x55, new TextEncoder().encode(html)); 102 106 const name = nameEl?.value ?? "nameless"; 103 107 const description = descriptionEl?.value ?? ""; 108 + const kind = /** @type {"interactive" | "prelude"} */ (kindEl?.value ?? "interactive"); 104 109 105 110 /** @type {Facet} */ 106 111 const facet = $editingFacet.value ··· 109 114 cid, 110 115 description, 111 116 html, 117 + kind, 112 118 name, 113 119 } 114 120 : { ··· 117 123 cid, 118 124 description, 119 125 html, 126 + kind, 120 127 name, 121 128 }; 122 129 ··· 144 151 document.querySelector("#description-input") 145 152 ); 146 153 154 + const kindEl = /** @type {HTMLSelectElement | null} */ ( 155 + document.querySelector("#kind-input") 156 + ); 157 + 147 158 if (!nameEl) return; 148 159 149 160 // Reset url — remove `id` param if not matching the facet ··· 169 180 170 181 $editingFacet.value = facet; 171 182 nameEl.value = facet.name; 183 + 184 + if (kindEl) { 185 + kindEl.value = facet.kind ?? "interactive"; 186 + } 172 187 173 188 if (descriptionEl) { 174 189 descriptionEl.value = facet.description ?? "";
+1 -1
src/facets/common/grid.js
··· 82 82 ? "ph-fill ph-toggle-right" 83 83 : "ph-fill ph-toggle-left"; 84 84 /** @type {HTMLElement} */ (icon).style.color = isActive 85 - ? "var(--accent-twist-2)" 85 + ? li.getAttribute("data-active-color") ?? "var(--accent-twist-2)" 86 86 : ""; 87 87 } 88 88 });
+11 -5
src/facets/common/you.js
··· 3 3 import { marked } from "marked"; 4 4 import { unsafeHTML } from "lit-html/directives/unsafe-html.js"; 5 5 6 + import * as FacetCategory from "~/common/facets/category.js"; 6 7 import foundation from "~/common/facets/foundation.js"; 7 8 import { effect } from "~/common/signal.js"; 8 9 import { facetFromURI } from "~/common/facets/utils.js"; ··· 202 203 const h = col.length 203 204 ? html` 204 205 <ul class="grid" style="margin: 0"> 205 - ${col.map((c, index) => 206 - keyed( 206 + ${col.map((c, index) => { 207 + const color = FacetCategory.color(c); 208 + const kind = FacetCategory.name(c); 209 + 210 + return keyed( 207 211 c.id, 208 212 html` 209 213 <li class="grid-item"> 210 214 <div class="grid-item__contents"> 211 - <div> 215 + <div class="grid-item__title"> 212 216 <a 213 217 href="facets/l/?id=${c 214 218 .id}" ··· 216 220 > 217 221 ${c.name} 218 222 </a> 223 + <span class="grid-item__kind" style="color: ${color};" 224 + >${kind}</span> 219 225 </div> 220 226 <div class="list-description"> 221 227 <div> ··· 263 269 </div> 264 270 </li> 265 271 `, 266 - ) 267 - )} ${ADD_FROM_URI_ITEM} 272 + ); 273 + })} ${ADD_FROM_URI_ITEM} 268 274 </ul> 269 275 ` 270 276 : html`
+26 -6
src/facets/l/index.js
··· 1 1 import foundation from "~/common/facets/foundation.js"; 2 2 import * as CID from "~/common/cid.js"; 3 - import { createLoader, renderError } from "~/common/loader.js"; 3 + import * as Output from "~/common/output.js"; 4 + import { createLoader, loadURI, renderError } from "~/common/loader.js"; 4 5 6 + // Output element 5 7 const output = await foundation.orchestrator.output(); 6 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 7 18 createLoader({ 8 19 $type: "sh.diffuse.output.facet", 9 20 label: "Facet", 10 21 source: () => output.facets, 11 22 async render(facet) { 12 - const container = /** @type {HTMLDivElement} */ ( 13 - document.querySelector("#container") 14 - ); 15 - 16 23 if (facet.cid) { 17 24 const valid = await CID.verify( 18 25 new TextEncoder().encode(facet.html ?? ""), ··· 28 35 } 29 36 } 30 37 38 + container.innerHTML = ""; 39 + 31 40 const range = document.createRange(); 32 41 range.selectNode(container); 33 42 const documentFragment = range.createContextualFragment(facet.html ?? ""); 34 43 35 - container.innerHTML = ""; 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 + 36 56 container.append(documentFragment); 37 57 }, 38 58 });
+31 -2
src/styles/diffuse/page.css
··· 222 222 width: 100%; 223 223 } 224 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 + 225 244 textarea { 226 245 padding: var(--space-xs); 227 246 resize: none; ··· 252 271 .grid-item__contents { 253 272 flex: 1; 254 273 padding: var(--space-md); 274 + position: relative; 255 275 } 256 276 257 277 .grid-item__menu { ··· 279 299 justify-content: center; 280 300 } 281 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 + 282 312 .grid-item__title { 283 313 align-items: center; 284 314 display: flex; 285 - gap: var(--space-xs); 286 - justify-content: space-between; 315 + gap: var(--space-2xs); 287 316 } 288 317 } 289 318