From 18d1481681bf6bfce59489b375901f8cbb2a40e8 Mon Sep 17 00:00:00 2001 From: Audiolib Date: Tue, 26 May 2026 14:42:35 +0200 Subject: [PATCH] Fix sidebar, add library editing, improve file browser - Sidebar: show Podcasts section only when podcast libraries exist; show hint text when no libraries are configured yet - Admin: extract LibraryForm component, add edit (Pencil) button per library with inline form and PATCH support - Backend: add media_type to LibraryUpdate schema and PATCH handler - FileBrowser: add quick-access shortcuts (/audiofiles /data /media /), show all files+dirs so users can confirm correct folder Co-Authored-By: Claude Sonnet 4.6 --- backend/app/routers/libraries.py | 2 + backend/app/schemas/library.py | 1 + frontend/src/api/libraries.ts | 3 + .../src/components/common/FileBrowser.tsx | 10 + frontend/src/components/common/Sidebar.tsx | 77 +++++--- frontend/src/pages/Admin.tsx | 176 +++++++++++------- 6 files changed, 172 insertions(+), 97 deletions(-) diff --git a/backend/app/routers/libraries.py b/backend/app/routers/libraries.py index d6c4a48..0393174 100644 --- a/backend/app/routers/libraries.py +++ b/backend/app/routers/libraries.py @@ -172,6 +172,8 @@ async def update_library( {"id": f.get("id", str(uuid.uuid4())), "fullPath": f.get("fullPath", f.get("full_path", ""))} for f in body.folders ] + if body.media_type is not None: + lib.media_type = body.media_type if body.settings is not None: lib.settings = {**(lib.settings or {}), **body.settings} diff --git a/backend/app/schemas/library.py b/backend/app/schemas/library.py index 2bdb0ab..cfcfaad 100644 --- a/backend/app/schemas/library.py +++ b/backend/app/schemas/library.py @@ -55,6 +55,7 @@ class LibraryCreate(BaseModel): class LibraryUpdate(BaseModel): name: str | None = None folders: list[dict] | None = None + media_type: str | None = None settings: dict | None = None diff --git a/frontend/src/api/libraries.ts b/frontend/src/api/libraries.ts index 84dd77a..8678cb1 100644 --- a/frontend/src/api/libraries.ts +++ b/frontend/src/api/libraries.ts @@ -17,5 +17,8 @@ export const scanLibrary = (libraryId: string) => export const createLibrary = (data: object) => api.post('/api/libraries', data).then((r) => r.data) +export const updateLibrary = (id: string, data: object) => + api.patch(`/api/libraries/${id}`, data).then((r) => r.data) + export const deleteLibrary = (id: string) => api.delete(`/api/libraries/${id}`).then((r) => r.data) diff --git a/frontend/src/components/common/FileBrowser.tsx b/frontend/src/components/common/FileBrowser.tsx index cddb9d4..222397e 100644 --- a/frontend/src/components/common/FileBrowser.tsx +++ b/frontend/src/components/common/FileBrowser.tsx @@ -49,6 +49,16 @@ export default function FileBrowser({ initialPath = '/', onSelect, onClose }: Pr + {/* Quick access */} +
+ {['/audiofiles', '/data', '/media', '/'].map((p) => ( + + ))} +
+ {/* Current path */}
{parent && ( diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index dd33947..c694d58 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -6,6 +6,9 @@ import { useAuthStore } from '../../store/authStore' export default function Sidebar() { const { libraries, user, logout } = useAuthStore() + const bookLibraries = libraries.filter((l: any) => l.mediaType !== 'podcast') + const podcastLibraries = libraries.filter((l: any) => l.mediaType === 'podcast') + return (
- {/* Libraries */} {/* Footer */} diff --git a/frontend/src/pages/Admin.tsx b/frontend/src/pages/Admin.tsx index 686533b..a4df4d0 100644 --- a/frontend/src/pages/Admin.tsx +++ b/frontend/src/pages/Admin.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' -import { Users, Library, Settings, Trash2, Plus, RefreshCw, Loader2, Check, X, FolderOpen } from 'lucide-react' +import { Users, Library, Settings, Trash2, Plus, RefreshCw, Loader2, Check, X, FolderOpen, Pencil } from 'lucide-react' import { getUsers, createUser, deleteUser, getSettings, updateSettings } from '../api/me' -import { getLibraries, scanLibrary, createLibrary, deleteLibrary } from '../api/libraries' +import { getLibraries, scanLibrary, createLibrary, updateLibrary, deleteLibrary } from '../api/libraries' import FileBrowser from '../components/common/FileBrowser' type Tab = 'users' | 'libraries' | 'settings' @@ -115,15 +115,74 @@ function UsersPanel() { ) } +function LibraryForm({ + initial, onSave, onCancel, title, +}: { + initial: { name: string; path: string; mediaType: string } + onSave: (v: { name: string; path: string; mediaType: string }) => Promise + onCancel: () => void + title: string +}) { + const [form, setForm] = useState(initial) + const [showBrowser, setShowBrowser] = useState(false) + const [saving, setSaving] = useState(false) + + const submit = async () => { + setSaving(true) + await onSave(form).finally(() => setSaving(false)) + } + + return ( +
+

{title}

+ setForm({ ...form, name: e.target.value })} + className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-primary" + /> +
+ setForm({ ...form, path: e.target.value })} + className="flex-1 bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-primary" + /> + +
+ {showBrowser && ( + setForm({ ...form, path: p })} + onClose={() => setShowBrowser(false)} + /> + )} + +
+ + +
+
+ ) +} + function LibrariesPanel() { const [libraries, setLibraries] = useState([]) const [loading, setLoading] = useState(true) const [scanning, setScanning] = useState(null) const [showCreate, setShowCreate] = useState(false) - const [showBrowser, setShowBrowser] = useState(false) - const [form, setForm] = useState({ name: '', path: '', mediaType: 'book' }) + const [editingId, setEditingId] = useState(null) - useEffect(() => { getLibraries().then(setLibraries).finally(() => setLoading(false)) }, []) + const reload = () => getLibraries().then(setLibraries) + useEffect(() => { reload().finally(() => setLoading(false)) }, []) const handleScan = async (id: string) => { setScanning(id) @@ -131,12 +190,16 @@ function LibrariesPanel() { setTimeout(() => setScanning(null), 5000) } - const handleCreate = async () => { + const handleCreate = async (form: { name: string; path: string; mediaType: string }) => { await createLibrary({ name: form.name, folders: [{ fullPath: form.path }], media_type: form.mediaType }) - const libs = await getLibraries() - setLibraries(libs) + await reload() setShowCreate(false) - setForm({ name: '', path: '', mediaType: 'book' }) + } + + const handleUpdate = async (id: string, form: { name: string; path: string; mediaType: string }) => { + await updateLibrary(id, { name: form.name, folders: [{ fullPath: form.path }], media_type: form.mediaType }) + await reload() + setEditingId(null) } const handleDelete = async (id: string) => { @@ -149,75 +212,54 @@ function LibrariesPanel() {

{libraries.length} Bibliotheken

-
{showCreate && ( -
-

Neue Bibliothek

- setForm({ ...form, name: e.target.value })} - className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-primary" - /> -
- setForm({ ...form, path: e.target.value })} - className="flex-1 bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-primary" - /> - -
- {showBrowser && ( - setForm({ ...form, path: p })} - onClose={() => setShowBrowser(false)} - /> - )} - -
- - -
-
+ setShowCreate(false)} + /> )} {loading ? : (
{libraries.map((lib: any) => ( -
-
-

{lib.name}

-

- {lib.folders?.[0]?.fullPath || ''} · {lib.mediaType} -

-
- - +
+ {editingId === lib.id ? ( + handleUpdate(lib.id, form)} + onCancel={() => setEditingId(null)} + /> + ) : ( +
+
+

{lib.name}

+

+ {lib.folders?.[0]?.fullPath || ''} · {lib.mediaType === 'podcast' ? 'Podcast' : 'Hörbücher'} +

+
+ + + +
+ )}
))}