web: single-pointer selection + post-action focus, video debounce, tags metadata sidebar

Selection: plain arrow nav now clears prior multi-selection so exactly
one tile is ringed at a time; shift-extend still grows from the anchor.
onApprove / onRestore / onDelete advance focus via focusAfter(ids)
before clearing selection, matching onArchive.

Preview: defer mounting <VideoPlayer> by 250ms so arrow-skim across
video tiles doesn't open and immediately cancel range requests; hard-
abort the underlying <video> on unmount so the connection releases.

Tags drill view: right-sidebar metadata wired in (single-photo
RightSidebar, BulkMetadataSidebar for >=2 selected), resizable edge
mirrors the timeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 23:52:16 +02:00
parent c783f129cc
commit 155e9bb126
7 changed files with 173 additions and 37 deletions

View File

@@ -10,7 +10,7 @@
- the two CSS imports ship the dark theme and video-layout styles
-->
<script lang="ts">
import { onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import 'vidstack/player/styles/default/theme.css';
import 'vidstack/player/styles/default/layouts/video.css';
@@ -22,6 +22,10 @@
let { src, poster, title }: Props = $props();
let ready = $state(false);
// Whichever branch is currently mounted (fallback <video> or vidstack
// <media-player>) binds here. On destroy we reach through this ref to
// abort the in-flight range request — see onDestroy below.
let host = $state<HTMLElement | undefined>();
onMount(async () => {
// Bundle these into the client chunk only — the modules side-effect
@@ -31,6 +35,27 @@
await import('vidstack/player/layouts/default');
ready = true;
});
// Hard-abort the underlying <video> on unmount. Detaching the element
// alone doesn't reliably release the HTTP range request — the connection
// can linger as "Pending" long enough to starve the grid's thumbnail
// queue. Explicitly pausing, clearing src, and calling load() tells the
// network layer to drop the stream now.
onDestroy(() => {
if (!host) return;
const v =
host.tagName.toLowerCase() === 'video'
? (host as HTMLVideoElement)
: host.querySelector('video');
if (!v) return;
try {
v.pause();
v.removeAttribute('src');
v.load();
} catch {
// Element may already be detached / GC'd; ignore.
}
});
</script>
{#if ready}
@@ -43,6 +68,7 @@
directly, no probe needed.
-->
<media-player
bind:this={host}
class="vds-player max-h-full max-w-full rounded-md shadow-2xl"
title={title ?? ''}
{poster}
@@ -64,6 +90,7 @@
visible so the transition into the full chrome isn't jarring. -->
<!-- svelte-ignore a11y_media_has_caption -->
<video
bind:this={host}
{src}
{poster}
controls