Files
psysonic/src/hooks/useConnectionStatus.ts
T
cucadmuh f6f76723d8 feat(servers/settings): magic string invites, Navidrome admin share, and add-user validation (#258)
* feat(servers): magic string invites and duplicate-aware labels

Add psysonic1- share payloads (URL, Subsonic credentials, optional server
name). Login and add-server forms decode on input and keep magic field
below standard fields. Navidrome User Management generates strings after
PUT password updates where required. Resolve duplicate display names as
username@host in chrome, settings, and login.

* feat(servers): tighten magic-string import and admin share flow

After a decoded magic string, username is read-only and the password field
shows a fixed-length mask so length is not inferred. Password reveal stays
disabled until saved-server quick connect (login) or reopening the add-
server form. Navidrome admin share UI persists the password via API before
copy and adds a plaintext-handling disclaimer. Locales updated.

* feat(settings): save new Navidrome user and copy magic string

Add a non-admin "Save and get magic string" flow: create the user, assign
libraries when needed, then encode credentials to the clipboard. Reuse
the plaintext-handling disclaimer without the password-update hint meant
for edits. Strings added for all locales.

* fix(settings): tighten Navidrome add-user validation and submit feedback

Require username, display name, and a non-empty password (trimmed) for new
users; gate Save and “save and get magic string” on explicit checks with
toast feedback. Show red borders only after a failed submit, and clear
them when the user fills the fields. When editing, keep an empty password
as “unchanged” and validate identity fields with a dedicated message.
Strings: userMgmtValidationMissingIdentity across locales.
2026-04-22 00:45:28 +02:00

95 lines
2.8 KiB
TypeScript

import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useAuthStore } from '../store/authStore';
import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../api/subsonic';
import { serverListDisplayLabel } from '../utils/serverDisplayName';
export type ConnectionStatus = 'connected' | 'disconnected' | 'checking';
export function isLanUrl(url: string): boolean {
try {
const hostname = new URL(url.startsWith('http') ? url : `http://${url}`).hostname;
return (
hostname === 'localhost' ||
hostname.endsWith('.local') ||
/^127\./.test(hostname) ||
/^10\./.test(hostname) ||
/^192\.168\./.test(hostname) ||
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname)
);
} catch {
return false;
}
}
export function useConnectionStatus() {
const [status, setStatus] = useState<ConnectionStatus>('checking');
const [isRetrying, setIsRetrying] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const check = useCallback(async () => {
const server = useAuthStore.getState().getActiveServer();
if (!server) {
setStatus('disconnected');
return;
}
if (!navigator.onLine) {
setStatus('disconnected');
return;
}
const ping = await pingWithCredentials(server.url, server.username, server.password);
if (ping.ok) {
const sid = useAuthStore.getState().activeServerId;
if (sid) {
const identity = {
type: ping.type,
serverVersion: ping.serverVersion,
openSubsonic: ping.openSubsonic,
};
useAuthStore.getState().setSubsonicServerIdentity(sid, identity);
scheduleInstantMixProbeForServer(sid, server.url, server.username, server.password, identity);
}
}
setStatus(ping.ok ? 'connected' : 'disconnected');
}, []);
const retry = useCallback(async () => {
setIsRetrying(true);
await check();
setIsRetrying(false);
}, [check]);
useEffect(() => {
check();
intervalRef.current = setInterval(check, 120_000);
const handleOnline = () => check();
const handleOffline = () => setStatus('disconnected');
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, [check]);
const server = useAuthStore(s => s.getActiveServer());
const servers = useAuthStore(s => s.servers);
const serverName = useMemo(
() => (server ? serverListDisplayLabel(server, servers) : ''),
[server, servers],
);
return {
status,
isRetrying,
retry,
isLan: server ? isLanUrl(server.url) : false,
serverName,
};
}