feat: safe deletion, album/asset delete, trash, README update

- volume-aware deletion: read-only volumes remove DB only, writable
  volumes soft-delete to trash with configurable grace period
- trash page with restore, worker purge sweep (TRASH_RETENTION_DAYS)
- album delete endpoint + sidebar trash icon
- asset delete from timeline selection toolbar
- all listing queries exclude trashed assets (deleted_at IS NULL)
- timeline ordered by EXIF capture date, date-summary endpoint
- README rewritten with features, setup, full env var table
This commit is contained in:
2026-06-01 01:57:53 +02:00
parent 957737ac9b
commit 0077caa743
36 changed files with 752 additions and 125 deletions

View File

@@ -14,11 +14,12 @@ import {
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { useAlbums } from "@/hooks/use-albums"
import { ImageIcon, PlusIcon } from "lucide-react"
import { toast } from "sonner"
import { ImageIcon, PlusIcon, Trash2Icon } from "lucide-react"
export function AlbumSidebar() {
const pathname = usePathname()
const { albums, createAlbum } = useAlbums()
const { albums, createAlbum, deleteAlbum } = useAlbums()
const [isCreating, setIsCreating] = useState(false)
const [newTitle, setNewTitle] = useState("")
@@ -59,14 +60,31 @@ export function AlbumSidebar() {
)}
<SidebarMenu>
{albums.map((album) => (
<SidebarMenuItem key={album.id}>
<SidebarMenuItem key={album.id} className="group/album">
<SidebarMenuButton
asChild
isActive={pathname === `/albums/${album.id}`}
>
<Link href={`/albums/${album.id}`}>
<ImageIcon className="h-4 w-4" />
<span>{album.title}</span>
<span className="flex-1">{album.title}</span>
<Button
variant="ghost"
size="icon"
className="h-5 w-5 opacity-0 group-hover/album:opacity-100"
onClick={async (e) => {
e.preventDefault()
e.stopPropagation()
try {
await deleteAlbum(album.id)
toast.success("Album deleted")
} catch {
toast.error("Failed to delete album")
}
}}
>
<Trash2Icon className="h-3 w-3" />
</Button>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>

View File

@@ -8,7 +8,7 @@ import { ImageViewer } from "./image-viewer"
import { AddToAlbumDialog } from "./add-to-album-dialog"
import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
import { ImagePlusIcon, XIcon, CheckSquareIcon } from "lucide-react"
import { ImagePlusIcon, XIcon, CheckSquareIcon, Trash2Icon } from "lucide-react"
interface PhotoGridProps {
groups: DateGroup[]
@@ -16,6 +16,7 @@ interface PhotoGridProps {
hasMore: boolean
onLoadMore: () => void
onRemoveAsset?: (assetId: string) => void
onDeleteAssets?: (assetIds: string[]) => void
}
export function PhotoGrid({
@@ -24,6 +25,7 @@ export function PhotoGrid({
hasMore,
onLoadMore,
onRemoveAsset,
onDeleteAssets,
}: PhotoGridProps) {
const sentinelRef = useRef<HTMLDivElement>(null)
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
@@ -94,13 +96,26 @@ export function PhotoGrid({
{onRemoveAsset && selectedIds.size > 0 && (
<Button
size="sm"
variant="destructive"
variant="secondary"
onClick={() => {
selectedIds.forEach((id) => onRemoveAsset(id))
exitSelection()
}}
>
Remove
Remove from Album
</Button>
)}
{onDeleteAssets && selectedIds.size > 0 && (
<Button
size="sm"
variant="destructive"
onClick={() => {
onDeleteAssets(Array.from(selectedIds))
exitSelection()
}}
>
<Trash2Icon className="mr-1.5 h-3.5 w-3.5" />
Delete
</Button>
)}
<Button size="sm" variant="ghost" onClick={exitSelection}>