diff --git a/src/facets/connect/common.css b/src/facets/connect/common.css
--- a/src/facets/connect/common.css
+++ b/src/facets/connect/common.css
@@ -45,6 +45,63 @@
gap: var(--space-3xs);
}
+.connect-item--disabled .connect-item__info {
+ opacity: 0.2;
+}
+
+.dropdown {
+ background: oklch(from var(--bg-color) calc(l + 0.2) c h);
+ border: 0;
+ border-radius: var(--radius-md);
+ box-shadow: var(--box-shadow-xl);
+ color: var(--text-color);
+ font-size: var(--fs-sm);
+ margin: 0;
+ margin-top: var(--space-3xs);
+ padding: 0;
+ position: fixed;
+ position-area: bottom span-left;
+ text-align: left;
+
+ @media (prefers-color-scheme: dark) {
+ background: oklch(from var(--bg-color) calc(l - 0.05) c h);
+ }
+
+ &::backdrop {
+ background: transparent;
+ }
+
+ & > button {
+ align-items: center;
+ background: none;
+ border: 0;
+ border-radius: 0;
+ color: inherit;
+ cursor: pointer;
+ display: flex;
+ font-family: inherit;
+ font-size: inherit;
+ font-weight: inherit;
+ gap: var(--space-xs);
+ min-width: var(--space-3xl);
+ padding: var(--space-xs) var(--space-sm);
+ text-align: left;
+ width: 100%;
+
+ & > * {
+ pointer-events: none;
+ }
+ }
+
+ & > button:not(:last-child) {
+ border-bottom: 1px solid var(--border-color);
+ }
+
+ i {
+ opacity: 0.4;
+ }
+}
+
.dropzone {
align-items: center;
border: 2px dashed var(--border-color);
diff --git a/src/facets/connect/common.js b/src/facets/connect/common.js
--- a/src/facets/connect/common.js
+++ b/src/facets/connect/common.js
@@ -5,7 +5,7 @@
*/
/**
- * @typedef {{ name: string; detail: string; isInput: boolean; isOutput: boolean; isSelectedOutput: boolean; onRemove: () => void }} ConnectItem
+ * @typedef {{ name: string; detail: string; isInput: boolean; isOutput: boolean; isSelectedOutput: boolean; isDisabled?: boolean; onRemove: () => void; onToggleDisabled?: () => void }} ConnectItem
*/
/**
@@ -203,9 +203,9 @@
litRender(
html`
${items.map(
- ({ name, detail, isInput, isOutput, isSelectedOutput, onRemove }) =>
+ ({ name, detail, isInput, isOutput, isSelectedOutput, isDisabled, onRemove, onToggleDisabled }, index) =>
html`
-
+
${name}
${detail}
@@ -220,11 +220,35 @@
+
`,
)}
diff --git a/src/components/orchestrator/sources/element.js b/src/components/orchestrator/sources/element.js
--- a/src/components/orchestrator/sources/element.js
+++ b/src/components/orchestrator/sources/element.js
@@ -22,14 +22,29 @@
// SIGNALS
#sources = signal(/** @type {{ [scheme: string]: Source[] }} */ ({}));
+ #disabled = signal(/** @type {string[]} */ ([]));
// STATE
sources = this.#sources.get;
+ disabled = this.#disabled.get;
#output = signal(/** @type {OutputElement | null} */ (null));
// METHODS
+
+ /**
+ * Returns whether the given source URI is disabled.
+ * Strips query params before comparing, matching how {@link toggle} stores keys.
+ *
+ * @param {string} uri
+ * @returns {boolean}
+ */
+ isDisabled(uri) {
+ const q = uri.indexOf("?");
+ const key = q === -1 ? uri : uri.slice(0, q);
+ return this.#disabled.get().includes(key);
+ }
/**
* @param {string} uri
@@ -104,6 +119,20 @@
// Signals
this.#output.value = output;
+
+ // Effects
+ this.effect(() => {
+ const col = output.settings.collection();
+ if (col.state !== "loaded") { this.#disabled.value = []; return; }
+ const setting = col.data.find((s) => s.key === DISABLED_KEY);
+ if (!setting) { this.#disabled.value = []; return; }
+ try {
+ const parsed = JSON.parse(setting.value);
+ this.#disabled.value = Array.isArray(parsed) ? parsed : [];
+ } catch {
+ this.#disabled.value = [];
+ }
+ });
// Single input mode + dependencies
const singleInputMode = !!input.SCHEME;
diff --git a/src/facets/connect/https/index.inline.js b/src/facets/connect/https/index.inline.js
--- a/src/facets/connect/https/index.inline.js
+++ b/src/facets/connect/https/index.inline.js
@@ -65,7 +65,9 @@
isInput: true,
isOutput: false,
isSelectedOutput: false,
+ isDisabled: sourcesOrchestrator.isDisabled(source.uri),
onRemove: () => removeUrl(source.uri),
+ onToggleDisabled: () => sourcesOrchestrator.toggle(source.uri),
};
}),
);
diff --git a/src/facets/connect/icecast/index.inline.js b/src/facets/connect/icecast/index.inline.js
--- a/src/facets/connect/icecast/index.inline.js
+++ b/src/facets/connect/icecast/index.inline.js
@@ -66,7 +66,9 @@
isInput: true,
isOutput: false,
isSelectedOutput: false,
+ isDisabled: sourcesOrchestrator.isDisabled(source.uri),
onRemove: () => removeStream(source.uri),
+ onToggleDisabled: () => sourcesOrchestrator.toggle(source.uri),
};
}),
);
diff --git a/src/facets/connect/local/index.inline.js b/src/facets/connect/local/index.inline.js
--- a/src/facets/connect/local/index.inline.js
+++ b/src/facets/connect/local/index.inline.js
@@ -172,7 +172,9 @@
isInput: true,
isOutput: false,
isSelectedOutput: false,
+ isDisabled: sourcesOrchestrator.isDisabled(uri),
onRemove: () => removeEntry(uri),
+ onToggleDisabled: () => sourcesOrchestrator.toggle(uri),
})),
);
});
diff --git a/src/facets/connect/opensubsonic/index.inline.js b/src/facets/connect/opensubsonic/index.inline.js
--- a/src/facets/connect/opensubsonic/index.inline.js
+++ b/src/facets/connect/opensubsonic/index.inline.js
@@ -104,7 +104,9 @@
isInput: true,
isOutput: false,
isSelectedOutput: false,
+ isDisabled: sourcesOrchestrator.isDisabled(uri),
onRemove: () => removeServer(uri),
+ onToggleDisabled: () => sourcesOrchestrator.toggle(uri),
})),
);
});
diff --git a/src/facets/connect/s3/index.inline.js b/src/facets/connect/s3/index.inline.js
--- a/src/facets/connect/s3/index.inline.js
+++ b/src/facets/connect/s3/index.inline.js
@@ -144,7 +144,9 @@
isInput,
isOutput,
isSelectedOutput: isOutput && isSelectedOutput,
+ isDisabled: uri ? sourcesOrchestrator.isDisabled(uri) : false,
onRemove: () => removeBucket(uri, isOutput),
+ onToggleDisabled: uri ? () => sourcesOrchestrator.toggle(uri) : undefined,
})),
);
});
diff --git a/src/facets/data/sources/index.inline.js b/src/facets/data/sources/index.inline.js
--- a/src/facets/data/sources/index.inline.js
+++ b/src/facets/data/sources/index.inline.js
@@ -2,9 +2,7 @@
import * as Output from "~/common/output.js";
import foundation from "~/common/foundation.js";
-import { computed, effect } from "~/common/signal.js";
-
-import { DISABLED_KEY as DISABLED_SOURCES_KEY } from "~/components/orchestrator/sources/constants.js";
+import { effect } from "~/common/signal.js";
import { SCHEME as SCHEME_EPHEMERAL_CACHE } from "~/components/input/ephemeral-cache/constants.js";
import { SCHEME as SCHEME_HTTPS } from "~/components/input/https/constants.js";
@@ -42,32 +40,6 @@
customElements.whenDefined(outputOrchestrator.localName),
]);
-const disabledSources = computed(() => {
- const col = outputOrchestrator.settings.collection();
- if (col.state !== "loaded") return /** @type {string[]} */ ([]);
-
- const setting = col.data.find((s) => s.key === DISABLED_SOURCES_KEY);
- if (!setting) return /** @type {string[]} */ ([]);
-
- try {
- const parsed = JSON.parse(setting.value);
- return Array.isArray(parsed) ? /** @type {string[]} */ (parsed) : [];
- } catch {
- return /** @type {string[]} */ ([]);
- }
-});
-
-/**
- * Returns the part of a source URI suitable for `startsWith` matching against
- * track URIs. Some inputs (e.g. OpenSubsonic) include query params in the
- * source URI that won't appear at the start of a track URI, so we strip them.
- *
- * @param {string} uri
- */
-function trackPrefix(uri) {
- const q = uri.indexOf("?");
- return q === -1 ? uri : uri.slice(0, q);
-}
////////////////////////////////////////////
// UI
@@ -78,9 +50,11 @@
const empty =
/** @type {HTMLElement} */ (document.querySelector("#sources-empty"));
+/** @param {string} uri */
+const trackPrefix = (uri) => { const q = uri.indexOf("?"); return q === -1 ? uri : uri.slice(0, q); };
+
effect(() => {
const sourcesRecord = sourcesOrchestrator.sources();
- const disabled = disabledSources();
const tracksCol = outputOrchestrator.tracks.collection();
const tracks = tracksCol.state === "loaded" ? tracksCol.data : [];
@@ -97,9 +71,9 @@
${entries.map(([scheme, sources]) => {
if (scheme === SCHEME_EPHEMERAL_CACHE) {
const uri = `${SCHEME_EPHEMERAL_CACHE}://`;
- const isDisabled = disabled.includes(uri);
+ const isDisabled = sourcesOrchestrator.isDisabled(uri);
const trackCount = tracks.filter((t) =>
- t.uri.startsWith(trackPrefix(uri))
+ t.uri.startsWith(uri)
).length;
return html`
${SCHEME_NAMES[scheme] ?? scheme}
@@ -136,7 +110,7 @@
return html`
${SCHEME_NAMES[scheme] ?? scheme}
${sources.map(({ label, uri }) => {
- const isDisabled = disabled.includes(trackPrefix(uri));
+ const isDisabled = sourcesOrchestrator.isDisabled(uri);
const trackCount = tracks.filter((t) =>
t.uri.startsWith(trackPrefix(uri))
).length;