feat: timeline and folder import

This commit is contained in:
2026-04-07 00:42:22 +02:00
parent 6d1b227fb9
commit 78e12e8309
20 changed files with 1036 additions and 75 deletions

View File

@@ -3,7 +3,6 @@ import {
ChevronRight,
ChevronDown,
Folder,
Image,
Calendar,
Star,
@@ -11,9 +10,14 @@ import {
Trash2,
Plus,
MoreHorizontal,
HardDrive
HardDrive,
RefreshCw
} from 'lucide-react'
import clsx from 'clsx'
import { AddSourceFolderDialog } from '../dialogs/AddSourceFolderDialog'
import { sourceFolders, library } from '../../services/api'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from '../ToastContainer'
interface TreeItem {
id: string
@@ -27,6 +31,65 @@ interface TreeItem {
export function LeftSidebar() {
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set(['library', 'folders', 'heaps']))
const [selectedItem, setSelectedItem] = useState<string | null>('all-photos')
const [showAddFolderDialog, setShowAddFolderDialog] = useState(false)
const [isScanning, setIsScanning] = useState(false)
const queryClient = useQueryClient()
// Fetch folders from API
const { data: foldersData, refetch: refetchFolders } = useQuery({
queryKey: ['folders'],
queryFn: sourceFolders.list,
})
// Mutation for adding folders
const addFolderMutation = useMutation({
mutationFn: async ({ path, recursive }: { path: string; recursive: boolean }) => {
// Add the folder
const folder = await sourceFolders.add(path, recursive)
// Trigger scan for the new folder
await sourceFolders.scan(folder.id)
return folder
},
onSuccess: (folder) => {
toast.success('Folder Added', `Scanning ${folder.name || folder.path}...`)
// Refetch folders list
refetchFolders()
// Refetch photos to show new ones
queryClient.invalidateQueries({ queryKey: ['photos'] })
},
onError: (error: any) => {
toast.error('Failed to Add Folder', error.message || 'An error occurred')
},
})
// Mutation for scanning all folders
const scanLibraryMutation = useMutation({
mutationFn: library.scan,
onMutate: () => {
setIsScanning(true)
toast.info('Scan Started', 'Scanning all folders for new photos...')
},
onSuccess: () => {
toast.success('Scan Complete', 'All folders have been scanned')
},
onError: (error: any) => {
toast.error('Scan Failed', error.message || 'Failed to scan folders')
},
onSettled: () => {
setIsScanning(false)
// Refetch photos after scan
queryClient.invalidateQueries({ queryKey: ['photos'] })
},
})
const handleAddFolder = async (path: string, recursive: boolean) => {
await addFolderMutation.mutateAsync({ path, recursive })
}
const handleScanAll = () => {
scanLibraryMutation.mutate()
}
const toggleExpanded = (id: string) => {
const newExpanded = new Set(expandedItems)
@@ -55,7 +118,13 @@ export function LeftSidebar() {
id: 'folders',
label: 'Folders',
icon: <Folder className="h-4 w-4" />,
children: [], // Will be populated from API
children: foldersData?.folders?.map((folder: any) => ({
id: `folder-${folder.id}`,
label: folder.name || folder.path.split('/').pop() || folder.path,
icon: <Folder className="h-4 w-4" />,
count: folder.photo_count,
type: 'folder',
})) || [],
},
{
id: 'heaps',
@@ -162,12 +231,32 @@ export function LeftSidebar() {
</div>
{/* Bottom Actions */}
<div className="border-t border-border p-3">
<button className="flex w-full items-center gap-2 rounded bg-surface-2 px-3 py-2 text-sm text-text hover:bg-surface-offset">
<div className="border-t border-border p-3 space-y-2">
<button
onClick={() => setShowAddFolderDialog(true)}
className="flex w-full items-center gap-2 rounded bg-surface-2 px-3 py-2 text-sm text-text hover:bg-surface-offset"
>
<Plus className="h-4 w-4" />
Add Source Folder
</button>
{foldersData?.folders?.length > 0 && (
<button
onClick={handleScanAll}
disabled={isScanning}
className="flex w-full items-center gap-2 rounded bg-surface-2 px-3 py-2 text-sm text-text hover:bg-surface-offset disabled:opacity-50"
>
<RefreshCw className={clsx("h-4 w-4", isScanning && "animate-spin")} />
{isScanning ? 'Scanning...' : 'Scan All Folders'}
</button>
)}
</div>
{/* Add Source Folder Dialog */}
<AddSourceFolderDialog
isOpen={showAddFolderDialog}
onClose={() => setShowAddFolderDialog(false)}
onAdd={handleAddFolder}
/>
</div>
)
}

View File

@@ -7,31 +7,71 @@ import {
FolderOpen,
Upload,
Settings,
Menu
Menu,
Trash2
} from 'lucide-react'
import clsx from 'clsx'
import { usePhotoStore } from '../../store/photoStore'
import { photos } from '../../services/api'
import { toast } from '../ToastContainer'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import muliLogo from '../../assets/muli-logo.png'
export function TopBar() {
const [searchQuery, setSearchQuery] = useState('')
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const selectedCount = usePhotoStore((state) => state.selectedPhotos.length)
const selectedPhotos = usePhotoStore((state) => state.selectedPhotos)
const clearSelection = usePhotoStore((state) => state.clearSelection)
const selectedCount = selectedPhotos.length
const queryClient = useQueryClient()
// Mutation for moving photos to trash
const trashPhotosMutation = useMutation({
mutationFn: async () => {
await photos.bulkUpdate(selectedPhotos, { trash: true })
},
onSuccess: () => {
toast.success('Moved to Trash', `${selectedCount} photo${selectedCount > 1 ? 's' : ''} moved to trash`)
clearSelection()
queryClient.invalidateQueries({ queryKey: ['photos'] })
},
onError: (error: any) => {
toast.error('Failed to Move to Trash', error.message || 'An error occurred')
},
})
return (
<header className="flex h-12 items-center justify-between border-b border-border bg-surface px-4">
{/* Left Section - Menu and App Name */}
<div className="flex items-center gap-3">
<button
className="rounded p-1 text-text-muted hover:bg-surface-2 hover:text-text"
title="Toggle sidebar"
className="group relative rounded p-1 text-text-muted hover:bg-surface-2 hover:text-text"
title="Toggle sidebar (Tab)"
>
<Menu className="h-5 w-5" />
<kbd className="absolute -bottom-5 left-1/2 -translate-x-1/2 rounded bg-surface-offset px-1 py-0.5 text-[9px] font-medium text-text opacity-0 group-hover:opacity-100">
Tab
</kbd>
</button>
<h1 className="text-lg font-semibold text-text">Mulita</h1>
<div className="flex items-center gap-2">
<img src={muliLogo} alt="Mulita" className="h-7 w-7 object-contain" />
<h1 className="text-lg font-semibold text-text">Mulita</h1>
</div>
{selectedCount > 0 && (
<span className="rounded bg-primary/20 px-2 py-0.5 text-sm text-primary">
{selectedCount} selected
</span>
<>
<span className="rounded bg-primary/20 px-2 py-0.5 text-sm text-primary">
{selectedCount} selected
</span>
<button
onClick={() => trashPhotosMutation.mutate()}
disabled={trashPhotosMutation.isPending}
className="flex items-center gap-1 rounded bg-reject/20 px-2 py-0.5 text-sm text-reject hover:bg-reject/30 disabled:opacity-50"
title="Move to trash"
>
<Trash2 className="h-3.5 w-3.5" />
Trash
</button>
</>
)}
</div>
@@ -81,10 +121,13 @@ export function TopBar() {
{/* Filter Button */}
<button
className="rounded p-1.5 text-text-muted hover:bg-surface-2 hover:text-text"
title="Filter photos"
className="group relative rounded p-1.5 text-text-muted hover:bg-surface-2 hover:text-text"
title="Filter photos (Ctrl+F)"
>
<SlidersHorizontal className="h-4 w-4" />
<kbd className="absolute -bottom-5 left-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-surface-offset px-1 py-0.5 text-[9px] font-medium text-text opacity-0 group-hover:opacity-100">
Ctrl+F
</kbd>
</button>
<div className="mx-1 h-6 w-px bg-border" />