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: externalise facet scripts so we can type check them, but inline them afterwards.

+482 -364
+61 -1
_config.ts
··· 216 216 site.add([".json"]); 217 217 site.use(sourceMaps()); 218 218 219 + // *.inline.js files are inlined into their companion HTML at build/serve time. 220 + // Exclude them from the regular build so esbuild doesn't try to bundle them. 221 + site.ignore((p) => p.endsWith(".inline.js")); 222 + 219 223 site.script("copy-type-defs", () => { 220 224 for ( 221 225 const f of walkSync( ··· 239 243 // Facet HTML files are HTML fragments fetched via JS, not full pages. 240 244 // Serving them as text/plain prevents Lume's dev server from injecting 241 245 // its live-reload <script> tag into the fetched content. 246 + // 247 + // Also inlines any <script type="module" src="./foo.inline.js"> references so 248 + // that forked facets contain readable JS rather than an external file reference. 242 249 async function facetHtmlMiddleware( 243 250 request: Request, 244 251 next: RequestHandler, ··· 251 258 return response; 252 259 } 253 260 261 + let content = await response.text(); 262 + content = await inlineScriptSrc( 263 + content, 264 + path.join("./src", path.dirname(pathname)), 265 + ); 266 + 254 267 const headers = new Headers(response.headers); 255 268 headers.set("content-type", "text/plain; charset=utf-8"); 256 - return new Response(response.body, { 269 + return new Response(content, { 257 270 status: response.status, 258 271 statusText: response.statusText, 259 272 headers, 260 273 }); 261 274 } 275 + 276 + const SCRIPT_SRC_RE = 277 + /<script type="module" src="\.\/([^"]+\.inline\.js)"><\/script>/; 278 + 279 + async function inlineScriptSrc(content: string, dir: string): Promise<string> { 280 + const match = SCRIPT_SRC_RE.exec(content); 281 + if (!match) return content; 282 + 283 + const jsPath = path.join(dir, match[1]); 284 + try { 285 + return htmlWithInlineJs({ content, jsPath, match: match[0] }); 286 + } catch { 287 + return content; 288 + } 289 + } 290 + 291 + site.addEventListener("afterBuild", async () => { 292 + for ( 293 + const f of walkSync("./dist/", { includeDirs: false, exts: [".html"] }) 294 + ) { 295 + const content = Deno.readTextFileSync(f.path); 296 + const match = SCRIPT_SRC_RE.exec(content); 297 + if (!match) continue; 298 + 299 + const srcDir = path.dirname(f.path).replace(/^dist\//, "src/"); 300 + const jsPath = path.join(srcDir, match[1]); 301 + 302 + try { 303 + const newContent = htmlWithInlineJs({ content, jsPath, match: match[0] }); 304 + Deno.writeTextFileSync(f.path, newContent); 305 + } catch { 306 + // leave as-is if the source file can't be read 307 + } 308 + } 309 + }); 310 + 311 + function htmlWithInlineJs({ content, match, jsPath }: { 312 + content: string; 313 + match: string; 314 + jsPath: string; 315 + }): string { 316 + const js = 317 + Deno.readTextFileSync(jsPath).split("\n").map((line) => ` ${line}`).join( 318 + "\n", 319 + ).trimEnd() + "\n"; 320 + return content.replace(match, `<script type="module">\n${js}</script>`); 321 + }
+8
deno.jsonc
··· 54 54 "@testing/": "./src/testing/", 55 55 "@tests/": "./tests/", 56 56 57 + "@diffuse/foundation": "./src/common/facets/foundation.js", 58 + 59 + "@diffuse/common/": "./src/common/", 60 + "@diffuse/components/": "./src/components/", 61 + "@diffuse/definitions/": "./src/definitions/", 62 + "@diffuse/styles/": "./src/styles/", 63 + "@diffuse/themes/": "./src/themes/", 64 + 57 65 "@common/": "./src/common/", 58 66 "@components/": "./src/components/", 59 67 "@definitions/": "./src/definitions/",
+1 -32
src/facets/examples/generate-playlist.html
··· 9 9 @import "./styles/diffuse/page.css"; 10 10 </style> 11 11 12 - <script type="module"> 13 - import foundation from "@diffuse/foundation"; 14 - 15 - const queue = foundation.engine.queue(); 16 - const output = foundation.orchestrator.output(); 17 - 18 - /** 19 - * Playlist generator 20 - */ 21 - function generatePlaylist() { 22 - const queueItems = [ 23 - ...queue.past(), 24 - ...(queue.now() ? [queue.now()] : []), 25 - ...queue.future().filter((i) => i.manualEntry), 26 - ]; 27 - 28 - const playlist = queueItems 29 - .map((item) => output.tracks.collection().find(t => t.id === item.id)) 30 - .filter((t) => t); 31 - 32 - const element = document.querySelector("main ol"); 33 - if (!element) return; 34 - 35 - element.innerHTML = playlist 36 - .map((track) => `<li>${track.tags.artist} - ${track.tags.title}</li>`) 37 - .join(""); 38 - } 39 - 40 - document.body.querySelector("button").onclick = () => { 41 - generatePlaylist(); 42 - }; 43 - </script> 12 + <script type="module" src="./generate-playlist.inline.js"></script>
+36
src/facets/examples/generate-playlist.inline.js
··· 1 + import foundation from "@diffuse/foundation"; 2 + 3 + const queue = foundation.engine.queue(); 4 + const output = foundation.orchestrator.output(); 5 + 6 + /** 7 + * Playlist generator 8 + */ 9 + function generatePlaylist() { 10 + const queueItems = [ 11 + ...queue.past(), 12 + ...(queue.now() ? [queue.now()] : []), 13 + ...queue.future().filter((i) => i.manualEntry), 14 + ]; 15 + 16 + const playlist = queueItems 17 + .map((item) => output.tracks.collection().find((t) => t.id === item?.id)) 18 + .filter((t) => t); 19 + 20 + const element = document.querySelector("main ol"); 21 + if (!element) return; 22 + 23 + element.innerHTML = playlist 24 + .map((track) => 25 + `<li> 26 + ${track?.tags?.artist ?? "Unknown artist"} - 27 + ${track?.tags?.title ?? "Unknown title"} 28 + </li>` 29 + ) 30 + .join(""); 31 + } 32 + 33 + /** @type {HTMLButtonElement} */ (document.body.querySelector("button")) 34 + .onclick = () => { 35 + generatePlaylist(); 36 + };
+1 -35
src/facets/examples/now-playing.html
··· 8 8 @import "./styles/diffuse/page.css"; 9 9 </style> 10 10 11 - <script type="module"> 12 - import foundation from "@diffuse/foundation"; 13 - import { computed, effect } from "./common/signal.js"; 14 - 15 - foundation.features.processInputs(); 16 - foundation.features.fillQueueAutomatically(); 17 - 18 - const output = foundation.orchestrator.output(); 19 - const queue = foundation.engine.queue(); 20 - 21 - const isLoadingTracks = computed(() => { 22 - return output.tracks.state() !== "loaded"; 23 - }); 24 - 25 - effect(() => { 26 - const now = queue.now(); 27 - const currentlyPlaying = now ? output.tracks.collection().find(t => t.id === now.id) : undefined; 28 - const tags = currentlyPlaying?.tags; 29 - 30 - const element = document.querySelector("#now-playing"); 31 - if (!element) return; 32 - 33 - if (currentlyPlaying) { 34 - element.innerText = `${tags.artist} - ${tags.title}`; 35 - } else if (isLoadingTracks()) { 36 - // Keep original text 37 - } else { 38 - element.innerText = "Nothing is playing yet"; 39 - } 40 - }); 41 - 42 - document.body.querySelector("button").onclick = () => { 43 - queue.shift(); 44 - }; 45 - </script> 11 + <script type="module" src="./now-playing.inline.js"></script>
+39
src/facets/examples/now-playing.inline.js
··· 1 + import foundation from "@diffuse/foundation"; 2 + import { computed, effect } from "@diffuse/common/signal.js"; 3 + 4 + foundation.features.processInputs(); 5 + foundation.features.fillQueueAutomatically(); 6 + 7 + const output = foundation.orchestrator.output(); 8 + const queue = foundation.engine.queue(); 9 + 10 + const isLoadingTracks = computed(() => { 11 + return output.tracks.state() !== "loaded"; 12 + }); 13 + 14 + effect(() => { 15 + const now = queue.now(); 16 + const currentlyPlaying = now 17 + ? output.tracks.collection().find((t) => t.id === now.id) 18 + : undefined; 19 + const tags = currentlyPlaying?.tags; 20 + 21 + const element = 22 + /** @type {HTMLElement | null} */ (document.querySelector("#now-playing")); 23 + if (!element) return; 24 + 25 + if (currentlyPlaying) { 26 + element.innerText = `${tags?.artist ?? "Unknown artist"} - ${ 27 + tags?.title ?? "Unknown title" 28 + }`; 29 + } else if (isLoadingTracks()) { 30 + // Keep original text 31 + } else { 32 + element.innerText = "Nothing is playing yet"; 33 + } 34 + }); 35 + 36 + /** @type {HTMLButtonElement} */ (document.body.querySelector("button")) 37 + .onclick = () => { 38 + queue.shift(); 39 + };
+5 -1
src/facets/l/index.vto
··· 22 22 <script type="importmap"> 23 23 { 24 24 "imports": { 25 + "@diffuse/foundation": "./common/facets/foundation.js", 26 + 25 27 "@diffuse/common/": "./common/", 26 28 "@diffuse/components/": "./components/", 27 - "@diffuse/foundation": "./common/facets/foundation.js", 29 + "@diffuse/definitions/": "./definitions/", 30 + "@diffuse/styles/": "./styles/", 31 + "@diffuse/themes/": "./themes/", 28 32 29 33 "@atcute/tid": "./vendor/@atcute/tid/index.js" 30 34 }
+1 -87
src/facets/tools/auto-queue.html
··· 54 54 } 55 55 </style> 56 56 57 - <script type="module"> 58 - import foundation from "@diffuse/foundation"; 59 - import { computed, effect } from "@diffuse/common/signal.js"; 60 - import * as Playlist from "@diffuse/common/playlist.js"; 61 - 62 - const ACTIVE_CLASS = "button--active"; 63 - 64 - // Setup 65 - foundation.features.fillQueueAutomatically(); 66 - foundation.features.processInputs(); 67 - 68 - const queue = foundation.engine.queue(); 69 - const repeatShuffle = foundation.engine.repeatShuffle(); 70 - const scope = foundation.engine.scope(); 71 - const output = foundation.orchestrator.output(); 72 - 73 - // Elements 74 - const repeatBtn = document.querySelector("#repeat"); 75 - const shuffleBtn = document.querySelector("#shuffle"); 76 - const searchInput = document.querySelector("#search"); 77 - const playlistSelect = document.querySelector("#playlist"); 78 - 79 - // Repeat & Shuffle state 80 - effect(() => { 81 - repeatBtn.classList.toggle(ACTIVE_CLASS, repeatShuffle.repeat()); 82 - }); 83 - 84 - effect(() => { 85 - shuffleBtn.classList.toggle(ACTIVE_CLASS, repeatShuffle.shuffle()); 86 - }); 87 - 88 - // Actions 89 - repeatBtn.onclick = () => { 90 - repeatShuffle.setRepeat(!repeatShuffle.repeat()); 91 - }; 92 - 93 - shuffleBtn.onclick = () => { 94 - repeatShuffle.setShuffle(!repeatShuffle.shuffle()); 95 - }; 96 - 97 - // Search state 98 - effect(() => { 99 - searchInput.value = scope.searchTerm() ?? ""; 100 - }); 101 - 102 - searchInput.oninput = () => { 103 - scope.setSearchTerm(searchInput.value.trim() || undefined); 104 - }; 105 - 106 - // Playlist state 107 - effect(() => { 108 - const items = output.playlistItems.collection(); 109 - const currentPlaylist = scope.playlist(); 110 - 111 - // Group items by playlist name 112 - const playlistMap = Playlist.gather(items); 113 - const all = [...playlistMap.values()].sort((a, b) => a.name.localeCompare(b.name)); 114 - const ordered = all.filter((p) => !p.unordered); 115 - const unordered = all.filter((p) => p.unordered); 116 - 117 - playlistSelect.innerHTML = `<option value="">All tracks</option>`; 118 - 119 - for (const [label, group] of [ 120 - ["Ordered", ordered], 121 - ["Unordered", unordered], 122 - ]) { 123 - if (group.length === 0) continue; 124 - 125 - const optgroup = document.createElement("optgroup"); 126 - optgroup.label = label; 127 - 128 - for (const playlist of group) { 129 - const option = document.createElement("option"); 130 - option.value = playlist.name; 131 - option.textContent = playlist.name; 132 - option.selected = playlist.name === currentPlaylist; 133 - optgroup.appendChild(option); 134 - } 135 - 136 - playlistSelect.appendChild(optgroup); 137 - } 138 - }); 139 - 140 - playlistSelect.onchange = () => { 141 - scope.setPlaylist(playlistSelect.value.length ? playlistSelect.value : undefined); 142 - }; 143 - </script> 57 + <script type="module" src="./auto-queue.inline.js"></script>
+98
src/facets/tools/auto-queue.inline.js
··· 1 + import foundation from "@diffuse/foundation"; 2 + import { effect } from "@diffuse/common/signal.js"; 3 + import * as Playlist from "@diffuse/common/playlist.js"; 4 + 5 + const ACTIVE_CLASS = "button--active"; 6 + 7 + // Setup 8 + foundation.features.fillQueueAutomatically(); 9 + foundation.features.processInputs(); 10 + 11 + const repeatShuffle = foundation.engine.repeatShuffle(); 12 + const scope = foundation.engine.scope(); 13 + const output = foundation.orchestrator.output(); 14 + 15 + // Elements 16 + const repeatBtn = 17 + /** @type {HTMLButtonElement} */ (document.querySelector("#repeat")); 18 + const shuffleBtn = 19 + /** @type {HTMLButtonElement} */ (document.querySelector("#shuffle")); 20 + const searchInput = 21 + /** @type {HTMLInputElement} */ (document.querySelector("#search")); 22 + const playlistSelect = 23 + /** @type {HTMLSelectElement} */ (document.querySelector("#playlist")); 24 + 25 + // Repeat & Shuffle state 26 + effect(() => { 27 + repeatBtn.classList.toggle(ACTIVE_CLASS, repeatShuffle.repeat()); 28 + }); 29 + 30 + effect(() => { 31 + shuffleBtn.classList.toggle(ACTIVE_CLASS, repeatShuffle.shuffle()); 32 + }); 33 + 34 + // Actions 35 + repeatBtn.onclick = () => { 36 + repeatShuffle.setRepeat(!repeatShuffle.repeat()); 37 + }; 38 + 39 + shuffleBtn.onclick = () => { 40 + repeatShuffle.setShuffle(!repeatShuffle.shuffle()); 41 + }; 42 + 43 + // Search state 44 + effect(() => { 45 + searchInput.value = scope.searchTerm() ?? ""; 46 + }); 47 + 48 + searchInput.oninput = () => { 49 + scope.setSearchTerm(searchInput.value.trim() || undefined); 50 + }; 51 + 52 + // Playlist state 53 + effect(() => { 54 + const items = output.playlistItems.collection(); 55 + const currentPlaylist = scope.playlist(); 56 + 57 + // Group items by playlist name 58 + const playlistMap = Playlist.gather(items); 59 + const all = [...playlistMap.values()].sort((a, b) => 60 + a.name.localeCompare(b.name) 61 + ); 62 + 63 + const ordered = all.filter((p) => !p.unordered); 64 + const unordered = all.filter((p) => p.unordered); 65 + 66 + playlistSelect.innerHTML = `<option value="">All tracks</option>`; 67 + 68 + for ( 69 + const [label, group] of [ 70 + ["Ordered", ordered], 71 + ["Unordered", unordered], 72 + ] 73 + ) { 74 + if (group.length === 0) continue; 75 + 76 + const optgroup = document.createElement("optgroup"); 77 + optgroup.label = /** @type {string} */ (label); 78 + 79 + for (const playlist of group) { 80 + if (typeof playlist === "string") continue; 81 + const option = document.createElement("option"); 82 + 83 + option.value = playlist.name; 84 + option.textContent = playlist.name; 85 + option.selected = playlist.name === currentPlaylist; 86 + 87 + optgroup.appendChild(option); 88 + } 89 + 90 + playlistSelect.appendChild(optgroup); 91 + } 92 + }); 93 + 94 + playlistSelect.onchange = () => { 95 + scope.setPlaylist( 96 + playlistSelect.value.length ? playlistSelect.value : undefined, 97 + ); 98 + };
+1 -133
src/facets/tools/v3-import.html
··· 78 78 } 79 79 </style> 80 80 81 - <script type="module"> 82 - import * as TID from "@atcute/tid"; 83 - import foundation from "@diffuse/foundation"; 84 - 85 - // Setup 86 - foundation.features.processInputs(); 87 - 88 - const favourites = foundation.orchestrator.favourites(); 89 - const output = foundation.orchestrator.output(); 90 - 91 - // Elements 92 - const fileInput = document.querySelector("#file"); 93 - const importFavouritesBtn = document.querySelector("#import-favourites"); 94 - const importPlaylistsBtn = document.querySelector("#import-playlists"); 95 - const statusEl = document.querySelector("#status"); 96 - 97 - // Parsed data 98 - let json = null; 99 - 100 - /** 101 - * Show a status message. 102 - * @param {string} message 103 - * @param {"success" | "error"} type 104 - */ 105 - function showStatus(message, type) { 106 - statusEl.textContent = message; 107 - statusEl.className = `status status--${type}`; 108 - statusEl.hidden = false; 109 - } 110 - 111 - // Parse file on selection 112 - fileInput.onchange = async () => { 113 - const file = fileInput.files?.[0]; 114 - 115 - json = null; 116 - statusEl.hidden = true; 117 - importFavouritesBtn.disabled = true; 118 - importPlaylistsBtn.disabled = true; 119 - 120 - if (!file) return; 121 - 122 - try { 123 - json = JSON.parse(await file.text()); 124 - } catch (err) { 125 - console.error("Failed to parse JSON:", err); 126 - showStatus(`Failed to parse JSON: ${err.message}`, "error"); 127 - return; 128 - } 129 - 130 - if (json.favourites?.data?.length > 0) { 131 - importFavouritesBtn.disabled = false; 132 - } 133 - 134 - if (json.playlists?.data?.length > 0) { 135 - importPlaylistsBtn.disabled = false; 136 - } 137 - }; 138 - 139 - // Import favourites on button click 140 - importFavouritesBtn.onclick = async () => { 141 - const items = json?.favourites?.data; 142 - if (!items || items.length === 0) return; 143 - 144 - try { 145 - const tracks = items.map((item) => ({ 146 - tags: { 147 - artist: item.artist ?? "", 148 - title: item.title ?? "", 149 - }, 150 - })); 151 - 152 - await favourites.include(tracks); 153 - showStatus(`Imported ${tracks.length} favourite(s).`, "success"); 154 - } catch (err) { 155 - console.error("Import failed:", err); 156 - showStatus(`Import failed: ${err.message}`, "error"); 157 - } 158 - }; 159 - 160 - // Import playlists on button click 161 - importPlaylistsBtn.onclick = async () => { 162 - const items = json?.playlists?.data; 163 - if (!items || items.length === 0) return; 164 - 165 - try { 166 - const now = new Date().toISOString(); 167 - const existing = output.playlistItems.collection() ?? []; 168 - const existingPlaylistNames = new Set(existing.map((p) => p.playlist)); 169 - 170 - const newPlaylistItems = items 171 - .filter((item) => !existingPlaylistNames.has(item.name ?? "Untitled")) 172 - .flatMap((item) => { 173 - const playlistName = item.name ?? "Untitled"; 174 - const isUnordered = !!item.collection; 175 - 176 - const playlistItems = []; 177 - 178 - (item.tracks ?? []).forEach((track, index) => { 179 - playlistItems.push({ 180 - $type: "sh.diffuse.output.playlistItem", 181 - id: TID.now(), 182 - playlist: playlistName, 183 - positionedAfter: isUnordered 184 - ? undefined 185 - : index > 0 186 - ? playlistItems[index - 1].id 187 - : undefined, 188 - criteria: [ 189 - { field: "tags.album", value: track.album ?? "", transformations: ["toLowerCase"] }, 190 - { 191 - field: "tags.artist", 192 - value: track.artist ?? "", 193 - transformations: ["toLowerCase"], 194 - }, 195 - { field: "tags.title", value: track.title ?? "", transformations: ["toLowerCase"] }, 196 - ], 197 - createdAt: now, 198 - updatedAt: now, 199 - }); 200 - }); 201 - 202 - return playlistItems; 203 - }); 204 - 205 - await output.playlistItems.save([...existing, ...newPlaylistItems]); 206 - const playlistCount = new Set(newPlaylistItems.map((p) => p.playlist)).size; 207 - showStatus(`Imported ${playlistCount} playlist(s).`, "success"); 208 - } catch (err) { 209 - console.error("Import failed:", err); 210 - showStatus(`Import failed: ${err.message}`, "error"); 211 - } 212 - }; 213 - </script> 81 + <script type="module" src="./v3-import.inline.js"></script>
+160
src/facets/tools/v3-import.inline.js
··· 1 + import * as TID from "@atcute/tid"; 2 + import foundation from "@diffuse/foundation"; 3 + 4 + /** 5 + * @import {PlaylistItem, Track} from "@diffuse/definitions/types.d.ts" 6 + */ 7 + 8 + // Setup 9 + foundation.features.processInputs(); 10 + 11 + const favourites = foundation.orchestrator.favourites(); 12 + const output = foundation.orchestrator.output(); 13 + 14 + // Elements 15 + const fileInput = 16 + /** @type {HTMLInputElement} */ (document.querySelector("#file")); 17 + const importFavouritesBtn = 18 + /** @type {HTMLButtonElement} */ (document.querySelector( 19 + "#import-favourites", 20 + )); 21 + const importPlaylistsBtn = 22 + /** @type {HTMLButtonElement} */ (document.querySelector( 23 + "#import-playlists", 24 + )); 25 + const statusEl = /** @type {HTMLElement} */ (document.querySelector("#status")); 26 + 27 + /** @type {Record<string, any> | null} */ 28 + let json = null; 29 + 30 + /** 31 + * Show a status message. 32 + * @param {string} message 33 + * @param {"success" | "error"} type 34 + */ 35 + function showStatus(message, type) { 36 + statusEl.textContent = message; 37 + statusEl.className = `status status--${type}`; 38 + statusEl.hidden = false; 39 + } 40 + 41 + // Parse file on selection 42 + fileInput.onchange = async () => { 43 + const file = fileInput.files?.[0]; 44 + 45 + json = null; 46 + statusEl.hidden = true; 47 + importFavouritesBtn.disabled = true; 48 + importPlaylistsBtn.disabled = true; 49 + 50 + if (!file) return; 51 + 52 + try { 53 + json = JSON.parse(await file.text()); 54 + } catch (err) { 55 + console.error("Failed to parse JSON:", err); 56 + showStatus( 57 + `Failed to parse JSON: ${/** @type {Error} */ (err).message}`, 58 + "error", 59 + ); 60 + return; 61 + } 62 + 63 + if (json?.favourites?.data?.length > 0) { 64 + importFavouritesBtn.disabled = false; 65 + } 66 + 67 + if (json?.playlists?.data?.length > 0) { 68 + importPlaylistsBtn.disabled = false; 69 + } 70 + }; 71 + 72 + // Import favourites on button click 73 + importFavouritesBtn.onclick = async () => { 74 + /** @type {any[]} */ 75 + const items = json?.favourites?.data; 76 + if (!items || items.length === 0) return; 77 + 78 + try { 79 + /** @type {Track[]} */ 80 + const tracks = items.map((item) => ({ 81 + $type: "sh.diffuse.output.track", 82 + id: "", 83 + uri: "", 84 + tags: { 85 + artist: item.artist ?? "", 86 + title: item.title ?? "", 87 + }, 88 + })); 89 + 90 + await favourites.include(tracks); 91 + showStatus(`Imported ${tracks.length} favourite(s).`, "success"); 92 + } catch (err) { 93 + console.error("Import failed:", err); 94 + showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error"); 95 + } 96 + }; 97 + 98 + // Import playlists on button click 99 + importPlaylistsBtn.onclick = async () => { 100 + /** @type {any[]} */ 101 + const items = json?.playlists?.data; 102 + if (!items || items.length === 0) return; 103 + 104 + try { 105 + const now = new Date().toISOString(); 106 + const existing = output.playlistItems.collection() ?? []; 107 + const existingPlaylistNames = new Set(existing.map((p) => p.playlist)); 108 + 109 + const newPlaylistItems = items 110 + .filter((item) => !existingPlaylistNames.has(item.name ?? "Untitled")) 111 + .flatMap((item) => { 112 + const playlistName = item.name ?? "Untitled"; 113 + const isUnordered = !!item.collection; 114 + 115 + /** @type {PlaylistItem[]} */ 116 + const playlistItems = []; 117 + 118 + /** @type {any[]} */ (item.tracks ?? []).forEach((track, index) => { 119 + playlistItems.push({ 120 + $type: "sh.diffuse.output.playlistItem", 121 + id: TID.now(), 122 + playlist: playlistName, 123 + positionedAfter: isUnordered 124 + ? undefined 125 + : index > 0 126 + ? playlistItems[index - 1].id 127 + : undefined, 128 + criteria: [ 129 + { 130 + field: "tags.album", 131 + value: track.album ?? "", 132 + transformations: ["toLowerCase"], 133 + }, 134 + { 135 + field: "tags.artist", 136 + value: track.artist ?? "", 137 + transformations: ["toLowerCase"], 138 + }, 139 + { 140 + field: "tags.title", 141 + value: track.title ?? "", 142 + transformations: ["toLowerCase"], 143 + }, 144 + ], 145 + createdAt: now, 146 + updatedAt: now, 147 + }); 148 + }); 149 + 150 + return playlistItems; 151 + }); 152 + 153 + await output.playlistItems.save([...existing, ...newPlaylistItems]); 154 + const playlistCount = new Set(newPlaylistItems.map((p) => p.playlist)).size; 155 + showStatus(`Imported ${playlistCount} playlist(s).`, "success"); 156 + } catch (err) { 157 + console.error("Import failed:", err); 158 + showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error"); 159 + } 160 + };
+1 -27
src/themes/blur/artwork-controller/facet.html
··· 4 4 @import "./styles/base.css"; 5 5 </style> 6 6 7 - <script type="module"> 8 - import foundation from "@diffuse/foundation"; 9 - import ArtworkController from "./themes/blur/artwork-controller/element.js"; 10 - 11 - // Setup the prerequisite elements 12 - foundation.features.playAudioFromQueue(); 13 - foundation.features.processInputs(); 14 - 15 - const aud = foundation.engine.audio(); 16 - const art = foundation.processor.artwork(); 17 - const fav = foundation.orchestrator.favourites(); 18 - const inp = foundation.orchestrator.input(); 19 - const out = foundation.orchestrator.output(); 20 - const que = foundation.engine.queue(); 21 - 22 - // Controller 23 - const dac = new ArtworkController(); 24 - dac.setAttribute("artwork-processor-selector", art.selector); 25 - dac.setAttribute("audio-engine-selector", aud.selector); 26 - dac.setAttribute("input-selector", inp.selector); 27 - dac.setAttribute("output-selector", out.selector); 28 - dac.setAttribute("queue-engine-selector", que.selector); 29 - dac.setAttribute("favourites-orchestrator-selector", fav.selector); 30 - 31 - // Add to DOM 32 - document.body.append(dac); 33 - </script> 7 + <script type="module" src="./facet.inline.js"></script>
+25
src/themes/blur/artwork-controller/facet.inline.js
··· 1 + import foundation from "@diffuse/foundation"; 2 + import ArtworkController from "@diffuse/themes/blur/artwork-controller/element.js"; 3 + 4 + // Setup the prerequisite elements 5 + foundation.features.playAudioFromQueue(); 6 + foundation.features.processInputs(); 7 + 8 + const aud = foundation.engine.audio(); 9 + const art = foundation.processor.artwork(); 10 + const fav = foundation.orchestrator.favourites(); 11 + const inp = foundation.orchestrator.input(); 12 + const out = foundation.orchestrator.output(); 13 + const que = foundation.engine.queue(); 14 + 15 + // Controller 16 + const dac = new ArtworkController(); 17 + dac.setAttribute("artwork-processor-selector", art.selector); 18 + dac.setAttribute("audio-engine-selector", aud.selector); 19 + dac.setAttribute("input-selector", inp.selector); 20 + dac.setAttribute("output-selector", out.selector); 21 + dac.setAttribute("queue-engine-selector", que.selector); 22 + dac.setAttribute("favourites-orchestrator-selector", fav.selector); 23 + 24 + // Add to DOM 25 + document.body.append(dac);
+1 -20
src/themes/webamp/browser/facet.html
··· 19 19 @import "./themes/webamp/facet.css"; 20 20 </style> 21 21 22 - <script type="module"> 23 - import foundation from "@diffuse/foundation"; 24 - import BrowserElement from "./themes/webamp/browser/element.js"; 25 - 26 - foundation.features.processInputs(); 27 - foundation.features.searchThroughCollection(); 28 - 29 - const out = foundation.orchestrator.output(); 30 - const que = foundation.engine.queue(); 31 - const scp = foundation.engine.scope(); 32 - const trc = foundation.orchestrator.scopedTracks(); 33 - 34 - const el = new BrowserElement(); 35 - el.setAttribute("output-selector", out.selector); 36 - el.setAttribute("queue-engine-selector", que.selector); 37 - el.setAttribute("scope-engine-selector", scp.selector); 38 - el.setAttribute("tracks-selector", trc.selector); 39 - 40 - document.querySelector("#placeholder")?.replaceWith(el); 41 - </script> 22 + <script type="module" src="./facet.inline.js"></script>
+18
src/themes/webamp/browser/facet.inline.js
··· 1 + import foundation from "@diffuse/foundation"; 2 + import BrowserElement from "@diffuse/themes/webamp/browser/element.js"; 3 + 4 + foundation.features.processInputs(); 5 + foundation.features.searchThroughCollection(); 6 + 7 + const out = foundation.orchestrator.output(); 8 + const que = foundation.engine.queue(); 9 + const scp = foundation.engine.scope(); 10 + const trc = foundation.orchestrator.scopedTracks(); 11 + 12 + const el = new BrowserElement(); 13 + el.setAttribute("output-selector", out.selector); 14 + el.setAttribute("queue-engine-selector", que.selector); 15 + el.setAttribute("scope-engine-selector", scp.selector); 16 + el.setAttribute("tracks-selector", trc.selector); 17 + 18 + document.querySelector("#placeholder")?.replaceWith(el);
+1 -17
src/themes/webamp/configurators/input/facet.html
··· 20 20 @import "./themes/webamp/facet.css"; 21 21 </style> 22 22 23 - <script type="module"> 24 - import foundation from "@diffuse/foundation"; 25 - import InputConfigElement from "./themes/webamp/configurators/input/element.js"; 26 - 27 - const inp = foundation.orchestrator.input(); 28 - const out = foundation.orchestrator.output(); 29 - const pro = foundation.orchestrator.processTracks({ disableWhenReady: true }); 30 - const sou = foundation.orchestrator.sources(); 31 - 32 - const el = new InputConfigElement(); 33 - el.setAttribute("input-selector", inp.selector); 34 - el.setAttribute("output-selector", out.selector); 35 - el.setAttribute("sources-orchestrator-selector", sou.selector); 36 - el.setAttribute("process-tracks-orchestrator-selector", pro.selector); 37 - 38 - document.querySelector("#placeholder")?.replaceWith(el); 39 - </script> 23 + <script type="module" src="./facet.inline.js"></script>
+15
src/themes/webamp/configurators/input/facet.inline.js
··· 1 + import foundation from "@diffuse/foundation"; 2 + import InputConfigElement from "@diffuse/themes/webamp/configurators/input/element.js"; 3 + 4 + const inp = foundation.orchestrator.input(); 5 + const out = foundation.orchestrator.output(); 6 + const pro = foundation.orchestrator.processTracks({ disableWhenReady: true }); 7 + const sou = foundation.orchestrator.sources(); 8 + 9 + const el = new InputConfigElement(); 10 + el.setAttribute("input-selector", inp.selector); 11 + el.setAttribute("output-selector", out.selector); 12 + el.setAttribute("sources-orchestrator-selector", sou.selector); 13 + el.setAttribute("process-tracks-orchestrator-selector", pro.selector); 14 + 15 + document.querySelector("#placeholder")?.replaceWith(el);
+1 -11
src/themes/webamp/configurators/output/facet.html
··· 24 24 @import "./themes/webamp/facet.css"; 25 25 </style> 26 26 27 - <script type="module"> 28 - import foundation from "@diffuse/foundation"; 29 - import OutputConfigElement from "./themes/webamp/configurators/output/element.js"; 30 - 31 - const out = foundation.orchestrator.output(); 32 - 33 - const el = new OutputConfigElement(); 34 - el.setAttribute("output-selector", out.selector); 35 - 36 - document.querySelector("#placeholder")?.replaceWith(el); 37 - </script> 27 + <script type="module" src="./facet.inline.js"></script>
+9
src/themes/webamp/configurators/output/facet.inline.js
··· 1 + import foundation from "@diffuse/foundation"; 2 + import OutputConfigElement from "@diffuse/themes/webamp/configurators/output/element.js"; 3 + 4 + const out = foundation.orchestrator.output(); 5 + 6 + const el = new OutputConfigElement(); 7 + el.setAttribute("output-selector", out.selector); 8 + 9 + document.querySelector("#placeholder")?.replaceWith(el);