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:
@@ -5,6 +5,7 @@
|
||||
import CoverPicker from './CoverPicker.svelte';
|
||||
import Comments from './Comments.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 ?? '');
|
||||
@@ -16,17 +17,20 @@
|
||||
let picking = $state(false);
|
||||
|
||||
let photo = $state<Photo | null>(null);
|
||||
let editingName = $state(false);
|
||||
let nameDraft = $state('');
|
||||
let savingName = $state(false);
|
||||
let nameError = $state('');
|
||||
|
||||
const canEdit = $derived(role === 'owner' || role === 'admin');
|
||||
const canRename = $derived(!!photo && (canEdit || photo.uploader_id === auth.user?.id));
|
||||
|
||||
$effect(() => {
|
||||
const id = albumId;
|
||||
error = '';
|
||||
if (!id || photoId) {
|
||||
if (!id) {
|
||||
album = null;
|
||||
role = '';
|
||||
}
|
||||
if (!id) {
|
||||
album = null;
|
||||
role = '';
|
||||
return;
|
||||
}
|
||||
api<{ album: Album; role: string }>(`/albums/${id}`)
|
||||
@@ -40,6 +44,8 @@
|
||||
|
||||
$effect(() => {
|
||||
const id = photoId;
|
||||
editingName = false;
|
||||
nameError = '';
|
||||
if (!id) {
|
||||
photo = null;
|
||||
return;
|
||||
@@ -49,6 +55,37 @@
|
||||
.catch((e) => (error = (e as Error).message));
|
||||
});
|
||||
|
||||
function startEdit() {
|
||||
if (!photo) return;
|
||||
nameDraft = photo.original_filename;
|
||||
nameError = '';
|
||||
editingName = true;
|
||||
}
|
||||
|
||||
async function saveName(ev: Event) {
|
||||
ev.preventDefault();
|
||||
if (!photo || !nameDraft.trim()) return;
|
||||
savingName = true;
|
||||
nameError = '';
|
||||
try {
|
||||
const data = await api<{ photo: Photo }>(`/photos/${photo.id}`, {
|
||||
method: 'PATCH',
|
||||
body: { original_filename: nameDraft.trim() }
|
||||
});
|
||||
photo = data.photo;
|
||||
editingName = false;
|
||||
} catch (e) {
|
||||
nameError = (e as Error).message;
|
||||
} finally {
|
||||
savingName = false;
|
||||
}
|
||||
}
|
||||
|
||||
function autofocus(node: HTMLInputElement) {
|
||||
node.focus();
|
||||
node.select();
|
||||
}
|
||||
|
||||
async function resetCover() {
|
||||
if (!album) return;
|
||||
try {
|
||||
@@ -66,9 +103,48 @@
|
||||
{#if photoId}
|
||||
{#if photo}
|
||||
<div class="space-y-4 p-4">
|
||||
<h2 class="text-text truncate text-sm font-semibold" title={photo.original_filename}>
|
||||
{photo.original_filename}
|
||||
</h2>
|
||||
{#if editingName}
|
||||
<form class="relative" onsubmit={saveName}>
|
||||
<input
|
||||
bind:value={nameDraft}
|
||||
use:autofocus
|
||||
aria-label="Имя файла"
|
||||
class="border-border bg-bg text-text w-full rounded-lg border py-1 pr-16 pl-2 text-sm"
|
||||
/>
|
||||
<div class="absolute top-1/2 right-1 flex -translate-y-1/2 items-center gap-0.5">
|
||||
<button
|
||||
type="submit"
|
||||
class="text-primary hover:bg-bg-muted inline-flex items-center justify-center rounded-md p-1 disabled:opacity-50"
|
||||
aria-label="Сохранить"
|
||||
title="Сохранить"
|
||||
disabled={savingName || !nameDraft.trim()}
|
||||
>
|
||||
<Icon name="check" class="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-text-muted hover:text-text inline-flex items-center justify-center rounded-md p-1"
|
||||
aria-label="Отмена"
|
||||
title="Отмена"
|
||||
onclick={() => (editingName = false)}
|
||||
>
|
||||
<Icon name="close" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{#if nameError}<p class="text-danger text-xs">{nameError}</p>{/if}
|
||||
{:else if canRename}
|
||||
<button
|
||||
type="button"
|
||||
class="text-text hover:text-primary block w-full truncate text-left text-sm font-semibold"
|
||||
title="Переименовать"
|
||||
onclick={startEdit}
|
||||
>
|
||||
{photo.original_filename}
|
||||
</button>
|
||||
{:else}
|
||||
<h2 class="text-text truncate text-sm font-semibold">{photo.original_filename}</h2>
|
||||
{/if}
|
||||
<div>
|
||||
<div class={metaRow}><span>Автор</span><span class="text-text">{photo.uploader.name}</span></div>
|
||||
{#if photo.taken_at}
|
||||
|
||||
Reference in New Issue
Block a user