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

feat: initial work for constituents loader

+503 -184
+130 -37
src/common/constituents/default.js
··· 1 + import ArtworkProcessor from "@components/processor/artwork/element.js"; 2 + import AudioEngine from "@components/engine/audio/element.js"; 1 3 import Queue from "@components/engine/queue/element.js"; 2 4 import InputOrchestrator from "@components/orchestrator/input/element.js"; 3 5 import OutputOrchestrator from "@components/orchestrator/output/element.js"; 4 6 import MetadataProcessor from "@components/processor/metadata/element.js"; 5 7 import ProcessTracksOrchestrator from "@components/orchestrator/process-tracks/element.js"; 8 + import QueueAudioOrchestrator from "@components/orchestrator/queue-audio/element.js"; 6 9 import QueueTracksOrchestrator from "@components/orchestrator/queue-tracks/element.js"; 7 10 import RepeatShuffleOrchestrator from "@components/orchestrator/repeat-shuffle/element.js"; 8 11 import SearchProcessor from "@components/processor/search/element.js"; 9 12 import SearchTracksOrchestrator from "@components/orchestrator/search-tracks/element.js"; 10 13 import SourcesOrchestrator from "@components/orchestrator/sources/element.js"; 11 14 15 + /** 16 + * @import { DiffuseElement } from "@toko/diffuse/common/element.js"; 17 + */ 18 + 12 19 export const GROUP = "constituents"; 13 20 14 21 /** 15 22 * Default config for constituents. 16 23 */ 17 24 export function config() { 18 - // Queue 19 - const queue = new Queue(); 20 - queue.setAttribute("group", GROUP); 21 - 22 - document.body.append(queue); 23 - 24 25 // Input 25 26 const input = new InputOrchestrator(); 26 27 input.setAttribute("group", GROUP); ··· 41 42 42 43 document.body.append(metadata); 43 44 44 - const search = new SearchProcessor(); 45 - search.setAttribute("group", GROUP); 46 - 47 - document.body.append(search); 48 - 49 45 // Orchestrators 50 46 const opt = new ProcessTracksOrchestrator(); 51 47 opt.setAttribute("group", GROUP); ··· 54 50 opt.setAttribute("metadata-processor-selector", metadata.selector); 55 51 opt.toggleAttribute("process-when-ready"); 56 52 57 - const oqt = new QueueTracksOrchestrator(); 58 - oqt.setAttribute("group", GROUP); 59 - oqt.setAttribute("input-selector", input.selector); 60 - oqt.setAttribute("output-selector", output.selector); 61 - oqt.setAttribute("queue-engine-selector", queue.selector); 53 + document.body.append(opt); 54 + 55 + // LAZY 56 + // ---- 57 + 58 + // Engines 59 + function audio() { 60 + const a = new AudioEngine(); 61 + a.setAttribute("group", GROUP); 62 + 63 + addToBodyIfNeeded(a); 64 + return a; 65 + } 66 + 67 + function queue() { 68 + const q = new Queue(); 69 + q.setAttribute("group", GROUP); 70 + 71 + addToBodyIfNeeded(q); 72 + return q; 73 + } 74 + 75 + // Processors 76 + function artwork() { 77 + const a = new ArtworkProcessor(); 78 + a.setAttribute("group", GROUP); 79 + 80 + addToBodyIfNeeded(a); 81 + return a; 82 + } 83 + 84 + function search() { 85 + const s = new SearchProcessor(); 86 + s.setAttribute("group", GROUP); 87 + 88 + addToBodyIfNeeded(s); 89 + return s; 90 + } 91 + 92 + // Orchestrators 93 + function queueAudio() { 94 + const a = audio(); 95 + const q = queue(); 96 + 97 + const oqa = new QueueAudioOrchestrator(); 98 + oqa.setAttribute("group", GROUP); 99 + oqa.setAttribute("audio-engine-selector", a.selector); 100 + oqa.setAttribute("input-selector", input.selector); 101 + oqa.setAttribute("queue-engine-selector", q.selector); 102 + 103 + addToBodyIfNeeded(oqa); 104 + return oqa; 105 + } 106 + 107 + function queueTracks() { 108 + const q = queue(); 109 + 110 + const oqt = new QueueTracksOrchestrator(); 111 + oqt.setAttribute("group", GROUP); 112 + oqt.setAttribute("input-selector", input.selector); 113 + oqt.setAttribute("output-selector", output.selector); 114 + oqt.setAttribute("queue-engine-selector", q.selector); 115 + 116 + addToBodyIfNeeded(oqt); 117 + return oqt; 118 + } 62 119 63 - const ors = new RepeatShuffleOrchestrator(); 64 - ors.setAttribute("group", GROUP); 65 - ors.setAttribute("queue-engine-selector", queue.selector); 120 + function repeatShuffle() { 121 + const q = queue(); 66 122 67 - const ost = new SearchTracksOrchestrator(); 68 - ost.setAttribute("group", GROUP); 69 - ost.setAttribute("input-selector", input.selector); 70 - ost.setAttribute("output-selector", output.selector); 71 - ost.setAttribute("search-processor-selector", search.selector); 123 + const ors = new RepeatShuffleOrchestrator(); 124 + ors.setAttribute("group", GROUP); 125 + ors.setAttribute("queue-engine-selector", q.selector); 72 126 73 - const osr = new SourcesOrchestrator(); 74 - osr.setAttribute("group", GROUP); 75 - osr.setAttribute("input-selector", input.selector); 76 - osr.setAttribute("output-selector", output.selector); 127 + addToBodyIfNeeded(ors); 128 + return ors; 129 + } 130 + 131 + function searchTracks() { 132 + const s = search(); 133 + 134 + const ost = new SearchTracksOrchestrator(); 135 + ost.setAttribute("group", GROUP); 136 + ost.setAttribute("input-selector", input.selector); 137 + ost.setAttribute("output-selector", output.selector); 138 + ost.setAttribute("search-processor-selector", s.selector); 139 + 140 + addToBodyIfNeeded(ost); 141 + return ost; 142 + } 143 + 144 + function sources() { 145 + const so = new SourcesOrchestrator(); 146 + so.setAttribute("group", GROUP); 147 + so.setAttribute("input-selector", input.selector); 148 + so.setAttribute("output-selector", output.selector); 77 149 78 - document.body.append(opt, oqt, ors, ost, osr); 150 + addToBodyIfNeeded(so); 151 + return so; 152 + } 79 153 80 154 // Return elements 81 155 return { 82 156 GROUP, 83 157 84 - engine: { 85 - queue, 86 - }, 158 + engine: {}, 87 159 orchestrator: { 88 160 input, 89 161 output, 90 162 processTracks: opt, 91 - queueTracks: oqt, 92 - repeatShuffle: ors, 93 - searchTracks: ost, 94 - sources: osr, 95 163 }, 96 164 processor: { 97 165 metadata, 98 - search, 166 + }, 167 + 168 + lazy: { 169 + engine: { 170 + audio, 171 + queue, 172 + }, 173 + orchestrator: { 174 + queueAudio, 175 + queueTracks, 176 + repeatShuffle, 177 + searchTracks, 178 + sources, 179 + }, 180 + processor: { 181 + artwork, 182 + search, 183 + }, 99 184 }, 100 185 }; 101 186 } 187 + 188 + /** 189 + * @param {DiffuseElement} element 190 + */ 191 + export function addToBodyIfNeeded(element) { 192 + const alreadyAdded = document.body.querySelector(element.selector); 193 + if (!alreadyAdded) document.body.append(element); 194 + }
+1
src/components/orchestrator/repeat-shuffle/element.js
··· 24 24 async connectedCallback() { 25 25 // Broadcast if needed 26 26 if (this.hasAttribute("group")) { 27 + // TODO: Replicate state (repeat & shuffle) 27 28 this.broadcast(this.nameWithGroup, {}); 28 29 } 29 30
src/fonts/CommitMonoVariable.woff2

This is a binary file and will not be displayed.

+47 -31
src/index.vto
··· 33 33 title: "Blur / Artwork controller" 34 34 desc: > 35 35 Audio playback controller with artwork display. 36 - - title: "Loader" 36 + - url: "themes/loader/constituent/" 37 + title: "Loader" 37 38 desc: > 38 39 **A constituent that loads other constituents!** Load a constituent from a URL, text snippet or from your user data output. 39 40 todo: true ··· 173 174 # DEFINITIONS 174 175 175 176 definitions: 177 + - title: "Output / Constituent" 178 + desc: > 179 + Custom constituents to keep around. 180 + todo: true 176 181 - title: "Output / Favourites" 177 - desc: "Indicate a user's favourite audio. Not a property of a track because tracks are associated with a specific source. Favourites may match with multiple tracks. Specified using the audio's title and artist." 182 + desc: > 183 + Indicate a user's favourite audio. Not a property of a track because tracks are associated with a specific source. Favourites may match with multiple tracks. Specified using the audio's title and artist. 178 184 todo: true 179 185 - title: "Output / Playlist" 180 - desc: "Just like favourites, does not refer to specific tracks. Unlike favourites, must also specify the album. Can also be considered a collection which is basically an unordered playlist." 186 + desc: > 187 + Just like favourites, does not refer to specific tracks. Unlike favourites, must also specify the album. Can also be considered a collection which is basically an unordered playlist. 181 188 todo: true 182 189 - title: "Output / Progress" 183 - desc: "Used to track progress of (long) audio playback." 190 + desc: > 191 + Used to track progress of (long) audio playback. 192 + todo: true 193 + - title: "Output / Theme" 194 + desc: > 195 + Custom theme to keep around. 184 196 todo: true 185 197 - title: "Output / Tracks" 186 - desc: "Represents audio that can be played, or a placeholder for a source of tracks. Contains a URI that will resolve to the audio. This object may be cached if convenient." 198 + desc: > 199 + Represents audio that can be played, or a placeholder for a source of tracks. Contains a URI that will resolve to the audio. This object may be cached if convenient. 187 200 url: "definitions/output/tracks.json" 188 201 189 202 --- 190 203 191 204 <header> 192 - <h1> 193 - {{ await comp.diffuse.logo() }} 194 - </h1> 195 - <p class="construct dither-mask"> 196 - Construct your audio player. 197 - </p> 198 - <p> 199 - Diffuse is a collection of components and software that make it possible to listen to audio from various sources on your devices and the web, and to create the ideal digital 200 - audio listening experience for you. 201 - </p> 202 - <p style="align-items: center; display: flex; gap: var(--space-2xs); opacity: 0.55;"> 203 - <i class="ph-fill ph-crane"></i> 204 - <strong style="font-weight: 700;">WORK IN PROGRESS</strong> 205 - </p> 206 - <ul class="table-of-contents"> 207 - <li><a href="#usage">Usage</a></li> 208 - <li><a href="#demo">Demo</a></li> 209 - <li><a href="#themes">Themes</a></li> 210 - <li><a href="#constituents">Constituents</a></li> 211 - <li><a href="#take-control">Take control</a></li> 212 - <li><a href="#elements">Elements</a></li> 213 - <li><a href="#definitions">Definitions</a></li> 214 - </ul> 215 - <p> 216 - <small>Built by <a href="https://tokono.ma">tokono.ma</a></small> 217 - </p> 205 + <div> 206 + <h1 class="diffuse-logo"> 207 + {{ await comp.diffuse.logo() }} 208 + </h1> 209 + <p class="construct dither-mask"> 210 + Construct your audio player. 211 + </p> 212 + <p> 213 + Diffuse is a collection of components and software that make it possible to listen to audio from various sources on your devices and the web, and to create the ideal digital 214 + audio listening experience for you. 215 + </p> 216 + <p style="align-items: center; display: flex; gap: var(--space-2xs); opacity: 0.55;"> 217 + <i class="ph-fill ph-crane"></i> 218 + <strong style="font-weight: 700;">WORK IN PROGRESS</strong> 219 + </p> 220 + <ul class="table-of-contents"> 221 + <li><a href="#usage">Usage</a></li> 222 + <li><a href="#demo">Demo</a></li> 223 + <li><a href="#themes">Themes</a></li> 224 + <li><a href="#constituents">Constituents</a></li> 225 + <li><a href="#take-control">Take control</a></li> 226 + <li><a href="#elements">Elements</a></li> 227 + <li><a href="#definitions">Definitions</a></li> 228 + </ul> 229 + <p> 230 + <small>Built by <a href="https://tokono.ma">tokono.ma</a></small> 231 + </p> 232 + </div> 233 + <div class="dither-mask filler"></div> 218 234 </header> 219 235 <main> 220 236 <div class="columns">
+1 -1
src/styles/base.css
··· 1 1 @import "./reset.css"; 2 2 @import "./variables.css"; 3 - @import "./fonts.css"; 3 + @import "./font-faces.css"; 4 4 @import "./animations.css"; 5 5 6 6 @import "./diffuse/colors.css";
+5
src/styles/diffuse/fonts.css
··· 11 11 font-optical-sizing: auto; 12 12 } 13 13 } 14 + 15 + .monospace-font, 16 + code { 17 + font-family: CommitMonoVariable, "Commit Mono", monospace; 18 + }
+158 -91
src/styles/diffuse/page.css
··· 7 7 scroll-margin-top: var(--space-md); 8 8 } 9 9 10 + /** 11 + * Containers 12 + */ 13 + 14 + p, 15 + ul, 16 + ol { 17 + margin: var(--space-sm) 0; 18 + max-width: var(--container-sm); 19 + } 20 + 21 + header { 22 + display: flex; 23 + gap: var(--space-lg); 24 + } 25 + 10 26 header, 11 27 main { 12 - margin: var(--space-md) var(--space-lg); 28 + margin: var(--space-md) auto; 29 + max-width: var(--container-7xl); 30 + padding: 0 var(--space-lg); 13 31 } 14 32 15 - a { 16 - color: inherit; 17 - text-underline-offset: 6px; 33 + main { 34 + margin-bottom: var(--space-lg); 18 35 } 19 36 20 - button { 21 - background: var(--accent); 22 - border: 0; 23 - border-radius: var(--radius-md); 24 - color: var(--bg-color); 25 - cursor: pointer; 26 - font-family: inherit; 27 - font-weight: 500; 28 - line-height: var(--leading-tight); 29 - padding: var(--space-2xs) var(--space-xs); 30 - transition-duration: 500ms; 31 - transition-property: opacity; 37 + .columns { 38 + display: flex; 39 + flex-wrap: wrap; 40 + gap: 0 var(--space-3xl); 41 + } 42 + 43 + ul.columns { 44 + list-style: none; 45 + padding: 0; 46 + max-width: 100%; 47 + 48 + li { 49 + margin-bottom: var(--space-xl); 50 + } 32 51 33 - &[disabled] { 34 - opacity: 0.5; 52 + li::marker { 53 + content: none; 35 54 } 36 55 37 - & > span { 38 - align-items: center; 39 - display: inline-flex; 40 - gap: var(--space-3xs); 41 - padding-top: 1px; 56 + li i.ph-fill { 57 + opacity: 0.4; 42 58 } 43 59 } 44 60 45 - h1 { 46 - margin: var(--space-lg) 0 var(--space-xl); 47 - padding-top: var(--space-2xs); 61 + .filler { 62 + background: oklch(from var(--accent) l c h / 0.2); 63 + flex: 1; 48 64 } 49 65 50 - h1 svg { 51 - fill: oklch(from var(--bg-color) calc(l - 0.5) c h); 52 - opacity: 0.2; 53 - width: 4.25em; 66 + .flex { 67 + flex: 1; 68 + margin-bottom: var(--space-xs); 69 + min-width: var(--container-3xs); 70 + } 71 + 72 + /** 73 + * Forms 74 + */ 54 75 55 - @media (prefers-color-scheme: dark) { 56 - & { 57 - fill: var(--text-color); 58 - opacity: 0.25; 59 - } 60 - } 76 + textarea { 77 + background: transparent; 78 + border: 3px solid oklch(from currentColor l c h / 0.25); 79 + border-radius: var(--radius-md); 80 + color: inherit; 81 + font-size: var(--fs-sm); 82 + height: var(--container-xs); 83 + padding: var(--space-xs); 84 + resize: none; 85 + width: 100%; 86 + } 87 + 88 + /** 89 + * Headers 90 + */ 91 + 92 + .construct { 93 + color: var(--accent); 94 + font-size: var(--fs-3xl); 95 + font-weight: 900; 96 + image-rendering: pixelated; 97 + letter-spacing: -0.0125em; 98 + line-height: 0.775em; 99 + line-height: 1.05cap; 100 + margin-bottom: var(--space-md); 101 + max-width: var(--container-xl); 102 + text-transform: uppercase; 61 103 } 62 104 63 105 h2 { ··· 84 126 margin-top: var(--space-md); 85 127 } 86 128 87 - ul, 88 - ol { 89 - padding-left: var(--space-md); 129 + /** 130 + * Inline 131 + */ 132 + 133 + a { 134 + color: inherit; 135 + text-underline-offset: 6px; 90 136 } 91 137 92 - p, 93 - ul, 94 - ol { 95 - margin: var(--space-sm) 0; 96 - max-width: var(--container-sm); 138 + button { 139 + background: var(--accent); 140 + border: 0; 141 + border-radius: var(--radius-md); 142 + color: var(--bg-color); 143 + cursor: pointer; 144 + font-family: inherit; 145 + font-weight: 500; 146 + line-height: var(--leading-tight); 147 + padding: var(--space-2xs) var(--space-xs); 148 + transition-duration: 500ms; 149 + transition-property: opacity; 150 + 151 + &[disabled] { 152 + cursor: not-allowed; 153 + opacity: 0.5; 154 + } 155 + 156 + & > span { 157 + align-items: center; 158 + display: inline-flex; 159 + gap: var(--space-3xs); 160 + padding-top: 1px; 161 + } 97 162 } 98 163 99 164 small { 100 165 font-size: var(--fs-xs); 101 166 } 102 167 103 - ul li::marker { 104 - color: oklch(from currentColor l c h / 0.4); 105 - content: "◦ "; 168 + .button-row { 169 + display: inline-flex; 170 + gap: var(--space-2xs); 106 171 } 107 172 108 - .columns { 109 - display: flex; 110 - flex-wrap: wrap; 111 - gap: 0 var(--space-3xl); 173 + .todo { 174 + font-size: var(--fs-sm); 175 + font-weight: 600; 176 + opacity: 0.4; 112 177 } 113 178 114 - ul.columns { 115 - list-style: none; 116 - padding: 0; 117 - max-width: 100%; 118 - 119 - li { 120 - margin-bottom: var(--space-xl); 121 - } 122 - 123 - li::marker { 124 - content: none; 125 - } 126 - 127 - li i.ph-fill { 128 - opacity: 0.4; 129 - } 179 + .with-icon { 180 + align-items: center; 181 + display: inline-flex; 182 + gap: var(--space-3xs); 130 183 } 131 184 132 - .construct { 133 - color: var(--accent); 134 - font-size: var(--fs-3xl); 135 - font-weight: 900; 136 - image-rendering: pixelated; 137 - letter-spacing: -0.0125em; 138 - line-height: 0.775em; 139 - line-height: 1.05cap; 140 - margin-bottom: var(--space-md); 141 - max-width: var(--container-xl); 142 - text-transform: uppercase; 185 + /** 186 + * Lists 187 + */ 188 + 189 + ul, 190 + ol { 191 + padding-left: var(--space-md); 143 192 } 144 193 145 - .dither-mask, 146 - h2 { 147 - mask: 148 - url("data:image/png;base64,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"), 149 - radial-gradient(circle at 50% 0, transparent 45%, oklab(0 0 0 / 0.65) 85%) 0 0/50% 100%; 194 + ul li::marker { 195 + color: oklch(from currentColor l c h / 0.4); 196 + content: "◦ "; 150 197 } 151 198 152 199 .element { ··· 191 238 } 192 239 } 193 240 194 - .todo { 195 - font-size: var(--fs-sm); 196 - font-weight: 600; 197 - opacity: 0.4; 241 + /** 242 + * Logo 243 + */ 244 + 245 + .diffuse-logo { 246 + font-size: var(--fs-lg); 247 + margin: var(--space-lg) 0 var(--space-xl); 248 + padding-top: var(--space-2xs); 249 + 250 + svg { 251 + fill: oklch(from var(--bg-color) calc(l - 0.5) c h); 252 + opacity: 0.2; 253 + width: 4.25em; 254 + 255 + @media (prefers-color-scheme: dark) { 256 + fill: var(--text-color); 257 + opacity: 0.25; 258 + } 259 + } 198 260 } 199 261 200 - .with-icon { 201 - align-items: center; 202 - display: inline-flex; 203 - gap: var(--space-3xs); 262 + /** 263 + * 😶‍🌫️ 264 + */ 265 + 266 + .dither-mask, 267 + h2 { 268 + mask: 269 + url("data:image/png;base64,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"), 270 + radial-gradient(circle at 50% 0, transparent 45%, oklab(0 0 0 / 0.65) 85%) 0 0/50% 100%; 204 271 }
+8
src/styles/fonts.css src/styles/font-faces.css
··· 1 + @font-face { 2 + font-display: swap; 3 + font-family: CommitMonoVariable; 4 + font-style: normal; 5 + font-weight: 100 900; 6 + src: url("../fonts/CommitMonoVariable.woff2") format("woff2"); 7 + } 8 + 1 9 @font-face { 2 10 font-display: swap; 3 11 font-family: InterVariable;
+2
src/styles/variables.css
··· 46 46 --container-5xl: 64rem; 47 47 --container-6xl: 72rem; 48 48 --container-7xl: 80rem; 49 + --container-8xl: 88rem; 50 + --container-9xl: 96rem; 49 51 50 52 /* Letter-spacing */ 51 53 --tracking-tighter: -0.05em;
+11 -15
src/themes/blur/artwork-controller/index.js
··· 1 1 import defaults from "@common/constituents/default/config.js"; 2 2 import { effect } from "@common/signal.js"; 3 3 4 - import AudioEngine from "@components/engine/audio/element.js"; 5 - import ArtworkProcessor from "@components/processor/artwork/element.js"; 6 - import QueueAudioOrchestrator from "@components/orchestrator/queue-audio/element.js"; 7 - 8 4 import ArtworkController from "@themes/blur/artwork-controller/element.js"; 9 5 10 6 // Prerequisites 11 - const aud = new AudioEngine(); 12 - aud.setAttribute("group", defaults.GROUP); 7 + const aud = defaults.lazy.engine.audio(); 8 + const queue = defaults.lazy.engine.queue(); 13 9 14 - const art = new ArtworkProcessor(); 15 - const oqa = new QueueAudioOrchestrator(); 16 - oqa.setAttribute("group", defaults.GROUP); 17 - oqa.setAttribute("input-selector", defaults.orchestrator.input.selector); 18 - oqa.setAttribute("audio-engine-selector", "de-audio"); 19 - oqa.setAttribute("queue-engine-selector", defaults.engine.queue.selector); 10 + const art = defaults.lazy.processor.artwork(); 11 + 12 + const oqa = defaults.lazy.orchestrator.queueAudio(); 13 + const rso = defaults.lazy.orchestrator.repeatShuffle(); 14 + 15 + defaults.lazy.orchestrator.queueTracks(); 16 + defaults.lazy.orchestrator.repeatShuffle(); 20 17 21 18 // Controller 22 19 const dac = new ArtworkController(); 23 20 dac.setAttribute("artwork-processor-selector", art.selector); 24 21 dac.setAttribute("audio-engine-selector", aud.selector); 25 22 dac.setAttribute("input-selector", defaults.orchestrator.input.selector); 26 - dac.setAttribute("queue-engine-selector", defaults.engine.queue.selector); 23 + dac.setAttribute("queue-engine-selector", queue.selector); 27 24 28 25 // Add to DOM 29 - document.body.append(aud, art, oqa, dac); 26 + document.body.append(dac); 30 27 31 28 // Effect - Link the repeat/shuffle & queue-audio orchestrators 32 29 effect(() => { 33 - const rso = defaults.orchestrator.repeatShuffle; 34 30 const repeat = rso.repeat(); 35 31 36 32 if (repeat && !oqa.hasAttribute("repeat")) {
src/themes/loader/constituent/index.js

This is a binary file and will not be displayed.

+122
src/themes/loader/constituent/index.vto
··· 1 + --- 2 + layout: layouts/constituent.vto 3 + base: ../../../ 4 + 5 + styles: 6 + - styles/base.css 7 + - styles/diffuse/page.css 8 + - styles/vendor/phosphor/fill/style.css 9 + 10 + scripts: 11 + - index.js 12 + --- 13 + 14 + <header> 15 + <div> 16 + <div> 17 + <a class="diffuse-logo" href="./" style="display: inline-block;"> 18 + {{ await comp.diffuse.logo() }} 19 + </a> 20 + </div> 21 + <p class="construct dither-mask" style="margin-top: 0;"> 22 + Constituent loader ❈ 23 + </p> 24 + <p> 25 + This tool allows you to load a custom <a href="#constituents">constituent</a> besides the <a href="#constituents">existing ones</a>. <strong>If you're missing a feature in Diffuse, this is the place to be!</strong> 26 + </p> 27 + </div> 28 + <div class="dither-mask filler"></div> 29 + </header> 30 + <main> 31 + <!-- COMMUNITY --> 32 + <div class="columns"> 33 + <section> 34 + <h2 id="generate">Generate</h2> 35 + <p> 36 + TODO: Explain how to instruct AI to generate a constituent for you. For example, "I want a nice album overview grouped by the month they were added to my collection" 37 + </p> 38 + </section> 39 + 40 + <section> 41 + <h2 id="community">Community</h2> 42 + <p> 43 + Check out some constituents from the community and load them here. 44 + </p> 45 + <p> 46 + <small><i class="ph-fill ph-info"></i> Nothing here yet, we're still in alpha.</small> 47 + </p> 48 + </section> 49 + </div> 50 + 51 + <!-- CONSTRUCT --> 52 + <section> 53 + <h2 id="construct">Build it yourself</h2> 54 + <!--<p></p>--> 55 + 56 + <div class="columns"> 57 + <div class="flex"> 58 + <p style="margin-top: 0; max-width: 100%;"> 59 + If you know a bit of HTML & Javascript, you can write your own or plug in some code you found elsewhere (be careful what to copy/paste though): 60 + </p> 61 + 62 + <form> 63 + <textarea class="monospace-font" placeholder="<code>goes here</code>"></textarea> 64 + </form> 65 + </div> 66 + 67 + <div class="flex"> 68 + <p style="margin-top: 0"> 69 + 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. 70 + </p> 71 + <p style="margin-bottom: 0;"> 72 + <small>Some tips:</small> 73 + </p> 74 + <ul style="margin-top: var(--space-3xs);"> 75 + <li><small>Use <code>type="module"</code> when writing scripts.</small></li> 76 + </ul> 77 + <p> 78 + <span class="button-row"> 79 + <button disabled>Save</button> 80 + <button disabled>Preview</button> 81 + <button disabled>Open in new tab</button> 82 + </span> 83 + </p> 84 + <p> 85 + Add dependencies for: 86 + </p> 87 + <form> 88 + <select> 89 + <option>Playing audio</option> 90 + <option>Adding items to the queue</option> 91 + <option>Browsing collection</option> 92 + </select> 93 + </form> 94 + <p> 95 + <span class="button-row"> 96 + <button disabled>Add code</button> 97 + </span> 98 + </p> 99 + </div> 100 + </div> 101 + </section> 102 + 103 + <!-- FOUNDATION --> 104 + <section> 105 + <h2 id="foundation">Foundation</h2> 106 + <p> 107 + The default configuration for constituents includes the following elements which are loaded automatically: 108 + </p> 109 + <ul> 110 + <li>orchestrator / input</li> 111 + <li>orchestrator / output</li> 112 + <li>orchestrator / process-tracks <small>(process on launch)</small></li> 113 + <li>processor / metadata</li> 114 + </ul> 115 + <p> 116 + Besides this the foundation provides most other elements preconfigured. These are lazy loaded so you do need to call the method in order to load it. For example: 117 + </p> 118 + <pre><code>{{- 119 + "import defaults from \"@common/constituents/default/config.js\"\ndefaults.lazy.engine.audio()" 120 + }}</code></pre> 121 + </section> 122 + </main>
+7
src/themes/loader/constituent/s/index.vto
··· 1 + --- 2 + layout: layouts/constituent.vto 3 + base: ../../../../ 4 + 5 + styles: 6 + - styles/base.css 7 + ---
+8 -5
src/themes/webamp/browser/index.js
··· 1 1 import defaults from "@common/constituents/default/config.js"; 2 2 import BrowserElement from "@themes/webamp/browser/element.js"; 3 3 4 + const queue = defaults.lazy.engine.queue(); 5 + const search = defaults.lazy.processor.search(); 6 + 7 + defaults.lazy.orchestrator.queueTracks(); 8 + defaults.lazy.orchestrator.searchTracks(); 9 + 4 10 const el = new BrowserElement(); 5 11 el.setAttribute("input-selector", defaults.orchestrator.input.selector); 6 12 el.setAttribute("output-selector", defaults.orchestrator.output.selector); 7 - el.setAttribute("queue-engine-selector", defaults.engine.queue.selector); 8 - el.setAttribute( 9 - "search-processor-selector", 10 - defaults.processor.search.selector, 11 - ); 13 + el.setAttribute("queue-engine-selector", queue.selector); 14 + el.setAttribute("search-processor-selector", search.selector); 12 15 13 16 document.querySelector("#placeholder")?.replaceWith(el);
+3 -4
src/themes/webamp/configurators/input/index.js
··· 1 1 import defaults from "@common/constituents/default/config.js"; 2 2 import InputConfigElement from "@themes/webamp/configurators/input/element.js"; 3 3 4 + const sources = defaults.lazy.orchestrator.sources(); 5 + 4 6 const el = new InputConfigElement(); 5 7 el.setAttribute("input-selector", defaults.orchestrator.input.selector); 6 8 el.setAttribute("output-selector", defaults.orchestrator.output.selector); 7 - el.setAttribute( 8 - "sources-orchestrator-selector", 9 - defaults.orchestrator.sources.selector, 10 - ); 9 + el.setAttribute("sources-orchestrator-selector", sources.selector); 11 10 12 11 document.querySelector("#placeholder")?.replaceWith(el);