191 lines
6.6 KiB
TypeScript
191 lines
6.6 KiB
TypeScript
import { useState } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { api, type Library, type AuthUser } from "../api/client";
|
|
import { useAuth } from "../auth/useAuth";
|
|
|
|
function LibraryRow({ lib, onRemove }: { lib: Library; onRemove: (id: number) => void }) {
|
|
const qc = useQueryClient();
|
|
const { data } = useQuery({
|
|
queryKey: ["scan-status", lib.id],
|
|
queryFn: () => api.libraries.scanStatus(lib.id),
|
|
refetchInterval: (query) => (query.state.data?.scanning ? 2000 : false),
|
|
});
|
|
|
|
const scanning = data?.scanning ?? false;
|
|
|
|
const rescanMutation = useMutation({
|
|
mutationFn: () => api.libraries.rescan(lib.id),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["scan-status", lib.id] }),
|
|
});
|
|
|
|
return (
|
|
<li style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", borderBottom: "1px solid var(--border-subtle)" }}>
|
|
<div>
|
|
<strong style={{ color: "var(--text)" }}>{lib.name}</strong>
|
|
{scanning && (
|
|
<span style={{ marginLeft: 8, fontSize: 12, color: "var(--accent)" }}>
|
|
Scanning…
|
|
</span>
|
|
)}
|
|
<div style={{ fontSize: 12, color: "var(--text-secondary)" }}>{lib.path}</div>
|
|
</div>
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
<button
|
|
onClick={() => rescanMutation.mutate()}
|
|
disabled={scanning}
|
|
style={{ color: scanning ? "var(--text-muted)" : "var(--accent)", background: "transparent", border: "none" }}
|
|
>
|
|
Rescan
|
|
</button>
|
|
<button
|
|
onClick={() => onRemove(lib.id)}
|
|
disabled={scanning}
|
|
style={{ color: scanning ? "var(--text-muted)" : "var(--danger)", background: "transparent", border: "none" }}
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
function UserManagement() {
|
|
const qc = useQueryClient();
|
|
const { user: currentUser } = useAuth();
|
|
const { data: users = [] } = useQuery<AuthUser[]>({
|
|
queryKey: ["users"],
|
|
queryFn: api.auth.listUsers,
|
|
});
|
|
|
|
const [newUsername, setNewUsername] = useState("");
|
|
const [newPassword, setNewPassword] = useState("");
|
|
const [userError, setUserError] = useState("");
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: () => api.auth.createUser(newUsername, newPassword),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ["users"] });
|
|
setNewUsername("");
|
|
setNewPassword("");
|
|
setUserError("");
|
|
},
|
|
onError: (e: Error) => setUserError(e.message),
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: number) => api.auth.deleteUser(id),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }),
|
|
onError: (e: Error) => setUserError(e.message),
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
<h2 style={{ color: "var(--text)", marginTop: 32 }}>Users</h2>
|
|
|
|
<form
|
|
onSubmit={(e) => { e.preventDefault(); createMutation.mutate(); }}
|
|
style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 24 }}
|
|
>
|
|
<input
|
|
placeholder="Username"
|
|
value={newUsername}
|
|
onChange={(e) => setNewUsername(e.target.value)}
|
|
required
|
|
autoComplete="off"
|
|
/>
|
|
<input
|
|
type="password"
|
|
placeholder="Password"
|
|
value={newPassword}
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
|
required
|
|
autoComplete="new-password"
|
|
/>
|
|
{userError && <p style={{ color: "var(--danger)", margin: 0, fontSize: 13 }}>{userError}</p>}
|
|
<button type="submit" disabled={createMutation.isPending} style={{ background: "var(--accent)", color: "#fff", border: "none" }}>
|
|
{createMutation.isPending ? "Creating…" : "Add User"}
|
|
</button>
|
|
</form>
|
|
|
|
<ul style={{ listStyle: "none", padding: 0 }}>
|
|
{users.map((u) => (
|
|
<li key={u.id} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", borderBottom: "1px solid var(--border-subtle)" }}>
|
|
<div>
|
|
<strong style={{ color: "var(--text)" }}>{u.username}</strong>
|
|
{u.is_admin && (
|
|
<span style={{ marginLeft: 8, fontSize: 11, color: "var(--accent)", fontWeight: 600 }}>
|
|
ADMIN
|
|
</span>
|
|
)}
|
|
</div>
|
|
{u.id !== currentUser?.id && (
|
|
<button
|
|
onClick={() => deleteMutation.mutate(u.id)}
|
|
disabled={deleteMutation.isPending}
|
|
style={{ color: "var(--danger)", background: "transparent", border: "none" }}
|
|
>
|
|
Remove
|
|
</button>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function SettingsPage() {
|
|
const qc = useQueryClient();
|
|
const { data: libraries = [] } = useQuery<Library[]>({
|
|
queryKey: ["libraries"],
|
|
queryFn: api.libraries.list,
|
|
});
|
|
|
|
const [name, setName] = useState("");
|
|
const [path, setPath] = useState("");
|
|
const [error, setError] = useState("");
|
|
|
|
const addMutation = useMutation({
|
|
mutationFn: () => api.libraries.create(name, path),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ["libraries"] });
|
|
setName("");
|
|
setPath("");
|
|
setError("");
|
|
},
|
|
onError: (e: Error) => setError(e.message),
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: number) => api.libraries.delete(id),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["libraries"] }),
|
|
});
|
|
|
|
return (
|
|
<div style={{ padding: "2rem", maxWidth: 600 }}>
|
|
<h1 style={{ color: "var(--text)" }}>Settings</h1>
|
|
<h2 style={{ color: "var(--text)" }}>Libraries</h2>
|
|
|
|
<form
|
|
onSubmit={(e) => { e.preventDefault(); addMutation.mutate(); }}
|
|
style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 24 }}
|
|
>
|
|
<input placeholder="Library name" value={name} onChange={(e) => setName(e.target.value)} required />
|
|
<input placeholder="/path/to/directory" value={path} onChange={(e) => setPath(e.target.value)} required />
|
|
{error && <p style={{ color: "var(--danger)", margin: 0 }}>{error}</p>}
|
|
<button type="submit" disabled={addMutation.isPending} style={{ background: "var(--accent)", color: "#fff", border: "none" }}>
|
|
{addMutation.isPending ? "Adding…" : "Add Library"}
|
|
</button>
|
|
</form>
|
|
|
|
<ul style={{ listStyle: "none", padding: 0 }}>
|
|
{libraries.map((lib) => (
|
|
<LibraryRow key={lib.id} lib={lib} onRemove={(id) => deleteMutation.mutate(id)} />
|
|
))}
|
|
</ul>
|
|
|
|
<UserManagement />
|
|
</div>
|
|
);
|
|
}
|