feat: photo multiselect, move/copy, trash and compact chrome
- backend: soft delete + trash (photos.deleted_at, migration 000005), GET /trash, restore, permanent delete with storage-key refcount (S3 objects removed only when no other album references them); batch endpoints photo-batch/{delete,move,copy} - metadata only, copies clone rows sharing storage keys
- frontend: Ctrl/Shift multiselect; right-panel 'Действия' as icon text rows (Открыть (просмотр), Переместить, Копировать, Удалить); drag selected photos onto rail album -> TransferDialog; /trash route; trash entry as last item in the left rail; empty right panel hidden on overview
- frontend: compact top bars (app header and viewer), compact theme switcher
- openspec: sync lightroom-workspace-ui artifacts (photos/ui-workspace specs, design #20-24, tasks 9.11-9.17)
This commit is contained in:
@@ -131,6 +131,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
function selectPhoto(i: number, e: MouseEvent) {
|
||||
const photo = photos[i];
|
||||
if (!photo) return;
|
||||
const cur = selection.albumId === albumId ? selection.items : [];
|
||||
if (e.shiftKey && selection.anchorId && selection.albumId === albumId) {
|
||||
const a = photos.findIndex((p) => p.id === selection.anchorId);
|
||||
if (a >= 0) {
|
||||
const lo = Math.min(a, i);
|
||||
const hi = Math.max(a, i);
|
||||
selection.set(albumId, photos.slice(lo, hi + 1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
const exists = cur.some((p) => p.id === photo.id);
|
||||
selection.set(
|
||||
albumId,
|
||||
exists ? cur.filter((p) => p.id !== photo.id) : [...cur, photo]
|
||||
);
|
||||
selection.anchorId = photo.id;
|
||||
return;
|
||||
}
|
||||
selection.set(albumId, [photo]);
|
||||
selection.anchorId = photo.id;
|
||||
}
|
||||
|
||||
function dragPhoto(i: number, e: DragEvent) {
|
||||
const photo = photos[i];
|
||||
if (!photo) return;
|
||||
if (!(selection.albumId === albumId && selection.has(photo.id))) {
|
||||
selection.set(albumId, [photo]);
|
||||
selection.anchorId = photo.id;
|
||||
}
|
||||
const ids = selection.ids;
|
||||
e.dataTransfer?.setData('application/x-galo-photos', JSON.stringify(ids));
|
||||
e.dataTransfer?.setData('text/plain', `${ids.length} фото`);
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'copyMove';
|
||||
}
|
||||
|
||||
function onDrop(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
dragging = false;
|
||||
@@ -151,6 +190,7 @@
|
||||
aria-label="Альбом"
|
||||
ondragover={(e) => {
|
||||
if (!canUpload) return;
|
||||
if (!e.dataTransfer?.types.includes('Files')) return;
|
||||
e.preventDefault();
|
||||
dragging = true;
|
||||
}}
|
||||
@@ -202,11 +242,9 @@
|
||||
{:else}
|
||||
<PhotoGrid
|
||||
{photos}
|
||||
selectedId={selection.albumId === albumId ? (selection.photo?.id ?? null) : null}
|
||||
onSelect={(i) => {
|
||||
const p = photos[i];
|
||||
if (p) selection.select(albumId, p);
|
||||
}}
|
||||
selectedIds={selection.albumId === albumId ? selection.ids : []}
|
||||
onSelect={selectPhoto}
|
||||
onDragStart={dragPhoto}
|
||||
onOpen={(i) => {
|
||||
const p = photos[i];
|
||||
if (p) goto(`/albums/${albumId}/photos/${p.id}`);
|
||||
|
||||
Reference in New Issue
Block a user