diff --git a/frontend/app.js b/frontend/app.js index 00bd42a..d1d5af5 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -6,6 +6,7 @@ let hasNextPage = true; let isLoading = false; let hlsPlayer = null; let currentLoadController = null; +let errorToastTimer = null; // 2. Observer Definition (Must be defined before initApp uses it) const observer = new IntersectionObserver((entries) => { @@ -198,6 +199,14 @@ async function initApp() { }; } + const errorToastClose = document.getElementById('error-toast-close'); + if (errorToastClose) { + errorToastClose.onclick = () => { + const toast = document.getElementById('error-toast'); + if (toast) toast.classList.remove('show'); + }; + } + window.addEventListener('resize', () => { ensureViewportFilled(); }); @@ -212,6 +221,20 @@ function applyTheme() { if (select) select.value = theme; } +function showError(message) { + const toast = document.getElementById('error-toast'); + const text = document.getElementById('error-toast-text'); + if (!toast || !text) return; + text.textContent = message; + toast.classList.add('show'); + if (errorToastTimer) { + clearTimeout(errorToastTimer); + } + errorToastTimer = setTimeout(() => { + toast.classList.remove('show'); + }, 4000); +} + async function openPlayer(url) { const modal = document.getElementById('video-modal'); const video = document.getElementById('player'); @@ -253,16 +276,28 @@ async function openPlayer(url) { hlsPlayer.on(window.Hls.Events.MANIFEST_PARSED, function() { video.play(); }); + hlsPlayer.on(window.Hls.Events.ERROR, function(event, data) { + if (data && data.fatal) { + showError('Unable to play this stream.'); + closePlayer(); + } + }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = streamUrl; } else { console.error("HLS not supported in this browser."); + showError('HLS is not supported in this browser.'); return; } } else { video.src = streamUrl; } + video.onerror = () => { + showError('Video failed to load.'); + closePlayer(); + }; + modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; } @@ -274,6 +309,7 @@ function closePlayer() { hlsPlayer.destroy(); hlsPlayer = null; } + video.onerror = null; video.pause(); video.src = ''; modal.style.display = 'none'; diff --git a/frontend/index.html b/frontend/index.html index 5b98c2e..e1fc249 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -93,6 +93,11 @@ + + diff --git a/frontend/style.css b/frontend/style.css index c8aaeee..8914265 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -768,6 +768,47 @@ video { object-fit: contain; } +.error-toast { + position: fixed; + right: 20px; + bottom: 20px; + max-width: min(360px, 90vw); + background: rgba(0, 0, 0, 0.85); + color: #ffffff; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 10px; + padding: 12px 14px; + display: flex; + align-items: center; + gap: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); + z-index: 3000; + opacity: 0; + pointer-events: none; + transform: translateY(8px); + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.error-toast.show { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.error-toast button { + background: transparent; + border: none; + color: inherit; + cursor: pointer; + font-size: 16px; +} + +body.theme-light .error-toast { + background: rgba(255, 255, 255, 0.95); + color: #000000; + border: 1px solid rgba(0, 0, 0, 0.12); +} + /* Scrollbar styling */ ::-webkit-scrollbar { width: 8px;