fixes
This commit is contained in:
@@ -1,13 +1,27 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { BrowserRouter, Routes, Route, NavLink } from "react-router-dom";
|
||||
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
|
||||
import { api, Library } from "./api/client";
|
||||
import { api, type Library } from "./api/client";
|
||||
import BrowserPage from "./pages/BrowserPage";
|
||||
import SettingsPage from "./pages/SettingsPage";
|
||||
import SearchPage from "./pages/SearchPage";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function Sidebar() {
|
||||
function useTheme() {
|
||||
const [dark, setDark] = useState(
|
||||
() => document.documentElement.getAttribute("data-theme") === "dark"
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
|
||||
localStorage.setItem("theme", dark ? "dark" : "light");
|
||||
}, [dark]);
|
||||
|
||||
return { dark, toggle: () => setDark((d) => !d) };
|
||||
}
|
||||
|
||||
function Sidebar({ onToggleTheme, dark }: { onToggleTheme: () => void; dark: boolean }) {
|
||||
const { data: libraries = [] } = useQuery<Library[]>({
|
||||
queryKey: ["libraries"],
|
||||
queryFn: api.libraries.list,
|
||||
@@ -18,20 +32,31 @@ function Sidebar() {
|
||||
padding: "6px 12px",
|
||||
textDecoration: "none",
|
||||
borderRadius: 4,
|
||||
color: isActive ? "#3b82f6" : "inherit",
|
||||
background: isActive ? "#eff6ff" : "transparent",
|
||||
color: isActive ? "var(--accent)" : "var(--text)",
|
||||
background: isActive ? "var(--accent-bg)" : "transparent",
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
});
|
||||
|
||||
return (
|
||||
<nav style={{ width: 220, borderRight: "1px solid #e5e7eb", padding: "1rem", display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 18, marginBottom: 12 }}>MediaLore</div>
|
||||
<nav style={{
|
||||
width: 220,
|
||||
borderRight: "1px solid var(--border)",
|
||||
background: "var(--bg)",
|
||||
padding: "1rem",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div style={{ fontWeight: 700, fontSize: 18, marginBottom: 12, color: "var(--text)" }}>
|
||||
MediaLore
|
||||
</div>
|
||||
|
||||
<NavLink to="/search" style={linkStyle}>Search</NavLink>
|
||||
|
||||
{libraries.length > 0 && (
|
||||
<>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", color: "#9ca3af", margin: "12px 0 4px", padding: "0 12px" }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", color: "var(--text-muted)", margin: "12px 0 4px", padding: "0 12px" }}>
|
||||
Libraries
|
||||
</div>
|
||||
{libraries.map((lib) => (
|
||||
@@ -42,18 +67,27 @@ function Sidebar() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: "auto" }}>
|
||||
<div style={{ marginTop: "auto", display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<NavLink to="/settings" style={linkStyle}>Settings</NavLink>
|
||||
<button
|
||||
onClick={onToggleTheme}
|
||||
title={dark ? "Switch to light mode" : "Switch to dark mode"}
|
||||
style={{ textAlign: "left", border: "none", background: "transparent", padding: "6px 12px", color: "var(--text-secondary)", borderRadius: 4 }}
|
||||
>
|
||||
{dark ? "☀ Light mode" : "☾ Dark mode"}
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function AppShell() {
|
||||
const { dark, toggle } = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", height: "100vh", fontFamily: "system-ui, sans-serif" }}>
|
||||
<Sidebar />
|
||||
<main style={{ flex: 1, overflow: "auto" }}>
|
||||
<div style={{ display: "flex", height: "100vh", background: "var(--bg)", color: "var(--text)" }}>
|
||||
<Sidebar onToggleTheme={toggle} dark={dark} />
|
||||
<main style={{ flex: 1, overflow: "auto", background: "var(--bg)" }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<SearchPage />} />
|
||||
<Route path="/search" element={<SearchPage />} />
|
||||
|
||||
@@ -67,6 +67,8 @@ export const api = {
|
||||
request<void>(`/libraries/${id}`, { method: "DELETE" }),
|
||||
browse: (id: number, path = "") =>
|
||||
request<BrowseResult>(`/libraries/${id}/browse?path=${encodeURIComponent(path)}`),
|
||||
scanStatus: (id: number) =>
|
||||
request<{ scanning: boolean }>(`/libraries/${id}/scan-status`),
|
||||
},
|
||||
|
||||
media: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, BrowseResult } from "../../api/client";
|
||||
import { api, type BrowseResult } from "../../api/client";
|
||||
import MediaViewer from "../MediaViewer/MediaViewer";
|
||||
|
||||
interface Props {
|
||||
@@ -27,15 +27,15 @@ export default function FileBrowser({ libraryId }: Props) {
|
||||
<div style={{ padding: "1rem" }}>
|
||||
{/* Breadcrumb */}
|
||||
<nav style={{ marginBottom: 16, display: "flex", gap: 4, alignItems: "center", flexWrap: "wrap" }}>
|
||||
<button onClick={() => navigate("")} style={{ background: "none", border: "none", cursor: "pointer", fontWeight: 600 }}>
|
||||
<button onClick={() => navigate("")} style={{ background: "none", border: "none", cursor: "pointer", fontWeight: 600, color: "var(--text)", padding: "2px 4px" }}>
|
||||
Root
|
||||
</button>
|
||||
{pathParts.map((part, i) => {
|
||||
const partPath = pathParts.slice(0, i + 1).join("/");
|
||||
return (
|
||||
<span key={partPath} style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
||||
<span style={{ color: "#888" }}>/</span>
|
||||
<button onClick={() => navigate(partPath)} style={{ background: "none", border: "none", cursor: "pointer" }}>
|
||||
<span style={{ color: "var(--text-muted)" }}>/</span>
|
||||
<button onClick={() => navigate(partPath)} style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text)", padding: "2px 4px" }}>
|
||||
{part}
|
||||
</button>
|
||||
</span>
|
||||
@@ -43,7 +43,7 @@ export default function FileBrowser({ libraryId }: Props) {
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{isLoading && <p>Loading…</p>}
|
||||
{isLoading && <p style={{ color: "var(--text-secondary)" }}>Loading…</p>}
|
||||
|
||||
{/* Grid */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))", gap: 12 }}>
|
||||
@@ -55,14 +55,15 @@ export default function FileBrowser({ libraryId }: Props) {
|
||||
else if (entry.media_item_id) setViewingId(entry.media_item_id);
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
border: "1px solid #ddd",
|
||||
cursor: entry.type === "dir" || entry.media_item_id ? "pointer" : "default",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 6,
|
||||
overflow: "hidden",
|
||||
background: "#fafafa",
|
||||
background: "var(--bg-card)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
opacity: entry.type !== "dir" && !entry.media_item_id ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
{entry.type === "dir" ? (
|
||||
@@ -76,7 +77,7 @@ export default function FileBrowser({ libraryId }: Props) {
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ padding: "4px 6px", fontSize: 12, width: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<div style={{ padding: "4px 6px", fontSize: 12, width: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--text)" }}>
|
||||
{entry.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, BrowseEntry, MediaItem } from "../../api/client";
|
||||
import { api, type BrowseEntry, type MediaItem } from "../../api/client";
|
||||
import TagPanel from "../TagPanel/TagPanel";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { api, Tag, TagsByCategory, MediaItem } from "../../api/client";
|
||||
import { api, type Tag, type TagsByCategory, type MediaItem } from "../../api/client";
|
||||
|
||||
interface Props {
|
||||
item: MediaItem;
|
||||
@@ -13,7 +13,6 @@ export default function TagPanel({ item }: Props) {
|
||||
queryFn: api.tags.list,
|
||||
});
|
||||
|
||||
const allTags = grouped.flatMap((g) => g.tags);
|
||||
const assignedIds = new Set(item.tags.map((t) => t.id));
|
||||
|
||||
const [newName, setNewName] = useState("");
|
||||
@@ -28,8 +27,7 @@ export default function TagPanel({ item }: Props) {
|
||||
mutationFn: () => api.tags.create(newName.trim(), newCategory.trim()),
|
||||
onSuccess: (tag: Tag) => {
|
||||
qc.invalidateQueries({ queryKey: ["tags"] });
|
||||
const newIds = [...assignedIds, tag.id];
|
||||
setTagsMutation.mutate(newIds);
|
||||
setTagsMutation.mutate([...assignedIds, tag.id]);
|
||||
setNewName("");
|
||||
setNewCategory("");
|
||||
},
|
||||
@@ -44,11 +42,11 @@ export default function TagPanel({ item }: Props) {
|
||||
|
||||
return (
|
||||
<div style={{ minWidth: 220 }}>
|
||||
<h3 style={{ margin: "0 0 12px" }}>Tags</h3>
|
||||
<h3 style={{ margin: "0 0 12px", color: "var(--text)" }}>Tags</h3>
|
||||
|
||||
{grouped.map((group) => (
|
||||
<div key={group.category} style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", color: "#888", marginBottom: 4 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", color: "var(--text-muted)", marginBottom: 4 }}>
|
||||
{group.category}
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
||||
@@ -61,9 +59,9 @@ export default function TagPanel({ item }: Props) {
|
||||
borderRadius: 12,
|
||||
border: "1px solid",
|
||||
cursor: "pointer",
|
||||
background: assignedIds.has(tag.id) ? "#3b82f6" : "transparent",
|
||||
color: assignedIds.has(tag.id) ? "#fff" : "inherit",
|
||||
borderColor: assignedIds.has(tag.id) ? "#3b82f6" : "#ccc",
|
||||
background: assignedIds.has(tag.id) ? "var(--accent)" : "transparent",
|
||||
color: assignedIds.has(tag.id) ? "#fff" : "var(--text)",
|
||||
borderColor: assignedIds.has(tag.id) ? "var(--accent)" : "var(--border)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
@@ -75,19 +73,17 @@ export default function TagPanel({ item }: Props) {
|
||||
))}
|
||||
|
||||
<details style={{ marginTop: 16 }}>
|
||||
<summary style={{ cursor: "pointer", fontSize: 13, color: "#3b82f6" }}>+ New tag</summary>
|
||||
<summary style={{ cursor: "pointer", fontSize: 13, color: "var(--accent-text)" }}>+ New tag</summary>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6, marginTop: 8 }}>
|
||||
<input
|
||||
placeholder="Tag name"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
style={{ padding: "4px 8px" }}
|
||||
/>
|
||||
<input
|
||||
placeholder="Category"
|
||||
value={newCategory}
|
||||
onChange={(e) => setNewCategory(e.target.value)}
|
||||
style={{ padding: "4px 8px" }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => createTagMutation.mutate()}
|
||||
|
||||
66
frontend/src/index.css
Normal file
66
frontend/src/index.css
Normal file
@@ -0,0 +1,66 @@
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-secondary: #fafafa;
|
||||
--bg-card: #fafafa;
|
||||
--border: #e5e7eb;
|
||||
--border-subtle: #eeeeee;
|
||||
--text: #111827;
|
||||
--text-secondary: #6b7280;
|
||||
--text-muted: #9ca3af;
|
||||
--accent: #3b82f6;
|
||||
--accent-bg: #eff6ff;
|
||||
--accent-text: #3b82f6;
|
||||
--tag-bg: #dbeafe;
|
||||
--danger: #ef4444;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg: #111827;
|
||||
--bg-secondary: #1f2937;
|
||||
--bg-card: #1f2937;
|
||||
--border: #374151;
|
||||
--border-subtle: #374151;
|
||||
--text: #f9fafb;
|
||||
--text-secondary: #9ca3af;
|
||||
--text-muted: #6b7280;
|
||||
--accent: #60a5fa;
|
||||
--accent-bg: #1e3a5f;
|
||||
--accent-text: #60a5fa;
|
||||
--tag-bg: #1e3a5f;
|
||||
--danger: #f87171;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font: inherit;
|
||||
outline-color: var(--accent);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text);
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, Library } from "../api/client";
|
||||
import { api, type Library } from "../api/client";
|
||||
import FileBrowser from "../components/FileBrowser/FileBrowser";
|
||||
|
||||
export default function BrowserPage() {
|
||||
@@ -14,13 +14,13 @@ export default function BrowserPage() {
|
||||
|
||||
const library = libraries.find((l) => l.id === id);
|
||||
|
||||
if (!library) return <p style={{ padding: "2rem" }}>Library not found.</p>;
|
||||
if (!library) return <p style={{ padding: "2rem", color: "var(--text)" }}>Library not found.</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ padding: "1rem 1rem 0", borderBottom: "1px solid #eee" }}>
|
||||
<h2 style={{ margin: 0 }}>{library.name}</h2>
|
||||
<div style={{ fontSize: 12, color: "#888" }}>{library.path}</div>
|
||||
<div style={{ padding: "1rem 1rem 0", borderBottom: "1px solid var(--border-subtle)" }}>
|
||||
<h2 style={{ margin: 0, color: "var(--text)" }}>{library.name}</h2>
|
||||
<div style={{ fontSize: 12, color: "var(--text-secondary)" }}>{library.path}</div>
|
||||
</div>
|
||||
<FileBrowser libraryId={id} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, MediaItem, TagsByCategory } from "../api/client";
|
||||
import { api, type MediaItem, type TagsByCategory } from "../api/client";
|
||||
|
||||
export default function SearchPage() {
|
||||
const [q, setQ] = useState("");
|
||||
@@ -31,24 +31,23 @@ export default function SearchPage() {
|
||||
|
||||
return (
|
||||
<div style={{ padding: "2rem", maxWidth: 900 }}>
|
||||
<h1>Search</h1>
|
||||
<h1 style={{ color: "var(--text)" }}>Search</h1>
|
||||
<form onSubmit={handleSubmit} style={{ display: "flex", gap: 8, marginBottom: 16, flexWrap: "wrap" }}>
|
||||
<input
|
||||
placeholder="Search by filename…"
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
style={{ flex: 1, minWidth: 200, padding: "6px 10px" }}
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
/>
|
||||
<button type="submit">Search</button>
|
||||
<button type="submit" style={{ background: "var(--accent)", color: "#fff", border: "none" }}>Search</button>
|
||||
</form>
|
||||
|
||||
{/* Tag filter */}
|
||||
{grouped.length > 0 && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{ fontSize: 13, color: "#666", marginBottom: 8 }}>Filter by tag:</div>
|
||||
<div style={{ fontSize: 13, color: "var(--text-secondary)", marginBottom: 8 }}>Filter by tag:</div>
|
||||
{grouped.map((group) => (
|
||||
<div key={group.category} style={{ marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", color: "#888", marginRight: 8 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", color: "var(--text-muted)", marginRight: 8 }}>
|
||||
{group.category}
|
||||
</span>
|
||||
{group.tags.map((tag) => (
|
||||
@@ -61,9 +60,9 @@ export default function SearchPage() {
|
||||
borderRadius: 12,
|
||||
border: "1px solid",
|
||||
cursor: "pointer",
|
||||
background: selectedTags.includes(tag.id) ? "#3b82f6" : "transparent",
|
||||
color: selectedTags.includes(tag.id) ? "#fff" : "inherit",
|
||||
borderColor: selectedTags.includes(tag.id) ? "#3b82f6" : "#ccc",
|
||||
background: selectedTags.includes(tag.id) ? "var(--accent)" : "transparent",
|
||||
color: selectedTags.includes(tag.id) ? "#fff" : "var(--text)",
|
||||
borderColor: selectedTags.includes(tag.id) ? "var(--accent)" : "var(--border)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
@@ -75,24 +74,24 @@ export default function SearchPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFetching && <p>Searching…</p>}
|
||||
{isFetching && <p style={{ color: "var(--text-secondary)" }}>Searching…</p>}
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))", gap: 12 }}>
|
||||
{results.map((item) => (
|
||||
<div key={item.id} style={{ border: "1px solid #ddd", borderRadius: 6, overflow: "hidden" }}>
|
||||
<div key={item.id} style={{ border: "1px solid var(--border)", borderRadius: 6, overflow: "hidden", background: "var(--bg-card)" }}>
|
||||
<img
|
||||
src={api.media.thumbnailUrl(item.id)}
|
||||
alt={item.filename}
|
||||
loading="lazy"
|
||||
style={{ width: "100%", height: 110, objectFit: "cover" }}
|
||||
/>
|
||||
<div style={{ padding: "4px 6px", fontSize: 12, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<div style={{ padding: "4px 6px", fontSize: 12, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--text)" }}>
|
||||
{item.filename}
|
||||
</div>
|
||||
{item.tags.length > 0 && (
|
||||
<div style={{ padding: "0 6px 4px", display: "flex", flexWrap: "wrap", gap: 4 }}>
|
||||
{item.tags.map((t) => (
|
||||
<span key={t.id} style={{ background: "#e0edff", color: "#3b82f6", borderRadius: 8, padding: "1px 6px", fontSize: 11 }}>
|
||||
<span key={t.id} style={{ background: "var(--tag-bg)", color: "var(--accent-text)", borderRadius: 8, padding: "1px 6px", fontSize: 11 }}>
|
||||
{t.name}
|
||||
</span>
|
||||
))}
|
||||
@@ -103,7 +102,7 @@ export default function SearchPage() {
|
||||
</div>
|
||||
|
||||
{submitted && !isFetching && results.length === 0 && (
|
||||
<p style={{ color: "#888" }}>No results found.</p>
|
||||
<p style={{ color: "var(--text-secondary)" }}>No results found.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { api, Library } from "../api/client";
|
||||
import { api, type Library } from "../api/client";
|
||||
|
||||
function LibraryRow({ lib, onRemove }: { lib: Library; onRemove: (id: number) => void }) {
|
||||
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;
|
||||
|
||||
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>
|
||||
<button
|
||||
onClick={() => onRemove(lib.id)}
|
||||
disabled={scanning}
|
||||
style={{ color: scanning ? "var(--text-muted)" : "var(--danger)", background: "transparent", border: "none" }}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const qc = useQueryClient();
|
||||
@@ -31,49 +62,24 @@ export default function SettingsPage() {
|
||||
|
||||
return (
|
||||
<div style={{ padding: "2rem", maxWidth: 600 }}>
|
||||
<h1>Settings</h1>
|
||||
<h2>Libraries</h2>
|
||||
<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: "red", margin: 0 }}>{error}</p>}
|
||||
<button type="submit" disabled={addMutation.isPending}>
|
||||
<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) => (
|
||||
<li
|
||||
key={lib.id}
|
||||
style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", borderBottom: "1px solid #eee" }}
|
||||
>
|
||||
<div>
|
||||
<strong>{lib.name}</strong>
|
||||
<div style={{ fontSize: 12, color: "#666" }}>{lib.path}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => deleteMutation.mutate(lib.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
style={{ color: "red" }}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</li>
|
||||
<LibraryRow key={lib.id} lib={lib} onRemove={(id) => deleteMutation.mutate(id)} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user