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

Configure Feed

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

feat: various facets improvements

+234 -246
+1 -1
.env
··· 1 1 ATPROTO_CLIENT_ID=https://handed-pixels-solo-folks.trycloudflare.com/oauth-client-metadata.tunnel.json 2 - DISABLE_AUTOMATIC_TRACKS_PROCESSING=t 2 + # DISABLE_AUTOMATIC_TRACKS_PROCESSING=t
+9 -3
src/_components/facets/grid.vto
··· 25 25 > 26 26 <div class="grid-item__contents"> 27 27 <div class="grid-item__title"> 28 - <a href="{{ item.url |> facetLoaderURL }}" style="padding: var(--space-3xs) 0"> 29 - {{item.title}} 30 - </a> 28 + {{ if item.kind === "prelude" }} 29 + <span style="padding: var(--space-3xs) 0"> 30 + {{item.title}} 31 + </span> 32 + {{ else }} 33 + <a href="{{ item.url |> facetLoaderURL }}" style="padding: var(--space-3xs) 0"> 34 + {{item.title}} 35 + </a> 36 + {{ /if }} 31 37 <span style="flex: 1"></span> 32 38 <span class="grid-item__kind" style="color: {{ color }};">{{ kind }}</span> 33 39 </div>
+1 -1
src/_components/facets/nav.vto
··· 33 33 <div class="divider"></div> 34 34 35 35 <a href="facets/data/" class="button {{ colorClass("facets/data/") }} button--border"> 36 - Data Input & Output 36 + Input & Output 37 37 </a> 38 38 39 39 <a href="facets/playback/" class="button {{ colorClass("facets/playback/") }} button--border">
+32 -17
src/_data/facets.yaml
··· 1 + - url: "facets/playback/auto-queue/feature/index.html" 2 + title: "Automatic Queue" 3 + kind: prelude 4 + category: Playback 5 + featured: true 6 + desc: > 7 + Automatically put tracks into the queue. **Always on.** 8 + - url: "facets/playback/auto-queue/index.html" 9 + title: "Automatic Queue" 10 + category: Playback 11 + featured: true 12 + desc: > 13 + Automatically put tracks into the queue when this interface is opened. Also allows for controlling shuffle, repeat, search, sorting and playlist selection. 1 14 - url: "themes/blur/artwork-controller/facet/index.html" 2 15 title: "Blur / Artwork controller" 3 16 category: Playback 4 17 featured: true 5 18 desc: > 6 - Audio playback controller with an artwork display. 7 - - url: "facets/tools/auto-queue/index.html" 8 - title: "Tools / Automatic Queue" 9 - category: Playback 19 + Audio playback controller with an artwork display. Play audio from the queue, add tracks to your favourites, control the queue and volume. 20 + - url: "facets/data/export-import/index.html" 21 + title: "Export & Import" 22 + category: Data 23 + desc: > 24 + Export all data as a JSON snapshot, or restore from a previously exported file. 25 + - url: "facets/data/process-tracks/index.html" 26 + title: "Process Tracks" 27 + kind: prelude 28 + category: Data 10 29 featured: true 11 30 desc: > 12 - Automatically put tracks into the queue. 13 - - url: "facets/scrobble/index.html" 31 + Process all your audio inputs automatically when opening any interface facet. 32 + - url: "facets/misc/scrobble/index.html" 14 33 title: "Scrobble" 15 - kind: "prelude" 34 + kind: prelude 16 35 category: Misc 17 36 featured: true 18 37 desc: > 19 38 Enable scrobbling, keep track of what you're listening to. 20 - - url: "facets/scrobble/last.fm/index.html" 39 + - url: "facets/misc/scrobble/last.fm/index.html" 21 40 title: "Scrobble / Last.fm" 22 41 category: Misc 42 + featured: true 23 43 desc: > 24 44 Enable Last.fm scrobbling. 25 - - url: "facets/tools/export-import/index.html" 26 - title: "Tools / Export & Import" 27 - category: Data 28 - desc: > 29 - Export all data as a JSON snapshot, or restore from a previously exported file. 30 - - url: "facets/tools/split-view/index.html" 31 - title: "Tools / Split View" 45 + - url: "facets/misc/split-view/index.html" 46 + title: "Split View" 32 47 category: "Misc" 33 48 desc: > 34 49 Arrange multiple facets side-by-side in a resizable split-panel layout. 35 - - url: "facets/tools/v3-import/index.html" 36 - title: "Tools / V3.x Import" 50 + - url: "facets/data/v3-import/index.html" 51 + title: "V3.x Import" 37 52 category: Data 38 53 desc: > 39 54 Import data from Diffuse v3.
+127 -121
src/common/facets/foundation.js
··· 1 + import { signal } from "~/common/signal.js"; 2 + 1 3 /** 2 4 * @import { DiffuseElement } from "~/common/element.js"; 5 + * @import { Signal } from "~/common/signal.d.ts"; 3 6 * @import { ScrobbleElement } from "~/components/supplement/types.d.ts"; 4 7 */ 5 8 ··· 7 10 export const GROUP = url.searchParams.get("group") ?? "facets"; 8 11 9 12 /** 13 + * [PRIVATE] Signals. 14 + */ 15 + const signals = { 16 + configurator: { 17 + scrobbles: signal( 18 + /** @type {import("~/components/configurator/scrobbles/element.js").CLASS | null} */ (null), 19 + ), 20 + }, 21 + 22 + engine: { 23 + audio: signal( 24 + /** @type {import("~/components/engine/audio/element.js").CLASS | null} */ (null), 25 + ), 26 + queue: signal( 27 + /** @type {import("~/components/engine/queue/element.js").CLASS | null} */ (null), 28 + ), 29 + repeatShuffle: signal( 30 + /** @type {import("~/components/engine/repeat-shuffle/element.js").CLASS | null} */ (null), 31 + ), 32 + scope: signal( 33 + /** @type {import("~/components/engine/scope/element.js").CLASS | null} */ (null), 34 + ), 35 + }, 36 + 37 + orchestrator: { 38 + autoQueue: signal( 39 + /** @type {import("~/components/orchestrator/auto-queue/element.js").CLASS | null} */ (null), 40 + ), 41 + favourites: signal( 42 + /** @type {import("~/components/orchestrator/favourites/element.js").CLASS | null} */ (null), 43 + ), 44 + input: signal( 45 + /** @type {import("~/components/orchestrator/input/element.js").CLASS | null} */ (null), 46 + ), 47 + mediaSession: signal( 48 + /** @type {import("~/components/orchestrator/media-session/element.js").CLASS | null} */ (null), 49 + ), 50 + output: signal( 51 + /** @type {import("~/components/orchestrator/output/element.js").CLASS | null} */ (null), 52 + ), 53 + processTracks: signal( 54 + /** @type {import("~/components/orchestrator/process-tracks/element.js").CLASS | null} */ (null), 55 + ), 56 + queueAudio: signal( 57 + /** @type {import("~/components/orchestrator/queue-audio/element.js").CLASS | null} */ (null), 58 + ), 59 + scopedTracks: signal( 60 + /** @type {import("~/components/orchestrator/scoped-tracks/element.js").CLASS | null} */ (null), 61 + ), 62 + scrobbleAudio: signal( 63 + /** @type {import("~/components/orchestrator/scrobble-audio/element.js").CLASS | null} */ (null), 64 + ), 65 + sources: signal( 66 + /** @type {import("~/components/orchestrator/sources/element.js").CLASS | null} */ (null), 67 + ), 68 + }, 69 + 70 + processor: { 71 + artwork: signal( 72 + /** @type {import("~/components/processor/artwork/element.js").CLASS | null} */ (null), 73 + ), 74 + metadata: signal( 75 + /** @type {import("~/components/processor/metadata/element.js").CLASS | null} */ (null), 76 + ), 77 + search: signal( 78 + /** @type {import("~/components/processor/search/element.js").CLASS | null} */ (null), 79 + ), 80 + }, 81 + }; 82 + 83 + /** 10 84 * Default config for facets. 11 85 */ 12 86 export const config = { 13 87 GROUP, 14 88 15 - features: { 16 - fillQueueAutomatically, 17 - playAudioFromQueue, 18 - processInputs, 19 - searchThroughCollection, 20 - }, 21 - 22 89 // Elements 23 90 configurator: { 24 91 scrobbles, 25 92 }, 93 + 26 94 engine: { 27 95 audio, 28 96 queue, 29 97 repeatShuffle, 30 98 scope, 31 99 }, 100 + 32 101 orchestrator: { 33 102 autoQueue, 34 103 favourites, ··· 36 105 mediaSession, 37 106 output, 38 107 queueAudio, 108 + // TODO: Path collections orchestrator 39 109 processTracks, 40 110 scopedTracks, 41 111 scrobbleAudio, 42 112 sources, 43 113 }, 114 + 44 115 processor: { 45 116 artwork, 46 117 metadata, 47 118 search, 48 119 }, 49 - }; 50 120 51 - export default config; 52 - 53 - // 📦️ 54 - 55 - async function fillQueueAutomatically() { 56 - const [q, rs, sc, aq, i, o, st] = await Promise.all([ 57 - // engine 58 - queue(), 59 - repeatShuffle(), 60 - scope(), 61 - 62 - // orchestrator 63 - autoQueue(), 64 - input(), 65 - output(), 66 - scopedTracks(), 67 - ]); 68 - 69 - return { 70 - engine: { 71 - queue: q, 72 - repeatShuffle: rs, 73 - scope: sc, 121 + /** 122 + * Element signals 123 + */ 124 + signals: { 125 + configurator: { 126 + scrobbles: signals.configurator.scrobbles.get, 74 127 }, 75 - orchestrator: { 76 - autoQueue: aq, 77 - input: i, 78 - output: o, 79 - scopedTracks: st, 80 - }, 81 - }; 82 - } 83 128 84 - async function playAudioFromQueue() { 85 - const [a, q, ms, qa] = await Promise.all([ 86 - // engine 87 - audio(), 88 - queue(), 89 - 90 - // orchestrator 91 - mediaSession(), 92 - queueAudio(), 93 - ]); 94 - 95 - return { 96 129 engine: { 97 - audio: a, 98 - queue: q, 130 + audio: signals.engine.audio.get, 131 + queue: signals.engine.queue.get, 132 + repeatShuffle: signals.engine.repeatShuffle.get, 133 + scope: signals.engine.scope.get, 99 134 }, 100 - orchestrator: { 101 - mediaSession: ms, 102 - queueAudio: qa, 103 - }, 104 - }; 105 - } 106 135 107 - async function processInputs() { 108 - const [i, o, pt, m] = await Promise.all([ 109 - // orchestrator 110 - input(), 111 - output(), 112 - processTracks(), 113 - 114 - // processor 115 - metadata(), 116 - ]); 117 - 118 - return { 119 136 orchestrator: { 120 - input: i, 121 - output: o, 122 - processTracks: pt, 137 + autoQueue: signals.orchestrator.autoQueue.get, 138 + favourites: signals.orchestrator.favourites.get, 139 + input: signals.orchestrator.input.get, 140 + mediaSession: signals.orchestrator.mediaSession.get, 141 + output: signals.orchestrator.output.get, 142 + processTracks: signals.orchestrator.processTracks.get, 143 + queueAudio: signals.orchestrator.queueAudio.get, 144 + scopedTracks: signals.orchestrator.scopedTracks.get, 145 + scrobbleAudio: signals.orchestrator.scrobbleAudio.get, 146 + sources: signals.orchestrator.sources.get, 123 147 }, 124 - processor: { 125 - metadata: m, 126 - }, 127 - }; 128 - } 129 - 130 - async function searchThroughCollection() { 131 - const [sc, o, st, s] = await Promise.all([ 132 - // engine 133 - scope(), 134 - 135 - // orchestrator 136 - output(), 137 - scopedTracks(), 138 148 139 - // processor 140 - search(), 141 - ]); 142 - 143 - return { 144 - engine: { 145 - scope: sc, 146 - }, 147 - orchestrator: { 148 - output: o, 149 - scopedTracks: st, 150 - }, 151 149 processor: { 152 - search: s, 150 + artwork: signals.processor.artwork.get, 151 + metadata: signals.processor.metadata.get, 152 + search: signals.processor.search.get, 153 153 }, 154 - }; 155 - } 154 + }, 155 + }; 156 + 157 + export default config; 156 158 157 159 // 🥡 158 160 ··· 196 198 const a = new AudioEngine(); 197 199 a.setAttribute("group", GROUP); 198 200 199 - return findExistingOrAdd(a); 201 + return findExistingOrAdd(a, signals.engine.audio); 200 202 } 201 203 202 204 async function queue() { ··· 207 209 const q = new Queue(); 208 210 q.setAttribute("group", GROUP); 209 211 210 - return findExistingOrAdd(q); 212 + return findExistingOrAdd(q, signals.engine.queue); 211 213 } 212 214 213 215 async function repeatShuffle() { ··· 218 220 const r = new RepeatShuffleEngine(); 219 221 r.setAttribute("group", GROUP); 220 222 221 - return findExistingOrAdd(r); 223 + return findExistingOrAdd(r, signals.engine.repeatShuffle); 222 224 } 223 225 224 226 async function scope() { ··· 229 231 const s = new ScopeEngine(); 230 232 s.setAttribute("group", GROUP); 231 233 232 - return findExistingOrAdd(s); 234 + return findExistingOrAdd(s, signals.engine.scope); 233 235 } 234 236 235 237 // Processors ··· 241 243 const a = new ArtworkProcessor(); 242 244 a.setAttribute("group", GROUP); 243 245 244 - return findExistingOrAdd(a); 246 + return findExistingOrAdd(a, signals.processor.artwork); 245 247 } 246 248 247 249 async function metadata() { ··· 252 254 const m = new MetadataProcessor(); 253 255 m.setAttribute("group", GROUP); 254 256 255 - return findExistingOrAdd(m); 257 + return findExistingOrAdd(m, signals.processor.metadata); 256 258 } 257 259 258 260 async function search() { ··· 263 265 const s = new SearchProcessor(); 264 266 s.setAttribute("group", GROUP); 265 267 266 - return findExistingOrAdd(s); 268 + return findExistingOrAdd(s, signals.processor.search); 267 269 } 268 270 269 271 // Orchestrators ··· 281 283 aqo.setAttribute("repeat-shuffle-engine-selector", r.selector); 282 284 aqo.setAttribute("tracks-selector", t.selector); 283 285 284 - return findExistingOrAdd(aqo); 286 + return findExistingOrAdd(aqo, signals.orchestrator.autoQueue); 285 287 } 286 288 287 289 async function favourites() { ··· 294 296 fo.setAttribute("group", GROUP); 295 297 fo.setAttribute("output-selector", o.selector); 296 298 297 - return findExistingOrAdd(fo); 299 + return findExistingOrAdd(fo, signals.orchestrator.favourites); 298 300 } 299 301 300 302 async function input() { ··· 306 308 i.setAttribute("group", GROUP); 307 309 i.setAttribute("id", "input"); 308 310 309 - return findExistingOrAdd(i); 311 + return findExistingOrAdd(i, signals.orchestrator.input); 310 312 } 311 313 312 314 async function mediaSession() { ··· 326 328 mso.setAttribute("output-selector", o.selector); 327 329 mso.setAttribute("queue-engine-selector", q.selector); 328 330 329 - return findExistingOrAdd(mso); 331 + return findExistingOrAdd(mso, signals.orchestrator.mediaSession); 330 332 } 331 333 332 334 async function output() { ··· 338 340 o.setAttribute("group", GROUP); 339 341 o.setAttribute("id", "output"); 340 342 341 - return findExistingOrAdd(o); 343 + return findExistingOrAdd(o, signals.orchestrator.output); 342 344 } 343 345 344 346 /** ··· 363 365 opt.toggleAttribute("process-when-ready"); 364 366 } 365 367 366 - return findExistingOrAdd(opt); 368 + return findExistingOrAdd(opt, signals.orchestrator.processTracks); 367 369 } 368 370 369 371 async function queueAudio() { ··· 385 387 oqa.setAttribute("queue-engine-selector", q.selector); 386 388 oqa.setAttribute("repeat-shuffle-engine-selector", r.selector); 387 389 388 - return findExistingOrAdd(oqa); 390 + return findExistingOrAdd(oqa, signals.orchestrator.queueAudio); 389 391 } 390 392 391 393 async function scopedTracks() { ··· 406 408 sto.setAttribute("scope-engine-selector", e.selector); 407 409 sto.setAttribute("search-processor-selector", s.selector); 408 410 409 - return findExistingOrAdd(sto); 411 + return findExistingOrAdd(sto, signals.orchestrator.scopedTracks); 410 412 } 411 413 412 414 async function scrobbleAudio() { ··· 421 423 sao.setAttribute("audio-engine-selector", a.selector); 422 424 sao.setAttribute("scrobble-selector", sc.selector); 423 425 424 - return findExistingOrAdd(sao); 426 + return findExistingOrAdd(sao, signals.orchestrator.scrobbleAudio); 425 427 } 426 428 427 429 async function sources() { ··· 436 438 so.setAttribute("input-selector", i.selector); 437 439 so.setAttribute("output-selector", o.selector); 438 440 439 - return findExistingOrAdd(so); 441 + return findExistingOrAdd(so, signals.orchestrator.sources); 440 442 } 441 443 442 444 // 🛠️ ··· 444 446 /** 445 447 * @template {DiffuseElement} T 446 448 * @param {T} element 449 + * @param {Signal<T | null>} signal 447 450 * @returns {T} 448 451 */ 449 - export function findExistingOrAdd(element) { 452 + export function findExistingOrAdd(element, signal) { 450 453 /** @type {T | null} */ 451 454 const alreadyAdded = document.body.querySelector(element.selector); 455 + 452 456 if (!alreadyAdded) { 453 457 document.body.append(element); 458 + signal.value = element; 454 459 return element; 455 460 } 456 461 462 + signal.value = alreadyAdded; 457 463 return alreadyAdded; 458 464 }
-31
src/facets/build.vto
··· 90 90 {{ echo -}}await foundation.processor.search(){{- /echo -}} 91 91 </code> 92 92 </div> 93 - 94 - <p> 95 - <small>Features:</small> 96 - </p> 97 - <ul style="margin-bottom: 0;"> 98 - <li> 99 - <span>Fill the queue automatically <small>(infinite play)</small></span> 100 - <div class="list-description"> 101 - <code>await foundation.features.fillQueueAutomatically()</code> 102 - </div> 103 - </li> 104 - <li> 105 - <span>Play audio from the queue</span> 106 - <div class="list-description"> 107 - <code>await foundation.features.playAudioFromQueue()</code> 108 - </div> 109 - </li> 110 - <li> 111 - <span>Process inputs <small>(into tracks, etc)</small></span> 112 - <div class="list-description"> 113 - <code>await foundation.features.processInputs()</code> 114 - </div> 115 - </li> 116 - <li> 117 - <span>Search through your collection</span> 118 - <div class="list-description" style="margin-bottom: 0;"> 119 - <code>await foundation.features.searchThroughCollection()</code> 120 - </div> 121 - </li> 122 - </ul> 123 93 </section> 124 94 125 95 <section class="flex"> ··· 136 106 While you have the ability to do whatever you want in a custom facet, the existing facets are designed to work a certain way; so here's some things to keep in mind: 137 107 </p> 138 108 <ul> 139 - <li><span>In most cases you'll want to call <code>foundation.features.processInputs()</code> so that your audio files and streams actually show up.</span></li> 140 109 <li><span>Most elements are configured in broadcast mode so they communicate across tabs. There are a few exceptions such as inputs, where we prefer parallelisation.</span></li> 141 110 <li><span>You can use facets in combination with themes by adding the elements used in the theme to a group and then passing in the group name as a URL query parameter (eg. <code>group=facets</code>)</span></li> 142 111 </ul>
-19
src/facets/common/build.js
··· 35 35 const editor = new EditorView({ 36 36 parent: editorContainer, 37 37 doc: ` 38 - <main> 39 - <h1 id="now-playing"> 40 - Waiting on tracks &amp; queue to load ... 41 - </h1> 42 - </main> 43 - 44 38 <style> 45 39 @import "./styles/base.css"; 46 - @import "./styles/diffuse/page.css"; 47 40 </style> 48 41 49 42 <script type="module"> 50 43 import foundation from "~/common/facets/foundation.js"; 51 - import { effect } from "~/common/signal.js"; 52 - 53 - const components = await foundation.features.fillQueueAutomatically(); 54 - const myHtmlElement = document.querySelector("#now-playing"); 55 - 56 - effect(() => { 57 - const now = components.engine.queue.now(); 58 - const currentlyPlaying = now ? components.orchestrator.output.tracks.collection().find(t => t.id === now.id) : undefined; 59 - if (currentlyPlaying && myHtmlElement) { 60 - myHtmlElement.innerText = \`\$\{currentlyPlaying.tags.artist} - \$\{currentlyPlaying.tags.title}\`; 61 - } 62 - }) 63 44 </script> 64 45 `.trim(), 65 46 extensions: [
+1 -1
src/facets/common/ppr.js
··· 63 63 const url = new URL(event.destination.url); 64 64 if (url.origin !== location.origin) return; 65 65 66 - // Only intercept /facets/[section]/ paths (not deeper sub-paths like /facets/tools/*) 66 + // Only intercept /facets/[section]/ paths (not deeper sub-paths like /facets/misc/*) 67 67 const relative = relativePathname(url.pathname); 68 68 const parts = relative.split("/").filter(Boolean); 69 69 if (parts[0] !== "facets") return;
+20 -8
src/facets/common/you.js
··· 143 143 const uri = uriEl?.value.trim() ?? ""; 144 144 if (!name || !uri) return; 145 145 146 - const facet = await facetFromURI({ kind, name, uri }, { fetchHTML: false }); 146 + const facet = await facetFromURI({ kind, name, uri }, { 147 + fetchHTML: false, 148 + }); 147 149 await saveFacet(facet); 148 150 149 151 /** @type {HTMLDialogElement} */ (dialog).close(); ··· 228 230 const color = FacetCategory.color(c); 229 231 const kind = FacetCategory.name(c); 230 232 233 + const title = c.kind === "prelude" 234 + ? html` 235 + <span style="display: inline-block; padding: var(--space-3xs) 0"> 236 + ${c.name} 237 + </span> 238 + ` 239 + : html` 240 + <a 241 + href="facets/l/?id=${c 242 + .id}" 243 + style="display: inline-block; padding: var(--space-3xs) 0" 244 + > 245 + ${c.name} 246 + </a> 247 + `; 248 + 231 249 return keyed( 232 250 c.id, 233 251 html` 234 252 <li class="grid-item"> 235 253 <div class="grid-item__contents"> 236 254 <div class="grid-item__title"> 237 - <a 238 - href="facets/l/?id=${c 239 - .id}" 240 - style="display: inline-block; padding: var(--space-3xs) 0" 241 - > 242 - ${c.name} 243 - </a> 255 + ${title} 244 256 <span class="grid-item__kind" style="color: ${color};" 245 257 >${kind}</span> 246 258 </div>
+1
src/facets/data/process-tracks/index.html
··· 1 + <script type="module" src="./index.inline.js"></script>
+2
src/facets/data/process-tracks/index.inline.js
··· 1 + import foundation from "~/common/facets/foundation.js"; 2 + foundation.orchestrator.processTracks({ disableWhenReady: false });
-3
src/facets/examples/now-playing/index.inline.js
··· 1 1 import foundation from "~/common/facets/foundation.js"; 2 2 import { effect } from "~/common/signal.js"; 3 3 4 - await foundation.features.processInputs(); 5 - await foundation.features.fillQueueAutomatically(); 6 - 7 4 const output = await foundation.orchestrator.output(); 8 5 const queue = await foundation.engine.queue(); 9 6
+5 -9
src/facets/guide.vto
··· 11 11 <h3>Getting started</h3> 12 12 13 13 <p> 14 - To use these facets, simply open whichever ones provide the functionality that you're looking for at a given moment. You can browse existing facets here and create one too. If you found something you like, you can save it to your list and adjust it if need be. 14 + To get started you can browse existing facets here on these pages, find one you like and then either you click the link in the title in case it's an <strong>interface</strong>. Or, you save it to your collection by using the toggle if it's a <strong>feature</strong>, which will activate it. 15 15 </p> 16 16 <p> 17 - For example, say you want to play music; two options would be: (1) <a href="{{ ('themes/webamp/browser/facet/index.html') |> facetLoaderURL }}">browse</a> for a specific song and add it to the queue, or (2) <a href="{{ ('facets/tools/auto-queue/index.html') |> facetLoaderURL }}">automatically</a> add a bunch of shuffled songs to the queue. Next, you need a way to play the items you added to the queue. That's where a <a href="{{ ('themes/blur/artwork-controller/facet/index.html') |> facetLoaderURL }}">controller</a> could be used. 17 + For example, say you want to play music; two options would be: (1) <a href="{{ ('themes/webamp/browser/facet/index.html') |> facetLoaderURL }}">browse</a> for a specific song and add it to the queue, or (2) <a href="{{ ('facets/playback/auto-queue/index.html') |> facetLoaderURL }}">automatically</a> add a bunch of shuffled songs to the queue. Next, you need a way to play the items you added to the queue. That's where a <a href="{{ ('themes/blur/artwork-controller/facet/index.html') |> facetLoaderURL }}">controller</a> could be used. 18 18 </p> 19 19 <p> 20 - <em>You might ask, why can't I do all of this in just one window? That's what <a href="themes/">themes</a> are for, if you need something more streamlined. If you however want a customised experience, or prefer certain interfaces for certain things, that's what facets are for.</em> 20 + <em>You might ask, why can't I do all of this in just one window? That's what <a href="themes/">themes</a> are for, if you need something more streamlined. If you however want a customised experience, or prefer certain interfaces for certain things with an infinite number of optional features, that's what facets are for.</em> 21 21 </p> 22 22 <p> 23 - <small><i class="ph-fill ph-info"></i> Every facet has access to your audio collection and your user data, along with any other shared state.</small> 23 + <small><i class="ph-fill ph-info"></i> Every facet has access to your audio collection and your user data, along with any other shared state, be mindful of which facets you're interacting with.</small> 24 24 </p> 25 25 </section> 26 26 ··· 32 32 </p> 33 33 34 34 <p> 35 - <strong>It provides every user the ability to choose what features and interface they want to layer on top of their data.</strong> The data flow goes like so: 36 - </p> 37 - 38 - <p> 39 - <em>Data → Transformers → Solo components → Configurators → Orchestrators → Facets</em> 35 + <strong>It provides every user the ability to choose what features and interface they want to layer on top of their data.</strong> 40 36 </p> 41 37 42 38 <p>
+8
src/facets/misc/scrobble/index.inline.js
··· 1 + import foundation from "~/common/facets/foundation.js"; 2 + import { effect } from "~/common/signal.js"; 3 + 4 + effect(() => { 5 + if (foundation.signals.engine.audio()) { 6 + foundation.orchestrator.scrobbleAudio(); 7 + } 8 + });
+1
src/facets/playback/auto-queue/feature/index.html
··· 1 + <script type="module" src="./index.inline.js"></script>
+8
src/facets/playback/auto-queue/feature/index.inline.js
··· 1 + import foundation from "~/common/facets/foundation.js"; 2 + import { effect } from "~/common/signal.js"; 3 + 4 + effect(() => { 5 + if (foundation.signals.engine.queue()) { 6 + foundation.orchestrator.autoQueue(); 7 + } 8 + });
src/facets/scrobble/index.html src/facets/misc/scrobble/index.html
-2
src/facets/scrobble/index.inline.js
··· 1 - import foundation from "~/common/facets/foundation.js"; 2 - await foundation.orchestrator.scrobbleAudio();
src/facets/scrobble/last.fm/index.html src/facets/misc/scrobble/last.fm/index.html
+11 -17
src/facets/scrobble/last.fm/index.inline.js src/facets/misc/scrobble/last.fm/index.inline.js
··· 6 6 7 7 import "~/common/webawesome/detect-dark.js"; 8 8 9 - import LastFmScrobbler from "~/components/supplement/last.fm/element.js"; 9 + import foundation from "~/common/facets/foundation.js"; 10 10 import { effect } from "~/common/signal.js"; 11 - import { GROUP } from "~/common/facets/foundation.js"; 12 11 13 12 /** 14 13 * @import { default as WaDrawer } from "@awesome.me/webawesome/dist/components/drawer/drawer.js" ··· 30 29 } 31 30 } 32 31 33 - // Find existing or create new ds-lastfm element 34 - let lastFm = /** @type {LastFmScrobbler | null} */ ( 35 - document.body.querySelector("ds-lastfm-scrobbler") 36 - ); 32 + const configurator = await foundation.configurator.scrobbles(); 33 + const orchestrator = await foundation.orchestrator.scrobbleAudio(); 37 34 38 - if (!lastFm) { 39 - lastFm = new LastFmScrobbler(); 40 - lastFm.setAttribute("group", GROUP); 35 + /** @type {import("~/components/supplement/last.fm/element.js").CLASS | null} */ 36 + const lastFm = configurator.querySelector("ds-lastfm-scrobbler"); 37 + if (!lastFm) throw new Error("Last.fm scrobbler element not found"); 41 38 42 - const creds = loadCredentials(); 43 - if (creds) { 44 - lastFm.setAttribute("api-key", creds.apiKey); 45 - lastFm.setAttribute("api-secret", creds.apiSecret); 46 - } 47 - 48 - document.body.append(lastFm); 49 - } 39 + // const creds = loadCredentials(); 40 + // if (creds) { 41 + // lastFm.setAttribute("api-key", creds.apiKey); 42 + // lastFm.setAttribute("api-secret", creds.apiSecret); 43 + // } 50 44 51 45 await customElements.whenDefined(lastFm.localName); 52 46
src/facets/tools/auto-queue/index.html src/facets/playback/auto-queue/index.html
+1 -2
src/facets/tools/auto-queue/index.inline.js src/facets/playback/auto-queue/index.inline.js
··· 5 5 const ACTIVE_CLASS = "button--active"; 6 6 7 7 // Setup 8 - await foundation.features.fillQueueAutomatically(); 9 - await foundation.features.processInputs(); 8 + await foundation.orchestrator.autoQueue(); 10 9 11 10 const [repeatShuffle, scope, output] = await Promise.all([ 12 11 foundation.engine.repeatShuffle(),
src/facets/tools/export-import/index.html src/facets/data/export-import/index.html
src/facets/tools/export-import/index.inline.js src/facets/data/export-import/index.inline.js
+2 -2
src/facets/tools/split-view/index.inline.js src/facets/misc/split-view/index.inline.js
··· 26 26 * @typedef {PaneNode | SplitNode} Node 27 27 */ 28 28 29 - const STORAGE_KEY = "diffuse/facets/tools/split-view/builder/layout"; 29 + const STORAGE_KEY = "diffuse/facets/misc/split-view/builder/layout"; 30 30 31 31 // ─── State ─────────────────────────────────────────────────────────────────── 32 32 ··· 407 407 layout.classList.remove("dragging"); 408 408 }); 409 409 410 - const POSITIONS_KEY = "diffuse/facets/tools/split-view/${id}/positions"; 410 + const POSITIONS_KEY = "diffuse/facets/misc/split-view/${id}/positions"; 411 411 const savedPositions = (() => { 412 412 try { return JSON.parse(localStorage.getItem(POSITIONS_KEY) ?? "{}"); } 413 413 catch { return {}; }
+2 -2
src/facets/tools/split-view/index.vto src/facets/misc/split-view/index.vto
··· 32 32 <wa-dialog id="facet-picker" label="Choose a facet" style="--width: 360px"> 33 33 <div class="wa-stack wa-gap-s"> 34 34 <wa-select id="facet-select" placeholder="Built-in facets…"> 35 - {{ for item of facets }}{{ if item.url !== "facets/tools/split-view/index.html" }} 35 + {{ for item of facets }}{{ if item.url !== "facets/misc/split-view/index.html" }} 36 36 <wa-option value="{{ item.url }}">{{ item.title }}</wa-option> 37 37 {{ /if }}{{ /for }} 38 38 </wa-select> 39 39 <wa-input 40 40 id="custom-path" 41 - placeholder="facets/tools/auto-queue/index.html" 41 + placeholder="facets/playback/auto-queue/index.html" 42 42 style="flex: 1" 43 43 ></wa-input> 44 44 <wa-button id="custom-confirm" variant="neutral" appearance="filled" pill>Load</wa-button>
src/facets/tools/v3-import/index.html src/facets/data/v3-import/index.html
-2
src/facets/tools/v3-import/index.inline.js src/facets/data/v3-import/index.inline.js
··· 6 6 */ 7 7 8 8 // Setup 9 - await foundation.features.processInputs(); 10 - 11 9 const favourites = await foundation.orchestrator.favourites(); 12 10 const output = await foundation.orchestrator.output(); 13 11
+2 -2
src/themes/blur/artwork-controller/facet/index.inline.js
··· 2 2 import ArtworkController from "~/themes/blur/artwork-controller/element.js"; 3 3 4 4 // Setup the prerequisite elements 5 - await foundation.features.playAudioFromQueue(); 6 - await foundation.features.processInputs(); 5 + await foundation.orchestrator.queueAudio(); 6 + await foundation.orchestrator.mediaSession(); 7 7 8 8 const [aud, art, fav, inp, out, que] = await Promise.all([ 9 9 foundation.engine.audio(),
-3
src/themes/webamp/browser/facet/index.inline.js
··· 1 1 import foundation from "~/common/facets/foundation.js"; 2 2 import BrowserElement from "~/themes/webamp/browser/element.js"; 3 3 4 - await foundation.features.processInputs(); 5 - await foundation.features.searchThroughCollection(); 6 - 7 4 const [out, que, scp, trc] = await Promise.all([ 8 5 foundation.orchestrator.output(), 9 6 foundation.engine.queue(),