import { useEffect, useRef, useState } from "react"; import { open } from "@tauri-apps/plugin-dialog"; import { useGalleryStore, Folder, IndexProgress } from "../store"; interface ContextMenuState { folderId: number; x: number; y: number; } function FolderContextMenu({ menu, folder, isMuted, onClose, onRename, onReindex, onLocate, onToggleMute, onRemove, }: { menu: ContextMenuState; folder: Folder; isMuted: boolean; onClose: () => void; onRename: () => void; onReindex: () => void; onLocate: () => void; onToggleMute: () => void; onRemove: () => void; }) { const ref = useRef(null); useEffect(() => { const handleDown = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) onClose(); }; const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("mousedown", handleDown); document.addEventListener("keydown", handleKey); return () => { document.removeEventListener("mousedown", handleDown); document.removeEventListener("keydown", handleKey); }; }, [onClose]); const item = (label: string, onClick: () => void, danger = false) => ( ); return (
{item("Reindex", onReindex)} {item("Rename", onRename)} {item(isMuted ? "Unmute notifications" : "Mute notifications", onToggleMute)} {folder.scan_error && item("Locate Folder", onLocate)}
{item("Remove from app", onRemove, true)}
); } function FolderItem({ folder, selected, progress, }: { folder: Folder; selected: boolean; progress: IndexProgress | undefined; }) { const { selectFolder, removeFolder, reindexFolder, renameFolder, updateFolderPath, toggleMutedFolder } = useGalleryStore(); const mutedFolderIds = useGalleryStore((state) => state.mutedFolderIds); const isMuted = mutedFolderIds.includes(folder.id); const isIndexing = progress && !progress.done; const isMissing = !!folder.scan_error && !isIndexing; const [contextMenu, setContextMenu] = useState(null); const [renaming, setRenaming] = useState(false); const [renameValue, setRenameValue] = useState(folder.name); const [confirmingRemoval, setConfirmingRemoval] = useState(false); const renameInputRef = useRef(null); useEffect(() => { if (renaming) { setRenameValue(folder.name); setTimeout(() => renameInputRef.current?.select(), 0); } }, [renaming, folder.name]); const handleContextMenu = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); // Keep menu inside viewport const x = Math.min(e.clientX, window.innerWidth - 180); const y = Math.min(e.clientY, window.innerHeight - 160); setContextMenu({ folderId: folder.id, x, y }); }; const handleLocateFolder = async () => { const picked = await open({ directory: true, multiple: false, title: `Locate "${folder.name}"` }); if (picked && typeof picked === "string") { await updateFolderPath(folder.id, picked); } }; const commitRename = async () => { const trimmed = renameValue.trim(); if (trimmed && trimmed !== folder.name) { await renameFolder(folder.id, trimmed); } setRenaming(false); }; const handleRenameKey = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); void commitRename(); } if (e.key === "Escape") { setRenaming(false); } }; return ( <>
!renaming && selectFolder(folder.id)} onContextMenu={handleContextMenu} > {isMissing ? ( ) : ( )}
{renaming ? ( setRenameValue(e.target.value)} onKeyDown={handleRenameKey} onBlur={() => void commitRename()} onClick={(e) => e.stopPropagation()} /> ) : (
{folder.name}
)} {isIndexing ? ( <>
{progress.indexed}/{progress.total}
0 ? (progress.indexed / progress.total) * 100 : 0}%` }} />
) : (
{folder.image_count.toLocaleString()}
)}
{/* Hover action buttons */} {!renaming && ( confirmingRemoval ? (
e.stopPropagation()}>
) : (
) )}
{isMissing && (

Folder not found

This folder may have been moved or renamed. Locate it to resume, or remove it from the app.

)} {contextMenu && contextMenu.folderId === folder.id && ( setContextMenu(null)} onRename={() => setRenaming(true)} onReindex={() => void reindexFolder(folder.id)} onLocate={() => void handleLocateFolder()} onToggleMute={() => toggleMutedFolder(folder.id)} onRemove={() => setConfirmingRemoval(true)} /> )} ); } export function Sidebar() { const folders = useGalleryStore((state) => state.folders); const selectedFolderId = useGalleryStore((state) => state.selectedFolderId); const addFolder = useGalleryStore((state) => state.addFolder); const indexingProgress = useGalleryStore((state) => state.indexingProgress); const selectFolder = useGalleryStore((state) => state.selectFolder); const activeView = useGalleryStore((state) => state.activeView); const setView = useGalleryStore((state) => state.setView); const handleAddFolder = async () => { const selected = await open({ directory: true, multiple: false, title: "Select Media Folder" }); if (selected && typeof selected === "string") { await addFolder(selected); } }; return ( ); }