Files
mule-image/web/src/lib/components/preview/VideoPlayer.svelte
dtoro e36f1939c6 feat(preview): inline split-pane preview + sidebar metadata pass
Replace the fullscreen PreviewOverlay with an inline top pane above
each grid surface. SplitGrid + InlinePreview render the focused
photo/video inside the host page; a resizableVertical action drives
the divider and the height persists via the view store. Applied to
the timeline, /tags drill-in, /review cause tabs, /photo/[uid], and
/map. selection.focused is now the single source of truth for both
the inline pane and the right sidebar — preview.svelte store and
PreviewOverlay are removed.

Sidebar: drop the thumb; lead with icon-led filename and folder
rows that match the date/place rhythm. Move dims+size to the top
(below date) and camera/lens/exposure into the collapsible File
section. Read-only spans share the input padding so the text column
aligns across rows. Folder row sits between date and dims+size.

VideoPlayer: stop forcing width/height: 100% so videos honour their
intrinsic aspect ratio inside the pane. Key the player on file hash
in InlinePreview so navigating between videos remounts the element
and autoplay fires again.

Sidebar (LeftSidebar): switch the labels badge to a dedicated
countPhotos('label:*') query so it reports photos with a label
rather than PhotoPrism's category roll-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:46:06 +02:00

98 lines
2.9 KiB
Svelte

<!--
Vidstack-based video player. Wraps the native `<video>` element with a
polished, PhotoPrism-style chrome (gradient bottom bar, large play
overlay, hover-revealed scrubber). Element registration is dynamically
imported so it never runs during SvelteKit's static prerender.
Imports are side-effecting:
- `vidstack/player` registers <media-player> / <media-provider>
- `vidstack/player/layouts/default` registers <media-video-layout>
- the two CSS imports ship the dark theme and video-layout styles
-->
<script lang="ts">
import { onMount } from 'svelte';
import 'vidstack/player/styles/default/theme.css';
import 'vidstack/player/styles/default/layouts/video.css';
interface Props {
src: string;
poster?: string;
title?: string;
}
let { src, poster, title }: Props = $props();
let ready = $state(false);
onMount(async () => {
// Bundle these into the client chunk only — the modules side-effect
// `customElements.define()` calls, which would crash under SSR.
await import('vidstack/player');
await import('vidstack/player/ui');
await import('vidstack/player/layouts/default');
ready = true;
});
</script>
{#if ready}
<!--
Note: `src` is *not* set as an attribute on <media-player>. Our backend
URL ends in `/avc` (not `.mp4`), so Vidstack's URL-suffix sniff fails
and it falls back to a HEAD probe — which can pick the wrong loader
and produce decode errors. Declaring the source via a nested
<source type="video/mp4"> tells Vidstack to use the native MP4 loader
directly, no probe needed.
-->
<media-player
class="vds-player max-h-full max-w-full rounded-md shadow-2xl"
title={title ?? ''}
{poster}
autoplay
muted
playsinline
load="eager"
posterLoad="eager"
streamType="on-demand"
viewType="video"
>
<media-provider>
<source {src} type="video/mp4" />
</media-provider>
<media-video-layout></media-video-layout>
</media-player>
{:else}
<!-- Fallback while the Vidstack module is loading. Keeps the poster
visible so the transition into the full chrome isn't jarring. -->
<!-- svelte-ignore a11y_media_has_caption -->
<video
{src}
{poster}
controls
autoplay
muted
playsinline
preload="auto"
class="max-h-full max-w-full rounded-md object-contain shadow-2xl"
></video>
{/if}
<style>
:global(.vds-player) {
/* Let the player size to the video's intrinsic aspect ratio,
capped by the host pane. Don't set width/height to 100% — that
was stretching widescreen video into the square inline-preview
pane. The default vidstack layout reads the loaded media's
aspect and sizes the box accordingly; we just clamp the upper
bound so it can't escape the SplitGrid top pane. */
max-width: 100%;
max-height: 100%;
--media-brand: #3b82f6;
--media-focus-ring-color: #3b82f6;
}
:global(.vds-player video),
:global(.vds-player media-provider) {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
</style>