feat(share): inline media preview on single-file share landing

The public share page only rendered media previews for FOLDER shares —
a single-file share got a bare icon + download button, even for images
and videos the browser can play natively.

Backend: resolve the shared file's mime_type + size at read time and
expose them on ShareDto (meta + password-verify endpoints, one shared
enrichment helper). Display-only enrichment: a failed file lookup
leaves the fields None instead of failing the response — the download
endpoint still surfaces the real error.

Frontend: the 'file' view now reuses the folder grid's lazyVideo
(poster-seek + retry) for video and imageRetry for images, with
Range-aware streaming already provided by /api/s/{token}/file/{id}.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-14 16:00:30 +08:00
parent 4d067b3fc6
commit 6ee26e46e6
5 changed files with 174 additions and 3 deletions
+10
View File
@@ -9,6 +9,16 @@ import type { ItemType } from '$lib/api/types';
export interface ShareMeta {
item_type: ItemType;
item_name: string;
/** The shared item's id — file shares use it to build the preview src. */
item_id: string;
/**
* File shares only: resolved by the server at read time so the landing
* page can inline a media preview (video player / image) instead of a
* bare download button. Absent for folder shares.
*/
mime_type?: string;
/** File shares only: the shared file's size in bytes. */
size?: number;
}
export interface ShareFolderEntry {
+30 -1
View File
@@ -287,7 +287,29 @@
</form>
{:else if view === 'file'}
<div class="share__center">
<Icon name="file" class="share__big-icon" />
{#if meta && mediaKind(meta.mime_type) === 'video'}
<!-- Inline player: Range-aware endpoint streams the video, so the
timeline seeks without downloading the whole file first.
lazyVideo defers the load, seeks a few frames in for a poster
and retries once on error (same behaviour as the folder grid). -->
<video
class="share__media"
data-testid="public-share-file-video"
use:lazyVideo={shareFileUrl(token, meta.item_id)}
controls
playsinline
></video>
{:else if meta && mediaKind(meta.mime_type) === 'image'}
<img
class="share__media"
data-testid="public-share-file-image"
src={shareFileUrl(token, meta.item_id)}
alt={meta.item_name}
use:imageRetry
/>
{:else}
<Icon name="file" class="share__big-icon" />
{/if}
<h1>{meta?.item_name}</h1>
<a
class="share__btn"
@@ -486,6 +508,13 @@
text-align: center;
}
.share__media {
max-width: 100%;
max-height: min(70vh, 40rem);
border-radius: var(--radius-2xl);
border: 1px solid var(--color-border);
}
:global(.share__big-icon) {
font-size: 3rem;
color: var(--color-text-muted);