Skip to content

Commit f2473b7

Browse files
cdzombakclaude
andauthored
Add video support to embedded galleries; fix gallery limit bug (#3821)
Co-authored-by: Claude <[email protected]>
1 parent 81874a9 commit f2473b7

File tree

6 files changed

+122
-11
lines changed

6 files changed

+122
-11
lines changed

app/Http/Requests/Embed/EmbededRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ protected function processValidatedValues(array $values, array $files): void
6666

6767
// Validate and cap limit to 500 max
6868
if ($limit !== null) {
69-
$this->limit = max(1, min((int) $this->limit, 500));
69+
$this->limit = max(1, min((int) $limit, 500));
7070
}
7171
$this->offset = max(0, (int) $offset);
7272

app/Http/Resources/Embed/EmbedPhotoResource.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace App\Http\Resources\Embed;
1010

11+
use App\Assets\Helpers;
1112
use App\Http\Resources\Models\SizeVariantsResouce;
1213
use App\Models\Photo;
1314
use Spatie\LaravelData\Data;
@@ -25,6 +26,8 @@ class EmbedPhotoResource extends Data
2526
public string $id;
2627
public ?string $title;
2728
public ?string $description;
29+
public bool $is_video;
30+
public ?string $duration;
2831
public SizeVariantsResouce $size_variants;
2932
/** @var array<string, string|null> */
3033
public array $exif;
@@ -34,6 +37,12 @@ public function __construct(Photo $photo)
3437
$this->id = $photo->id;
3538
$this->title = $photo->title;
3639
$this->description = $photo->description;
40+
$this->is_video = $photo->isVideo();
41+
42+
// For videos, aperture field stores duration in seconds
43+
$this->duration = $this->is_video && $photo->aperture !== null
44+
? app(Helpers::class)->secondsToHMS(intval($photo->aperture))
45+
: null;
3746

3847
// Reuse existing SizeVariantsResouce instead of duplicating logic
3948
// Pass null for album since embeds are always public

resources/js/embed/components/EmbedWidget.vue

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@
5757
@keydown.enter="openLightbox(albumData.photos[filmstripActiveIndex].id)"
5858
@keydown.space.prevent="openLightbox(albumData.photos[filmstripActiveIndex].id)"
5959
/>
60+
<!-- Video play icon overlay for filmstrip main viewer -->
61+
<div v-if="albumData && albumData.photos[filmstripActiveIndex]?.is_video" class="lychee-embed__video-overlay">
62+
<svg class="lychee-embed__play-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
63+
<circle cx="50" cy="50" r="45" fill="rgba(0, 0, 0, 0.6)" />
64+
<polygon points="37,30 37,70 70,50" fill="white" />
65+
</svg>
66+
</div>
6067

6168
<!-- Navigation arrows -->
6269
<button
@@ -117,6 +124,13 @@
117124
loading="lazy"
118125
class="lychee-embed__photo-img"
119126
/>
127+
<!-- Video play icon overlay -->
128+
<div v-if="thumb.photo.is_video" class="lychee-embed__video-overlay">
129+
<svg class="lychee-embed__play-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
130+
<circle cx="50" cy="50" r="45" fill="rgba(0, 0, 0, 0.6)" />
131+
<polygon points="37,30 37,70 70,50" fill="white" />
132+
</svg>
133+
</div>
120134
</div>
121135
</div>
122136
</div>
@@ -148,6 +162,13 @@
148162
loading="lazy"
149163
class="lychee-embed__photo-img"
150164
/>
165+
<!-- Video play icon overlay -->
166+
<div v-if="photo.is_video" class="lychee-embed__video-overlay">
167+
<svg class="lychee-embed__play-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
168+
<circle cx="50" cy="50" r="45" fill="rgba(0, 0, 0, 0.6)" />
169+
<polygon points="37,30 37,70 70,50" fill="white" />
170+
</svg>
171+
</div>
151172
</div>
152173
</div>
153174

@@ -611,4 +632,34 @@ watch(
611632
object-fit: cover;
612633
display: block;
613634
}
635+
636+
/* Video play icon overlay */
637+
.lychee-embed__video-overlay {
638+
position: absolute;
639+
top: 0;
640+
left: 0;
641+
width: 100%;
642+
height: 100%;
643+
display: flex;
644+
align-items: center;
645+
justify-content: center;
646+
pointer-events: none;
647+
transition: opacity 0.3s ease;
648+
}
649+
650+
.lychee-embed__photo:hover .lychee-embed__video-overlay,
651+
.lychee-embed__filmstrip-thumb:hover .lychee-embed__video-overlay,
652+
.lychee-embed__filmstrip-main:hover .lychee-embed__video-overlay {
653+
opacity: 0.7;
654+
}
655+
656+
.lychee-embed__play-icon {
657+
width: 20%;
658+
height: 20%;
659+
min-width: 40px;
660+
min-height: 40px;
661+
max-width: 80px;
662+
max-height: 80px;
663+
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
664+
}
614665
</style>

resources/js/embed/components/Lightbox.vue

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,27 @@
3333
</svg>
3434
</button>
3535

36-
<!-- Photo display -->
36+
<!-- Photo/Video display -->
3737
<div class="lychee-lightbox-content">
3838
<div class="lychee-lightbox-image-container">
39+
<!-- Video player -->
40+
<video
41+
v-if="currentPhoto && currentPhoto.is_video"
42+
:src="getVideoUrl(currentPhoto)"
43+
class="lychee-lightbox-image"
44+
controls
45+
autoplay
46+
@loadeddata="handleImageLoad"
47+
@error="handleImageError"
48+
@click.stop
49+
:title="currentPhoto.title || undefined"
50+
>
51+
Your browser does not support the video tag.
52+
</video>
53+
54+
<!-- Image display -->
3955
<img
40-
v-if="currentPhoto"
56+
v-else-if="currentPhoto"
4157
:src="getPhotoUrl(currentPhoto)"
4258
:alt="currentPhoto.title || 'Photo'"
4359
class="lychee-lightbox-image"
@@ -66,16 +82,26 @@
6682
<span>{{ [currentPhoto.exif.make, currentPhoto.exif.model].filter(Boolean).join(" ") }}</span>
6783
</div>
6884

69-
<div v-if="currentPhoto.exif.lens" class="lychee-lightbox-exif-item">
85+
<div v-if="!currentPhoto.is_video && currentPhoto.exif.lens" class="lychee-lightbox-exif-item">
7086
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
7187
<circle cx="12" cy="12" r="10"></circle>
7288
<circle cx="12" cy="12" r="3"></circle>
7389
</svg>
7490
<span>{{ currentPhoto.exif.lens }}</span>
7591
</div>
7692

77-
<div v-if="currentPhoto.exif.focal" class="lychee-lightbox-exif-item">
78-
<span class="lychee-lightbox-exif-label">{{ currentPhoto.exif.focal }}</span>
93+
<!-- Photo-specific EXIF (focal length, aperture, shutter) -->
94+
<div
95+
v-if="
96+
!currentPhoto.is_video &&
97+
(currentPhoto.exif.focal ||
98+
currentPhoto.exif.aperture ||
99+
currentPhoto.exif.shutter ||
100+
currentPhoto.exif.iso)
101+
"
102+
class="lychee-lightbox-exif-item"
103+
>
104+
<span v-if="currentPhoto.exif.focal" class="lychee-lightbox-exif-label">{{ currentPhoto.exif.focal }}</span>
79105
<span v-if="currentPhoto.exif.aperture" class="lychee-lightbox-exif-label"
80106
>f/{{ currentPhoto.exif.aperture }}</span
81107
>
@@ -85,6 +111,24 @@
85111
<span v-if="currentPhoto.exif.iso" class="lychee-lightbox-exif-label">ISO {{ currentPhoto.exif.iso }}</span>
86112
</div>
87113

114+
<!-- Video-specific metadata (duration and framerate) -->
115+
<div
116+
v-if="currentPhoto.is_video && (currentPhoto.duration || currentPhoto.exif.focal)"
117+
class="lychee-lightbox-exif-item"
118+
>
119+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
120+
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect>
121+
<line x1="7" y1="2" x2="7" y2="22"></line>
122+
<line x1="17" y1="2" x2="17" y2="22"></line>
123+
<line x1="2" y1="12" x2="22" y2="12"></line>
124+
</svg>
125+
<span v-if="currentPhoto.duration" class="lychee-lightbox-exif-label">{{ currentPhoto.duration }}</span>
126+
<span v-if="currentPhoto.exif.focal" class="lychee-lightbox-exif-label"
127+
>{{ currentPhoto.exif.focal }} fps</span
128+
>
129+
<span v-if="currentPhoto.exif.iso" class="lychee-lightbox-exif-label">ISO {{ currentPhoto.exif.iso }}</span>
130+
</div>
131+
88132
<div v-if="currentPhoto.exif.taken_at" class="lychee-lightbox-exif-item">
89133
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
90134
<circle cx="12" cy="12" r="10"></circle>
@@ -172,6 +216,14 @@ function getPhotoUrl(photo: Photo): string {
172216
);
173217
}
174218
219+
/**
220+
* Get the video URL for lightbox playback
221+
*/
222+
function getVideoUrl(photo: Photo): string {
223+
// For videos, always use the original size variant
224+
return photo.size_variants.original?.url || "";
225+
}
226+
175227
/**
176228
* Format date for display
177229
*/

resources/js/embed/types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ export interface Photo {
9696
id: string;
9797
title: string | null;
9898
description: string | null;
99+
is_video: boolean;
100+
duration: string | null;
99101
size_variants: {
100102
placeholder: SizeVariantData | null;
101103
thumb: SizeVariantData | null;
@@ -104,10 +106,7 @@ export interface Photo {
104106
small2x: SizeVariantData | null;
105107
medium: SizeVariantData | null;
106108
medium2x: SizeVariantData | null;
107-
original: {
108-
width: number;
109-
height: number;
110-
};
109+
original: SizeVariantData | null;
111110
};
112111
exif: PhotoExif;
113112
}

resources/js/embed/utils/columns.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export function getAspectRatio(width: number, height: number): number {
172172
* @returns Object with width and height, defaults to { width: 1, height: 1 } if all variants are null
173173
*/
174174
export function getSafeDimensions(sizeVariants: {
175-
original: { width: number; height: number };
175+
original: { width?: number; height?: number } | null;
176176
medium: { width?: number; height?: number } | null;
177177
small: { width?: number; height?: number } | null;
178178
thumb: { width?: number; height?: number } | null;

0 commit comments

Comments
 (0)