feat: photo rename and viewer delete with confirmation

Backend:
- add PATCH /api/photos/:id to rename original_filename (uploader/owner/admin)
- emit photo.updated SSE event

Frontend:
- inline filename editor in the right panel
- viewer: back button (arrow_back) on the left, delete on the right
- delete confirmed via a modal dialog; filmstrip updates after deletion
This commit is contained in:
iu
2026-10-10 18:50:16 +03:00
parent 1f0e909ffd
commit aa396bdb85
9 changed files with 296 additions and 13 deletions
@@ -90,6 +90,11 @@
photos = photos.filter((p) => p.id !== data.id);
break;
}
case 'photo.updated': {
const photo = data as unknown as Photo;
photos = photos.map((p) => (p.id === photo.id ? photo : p));
break;
}
case 'like.added': {
photos = photos.map((p) =>
p.id === data.photo_id
@@ -3,7 +3,9 @@
import { goto } from '$app/navigation';
import { api } from '$lib/api';
import Viewer from '$lib/components/Viewer.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import { albums } from '$lib/stores/albums.svelte';
import { auth } from '$lib/stores/auth.svelte';
import type { Album, Photo } from '$lib/types';
const albumId = $derived($page.params.id ?? '');
@@ -14,9 +16,15 @@
let loading = $state(true);
let error = $state('');
let saved = $state(false);
let pendingDelete = $state<Photo | null>(null);
let deleting = $state(false);
const canEdit = $derived(role === 'owner' || role === 'admin');
const index = $derived(photos.findIndex((p) => p.id === photoId));
const currentPhoto = $derived(index >= 0 ? photos[index] : null);
const canDelete = $derived(
canEdit || (!!currentPhoto && currentPhoto.uploader_id === auth.user?.id)
);
$effect(() => {
const id = albumId;
@@ -53,6 +61,36 @@
error = (e as Error).message;
}
}
async function removePhoto(photo: Photo) {
pendingDelete = photo;
}
async function confirmDelete() {
const photo = pendingDelete;
if (!photo) return;
deleting = true;
error = '';
try {
await api(`/photos/${photo.id}`, { method: 'DELETE' });
pendingDelete = null;
const remaining = photos.filter((p) => p.id !== photo.id);
if (remaining.length > 0) {
const next = remaining[Math.min(Math.max(index, 0), remaining.length - 1)];
await goto(`/albums/${albumId}/photos/${next.id}`, {
replaceState: true,
noScroll: true
});
} else {
await goto(`/albums/${albumId}`);
}
photos = remaining;
} catch (e) {
error = (e as Error).message;
} finally {
deleting = false;
}
}
</script>
{#if loading}
@@ -74,10 +112,22 @@
{photos}
{index}
{canEdit}
{canDelete}
onClose={close}
onIndexChange={change}
onMakeCover={makeCover}
onDelete={removePhoto}
/>
</div>
</div>
{#if pendingDelete}
<ConfirmDialog
title="Удалить фото?"
message={pendingDelete.original_filename}
busy={deleting}
onConfirm={confirmDelete}
onClose={() => (pendingDelete = null)}
/>
{/if}
{/if}