feat: manual albums + gallery multi-select with bulk actions
Albums (manual collections): - New albums/album_images tables with FK cascades; DB functions and Tauri commands for create/rename/delete/delete-many/reorder/list, add/remove images, and paginated get_album_images. - Distinct sidebar "ALBUMS" section with cover thumbnails, create/rename/ delete, and a Manage multi-select mode for bulk album deletion. - Album view reuses the gallery grid (activeView "album" + selectedAlbumId); spans folders; add from the bulk bar or the lightbox, remove from within. Gallery multi-select + bulk actions: - gallerySelectedIds selection model with a top-left corner checkbox that reveals on corner hover; click-to-toggle in selection mode, double-click to open. - Floating BulkActionBar: tag (inline autocomplete popover), rating, favorite, add-to-album, and a delete with an explicit "from disk" confirmation. Batch commands bulk_update_details/bulk_add_tags/ bulk_remove_tag. Also: - Duplicate Finder delete now requires confirmation with clear "from disk" wording (was single-click fire-and-forget). - CPU/CUDA build-variant badge in Settings (get_build_variant). - Rating/favorite no longer re-sorts derived collections (similar/region/ semantic/tag/album results); single and bulk paths replace in place there. - Album-aware indexed-images/media-updated handlers so thumbnails paint and newly-indexed files don't leak into an album view. - CHANGELOG updated.
This commit is contained in:
@@ -153,6 +153,9 @@ export function Lightbox() {
|
||||
const removeTag = useGalleryStore((state) => state.removeTag);
|
||||
const taggerModelStatus = useGalleryStore((state) => state.taggerModelStatus);
|
||||
const queueTaggingForImage = useGalleryStore((state) => state.queueTaggingForImage);
|
||||
const albums = useGalleryStore((state) => state.albums);
|
||||
const addToAlbum = useGalleryStore((state) => state.addToAlbum);
|
||||
const createAlbum = useGalleryStore((state) => state.createAlbum);
|
||||
|
||||
// Tracks the image id that is currently displayed, used to discard async
|
||||
// tag mutations that resolve after the user has navigated to another image.
|
||||
@@ -170,6 +173,9 @@ export function Lightbox() {
|
||||
const [taggingQueued, setTaggingQueued] = useState(false);
|
||||
|
||||
// Region selection state
|
||||
const [albumMenuOpen, setAlbumMenuOpen] = useState(false);
|
||||
const [albumAddedTo, setAlbumAddedTo] = useState<number | null>(null);
|
||||
const [newAlbumName, setNewAlbumName] = useState("");
|
||||
const [regionSelectMode, setRegionSelectMode] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragRect, setDragRect] = useState<DragRect | null>(null);
|
||||
@@ -780,6 +786,72 @@ export function Lightbox() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<p className="text-xs uppercase tracking-wider text-gray-500">Albums</p>
|
||||
<button
|
||||
className="rounded-md border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-gray-400 transition-colors hover:bg-white/10 hover:text-white"
|
||||
onClick={() => { setAlbumMenuOpen((v) => !v); setAlbumAddedTo(null); }}
|
||||
>
|
||||
Add to album
|
||||
</button>
|
||||
</div>
|
||||
{albumMenuOpen ? (
|
||||
<div className="rounded-lg border border-white/10 bg-white/[0.03] p-1.5">
|
||||
<div className="max-h-40 overflow-y-auto">
|
||||
{albums.length === 0 ? (
|
||||
<p className="px-2 py-1.5 text-[11px] text-gray-600">No albums yet — create one below.</p>
|
||||
) : (
|
||||
albums.map((album) => (
|
||||
<button
|
||||
key={album.id}
|
||||
className="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1 text-left text-xs text-gray-300 transition-colors hover:bg-white/5 hover:text-white"
|
||||
onClick={() => {
|
||||
void addToAlbum(album.id, [selectedImage.id]);
|
||||
setAlbumAddedTo(album.id);
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{album.name}</span>
|
||||
{albumAddedTo === album.id ? (
|
||||
<span className="shrink-0 text-[10px] text-emerald-400">Added</span>
|
||||
) : (
|
||||
<span className="shrink-0 text-[10px] text-gray-600">{album.image_count}</span>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
className="mt-1 flex gap-1 border-t border-white/[0.06] pt-1.5"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const name = newAlbumName.trim();
|
||||
if (!name) return;
|
||||
void createAlbum(name).then((album) => {
|
||||
void addToAlbum(album.id, [selectedImage.id]);
|
||||
setAlbumAddedTo(album.id);
|
||||
});
|
||||
setNewAlbumName("");
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="min-w-0 flex-1 rounded-md border border-white/10 bg-white/5 px-2 py-1 text-xs text-white placeholder-gray-600 focus:border-white/20 focus:outline-none"
|
||||
placeholder="New album…"
|
||||
value={newAlbumName}
|
||||
onChange={(e) => setNewAlbumName(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md border border-white/10 bg-white/5 px-2 py-1 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white disabled:opacity-50"
|
||||
disabled={!newAlbumName.trim()}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<p className="text-xs uppercase tracking-wider text-gray-500">Path</p>
|
||||
|
||||
Reference in New Issue
Block a user