diff --git a/frontend/css/style.css b/frontend/css/style.css index 9d61029..964e302 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -939,6 +939,39 @@ body.theme-light .setting-item select option { text-align: left; } +.video-loading { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.45); + opacity: 0; + transition: opacity 0.2s ease; + pointer-events: none; + z-index: 4; +} + +.video-card.is-loading .video-loading, +.favorite-card.is-loading .video-loading { + opacity: 1; +} + +.video-loading-spinner { + width: 34px; + height: 34px; + border-radius: 50%; + border: 3px solid rgba(255, 255, 255, 0.35); + border-top-color: var(--text-primary); + animation: video-card-spinner 0.8s linear infinite; +} + +@keyframes video-card-spinner { + to { + transform: rotate(360deg); + } +} + .uploader-link:hover { color: var(--text-primary); text-decoration: underline; diff --git a/frontend/js/favorites.js b/frontend/js/favorites.js index 1426d9a..dcded10 100644 --- a/frontend/js/favorites.js +++ b/frontend/js/favorites.js @@ -111,6 +111,9 @@ App.favorites = App.favorites || {}; ${item.title} +

${item.title}

${uploaderText ? `

` : ''} @@ -120,7 +123,11 @@ App.favorites = App.favorites || {}; if (App.videos && typeof App.videos.attachNoReferrerRetry === 'function') { App.videos.attachNoReferrerRetry(thumb); } - card.onclick = () => App.player.open(item.meta || item); + card.onclick = () => { + if (card.classList.contains('is-loading')) return; + card.classList.add('is-loading'); + App.player.open(item.meta || item, { originEl: card }); + }; const favoriteBtn = card.querySelector('.favorite-btn'); if (favoriteBtn) { favoriteBtn.onclick = (event) => { diff --git a/frontend/js/player.js b/frontend/js/player.js index 93926f9..cc93a55 100644 --- a/frontend/js/player.js +++ b/frontend/js/player.js @@ -29,10 +29,22 @@ App.player = App.player || {}; return host; } - App.player.open = async function(source) { + App.player.open = async function(source, opts) { const modal = document.getElementById('video-modal'); const video = document.getElementById('player'); - if (!modal || !video) return; + const originEl = opts && opts.originEl ? opts.originEl : null; + const clearLoading = () => { + if (originEl) { + originEl.classList.remove('is-loading'); + } + }; + if (originEl) { + originEl.classList.add('is-loading'); + } + if (!modal || !video) { + clearLoading(); + return; + } const useMobileFullscreen = isMobilePlayback() || isTvPlayback(); let playbackStarted = false; @@ -60,6 +72,7 @@ App.player = App.player || {}; if (App.ui && App.ui.showError) { App.ui.showError('Unable to play this stream.'); } + clearLoading(); return; } const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : ''; @@ -128,6 +141,7 @@ App.player = App.player || {}; const startPlayback = () => { if (playbackStarted) return; playbackStarted = true; + clearLoading(); const playPromise = video.play(); if (playPromise && typeof playPromise.catch === 'function') { playPromise.catch(() => {}); @@ -152,6 +166,7 @@ App.player = App.player || {}; startPlayback(); state.hlsPlayer.on(window.Hls.Events.ERROR, function(event, data) { if (data && data.fatal) { + clearLoading(); if (App.ui && App.ui.showError) { App.ui.showError('Unable to play this stream.'); } @@ -166,6 +181,7 @@ App.player = App.player || {}; if (App.ui && App.ui.showError) { App.ui.showError('HLS is not supported in this browser.'); } + clearLoading(); return; } } else { @@ -174,6 +190,7 @@ App.player = App.player || {}; } video.onerror = () => { + clearLoading(); if (App.ui && App.ui.showError) { App.ui.showError('Video failed to load.'); } diff --git a/frontend/js/videos.js b/frontend/js/videos.js index 218d4ae..65f8224 100644 --- a/frontend/js/videos.js +++ b/frontend/js/videos.js @@ -134,6 +134,9 @@ App.videos = App.videos || {};
${v.title} +

${v.title}

${uploaderText ? `

` : ''} ${durationText ? `

${durationText}

` : ''} @@ -183,7 +186,11 @@ App.videos = App.videos || {}; App.videos.closeAllMenus(); }; } - card.onclick = () => App.player.open(v); + card.onclick = () => { + if (card.classList.contains('is-loading')) return; + card.classList.add('is-loading'); + App.player.open(v, { originEl: card }); + }; grid.appendChild(card); state.renderedVideoIds.add(v.id); });