feat: timeline and folder import
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user