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:
@@ -5,9 +5,36 @@
|
||||
import CreateAlbumDialog from './CreateAlbumDialog.svelte';
|
||||
import { albums } from '$lib/stores/albums.svelte';
|
||||
import { auth } from '$lib/stores/auth.svelte';
|
||||
import { transfer } from '$lib/stores/transfer.svelte';
|
||||
|
||||
const activeId = $derived($page.params.id ?? '');
|
||||
const onTrash = $derived($page.url.pathname === '/trash');
|
||||
let creating = $state(false);
|
||||
let dropId = $state<string | null>(null);
|
||||
|
||||
function onDragOver(ev: DragEvent, albumId: string) {
|
||||
if (!ev.dataTransfer?.types.includes('application/x-galo-photos')) return;
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
dropId = albumId;
|
||||
}
|
||||
|
||||
function onDragLeave(albumId: string) {
|
||||
if (dropId === albumId) dropId = null;
|
||||
}
|
||||
|
||||
function onDrop(ev: DragEvent, albumId: string) {
|
||||
dropId = null;
|
||||
const raw = ev.dataTransfer?.getData('application/x-galo-photos');
|
||||
if (!raw) return;
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const ids = JSON.parse(raw) as string[];
|
||||
transfer.start(ids, albumId);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (auth.isAuthenticated && !albums.loaded) albums.load();
|
||||
@@ -42,11 +69,16 @@
|
||||
<li>
|
||||
<a
|
||||
href="/albums/{album.id}"
|
||||
class="flex items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors {album.id ===
|
||||
class="flex items-center gap-2.5 rounded-md border px-2 py-1.5 text-sm transition-colors {album.id ===
|
||||
activeId
|
||||
? 'bg-bg-muted text-text'
|
||||
: 'text-text-muted hover:bg-bg-muted hover:text-text'}"
|
||||
: 'text-text-muted hover:bg-bg-muted hover:text-text'} {dropId === album.id
|
||||
? 'border-primary ring-primary ring-2'
|
||||
: 'border-transparent'}"
|
||||
aria-current={album.id === activeId ? 'page' : undefined}
|
||||
ondragover={(e) => onDragOver(e, album.id)}
|
||||
ondragleave={() => onDragLeave(album.id)}
|
||||
ondrop={(e) => onDrop(e, album.id)}
|
||||
>
|
||||
<span
|
||||
class="bg-bg-muted border-border flex size-9 shrink-0 items-center justify-center overflow-hidden rounded border"
|
||||
@@ -69,6 +101,25 @@
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{#if auth.isAuthenticated}
|
||||
<div class="border-border mt-auto border-t p-2">
|
||||
<a
|
||||
href="/trash"
|
||||
class="flex items-center gap-2.5 rounded-md border px-2 py-1.5 text-sm transition-colors {onTrash
|
||||
? 'bg-bg-muted text-text border-transparent'
|
||||
: 'text-text-muted hover:bg-bg-muted hover:text-text border-transparent'}"
|
||||
aria-current={onTrash ? 'page' : undefined}
|
||||
>
|
||||
<span
|
||||
class="bg-bg-muted border-border flex size-9 shrink-0 items-center justify-center overflow-hidden rounded border"
|
||||
>
|
||||
<Icon name="delete" class="size-4 text-text-subtle" />
|
||||
</span>
|
||||
<span class="truncate">Корзина</span>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if creating}
|
||||
<CreateAlbumDialog
|
||||
onClose={() => (creating = false)}
|
||||
|
||||
Reference in New Issue
Block a user