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: add remaining connect facets

+503 -1
+21
src/_data/facets.json
··· 37 37 "desc": "Use your AT Protocol identity for user-data storage." 38 38 }, 39 39 { 40 + "url": "facets/connect/https/index.html", 41 + "title": "Connect / HTTPS", 42 + "category": "Data", 43 + "featured": true, 44 + "desc": "Add HTTPS audio files as input." 45 + }, 46 + { 47 + "url": "facets/connect/icecast/index.html", 48 + "title": "Connect / Icecast", 49 + "category": "Data", 50 + "featured": true, 51 + "desc": "Add an Icecast stream as audio input." 52 + }, 53 + { 54 + "url": "facets/connect/local/index.html", 55 + "title": "Connect / Local", 56 + "category": "Data", 57 + "featured": true, 58 + "desc": "Add local directories or files as audio input." 59 + }, 60 + { 40 61 "url": "facets/connect/opensubsonic/index.html", 41 62 "title": "Connect / OpenSubsonic", 42 63 "category": "Data",
+3 -1
src/common/webawesome/phosphor/_lib.js
··· 7 7 for (const icon of selection.icons) { 8 8 const { name } = icon.properties; 9 9 // Strip weight suffix to get base name (e.g. "gear-bold" → "gear") 10 - const baseName = name.replace(/-(?:bold|fill|light|thin|regular|duotone)$/, ""); 10 + // Take only the primary name before any comma-separated aliases 11 + const primaryName = name.split(",")[0].trim(); 12 + const baseName = primaryName.replace(/-(?:bold|fill|light|thin|regular|duotone)$/, ""); 11 13 map.set(baseName, { paths: icon.icon.paths, attrs: icon.icon.attrs }); 12 14 } 13 15 return map;
+15
src/facets/connect/https/index.html
··· 1 + <style> 2 + @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 + @import "./facets/connect/common.css" layer(connect); 4 + 5 + @layer base, diffuse, wa; 6 + </style> 7 + 8 + <main class="wa-theme-default"></main> 9 + 10 + <script type="module" src="facets/connect/https/index.inline.js"></script> 11 + 12 + <script type="module"> 13 + await customElements.whenDefined("wa-card"); 14 + document.querySelector("main")?.classList.add("has-loaded"); 15 + </script>
+125
src/facets/connect/https/index.inline.js
··· 1 + import "@awesome.me/webawesome/dist/components/input/input.js"; 2 + 3 + import * as TID from "@atcute/tid"; 4 + import { html } from "lit-html"; 5 + 6 + import * as Output from "~/common/output.js"; 7 + import { SCHEME } from "~/components/input/https/constants.js"; 8 + import { parseURI } from "~/components/input/https/common.js"; 9 + import { effect } from "~/common/signal.js"; 10 + import foundation from "~/common/foundation.js"; 11 + 12 + import { setup } from "~/facets/connect/common.js"; 13 + 14 + /** 15 + * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 16 + */ 17 + 18 + document.title = "Connect HTTPS | Diffuse"; 19 + 20 + //////////////////////////////////////////// 21 + // SETUP 22 + //////////////////////////////////////////// 23 + 24 + const [inputConfigurator, outputOrchestrator, sourcesOrchestrator] = 25 + await Promise.all([ 26 + foundation.configurator.input(), 27 + foundation.orchestrator.output(), 28 + foundation.orchestrator.sources(), 29 + ]); 30 + 31 + await Promise.all([ 32 + customElements.whenDefined(inputConfigurator.localName), 33 + customElements.whenDefined(outputOrchestrator.localName), 34 + customElements.whenDefined(sourcesOrchestrator.localName), 35 + ]); 36 + 37 + //////////////////////////////////////////// 38 + // UI 39 + //////////////////////////////////////////// 40 + 41 + const { setItems, setError } = setup({ 42 + title: "HTTPS", 43 + hasOutput: false, 44 + 45 + description: html` 46 + <p>Add HTTPS urls as input.</p> 47 + `, 48 + 49 + formFields: html` 50 + <wa-input 51 + id="https-url" 52 + label="URL" 53 + type="url" 54 + placeholder="https://example.com/audio.mp3" 55 + required 56 + ></wa-input> 57 + `, 58 + 59 + onSubmit: () => addUrl(), 60 + }); 61 + 62 + const urlInput = /** @type {WaInput} */ (document.querySelector("#https-url")); 63 + 64 + //////////////////////////////////////////// 65 + // REACTIVE LIST 66 + //////////////////////////////////////////// 67 + 68 + effect(() => { 69 + const inputSources = sourcesOrchestrator.sources()[SCHEME] ?? []; 70 + 71 + setItems( 72 + inputSources.map((source) => { 73 + const parsed = parseURI(source.uri); 74 + return { 75 + name: source.uri, 76 + detail: parsed?.host ?? "", 77 + isInput: true, 78 + isOutput: false, 79 + isSelectedOutput: false, 80 + onRemove: () => removeUrl(source.uri), 81 + }; 82 + }), 83 + ); 84 + }); 85 + 86 + //////////////////////////////////////////// 87 + // ACTIONS 88 + //////////////////////////////////////////// 89 + 90 + /** @param {string} uri */ 91 + async function removeUrl(uri) { 92 + setError(null); 93 + try { 94 + const tracks = await Output.data(outputOrchestrator.tracks); 95 + const detachedTracks = await inputConfigurator.detach({ 96 + fileUriOrScheme: uri, 97 + tracks, 98 + }); 99 + 100 + if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks); 101 + } catch (err) { 102 + setError(err instanceof Error ? err.message : "Failed to remove URL"); 103 + } 104 + } 105 + 106 + async function addUrl() { 107 + const url = urlInput.value?.trim(); 108 + if (!url) return; 109 + 110 + const now = new Date().toISOString(); 111 + const tracksCol = outputOrchestrator.tracks.collection(); 112 + const existingTracks = tracksCol.state === "loaded" ? tracksCol.data : []; 113 + 114 + await outputOrchestrator.tracks.save([ 115 + ...existingTracks, 116 + { 117 + $type: "sh.diffuse.output.track", 118 + id: TID.now(), 119 + createdAt: now, 120 + updatedAt: now, 121 + kind: "placeholder", 122 + uri: url, 123 + }, 124 + ]); 125 + }
+15
src/facets/connect/icecast/index.html
··· 1 + <style> 2 + @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 + @import "./facets/connect/common.css" layer(connect); 4 + 5 + @layer base, diffuse, wa; 6 + </style> 7 + 8 + <main class="wa-theme-default"></main> 9 + 10 + <script type="module" src="facets/connect/icecast/index.inline.js"></script> 11 + 12 + <script type="module"> 13 + await customElements.whenDefined("wa-card"); 14 + document.querySelector("main")?.classList.add("has-loaded"); 15 + </script>
+127
src/facets/connect/icecast/index.inline.js
··· 1 + import "@awesome.me/webawesome/dist/components/input/input.js"; 2 + 3 + import * as TID from "@atcute/tid"; 4 + import { html } from "lit-html"; 5 + 6 + import * as Output from "~/common/output.js"; 7 + import { SCHEME } from "~/components/input/icecast/constants.js"; 8 + import { buildURI, parseURI } from "~/components/input/icecast/common.js"; 9 + import { effect } from "~/common/signal.js"; 10 + import foundation from "~/common/foundation.js"; 11 + 12 + import { setup } from "~/facets/connect/common.js"; 13 + 14 + /** 15 + * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 16 + */ 17 + 18 + document.title = "Connect Icecast | Diffuse"; 19 + 20 + //////////////////////////////////////////// 21 + // SETUP 22 + //////////////////////////////////////////// 23 + 24 + const [inputConfigurator, outputOrchestrator, sourcesOrchestrator] = 25 + await Promise.all([ 26 + foundation.configurator.input(), 27 + foundation.orchestrator.output(), 28 + foundation.orchestrator.sources(), 29 + ]); 30 + 31 + await Promise.all([ 32 + customElements.whenDefined(inputConfigurator.localName), 33 + customElements.whenDefined(outputOrchestrator.localName), 34 + customElements.whenDefined(sourcesOrchestrator.localName), 35 + ]); 36 + 37 + //////////////////////////////////////////// 38 + // UI 39 + //////////////////////////////////////////// 40 + 41 + const { setItems, setError } = setup({ 42 + title: "Icecast", 43 + hasOutput: false, 44 + 45 + description: html` 46 + <p>Add an Icecast stream as audio input.</p> 47 + `, 48 + 49 + formFields: html` 50 + <wa-input 51 + id="icecast-url" 52 + label="Stream URL" 53 + type="url" 54 + placeholder="https://example.com/stream" 55 + required 56 + ></wa-input> 57 + `, 58 + 59 + onSubmit: () => addStream(), 60 + }); 61 + 62 + const urlInput = 63 + /** @type {WaInput} */ (document.querySelector("#icecast-url")); 64 + 65 + //////////////////////////////////////////// 66 + // REACTIVE LIST 67 + //////////////////////////////////////////// 68 + 69 + effect(() => { 70 + const inputSources = sourcesOrchestrator.sources()[SCHEME] ?? []; 71 + 72 + setItems( 73 + inputSources.map((source) => { 74 + const parsed = parseURI(source.uri); 75 + return { 76 + name: parsed?.streamUrl ?? source.uri, 77 + detail: parsed?.host ?? "", 78 + isInput: true, 79 + isOutput: false, 80 + isSelectedOutput: false, 81 + onRemove: () => removeStream(source.uri), 82 + }; 83 + }), 84 + ); 85 + }); 86 + 87 + //////////////////////////////////////////// 88 + // ACTIONS 89 + //////////////////////////////////////////// 90 + 91 + /** @param {string} uri */ 92 + async function removeStream(uri) { 93 + setError(null); 94 + try { 95 + const tracks = await Output.data(outputOrchestrator.tracks); 96 + const detachedTracks = await inputConfigurator.detach({ 97 + fileUriOrScheme: uri, 98 + tracks, 99 + }); 100 + 101 + if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks); 102 + } catch (err) { 103 + setError(err instanceof Error ? err.message : "Failed to remove stream"); 104 + } 105 + } 106 + 107 + async function addStream() { 108 + const rawUrl = urlInput.value?.trim(); 109 + if (!rawUrl) return; 110 + 111 + const uri = buildURI(rawUrl); 112 + const now = new Date().toISOString(); 113 + const tracksCol = outputOrchestrator.tracks.collection(); 114 + const existingTracks = tracksCol.state === "loaded" ? tracksCol.data : []; 115 + 116 + await outputOrchestrator.tracks.save([ 117 + ...existingTracks, 118 + { 119 + $type: "sh.diffuse.output.track", 120 + id: TID.now(), 121 + createdAt: now, 122 + updatedAt: now, 123 + kind: "placeholder", 124 + uri, 125 + }, 126 + ]); 127 + }
+15
src/facets/connect/local/index.html
··· 1 + <style> 2 + @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 + @import "./facets/connect/common.css" layer(connect); 4 + 5 + @layer base, diffuse, wa; 6 + </style> 7 + 8 + <main class="wa-theme-default"></main> 9 + 10 + <script type="module" src="facets/connect/local/index.inline.js"></script> 11 + 12 + <script type="module"> 13 + await customElements.whenDefined("wa-card"); 14 + document.querySelector("main")?.classList.add("has-loaded"); 15 + </script>
+182
src/facets/connect/local/index.inline.js
··· 1 + import * as TID from "@atcute/tid"; 2 + import { html } from "lit-html"; 3 + 4 + import * as Output from "~/common/output.js"; 5 + import { SCHEME } from "~/components/input/local/constants.js"; 6 + import { isSupported } from "~/components/input/local/common.js"; 7 + import { effect } from "~/common/signal.js"; 8 + import foundation from "~/common/foundation.js"; 9 + 10 + import { setup } from "~/facets/connect/common.js"; 11 + 12 + /** 13 + * @import {Track} from "~/definitions/types.d.ts" 14 + */ 15 + 16 + document.title = "Connect Local | Diffuse"; 17 + 18 + //////////////////////////////////////////// 19 + // SETUP 20 + //////////////////////////////////////////// 21 + 22 + const [inputConfigurator, outputOrchestrator, sourcesOrchestrator] = 23 + await Promise.all([ 24 + foundation.configurator.input(), 25 + foundation.orchestrator.output(), 26 + foundation.orchestrator.sources(), 27 + ]); 28 + 29 + await Promise.all([ 30 + customElements.whenDefined(inputConfigurator.localName), 31 + customElements.whenDefined(outputOrchestrator.localName), 32 + customElements.whenDefined(sourcesOrchestrator.localName), 33 + ]); 34 + 35 + const localInput = 36 + /** @type {import("~/components/input/local/element.js").CLASS} */ (inputConfigurator 37 + .inputs?.()[SCHEME]); 38 + 39 + //////////////////////////////////////////// 40 + // UI 41 + //////////////////////////////////////////// 42 + 43 + const supported = isSupported(); 44 + 45 + const { setItems, setError } = setup({ 46 + title: "Local files", 47 + hasInput: false, 48 + hasOutput: false, 49 + 50 + description: html` 51 + <p>Add local directories or files as audio input.</p> 52 + ${supported 53 + ? html` 54 + <div class="button-row"> 55 + <wa-button id="local-add-dir-btn" variant="neutral" appearance="filled"> 56 + <wa-icon slot="start" library="phosphor/fill" name="folder-open"></wa-icon> 57 + Add directory 58 + </wa-button> 59 + <wa-button id="local-add-files-btn" variant="neutral" appearance="filled"> 60 + <wa-icon slot="start" library="phosphor/fill" name="music-notes"></wa-icon> 61 + Add files 62 + </wa-button> 63 + </div> 64 + ` 65 + : html` 66 + <wa-callout variant="warning"> 67 + Your browser does not support the File System Access API. Use a Chromium-based 68 + browser to add local files. 69 + </wa-callout> 70 + `} 71 + `, 72 + 73 + formFields: html` 74 + 75 + `, 76 + onSubmit: async () => {}, 77 + }); 78 + 79 + document 80 + .querySelector("#local-add-dir-btn") 81 + ?.addEventListener("click", () => addDirectory()); 82 + 83 + document 84 + .querySelector("#local-add-files-btn") 85 + ?.addEventListener("click", () => addFiles()); 86 + 87 + //////////////////////////////////////////// 88 + // REACTIVE LIST 89 + //////////////////////////////////////////// 90 + 91 + effect(() => { 92 + const tracksCol = outputOrchestrator.tracks.collection(); 93 + const tracks = tracksCol.state === "loaded" ? tracksCol.data : []; 94 + const entries = localInput?.sources(tracks) ?? []; 95 + 96 + setItems( 97 + entries.map(({ label, uri }) => ({ 98 + name: label, 99 + detail: "local", 100 + isInput: true, 101 + isOutput: false, 102 + isSelectedOutput: false, 103 + onRemove: () => removeEntry(uri), 104 + })), 105 + ); 106 + }); 107 + 108 + //////////////////////////////////////////// 109 + // ACTIONS 110 + //////////////////////////////////////////// 111 + 112 + /** @param {string} uri */ 113 + async function removeEntry(uri) { 114 + setError(null); 115 + try { 116 + const tracks = await Output.data(outputOrchestrator.tracks); 117 + const detachedTracks = await inputConfigurator.detach({ 118 + fileUriOrScheme: uri, 119 + tracks, 120 + }); 121 + 122 + if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks); 123 + } catch (err) { 124 + setError(err instanceof Error ? err.message : "Failed to remove entry"); 125 + } 126 + } 127 + 128 + async function addDirectory() { 129 + setError(null); 130 + try { 131 + const uri = await localInput.addDirectory(); 132 + const now = new Date().toISOString(); 133 + const tracksCol = outputOrchestrator.tracks.collection(); 134 + const existingTracks = tracksCol.state === "loaded" ? tracksCol.data : []; 135 + 136 + await outputOrchestrator.tracks.save([ 137 + ...existingTracks, 138 + { 139 + $type: "sh.diffuse.output.track", 140 + id: TID.now(), 141 + createdAt: now, 142 + updatedAt: now, 143 + kind: "placeholder", 144 + uri, 145 + }, 146 + ]); 147 + } catch (err) { 148 + if (err instanceof Error && err.name !== "AbortError") { 149 + setError(err.message); 150 + } 151 + } 152 + } 153 + 154 + async function addFiles() { 155 + setError(null); 156 + try { 157 + const uris = await localInput.addFiles(); 158 + const now = new Date().toISOString(); 159 + const tracksCol = outputOrchestrator.tracks.collection(); 160 + const existingTracks = tracksCol.state === "loaded" ? tracksCol.data : []; 161 + await outputOrchestrator.tracks.save([ 162 + ...existingTracks, 163 + ...uris.map((uri) => { 164 + /** @type {Track} */ 165 + const track = { 166 + $type: "sh.diffuse.output.track", 167 + id: TID.now(), 168 + createdAt: now, 169 + updatedAt: now, 170 + kind: "placeholder", 171 + uri, 172 + }; 173 + 174 + return track; 175 + }), 176 + ]); 177 + } catch (err) { 178 + if (err instanceof Error && err.name !== "AbortError") { 179 + setError(err.message); 180 + } 181 + } 182 + }