feat: context menus, unified photo panel, tags, album trash and counters

- backend: deleting an album moves its photos to the trash without losing files (photos.album_id nullable, FK ON DELETE SET NULL, migration 000006; dropped S3 prefix delete); restore accepts an album target (album_required when the album is gone); user-scoped tags (000007) with GET /tags, POST/DELETE /photos/:id/tags, tags included in photo responses; GET /trash/count
- frontend: right-click context menus for photos (grid + viewer) and albums (rail + overview); single shared PhotoPanel reused in viewer and gallery selection (not a copy); viewer download/delete moved to the panel 'Действия'; transparent compact viewer top bar; trash entry as last rail item with per-album photo counts and trash count; album rename/delete dialogs
- openspec: sync lightroom-workspace-ui artifacts (specs photos/ui-workspace, design #24-30, tasks 9.17-9.24)
This commit is contained in:
iu
2026-10-10 22:45:41 +03:00
parent 2d6a3fea1a
commit 61b45b2742
37 changed files with 1639 additions and 328 deletions
@@ -6,6 +6,10 @@
import { albums } from '$lib/stores/albums.svelte';
import { auth } from '$lib/stores/auth.svelte';
import { transfer } from '$lib/stores/transfer.svelte';
import { albumActions } from '$lib/stores/album-actions.svelte';
import { contextMenu, type ContextMenuItem } from '$lib/stores/context-menu.svelte';
import { trash } from '$lib/stores/trash.svelte';
import type { Album } from '$lib/types';
const activeId = $derived($page.params.id ?? '');
const onTrash = $derived($page.url.pathname === '/trash');
@@ -36,9 +40,24 @@
}
}
function onContextMenu(ev: MouseEvent, album: Album) {
ev.preventDefault();
ev.stopPropagation();
const items: ContextMenuItem[] = [
{ label: 'Переименовать', icon: 'edit', run: () => albumActions.openRename(album) },
{ label: 'Удалить', icon: 'delete', danger: true, run: () => albumActions.openDelete(album) }
];
contextMenu.show(ev.clientX, ev.clientY, items);
}
$effect(() => {
if (auth.isAuthenticated && !albums.loaded) albums.load();
});
$effect(() => {
$page.url.pathname;
if (auth.isAuthenticated) trash.load();
});
</script>
<div class="flex h-full flex-col">
@@ -79,6 +98,7 @@
ondragover={(e) => onDragOver(e, album.id)}
ondragleave={() => onDragLeave(album.id)}
ondrop={(e) => onDrop(e, album.id)}
oncontextmenu={(e) => onContextMenu(e, album)}
>
<span
class="bg-bg-muted border-border flex size-9 shrink-0 items-center justify-center overflow-hidden rounded border"
@@ -95,6 +115,7 @@
{/if}
</span>
<span class="truncate">{album.title}</span>
<span class="text-text-subtle ml-auto shrink-0 text-xs">{album.photos_count ?? 0}</span>
</a>
</li>
{/each}
@@ -116,6 +137,7 @@
<Icon name="delete" class="size-4 text-text-subtle" />
</span>
<span class="truncate">Корзина</span>
<span class="text-text-subtle ml-auto shrink-0 text-xs">{trash.count}</span>
</a>
</div>
{/if}