import { useState, type MouseEvent } from 'react' import { Reorder, useDragControls } from 'framer-motion' import { open } from '@tauri-apps/plugin-dialog' import { useGalleryStore, type Folder, type IndexProgress } from '../../store' import { ContextMenu, MenuItem, MenuSeparator } from '../menu' import { InlineConfirm } from '../InlineConfirm' import { InlineRename } from '../InlineRename' import { Tooltip } from '../Tooltip' import { CloseIcon, FolderIcon } from '../icons' export function FolderItem({ folder, selected, progress, customOrdering, dragging, onDragStart, onDragEnd, onKeyboardMove, }: { folder: Folder selected: boolean progress: IndexProgress | undefined customOrdering: boolean dragging: boolean onDragStart: (pointerY: number) => void onDragEnd: () => void onKeyboardMove: (direction: -1 | 1) => void }) { const dragControls = useDragControls() const { selectFolder, removeFolder, reindexFolder, renameFolder, updateFolderPath, toggleMutedFolder, } = useGalleryStore() const mutedFolderIds = useGalleryStore((state) => state.mutedFolderIds) const setAllWorkersPaused = useGalleryStore((state) => state.setAllWorkersPaused) const folderWorkers = useGalleryStore((state) => state.workerPaused[folder.id]) const folderJobs = useGalleryStore((state) => state.mediaJobProgress[folder.id]) const isMuted = mutedFolderIds.includes(folder.id) // "Fully paused" means every worker that currently has pending work is // paused. Workers with nothing pending are ignored — the background tasks // panel only lets you toggle the stages it actually shows, so requiring // every worker key to be literally true left this permanently out of sync // (menu kept offering "Pause" after the visible work was already paused). const hasPendingWork = (folderJobs?.thumbnail_pending ?? 0) > 0 || (folderJobs?.metadata_pending ?? 0) > 0 || (folderJobs?.embedding_pending ?? 0) > 0 || (folderJobs?.tagging_pending ?? 0) > 0 const isPausedAll = hasPendingWork ? ((folderJobs?.thumbnail_pending ?? 0) === 0 || !!folderWorkers?.thumbnail) && ((folderJobs?.metadata_pending ?? 0) === 0 || !!folderWorkers?.metadata) && ((folderJobs?.embedding_pending ?? 0) === 0 || !!folderWorkers?.embedding) && ((folderJobs?.tagging_pending ?? 0) === 0 || !!folderWorkers?.tagging) : !!folderWorkers && folderWorkers.thumbnail && folderWorkers.metadata && folderWorkers.embedding && folderWorkers.tagging const isIndexing = progress && !progress.done const isMissing = !!folder.scan_error && !isIndexing const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null) const [renaming, setRenaming] = useState(false) const [confirmingRemoval, setConfirmingRemoval] = useState(false) const handleContextMenu = (e: MouseEvent) => { e.preventDefault() e.stopPropagation() setContextMenu({ x: e.clientX, y: e.clientY }) } 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) } } return (
!renaming && selectFolder(folder.id)} onContextMenu={handleContextMenu} > {customOrdering ? ( ) : null} {isMissing ? ( ) : ( )}
{renaming ? ( renameFolder(folder.id, next)} onClose={() => setRenaming(false)} /> ) : (
{folder.name}
)} {isIndexing ? ( <>
{progress.indexed}/{progress.total}
0 ? (progress.indexed / progress.total) * 100 : 0}%`, }} />
) : (
{folder.image_count.toLocaleString()}
)}
{/* Hover action buttons */} {!renaming && (confirmingRemoval ? ( { void removeFolder(folder.id) setConfirmingRemoval(false) }} onCancel={() => setConfirmingRemoval(false)} /> ) : (
))}
{isMissing && (

Folder not found

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

)} {contextMenu && ( setContextMenu(null)} > void reindexFolder(folder.id)} /> setRenaming(true)} /> setAllWorkersPaused(folder.id, !isPausedAll)} /> toggleMutedFolder(folder.id)} /> {folder.scan_error ? ( void handleLocateFolder()} /> ) : null} setConfirmingRemoval(true)} /> )} ) }