Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/Http/Requests/Embed/EmbededRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ protected function processValidatedValues(array $values, array $files): void

// Validate and cap limit to 500 max
if ($limit !== null) {
$this->limit = max(1, min((int) $this->limit, 500));
$this->limit = max(1, min((int) $limit, 500));
}
$this->offset = max(0, (int) $offset);

Expand Down
9 changes: 9 additions & 0 deletions app/Http/Resources/Embed/EmbedPhotoResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace App\Http\Resources\Embed;

use App\Assets\Helpers;
use App\Http\Resources\Models\SizeVariantsResouce;
use App\Models\Photo;
use Spatie\LaravelData\Data;
Expand All @@ -25,6 +26,8 @@ class EmbedPhotoResource extends Data
public string $id;
public ?string $title;
public ?string $description;
public bool $is_video;
public ?string $duration;
public SizeVariantsResouce $size_variants;
/** @var array<string, string|null> */
public array $exif;
Expand All @@ -34,6 +37,12 @@ public function __construct(Photo $photo)
$this->id = $photo->id;
$this->title = $photo->title;
$this->description = $photo->description;
$this->is_video = $photo->isVideo();

// For videos, aperture field stores duration in seconds
$this->duration = $this->is_video && $photo->aperture !== null
? app(Helpers::class)->secondsToHMS(intval($photo->aperture))
: null;

// Reuse existing SizeVariantsResouce instead of duplicating logic
// Pass null for album since embeds are always public
Expand Down
51 changes: 51 additions & 0 deletions resources/js/embed/components/EmbedWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
@keydown.enter="openLightbox(albumData.photos[filmstripActiveIndex].id)"
@keydown.space.prevent="openLightbox(albumData.photos[filmstripActiveIndex].id)"
/>
<!-- Video play icon overlay for filmstrip main viewer -->
<div v-if="albumData && albumData.photos[filmstripActiveIndex]?.is_video" class="lychee-embed__video-overlay">
<svg class="lychee-embed__play-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="45" fill="rgba(0, 0, 0, 0.6)" />
<polygon points="37,30 37,70 70,50" fill="white" />
</svg>
</div>

<!-- Navigation arrows -->
<button
Expand Down Expand Up @@ -117,6 +124,13 @@
loading="lazy"
class="lychee-embed__photo-img"
/>
<!-- Video play icon overlay -->
<div v-if="thumb.photo.is_video" class="lychee-embed__video-overlay">
<svg class="lychee-embed__play-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="45" fill="rgba(0, 0, 0, 0.6)" />
<polygon points="37,30 37,70 70,50" fill="white" />
</svg>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -148,6 +162,13 @@
loading="lazy"
class="lychee-embed__photo-img"
/>
<!-- Video play icon overlay -->
<div v-if="photo.is_video" class="lychee-embed__video-overlay">
<svg class="lychee-embed__play-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="45" fill="rgba(0, 0, 0, 0.6)" />
<polygon points="37,30 37,70 70,50" fill="white" />
</svg>
</div>
</div>
</div>

Expand Down Expand Up @@ -611,4 +632,34 @@ watch(
object-fit: cover;
display: block;
}

/* Video play icon overlay */
.lychee-embed__video-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
transition: opacity 0.3s ease;
}

.lychee-embed__photo:hover .lychee-embed__video-overlay,
.lychee-embed__filmstrip-thumb:hover .lychee-embed__video-overlay,
.lychee-embed__filmstrip-main:hover .lychee-embed__video-overlay {
opacity: 0.7;
}

.lychee-embed__play-icon {
width: 20%;
height: 20%;
min-width: 40px;
min-height: 40px;
max-width: 80px;
max-height: 80px;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
}
</style>
62 changes: 57 additions & 5 deletions resources/js/embed/components/Lightbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,27 @@
</svg>
</button>

<!-- Photo display -->
<!-- Photo/Video display -->
<div class="lychee-lightbox-content">
<div class="lychee-lightbox-image-container">
<!-- Video player -->
<video
v-if="currentPhoto && currentPhoto.is_video"
:src="getVideoUrl(currentPhoto)"
class="lychee-lightbox-image"
controls
autoplay
@loadeddata="handleImageLoad"
@error="handleImageError"
@click.stop
:title="currentPhoto.title || undefined"
>
Your browser does not support the video tag.
</video>

<!-- Image display -->
<img
v-if="currentPhoto"
v-else-if="currentPhoto"
:src="getPhotoUrl(currentPhoto)"
:alt="currentPhoto.title || 'Photo'"
class="lychee-lightbox-image"
Expand Down Expand Up @@ -66,16 +82,26 @@
<span>{{ [currentPhoto.exif.make, currentPhoto.exif.model].filter(Boolean).join(" ") }}</span>
</div>

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

<div v-if="currentPhoto.exif.focal" class="lychee-lightbox-exif-item">
<span class="lychee-lightbox-exif-label">{{ currentPhoto.exif.focal }}</span>
<!-- Photo-specific EXIF (focal length, aperture, shutter) -->
<div
v-if="
!currentPhoto.is_video &&
(currentPhoto.exif.focal ||
currentPhoto.exif.aperture ||
currentPhoto.exif.shutter ||
currentPhoto.exif.iso)
"
class="lychee-lightbox-exif-item"
>
<span v-if="currentPhoto.exif.focal" class="lychee-lightbox-exif-label">{{ currentPhoto.exif.focal }}</span>
<span v-if="currentPhoto.exif.aperture" class="lychee-lightbox-exif-label"
>f/{{ currentPhoto.exif.aperture }}</span
>
Expand All @@ -85,6 +111,24 @@
<span v-if="currentPhoto.exif.iso" class="lychee-lightbox-exif-label">ISO {{ currentPhoto.exif.iso }}</span>
</div>

<!-- Video-specific metadata (duration and framerate) -->
<div
v-if="currentPhoto.is_video && (currentPhoto.duration || currentPhoto.exif.focal)"
class="lychee-lightbox-exif-item"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect>
<line x1="7" y1="2" x2="7" y2="22"></line>
<line x1="17" y1="2" x2="17" y2="22"></line>
<line x1="2" y1="12" x2="22" y2="12"></line>
</svg>
<span v-if="currentPhoto.duration" class="lychee-lightbox-exif-label">{{ currentPhoto.duration }}</span>
<span v-if="currentPhoto.exif.focal" class="lychee-lightbox-exif-label"
>{{ currentPhoto.exif.focal }} fps</span
>
<span v-if="currentPhoto.exif.iso" class="lychee-lightbox-exif-label">ISO {{ currentPhoto.exif.iso }}</span>
</div>

<div v-if="currentPhoto.exif.taken_at" class="lychee-lightbox-exif-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
Expand Down Expand Up @@ -172,6 +216,14 @@ function getPhotoUrl(photo: Photo): string {
);
}

/**
* Get the video URL for lightbox playback
*/
function getVideoUrl(photo: Photo): string {
// For videos, always use the original size variant
return photo.size_variants.original?.url || "";
}

/**
* Format date for display
*/
Expand Down
7 changes: 3 additions & 4 deletions resources/js/embed/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export interface Photo {
id: string;
title: string | null;
description: string | null;
is_video: boolean;
duration: string | null;
size_variants: {
placeholder: SizeVariantData | null;
thumb: SizeVariantData | null;
Expand All @@ -104,10 +106,7 @@ export interface Photo {
small2x: SizeVariantData | null;
medium: SizeVariantData | null;
medium2x: SizeVariantData | null;
original: {
width: number;
height: number;
};
original: SizeVariantData | null;
};
exif: PhotoExif;
}
Expand Down
2 changes: 1 addition & 1 deletion resources/js/embed/utils/columns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export function getAspectRatio(width: number, height: number): number {
* @returns Object with width and height, defaults to { width: 1, height: 1 } if all variants are null
*/
export function getSafeDimensions(sizeVariants: {
original: { width: number; height: number };
original: { width?: number; height?: number } | null;
medium: { width?: number; height?: number } | null;
small: { width?: number; height?: number } | null;
thumb: { width?: number; height?: number } | null;
Expand Down