Add video viewer modal for gallery videos
All checks were successful
Build and Push Frontend Docker Image / build (push) Successful in 30s
All checks were successful
Build and Push Frontend Docker Image / build (push) Successful in 30s
- Click on video thumbnail to open in large viewer - Video plays on hover, pauses when mouse leaves - Close viewer by clicking X or clicking outside video Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -695,6 +695,52 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Video Viewer */
|
||||||
|
.video-viewer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-viewer-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-viewer-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-viewer-player {
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-media video {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Privacy Mode */
|
/* Privacy Mode */
|
||||||
.privacy-toggle.active {
|
.privacy-toggle.active {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
|
|||||||
@@ -182,6 +182,12 @@
|
|||||||
<div id="modal-overlay" class="modal-overlay hidden">
|
<div id="modal-overlay" class="modal-overlay hidden">
|
||||||
<div id="modal-content" class="modal-content"></div>
|
<div id="modal-content" class="modal-content"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Viewer -->
|
||||||
|
<div id="video-viewer" class="video-viewer hidden">
|
||||||
|
<button id="video-viewer-close" class="video-viewer-close">×</button>
|
||||||
|
<video id="video-viewer-player" class="video-viewer-player" controls loop></video>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ function renderGallery(container, items) {
|
|||||||
<div class="gallery-item">
|
<div class="gallery-item">
|
||||||
<div class="gallery-item-media">
|
<div class="gallery-item-media">
|
||||||
${item.status === 'completed'
|
${item.status === 'completed'
|
||||||
? `<video src="/api/content/${item.id}/stream" muted loop onmouseenter="this.play()" onmouseleave="this.pause()"></video>`
|
? `<video src="/api/content/${item.id}/stream" muted loop data-content-id="${item.id}"></video>`
|
||||||
: '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--gray-500)">' + item.status + '</div>'
|
: '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--gray-500)">' + item.status + '</div>'
|
||||||
}
|
}
|
||||||
<span class="gallery-item-status ${item.status}">${item.status}</span>
|
<span class="gallery-item-status ${item.status}">${item.status}</span>
|
||||||
@@ -445,6 +445,19 @@ function renderGallery(container, items) {
|
|||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
|
// Video hover play/pause and click to view
|
||||||
|
container.querySelectorAll('.gallery-item-media video').forEach(video => {
|
||||||
|
video.addEventListener('mouseenter', () => video.play());
|
||||||
|
video.addEventListener('mouseleave', () => {
|
||||||
|
video.pause();
|
||||||
|
video.currentTime = 0;
|
||||||
|
});
|
||||||
|
video.addEventListener('click', () => {
|
||||||
|
const contentId = video.dataset.contentId;
|
||||||
|
openVideoViewer(`/api/content/${contentId}/stream`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
container.querySelectorAll('.delete-content-btn').forEach(btn => {
|
container.querySelectorAll('.delete-content-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async function() {
|
btn.addEventListener('click', async function() {
|
||||||
const contentId = parseInt(this.dataset.contentId, 10);
|
const contentId = parseInt(this.dataset.contentId, 10);
|
||||||
@@ -673,6 +686,28 @@ document.getElementById('modal-overlay').addEventListener('click', (e) => {
|
|||||||
if (e.target === e.currentTarget) hideModal();
|
if (e.target === e.currentTarget) hideModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Video Viewer
|
||||||
|
const videoViewer = document.getElementById('video-viewer');
|
||||||
|
const videoViewerPlayer = document.getElementById('video-viewer-player');
|
||||||
|
const videoViewerClose = document.getElementById('video-viewer-close');
|
||||||
|
|
||||||
|
function openVideoViewer(src) {
|
||||||
|
videoViewerPlayer.src = src;
|
||||||
|
videoViewer.classList.remove('hidden');
|
||||||
|
videoViewerPlayer.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeVideoViewer() {
|
||||||
|
videoViewerPlayer.pause();
|
||||||
|
videoViewerPlayer.src = '';
|
||||||
|
videoViewer.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
videoViewerClose.addEventListener('click', closeVideoViewer);
|
||||||
|
videoViewer.addEventListener('click', (e) => {
|
||||||
|
if (e.target === videoViewer) closeVideoViewer();
|
||||||
|
});
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
function renderPagination(container, pagination, loadFn) {
|
function renderPagination(container, pagination, loadFn) {
|
||||||
const { page, totalPages } = pagination;
|
const { page, totalPages } = pagination;
|
||||||
|
|||||||
Reference in New Issue
Block a user