-
-
+
+ {isEdit && onDelete ? (
+
+ ) : (
+
+ )}
+
+
+
+
);
diff --git a/src/components/settings/ServerCapabilityHeaderBadge.tsx b/src/components/settings/ServerCapabilityHeaderBadge.tsx
new file mode 100644
index 00000000..5c344814
--- /dev/null
+++ b/src/components/settings/ServerCapabilityHeaderBadge.tsx
@@ -0,0 +1,25 @@
+import { useTranslation } from 'react-i18next';
+import { getCapabilityDefinition } from '../../serverCapabilities/catalog';
+import { isFeatureActiveForServer, resolveFeatureForServer } from '../../serverCapabilities/storeView';
+
+/** Inline badge for auto-managed server capabilities (Settings → Servers header row). */
+export function ServerCapabilityHeaderBadge({
+ serverId,
+ feature,
+}: {
+ serverId: string;
+ feature: string;
+}) {
+ const { t } = useTranslation();
+ const resolved = resolveFeatureForServer(serverId, feature);
+ if (!resolved || resolved.activation !== 'auto') return null;
+ if (!isFeatureActiveForServer(serverId, feature)) return null;
+
+ const def = getCapabilityDefinition(feature);
+ const labelKey = def?.badgeLabelKey ?? def?.labelKey;
+ if (!labelKey) return null;
+
+ return (
+
{t(labelKey)}
+ );
+}
diff --git a/src/components/settings/ServersTab.tsx b/src/components/settings/ServersTab.tsx
index bf2a4e72..e271e3d8 100644
--- a/src/components/settings/ServersTab.tsx
+++ b/src/components/settings/ServersTab.tsx
@@ -2,9 +2,10 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { open as openUrl } from '@tauri-apps/plugin-shell';
-import { AlertTriangle, CheckCircle2, Globe, Lock, LogOut, Pencil, Plus, Power, Server, Sparkles, Trash2, User, Wifi, WifiOff } from 'lucide-react';
+import { AlertTriangle, CheckCircle2, Info, Lock, LogOut, Pencil, Plus, Power, Server, Sparkles, Wifi, WifiOff } from 'lucide-react';
import { useAuthStore } from '../../store/authStore';
-import { formatServerSoftware } from '../../utils/server/subsonicServerIdentity';
+import { formatServerSoftware, isNavidromeAudiomuseSoftwareEligible, type InstantMixProbeResult, type SubsonicServerIdentity } from '../../utils/server/subsonicServerIdentity';
+import { buildCapabilityContext } from '../../serverCapabilities/context';
import { useLibraryIndexStore } from '../../store/libraryIndexStore';
import { libraryDeleteServerData, librarySyncClearSession } from '../../api/library';
import { bootstrapIndexedServer } from '../../utils/library/librarySession';
@@ -25,36 +26,37 @@ import {
} from '../../utils/server/serverUrlRemigration';
import { useConfirmModalStore } from '../../store/confirmModalStore';
import { showToast } from '../../utils/ui/toast';
-import PerfProbeStatusBadge, { type PerfProbeBadgeTone } from '../sidebar/perfProbe/PerfProbeStatusBadge';
import { FEATURE_AUDIOMUSE_SIMILAR_TRACKS } from '../../serverCapabilities/catalog';
-import { resolveFeatureForServer } from '../../serverCapabilities/storeView';
-import type { CapabilityStatus, ResolvedCapability } from '../../serverCapabilities/types';
-import { serverListDisplayLabel } from '../../utils/server/serverDisplayName';
+import { isFeatureActiveForServer, resolveFeatureForServer } from '../../serverCapabilities/storeView';
+import type { ResolvedCapability } from '../../serverCapabilities/types';
+import { serverIdentityLabel, serverListDisplayLabel, serverSettingsEntryTitle } from '../../utils/server/serverDisplayName';
import { serverIndexKeyForProfile } from '../../utils/server/serverIndexKey';
import { switchActiveServer } from '../../utils/server/switchActiveServer';
import { AddServerForm } from './AddServerForm';
+import { ServerCapabilityHeaderBadge } from './ServerCapabilityHeaderBadge';
import { ServerGripHandle } from './ServerGripHandle';
+import { tooltipAttrs } from '../tooltipAttrs';
const AUDIOMUSE_NV_PLUGIN_URL = 'https://github.com/NeptuneHub/AudioMuse-AI-NV-plugin';
-function audiomuseProbeBadge(
- status: CapabilityStatus,
- t: (key: string) => string,
-): { tone: PerfProbeBadgeTone; label: string } {
- switch (status) {
- case 'present': return { tone: 'ok', label: t('settings.audiomuseStatusActive') };
- case 'absent': return { tone: 'muted', label: t('settings.audiomuseStatusNotDetected') };
- case 'error': return { tone: 'error', label: t('settings.audiomuseStatusProbeFailed') };
- default: return { tone: 'warn', label: t('settings.audiomuseStatusChecking') };
- }
-}
-
-/** Row visibility: hide only when a manual (legacy) strategy proves the feature absent. */
+/** Row visibility: same as main — hide only when manual strategy proves the feature absent. */
function showAudiomuseRow(resolved: ResolvedCapability | null): boolean {
if (!resolved || resolved.strategyId === null || resolved.status === 'ineligible') return false;
return !(resolved.activation === 'manual' && resolved.status === 'absent');
}
+/** Legacy Navidrome (< 0.62): manual toggle row below the card (not the auto header badge). */
+function showLegacyAudiomuseToggleRow(
+ identity: SubsonicServerIdentity | undefined,
+ instantMixProbe: InstantMixProbeResult | undefined,
+ resolved: ResolvedCapability | null,
+): boolean {
+ const ctx = buildCapabilityContext(identity);
+ if (ctx.isNavidrome && ctx.semverGte([0, 62, 0])) return false;
+ if (showAudiomuseRow(resolved)) return true;
+ return isNavidromeAudiomuseSoftwareEligible(identity) && instantMixProbe !== 'empty';
+}
+
type ServerDropTarget = { idx: number; before: boolean } | null;
export function ServersTab({
@@ -388,6 +390,10 @@ export function ServersTab({
editingServer={srv}
onSave={(data) => handleEditServer(srv.id, data)}
onCancel={() => setEditingServerId(null)}
+ onDelete={async () => {
+ await deleteServer(srv);
+ setEditingServerId(null);
+ }}
/>
);
}
@@ -396,6 +402,15 @@ export function ServersTab({
const isBefore = psyDragState.isDragging && serverDropTarget?.idx === srvIdx && serverDropTarget.before;
const isAfter = psyDragState.isDragging && serverDropTarget?.idx === srvIdx && !serverDropTarget.before;
const serverSoftware = formatServerSoftware(auth.subsonicServerIdentityByServer[srv.id]);
+ const serverIdentity = auth.subsonicServerIdentityByServer[srv.id];
+ const resolvedAudiomuse = resolveFeatureForServer(srv.id, FEATURE_AUDIOMUSE_SIMILAR_TRACKS);
+ const versionTooltip = serverSoftware ?? t('settings.serverVersionUnknown');
+ const audiomuseActive = isFeatureActiveForServer(srv.id, FEATURE_AUDIOMUSE_SIMILAR_TRACKS);
+ const showLegacyAudiomuseToggle = showLegacyAudiomuseToggleRow(
+ serverIdentity,
+ auth.instantMixProbeByServer[srv.id],
+ resolvedAudiomuse,
+ );
return (
-
-
{serverListDisplayLabel(srv, auth.servers)}
- {isActive && (
-
- {t('settings.serverActive')}
-
+
+
{serverSettingsEntryTitle(srv)}
+
+ {resolvedAudiomuse?.activation === 'auto' && audiomuseActive && auth.audiomuseNavidromeIssueByServer[srv.id] && (
+
)}
- {serverSoftware && (
-
-
- {serverSoftware}
-
- )}
- {srv.url.startsWith('https://') ? (
-
- ) : (
-
+ {srv.url.startsWith('https://') && (
+
)}
- {srv.url.replace(/^https?:\/\//, '')}
+ {serverIdentityLabel(srv)}
-
-
-
- {srv.username}
+
{status === 'ok' &&
}
{status === 'error' &&
}
{status === 'testing' &&
}
-
- {!isActive && (
-
- )}
+ {isActive ? (
+
+ {t('settings.serverActive')}
+
+ ) : (
+
+ )}
@@ -515,11 +527,8 @@ export function ServersTab({
onCancel={() => void librarySync.handleCancel()}
/>
{(() => {
- const resolved = resolveFeatureForServer(srv.id, FEATURE_AUDIOMUSE_SIMILAR_TRACKS);
- if (!showAudiomuseRow(resolved) || !resolved) return null;
- const autoManaged = resolved.activation === 'auto';
- const probeBadge = audiomuseProbeBadge(resolved.status, t);
- const audiomuseActive = !!auth.audiomuseNavidromeByServer[srv.id];
+ if (!showLegacyAudiomuseToggle) return null;
+ const audiomuseManualActive = !!auth.audiomuseNavidromeByServer[srv.id];
return (
{t('settings.audiomuseTitle')}
- {audiomuseActive && auth.audiomuseNavidromeIssueByServer[srv.id] && (
+ {audiomuseManualActive && auth.audiomuseNavidromeIssueByServer[srv.id] && (
)}
- {!autoManaged && (
-
- {
- e.preventDefault();
- void openUrl(AUDIOMUSE_NV_PLUGIN_URL);
- }}
- style={{ color: 'var(--accent)', textDecoration: 'underline' }}
- />
- ),
- }}
- />
-
- )}
+
+ {
+ e.preventDefault();
+ void openUrl(AUDIOMUSE_NV_PLUGIN_URL);
+ }}
+ style={{ color: 'var(--accent)', textDecoration: 'underline' }}
+ />
+ ),
+ }}
+ />
+
- {autoManaged ? (
-