From b243266ad3393462d718f7733f432b612400ff89 Mon Sep 17 00:00:00 2001 From: Garret Patti Date: Sat, 16 May 2026 14:39:00 -0400 Subject: [PATCH] add rescan button --- backend/app/routers/libraries.py | 18 ++- frontend/src/api/client.ts | 2 + .../components/FileBrowser/FileBrowser.tsx | 6 +- frontend/src/components/TagPanel/TagPanel.tsx | 141 ++++++++++++++---- frontend/src/pages/SettingsPage.tsx | 29 +++- 5 files changed, 157 insertions(+), 39 deletions(-) diff --git a/backend/app/routers/libraries.py b/backend/app/routers/libraries.py index 93f1259..2321371 100644 --- a/backend/app/routers/libraries.py +++ b/backend/app/routers/libraries.py @@ -47,6 +47,22 @@ async def get_scan_status(library_id: int): return {"scanning": scanner.is_scanning(library_id)} +@router.post("/{library_id}/rescan") +async def rescan_library( + library_id: int, + background_tasks: BackgroundTasks, + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(Library).where(Library.id == library_id)) + lib = result.scalars().first() + if not lib: + raise HTTPException(404, "Library not found") + if scanner.is_scanning(library_id): + raise HTTPException(409, "Scan already in progress") + background_tasks.add_task(scanner.scan_library_background, lib.id, lib.path) + return {"scanning": True} + + @router.delete("/{library_id}", status_code=204) async def delete_library(library_id: int, db: AsyncSession = Depends(get_db)): result = await db.execute(select(Library).where(Library.id == library_id)) @@ -88,7 +104,7 @@ async def browse_library(library_id: int, path: str = "", db: AsyncSession = Dep entries: list[BrowseEntry] = [] - for entry in sorted(target.iterdir(), key=lambda e: (e.is_file(), e.name.lower())): + for entry in sorted((e for e in target.iterdir() if not e.name.startswith(".")), key=lambda e: (e.is_file(), e.name.lower())): rel_entry = str(entry.relative_to(root)) if entry.is_dir(): entries.append(BrowseEntry(name=entry.name, type="dir", rel_path=rel_entry)) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 9bcce0e..0ef358c 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -69,6 +69,8 @@ export const api = { request(`/libraries/${id}/browse?path=${encodeURIComponent(path)}`), scanStatus: (id: number) => request<{ scanning: boolean }>(`/libraries/${id}/scan-status`), + rescan: (id: number) => + request<{ scanning: boolean }>(`/libraries/${id}/rescan`, { method: "POST" }), }, media: { diff --git a/frontend/src/components/FileBrowser/FileBrowser.tsx b/frontend/src/components/FileBrowser/FileBrowser.tsx index ec30723..6292ff3 100644 --- a/frontend/src/components/FileBrowser/FileBrowser.tsx +++ b/frontend/src/components/FileBrowser/FileBrowser.tsx @@ -8,9 +8,11 @@ interface Props { } export default function FileBrowser({ libraryId }: Props) { - const [currentPath, setCurrentPath] = useState(""); + const [libraryPaths, setLibraryPaths] = useState>({}); const [viewingId, setViewingId] = useState(null); + const currentPath = libraryPaths[libraryId] ?? ""; + const { data, isLoading } = useQuery({ queryKey: ["browse", libraryId, currentPath], queryFn: () => api.libraries.browse(libraryId, currentPath), @@ -19,7 +21,7 @@ export default function FileBrowser({ libraryId }: Props) { const pathParts = currentPath ? currentPath.split("/").filter(Boolean) : []; function navigate(relPath: string) { - setCurrentPath(relPath); + setLibraryPaths((prev) => ({ ...prev, [libraryId]: relPath })); setViewingId(null); } diff --git a/frontend/src/components/TagPanel/TagPanel.tsx b/frontend/src/components/TagPanel/TagPanel.tsx index 0fa8ca1..a35c908 100644 --- a/frontend/src/components/TagPanel/TagPanel.tsx +++ b/frontend/src/components/TagPanel/TagPanel.tsx @@ -17,6 +17,9 @@ export default function TagPanel({ item }: Props) { const [newName, setNewName] = useState(""); const [newCategory, setNewCategory] = useState(""); + const [quickAddCategory, setQuickAddCategory] = useState(null); + const [quickAddName, setQuickAddName] = useState(""); + const [categorySearch, setCategorySearch] = useState>({}); const setTagsMutation = useMutation({ mutationFn: (ids: number[]) => api.media.setTags(item.id, ids), @@ -24,12 +27,15 @@ export default function TagPanel({ item }: Props) { }); const createTagMutation = useMutation({ - mutationFn: () => api.tags.create(newName.trim(), newCategory.trim()), + mutationFn: ({ name, category }: { name: string; category: string }) => + api.tags.create(name.trim(), category.trim()), onSuccess: (tag: Tag) => { qc.invalidateQueries({ queryKey: ["tags"] }); setTagsMutation.mutate([...assignedIds, tag.id]); setNewName(""); setNewCategory(""); + setQuickAddCategory(null); + setQuickAddName(""); }, }); @@ -40,53 +46,130 @@ export default function TagPanel({ item }: Props) { setTagsMutation.mutate(next); } + function toggleQuickAdd(category: string) { + if (quickAddCategory === category) { + setQuickAddCategory(null); + setQuickAddName(""); + } else { + setQuickAddCategory(category); + setQuickAddName(""); + } + } + return (

Tags

- {grouped.map((group) => ( -
-
- {group.category} -
-
- {group.tags.map((tag) => ( + {grouped.map((group) => { + const search = categorySearch[group.category] ?? ""; + const visibleTags = search + ? group.tags.filter((t) => t.name.toLowerCase().includes(search.toLowerCase())) + : group.tags; + + return ( +
+ {/* Category header row */} +
+ + {group.category} + - ))} -
-
- ))} +
-
+ {/* Per-category search */} + + setCategorySearch((prev) => ({ ...prev, [group.category]: e.target.value })) + } + style={{ marginBottom: 6, fontSize: 12, padding: "3px 8px" }} + /> + + {/* Tag chips */} +
+ {visibleTags.map((tag) => ( + + ))} + {search && visibleTags.length === 0 && ( + No matches + )} +
+ + {/* Inline quick-add form */} + {quickAddCategory === group.category && ( +
+ setQuickAddName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && quickAddName.trim()) { + createTagMutation.mutate({ name: quickAddName, category: group.category }); + } + }} + autoFocus + style={{ flex: 1, fontSize: 12, padding: "3px 8px" }} + /> + +
+ )} +
+ ); + })} + + {/* Global new-tag form */} +
+ New tag
- setNewName(e.target.value)} - /> setNewCategory(e.target.value)} /> + setNewName(e.target.value)} + /> +
+ + +
); }