From 3377ccde403a966c377ab4a227a3078dfad3a0cc Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sat, 21 Mar 2026 22:47:25 +0100 Subject: [PATCH] feat: add remaining connect facets --- src/_data/facets.json | 21 +++ src/common/webawesome/phosphor/_lib.js | 4 +- src/facets/connect/https/index.html | 15 ++ src/facets/connect/https/index.inline.js | 125 ++++++++++++++ src/facets/connect/icecast/index.html | 15 ++ src/facets/connect/icecast/index.inline.js | 127 ++++++++++++++ src/facets/connect/local/index.html | 15 ++ src/facets/connect/local/index.inline.js | 182 +++++++++++++++++++++ 8 files changed, 503 insertions(+), 1 deletion(-) create mode 100644 src/facets/connect/https/index.html create mode 100644 src/facets/connect/https/index.inline.js create mode 100644 src/facets/connect/icecast/index.html create mode 100644 src/facets/connect/icecast/index.inline.js create mode 100644 src/facets/connect/local/index.html create mode 100644 src/facets/connect/local/index.inline.js diff --git a/src/_data/facets.json b/src/_data/facets.json index e867fe60..42aa6ffa 100644 --- a/src/_data/facets.json +++ b/src/_data/facets.json @@ -36,6 +36,27 @@ "featured": true, "desc": "Use your AT Protocol identity for user-data storage." }, + { + "url": "facets/connect/https/index.html", + "title": "Connect / HTTPS", + "category": "Data", + "featured": true, + "desc": "Add HTTPS audio files as input." + }, + { + "url": "facets/connect/icecast/index.html", + "title": "Connect / Icecast", + "category": "Data", + "featured": true, + "desc": "Add an Icecast stream as audio input." + }, + { + "url": "facets/connect/local/index.html", + "title": "Connect / Local", + "category": "Data", + "featured": true, + "desc": "Add local directories or files as audio input." + }, { "url": "facets/connect/opensubsonic/index.html", "title": "Connect / OpenSubsonic", diff --git a/src/common/webawesome/phosphor/_lib.js b/src/common/webawesome/phosphor/_lib.js index bf33e442..db61945c 100644 --- a/src/common/webawesome/phosphor/_lib.js +++ b/src/common/webawesome/phosphor/_lib.js @@ -7,7 +7,9 @@ export function buildIconMap(selection) { for (const icon of selection.icons) { const { name } = icon.properties; // Strip weight suffix to get base name (e.g. "gear-bold" → "gear") - const baseName = name.replace(/-(?:bold|fill|light|thin|regular|duotone)$/, ""); + // Take only the primary name before any comma-separated aliases + const primaryName = name.split(",")[0].trim(); + const baseName = primaryName.replace(/-(?:bold|fill|light|thin|regular|duotone)$/, ""); map.set(baseName, { paths: icon.icon.paths, attrs: icon.icon.attrs }); } return map; diff --git a/src/facets/connect/https/index.html b/src/facets/connect/https/index.html new file mode 100644 index 00000000..39022ca4 --- /dev/null +++ b/src/facets/connect/https/index.html @@ -0,0 +1,15 @@ + + +
+ + + + diff --git a/src/facets/connect/https/index.inline.js b/src/facets/connect/https/index.inline.js new file mode 100644 index 00000000..0df47b6d --- /dev/null +++ b/src/facets/connect/https/index.inline.js @@ -0,0 +1,125 @@ +import "@awesome.me/webawesome/dist/components/input/input.js"; + +import * as TID from "@atcute/tid"; +import { html } from "lit-html"; + +import * as Output from "~/common/output.js"; +import { SCHEME } from "~/components/input/https/constants.js"; +import { parseURI } from "~/components/input/https/common.js"; +import { effect } from "~/common/signal.js"; +import foundation from "~/common/foundation.js"; + +import { setup } from "~/facets/connect/common.js"; + +/** + * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" + */ + +document.title = "Connect HTTPS | Diffuse"; + +//////////////////////////////////////////// +// SETUP +//////////////////////////////////////////// + +const [inputConfigurator, outputOrchestrator, sourcesOrchestrator] = + await Promise.all([ + foundation.configurator.input(), + foundation.orchestrator.output(), + foundation.orchestrator.sources(), + ]); + +await Promise.all([ + customElements.whenDefined(inputConfigurator.localName), + customElements.whenDefined(outputOrchestrator.localName), + customElements.whenDefined(sourcesOrchestrator.localName), +]); + +//////////////////////////////////////////// +// UI +//////////////////////////////////////////// + +const { setItems, setError } = setup({ + title: "HTTPS", + hasOutput: false, + + description: html` +

Add HTTPS urls as input.

+ `, + + formFields: html` + + `, + + onSubmit: () => addUrl(), +}); + +const urlInput = /** @type {WaInput} */ (document.querySelector("#https-url")); + +//////////////////////////////////////////// +// REACTIVE LIST +//////////////////////////////////////////// + +effect(() => { + const inputSources = sourcesOrchestrator.sources()[SCHEME] ?? []; + + setItems( + inputSources.map((source) => { + const parsed = parseURI(source.uri); + return { + name: source.uri, + detail: parsed?.host ?? "", + isInput: true, + isOutput: false, + isSelectedOutput: false, + onRemove: () => removeUrl(source.uri), + }; + }), + ); +}); + +//////////////////////////////////////////// +// ACTIONS +//////////////////////////////////////////// + +/** @param {string} uri */ +async function removeUrl(uri) { + setError(null); + try { + const tracks = await Output.data(outputOrchestrator.tracks); + const detachedTracks = await inputConfigurator.detach({ + fileUriOrScheme: uri, + tracks, + }); + + if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to remove URL"); + } +} + +async function addUrl() { + const url = urlInput.value?.trim(); + if (!url) return; + + const now = new Date().toISOString(); + const tracksCol = outputOrchestrator.tracks.collection(); + const existingTracks = tracksCol.state === "loaded" ? tracksCol.data : []; + + await outputOrchestrator.tracks.save([ + ...existingTracks, + { + $type: "sh.diffuse.output.track", + id: TID.now(), + createdAt: now, + updatedAt: now, + kind: "placeholder", + uri: url, + }, + ]); +} diff --git a/src/facets/connect/icecast/index.html b/src/facets/connect/icecast/index.html new file mode 100644 index 00000000..940821e6 --- /dev/null +++ b/src/facets/connect/icecast/index.html @@ -0,0 +1,15 @@ + + +
+ + + + diff --git a/src/facets/connect/icecast/index.inline.js b/src/facets/connect/icecast/index.inline.js new file mode 100644 index 00000000..a1fc5084 --- /dev/null +++ b/src/facets/connect/icecast/index.inline.js @@ -0,0 +1,127 @@ +import "@awesome.me/webawesome/dist/components/input/input.js"; + +import * as TID from "@atcute/tid"; +import { html } from "lit-html"; + +import * as Output from "~/common/output.js"; +import { SCHEME } from "~/components/input/icecast/constants.js"; +import { buildURI, parseURI } from "~/components/input/icecast/common.js"; +import { effect } from "~/common/signal.js"; +import foundation from "~/common/foundation.js"; + +import { setup } from "~/facets/connect/common.js"; + +/** + * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" + */ + +document.title = "Connect Icecast | Diffuse"; + +//////////////////////////////////////////// +// SETUP +//////////////////////////////////////////// + +const [inputConfigurator, outputOrchestrator, sourcesOrchestrator] = + await Promise.all([ + foundation.configurator.input(), + foundation.orchestrator.output(), + foundation.orchestrator.sources(), + ]); + +await Promise.all([ + customElements.whenDefined(inputConfigurator.localName), + customElements.whenDefined(outputOrchestrator.localName), + customElements.whenDefined(sourcesOrchestrator.localName), +]); + +//////////////////////////////////////////// +// UI +//////////////////////////////////////////// + +const { setItems, setError } = setup({ + title: "Icecast", + hasOutput: false, + + description: html` +

Add an Icecast stream as audio input.

+ `, + + formFields: html` + + `, + + onSubmit: () => addStream(), +}); + +const urlInput = + /** @type {WaInput} */ (document.querySelector("#icecast-url")); + +//////////////////////////////////////////// +// REACTIVE LIST +//////////////////////////////////////////// + +effect(() => { + const inputSources = sourcesOrchestrator.sources()[SCHEME] ?? []; + + setItems( + inputSources.map((source) => { + const parsed = parseURI(source.uri); + return { + name: parsed?.streamUrl ?? source.uri, + detail: parsed?.host ?? "", + isInput: true, + isOutput: false, + isSelectedOutput: false, + onRemove: () => removeStream(source.uri), + }; + }), + ); +}); + +//////////////////////////////////////////// +// ACTIONS +//////////////////////////////////////////// + +/** @param {string} uri */ +async function removeStream(uri) { + setError(null); + try { + const tracks = await Output.data(outputOrchestrator.tracks); + const detachedTracks = await inputConfigurator.detach({ + fileUriOrScheme: uri, + tracks, + }); + + if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to remove stream"); + } +} + +async function addStream() { + const rawUrl = urlInput.value?.trim(); + if (!rawUrl) return; + + const uri = buildURI(rawUrl); + const now = new Date().toISOString(); + const tracksCol = outputOrchestrator.tracks.collection(); + const existingTracks = tracksCol.state === "loaded" ? tracksCol.data : []; + + await outputOrchestrator.tracks.save([ + ...existingTracks, + { + $type: "sh.diffuse.output.track", + id: TID.now(), + createdAt: now, + updatedAt: now, + kind: "placeholder", + uri, + }, + ]); +} diff --git a/src/facets/connect/local/index.html b/src/facets/connect/local/index.html new file mode 100644 index 00000000..90120042 --- /dev/null +++ b/src/facets/connect/local/index.html @@ -0,0 +1,15 @@ + + +
+ + + + diff --git a/src/facets/connect/local/index.inline.js b/src/facets/connect/local/index.inline.js new file mode 100644 index 00000000..401b5329 --- /dev/null +++ b/src/facets/connect/local/index.inline.js @@ -0,0 +1,182 @@ +import * as TID from "@atcute/tid"; +import { html } from "lit-html"; + +import * as Output from "~/common/output.js"; +import { SCHEME } from "~/components/input/local/constants.js"; +import { isSupported } from "~/components/input/local/common.js"; +import { effect } from "~/common/signal.js"; +import foundation from "~/common/foundation.js"; + +import { setup } from "~/facets/connect/common.js"; + +/** + * @import {Track} from "~/definitions/types.d.ts" + */ + +document.title = "Connect Local | Diffuse"; + +//////////////////////////////////////////// +// SETUP +//////////////////////////////////////////// + +const [inputConfigurator, outputOrchestrator, sourcesOrchestrator] = + await Promise.all([ + foundation.configurator.input(), + foundation.orchestrator.output(), + foundation.orchestrator.sources(), + ]); + +await Promise.all([ + customElements.whenDefined(inputConfigurator.localName), + customElements.whenDefined(outputOrchestrator.localName), + customElements.whenDefined(sourcesOrchestrator.localName), +]); + +const localInput = + /** @type {import("~/components/input/local/element.js").CLASS} */ (inputConfigurator + .inputs?.()[SCHEME]); + +//////////////////////////////////////////// +// UI +//////////////////////////////////////////// + +const supported = isSupported(); + +const { setItems, setError } = setup({ + title: "Local files", + hasInput: false, + hasOutput: false, + + description: html` +

Add local directories or files as audio input.

+ ${supported + ? html` +
+ + + Add directory + + + + Add files + +
+ ` + : html` + + Your browser does not support the File System Access API. Use a Chromium-based + browser to add local files. + + `} + `, + + formFields: html` + + `, + onSubmit: async () => {}, +}); + +document + .querySelector("#local-add-dir-btn") + ?.addEventListener("click", () => addDirectory()); + +document + .querySelector("#local-add-files-btn") + ?.addEventListener("click", () => addFiles()); + +//////////////////////////////////////////// +// REACTIVE LIST +//////////////////////////////////////////// + +effect(() => { + const tracksCol = outputOrchestrator.tracks.collection(); + const tracks = tracksCol.state === "loaded" ? tracksCol.data : []; + const entries = localInput?.sources(tracks) ?? []; + + setItems( + entries.map(({ label, uri }) => ({ + name: label, + detail: "local", + isInput: true, + isOutput: false, + isSelectedOutput: false, + onRemove: () => removeEntry(uri), + })), + ); +}); + +//////////////////////////////////////////// +// ACTIONS +//////////////////////////////////////////// + +/** @param {string} uri */ +async function removeEntry(uri) { + setError(null); + try { + const tracks = await Output.data(outputOrchestrator.tracks); + const detachedTracks = await inputConfigurator.detach({ + fileUriOrScheme: uri, + tracks, + }); + + if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to remove entry"); + } +} + +async function addDirectory() { + setError(null); + try { + const uri = await localInput.addDirectory(); + const now = new Date().toISOString(); + const tracksCol = outputOrchestrator.tracks.collection(); + const existingTracks = tracksCol.state === "loaded" ? tracksCol.data : []; + + await outputOrchestrator.tracks.save([ + ...existingTracks, + { + $type: "sh.diffuse.output.track", + id: TID.now(), + createdAt: now, + updatedAt: now, + kind: "placeholder", + uri, + }, + ]); + } catch (err) { + if (err instanceof Error && err.name !== "AbortError") { + setError(err.message); + } + } +} + +async function addFiles() { + setError(null); + try { + const uris = await localInput.addFiles(); + const now = new Date().toISOString(); + const tracksCol = outputOrchestrator.tracks.collection(); + const existingTracks = tracksCol.state === "loaded" ? tracksCol.data : []; + await outputOrchestrator.tracks.save([ + ...existingTracks, + ...uris.map((uri) => { + /** @type {Track} */ + const track = { + $type: "sh.diffuse.output.track", + id: TID.now(), + createdAt: now, + updatedAt: now, + kind: "placeholder", + uri, + }; + + return track; + }), + ]); + } catch (err) { + if (err instanceof Error && err.name !== "AbortError") { + setError(err.message); + } + } +} -- 2.51.2