fix(photos): faster large-album loading and album cover lifecycle

Large albums opened slowly and flickered; the default album cover was
also lost in the media-objects refactor.

Large-album loading:
- quantize signed object URLs within a window and send Cache-Control so
  previews are reused instead of re-fetched
- publish photo.updated (not photo.created) when variants are ready, so
  clients update in place without reordering the grid
- worker concurrency + newest-first variant priority; raise default batch

Album cover:
- first uploaded photo becomes the cover when none is set
- clear the cover when the cover photo is trashed or moved out
- "set as album cover" in the photo context menu and actions panel

OpenSpec changes: fix-large-album-load, fix-album-cover
This commit is contained in:
iu
2026-10-11 14:15:59 +03:00
parent 6361e48b20
commit d7a37328f0
40 changed files with 999 additions and 57 deletions
@@ -75,6 +75,13 @@
.catch((e) => (error = (e as Error).message));
});
$effect(() => {
// Keep the local album in sync with cover changes made elsewhere
// (context menu, photo panel) that go through the albums store.
const updated = albums.list.find((a) => a.id === albumId);
if (updated) album = updated;
});
$effect(() => {
const id = photoId;
if (!id) {
@@ -6,6 +6,7 @@
import PanelSection from './PanelSection.svelte';
import PhotoInfoSettingsDialog from './PhotoInfoSettingsDialog.svelte';
import { auth } from '$lib/stores/auth.svelte';
import { albums } from '$lib/stores/albums.svelte';
import { transfer } from '$lib/stores/transfer.svelte';
import { viewerActions } from '$lib/stores/viewer-actions.svelte';
import { buildExifRows, EXIF_BASE, EXIF_CATALOG, EXIF_LABELS } from '$lib/exif';
@@ -179,6 +180,14 @@
}
}
async function setAsCover() {
try {
await albums.setCover(photo.album_id, photo.id);
} catch {
/* ignore */
}
}
async function confirmDelete() {
deleting = true;
try {
@@ -273,6 +282,12 @@
<Icon name="content_copy" class="size-4" />
Копировать
</button>
{#if canEdit}
<button type="button" class={navLink} onclick={setAsCover}>
<Icon name="wallpaper" class="size-4" />
Установить как обложку альбома
</button>
{/if}
{#if canDelete}
<button
type="button"
+1
View File
@@ -16,6 +16,7 @@ export class SSEClient {
private attempt = 0;
private readonly knownEvents = [
'photo.created',
'photo.updated',
'photo.deleted',
'comment.created',
'like.added',
@@ -29,6 +29,7 @@
let uploader = $state<{ pick: () => void; upload: (files: FileList | File[]) => void }>();
let canUpload = $derived(['owner', 'admin', 'editor', 'member'].includes(role));
let canEdit = $derived(role === 'owner' || role === 'admin');
async function loadAlbum() {
const data = await api<{ album: Album; role: string }>(`/albums/${albumId}`);
@@ -100,6 +101,8 @@
break;
}
case 'photo.updated': {
// Variant readiness updates a photo already in the grid; never
// insert an unseen photo here, or the grid would reflow.
const photo = data as unknown as Photo;
photos = photos.map((p) => (p.id === photo.id ? photo : p));
break;
@@ -221,6 +224,15 @@
}
]
: []),
...(canEdit
? [
{
label: 'Установить как обложку альбома',
icon: 'wallpaper' as const,
run: () => albums.setCover(albumId, photo.id)
}
]
: []),
{
label: 'Переместить',
icon: 'arrow_back',