refactor(hooks): move cross-cutting hooks into lib/hooks

The five residual src/hooks/ hooks (useConnectionStatus, useCardGridMetrics,
useTracklistColumns, useNavidromeAdminRole, useAnalysisPerfListener) are all
domain-agnostic — none imports @/features — so they join the other shared hooks
in src/lib/hooks/ (with their two colocated tests). Consumers repointed;
relative-up imports in the moved files become the @/ alias. src/hooks/ is removed.

Pure move + import updates, no behavior change. The layering baseline gains five
lib->store entries (the three hooks that read authStore/devOfflineBrowseStore are
only now caught under lib/) — same documented debt class as the api move.
This commit is contained in:
Psychotoxical
2026-07-01 14:38:23 +02:00
parent 25055dc79a
commit f2cf8a08df
38 changed files with 120 additions and 35 deletions
+84
View File
@@ -0,0 +1,84 @@
import { useEffect, useState } from 'react';
import { ndLogin } from '@/lib/api/navidromeAdmin';
import { useAuthStore } from '@/store/authStore';
import { isNavidromeServer } from '@/lib/server/subsonicServerIdentity';
export type NavidromeAdminRole = 'idle' | 'checking' | 'admin' | 'user' | 'na' | 'error';
/**
* Navidrome ≥ 0.62 restricts internet-radio management (create/update/delete) to
* admins (GHSA-jw24-qqrj-633c). Block those actions only for a *confirmed*
* standard Navidrome user; everything else — admin, non-Navidrome servers
* (`'na'`), and transient/unknown states — stays allowed, with the server as the
* final authority. Non-Navidrome servers never carried this restriction.
*/
export function canManageNavidromeRadio(role: NavidromeAdminRole): boolean {
return role !== 'user';
}
function normalizeServerUrl(url: string): string {
const withScheme = url.startsWith('http') ? url : `http://${url}`;
return withScheme.replace(/\/$/, '');
}
/**
* Probes Navidrome native login for the active server to learn whether the
* current Subsonic credentials belong to an admin account.
*/
export function useNavidromeAdminRole(): NavidromeAdminRole {
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
const activeServerId = useAuthStore(s => s.activeServerId);
const server = useAuthStore(s => s.servers.find(srv => srv.id === s.activeServerId));
const identity = useAuthStore(s =>
activeServerId ? s.subsonicServerIdentityByServer[activeServerId] : undefined,
);
const [role, setRole] = useState<NavidromeAdminRole>('idle');
useEffect(() => {
if (!isLoggedIn || !server) {
// React Compiler set-state-in-effect rule: local state synced with store/prop inputs when the effects dependencies change.
// eslint-disable-next-line react-hooks/set-state-in-effect
setRole('na');
return;
}
if (!identity) {
setRole('checking');
return;
}
if (!isNavidromeServer(identity)) {
setRole('na');
return;
}
let cancelled = false;
setRole('checking');
const serverUrl = normalizeServerUrl(server.url);
ndLogin(serverUrl, server.username, server.password)
.then(res => {
if (cancelled) return;
setRole(res.isAdmin ? 'admin' : 'user');
})
.catch(() => {
if (!cancelled) setRole('error');
});
return () => {
cancelled = true;
};
// Keyed on the server's and identity's primitive fields; depending on the
// `server` / `identity` objects would re-probe the admin role on every render
// when their identities change but their fields do not.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isLoggedIn,
activeServerId,
server?.id,
server?.url,
server?.username,
server?.password,
identity?.type,
identity?.serverVersion,
]);
return role;
}