feat: add video playback support in lightbox and photo grid
- Lightbox renders <video> with controls for video assets - PhotoGrid shows video indicator overlay on video thumbnails - Skip video preloading in lightbox (streams on demand) - Add video-related CSS styles - Update catch-all page to pass video URLs to components
This commit is contained in:
@@ -34,9 +34,9 @@ export function AlbumDetailView({
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
{album.description && <p className="album-header__description">{album.description}</p>}
|
||||
<p className="album-header__meta">
|
||||
{images.length} {images.length === 1 ? 'photo' : 'photos'}
|
||||
{album.description && ` · ${album.description}`}
|
||||
</p>
|
||||
</div>
|
||||
<PhotoGrid assets={images} layout={layout} gridStyle={gridStyle} />
|
||||
|
||||
@@ -15,9 +15,11 @@ import { FadeIn } from '@/components/FadeIn';
|
||||
|
||||
export interface PhotoItem {
|
||||
id: string;
|
||||
type: 'image' | 'video';
|
||||
thumbUrl: string;
|
||||
previewUrl: string;
|
||||
originalUrl: string;
|
||||
videoUrl?: string;
|
||||
exifUrl: string;
|
||||
blurDataURL?: string;
|
||||
dominantColor?: string;
|
||||
@@ -173,6 +175,14 @@ export function PhotoGrid({ assets, layout = 'masonry', gridStyle }: PhotoGridPr
|
||||
? { placeholder: 'blur' as const, blurDataURL: asset.blurDataURL }
|
||||
: {})}
|
||||
/>
|
||||
{asset.type === 'video' && (
|
||||
<div className="photo-grid__item-play" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="12" fillOpacity="0.55" />
|
||||
<polygon points="10,8 10,16 17,12" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{(asset.camera || asset.lens) && (
|
||||
<div className="photo-grid__item-exif" aria-hidden="true">
|
||||
{[asset.camera, asset.lens, asset.focalLength].filter(Boolean).join(' · ')}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { PhotoGrid, type PhotoItem } from './PhotoGrid';
|
||||
import {
|
||||
imageUrl,
|
||||
exifUrl,
|
||||
videoUrl,
|
||||
assetPlaceholder,
|
||||
assetExifSummary,
|
||||
assetAspectRatio,
|
||||
@@ -50,7 +51,7 @@ export async function generateMetadata({ params }: PathPageProps): Promise<Metad
|
||||
const album = await immich.getAlbumBySlug(path[1], slug);
|
||||
if (album) {
|
||||
title = album.albumName;
|
||||
const count = album.assets.filter((a) => a.type === 'IMAGE').length;
|
||||
const count = album.assets.filter((a) => a.type === 'IMAGE' || a.type === 'VIDEO').length;
|
||||
subtitle = `${count} photo${count === 1 ? '' : 's'}`;
|
||||
}
|
||||
} else if (immich.isSubpageSlug(slug)) {
|
||||
@@ -61,7 +62,7 @@ export async function generateMetadata({ params }: PathPageProps): Promise<Metad
|
||||
const album = await immich.getAlbumBySlug(result.albums[0].slug, slug);
|
||||
if (album) {
|
||||
title = album.albumName;
|
||||
const count = album.assets.filter((a) => a.type === 'IMAGE').length;
|
||||
const count = album.assets.filter((a) => a.type === 'IMAGE' || a.type === 'VIDEO').length;
|
||||
subtitle = `${count} photo${count === 1 ? '' : 's'}`;
|
||||
}
|
||||
} else {
|
||||
@@ -72,7 +73,7 @@ export async function generateMetadata({ params }: PathPageProps): Promise<Metad
|
||||
const album = await immich.getAlbumBySlug(slug);
|
||||
if (album) {
|
||||
title = album.albumName;
|
||||
const count = album.assets.filter((a) => a.type === 'IMAGE').length;
|
||||
const count = album.assets.filter((a) => a.type === 'IMAGE' || a.type === 'VIDEO').length;
|
||||
subtitle = `${count} photo${count === 1 ? '' : 's'}`;
|
||||
}
|
||||
}
|
||||
@@ -89,15 +90,18 @@ export async function generateMetadata({ params }: PathPageProps): Promise<Metad
|
||||
/** Map Immich assets to PhotoItem props for the grid/lightbox. */
|
||||
function toPhotoItems(assets: ImmichAsset[], showExif: boolean): PhotoItem[] {
|
||||
return assets
|
||||
.filter((a) => a.type === 'IMAGE')
|
||||
.filter((a) => a.type === 'IMAGE' || a.type === 'VIDEO')
|
||||
.map((a) => {
|
||||
const ph = assetPlaceholder(a);
|
||||
const exif = showExif ? assetExifSummary(a) : undefined;
|
||||
const exif = showExif && a.type === 'IMAGE' ? assetExifSummary(a) : undefined;
|
||||
const isVideo = a.type === 'VIDEO';
|
||||
return {
|
||||
id: a.id,
|
||||
thumbUrl: imageUrl(a.id, 'preview'),
|
||||
type: isVideo ? 'video' : 'image',
|
||||
thumbUrl: imageUrl(a.id, 'thumbnail'),
|
||||
previewUrl: imageUrl(a.id, 'preview'),
|
||||
originalUrl: imageUrl(a.id, 'original'),
|
||||
...(isVideo ? { videoUrl: videoUrl(a.id) } : {}),
|
||||
exifUrl: exifUrl(a.id),
|
||||
...(ph ? { blurDataURL: ph.blurDataURL, dominantColor: ph.dominantColor } : {}),
|
||||
...(exif ?? {}),
|
||||
|
||||
Reference in New Issue
Block a user