diff --git a/shaarli-pro/js/script.js b/shaarli-pro/js/script.js index 97b844b..5d12c9e 100644 --- a/shaarli-pro/js/script.js +++ b/shaarli-pro/js/script.js @@ -1135,7 +1135,10 @@ document.addEventListener('DOMContentLoaded', () => { } }); - // ===== Persistent Media Player ===== + // ===== Persistent Media Player (Popup Strategy) ===== + // Audio plays in a separate popup window that survives page navigation. + // The inline bar serves as a "Now Playing" indicator and control relay. + const MEDIA_EXTENSIONS = [ '.mp3', '.mp4', '.ogg', '.webm', '.m3u8', '.flac', '.wav', '.aac', '.m4a', '.opus', '.wma', '.oga', '.m3u', '.pls' @@ -1144,13 +1147,10 @@ document.addEventListener('DOMContentLoaded', () => { function isMediaUrl(url) { if (!url) return false; const lower = url.toLowerCase(); - // Strip query params and fragment for cleaner extension matching const pathname = lower.split('?')[0].split('#')[0]; - // Check file extensions at end of path for (const ext of MEDIA_EXTENSIONS) { if (pathname.endsWith(ext)) return true; } - // Check streaming URL patterns (path-segment matching) if (/\/(stream|listen|live|icecast|shoutcast)(\/|$|\?)/i.test(url)) { return true; } @@ -1158,7 +1158,6 @@ document.addEventListener('DOMContentLoaded', () => { } const playerBar = document.getElementById('media-player-bar'); - const playerAudio = document.getElementById('media-player-audio'); const playerPlayBtn = document.getElementById('media-player-play'); const playerPlayIcon = document.getElementById('media-player-play-icon'); const playerTitle = document.getElementById('media-player-title'); @@ -1169,6 +1168,507 @@ document.addEventListener('DOMContentLoaded', () => { const playerVolIcon = document.getElementById('media-player-vol-icon'); const playerCloseBtn = document.getElementById('media-player-close'); + // Reference to the popup window + let playerPopup = null; + + /** + * Generate the full HTML for the popup player window. + * It is self-contained with its own styles and audio logic. + */ + function buildPopupHTML(url, title, volume) { + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + const escapedTitle = title.replace(/'/g, "\\'").replace(/"/g, '"'); + const escapedUrl = url.replace(/'/g, "\\'").replace(/"/g, '"'); + + return ` + + + + +♪ ${escapedTitle} + + + + +
+
+ + LIVE +
+
${escapedTitle}
+
+ +
+ 0:00 + 0:00 +
+
+
+ + + +
+
+ + + +
+
+ + + +`; + } + + /** + * Open (or re-use) the popup player window + */ + function showPlayer(url, title) { + const volume = playerVolume?.value || 0.8; + + // If popup already exists and is open, send it a new track + if (playerPopup && !playerPopup.closed) { + playerPopup.postMessage({ + type: 'shaarli-player-load', + url: url, + title: title + }, '*'); + playerPopup.focus(); + } else { + // Open a new popup window + const popupHTML = buildPopupHTML(url, title, volume); + playerPopup = window.open('', 'shaarli-media-player', + 'width=460,height=380,resizable=yes,scrollbars=no,toolbar=no,menubar=no,location=no,status=no' + ); + if (playerPopup) { + playerPopup.document.write(popupHTML); + playerPopup.document.close(); + } else { + // Popup blocked — fallback to inline player + fallbackInlinePlay(url, title); + return; + } + } + + // Update inline bar as "Now Playing" indicator + updateInlineBar(title, true); + + // Save state + localStorage.setItem('mediaPlayerUrl', url); + localStorage.setItem('mediaPlayerTitle', title || url); + localStorage.setItem('mediaPlayerPlaying', 'true'); + } + + /** + * Fallback: Play inline if popup is blocked + */ + function fallbackInlinePlay(url, title) { + const playerAudio = document.getElementById('media-player-audio'); + if (!playerBar || !playerAudio) return; + + playerAudio.src = url; + playerAudio.volume = parseFloat(playerVolume?.value || 0.8); + playerAudio.play().catch(() => { }); + updateInlineBar(title, true); + + localStorage.setItem('mediaPlayerUrl', url); + localStorage.setItem('mediaPlayerTitle', title || url); + localStorage.setItem('mediaPlayerPlaying', 'true'); + } + + /** + * Update the inline "Now Playing" bar + */ + function updateInlineBar(title, playing) { + if (!playerBar) return; + + if (playerTitle) playerTitle.textContent = title || 'No media'; + if (playerPlayIcon) { + playerPlayIcon.className = playing ? 'mdi mdi-pause' : 'mdi mdi-play'; + } + playerBar.classList.add('show'); + } + function formatTime(seconds) { if (!seconds || isNaN(seconds) || !isFinite(seconds)) return '0:00'; const m = Math.floor(seconds / 60); @@ -1176,118 +1676,29 @@ document.addEventListener('DOMContentLoaded', () => { return m + ':' + (s < 10 ? '0' : '') + s; } - function showPlayer(url, title) { - if (!playerBar || !playerAudio) return; - - playerAudio.src = url; - playerAudio.volume = parseFloat(playerVolume?.value || 0.8); - playerAudio.play().catch(() => { }); - - if (playerTitle) playerTitle.textContent = title || url; - if (playerPlayIcon) { - playerPlayIcon.className = 'mdi mdi-pause'; - } - playerBar.classList.add('show'); - - // Save to localStorage for persistence - localStorage.setItem('mediaPlayerUrl', url); - localStorage.setItem('mediaPlayerTitle', title || url); - localStorage.setItem('mediaPlayerPlaying', 'true'); - } - function closePlayer() { + // Close popup if open + if (playerPopup && !playerPopup.closed) { + playerPopup.postMessage({ type: 'shaarli-player-stop' }, '*'); + } + + // Also stop inline audio fallback + const playerAudio = document.getElementById('media-player-audio'); if (playerAudio) { playerAudio.pause(); playerAudio.src = ''; } + if (playerBar) playerBar.classList.remove('show'); localStorage.removeItem('mediaPlayerUrl'); localStorage.removeItem('mediaPlayerTitle'); localStorage.removeItem('mediaPlayerPosition'); localStorage.removeItem('mediaPlayerPlaying'); + localStorage.removeItem('mediaPopupState'); + + playerPopup = null; } - function togglePlayPause() { - if (!playerAudio) return; - if (playerAudio.paused) { - playerAudio.play().catch(() => { }); - } else { - playerAudio.pause(); - } - } - - if (playerAudio) { - playerAudio.addEventListener('play', () => { - if (playerPlayIcon) playerPlayIcon.className = 'mdi mdi-pause'; - localStorage.setItem('mediaPlayerPlaying', 'true'); - }); - - playerAudio.addEventListener('pause', () => { - if (playerPlayIcon) playerPlayIcon.className = 'mdi mdi-play'; - localStorage.setItem('mediaPlayerPlaying', 'false'); - }); - - playerAudio.addEventListener('timeupdate', () => { - if (!playerAudio.duration) return; - const pct = (playerAudio.currentTime / playerAudio.duration) * 100; - if (playerProgress) playerProgress.value = pct; - if (playerTime) { - playerTime.textContent = formatTime(playerAudio.currentTime) + ' / ' + formatTime(playerAudio.duration); - } - // Persist position every 2 seconds - if (Math.floor(playerAudio.currentTime) % 2 === 0) { - localStorage.setItem('mediaPlayerPosition', playerAudio.currentTime); - } - }); - - playerAudio.addEventListener('ended', () => { - if (playerPlayIcon) playerPlayIcon.className = 'mdi mdi-play'; - if (playerProgress) playerProgress.value = 0; - localStorage.setItem('mediaPlayerPlaying', 'false'); - }); - - playerAudio.addEventListener('loadedmetadata', () => { - // For streams with infinite duration, hide the time display - if (!isFinite(playerAudio.duration)) { - if (playerTime) playerTime.textContent = 'LIVE'; - if (playerProgress) playerProgress.style.display = 'none'; - } else { - if (playerProgress) playerProgress.style.display = ''; - } - }); - } - - // Player controls - playerPlayBtn?.addEventListener('click', togglePlayPause); - playerCloseBtn?.addEventListener('click', closePlayer); - - playerProgress?.addEventListener('input', () => { - if (playerAudio && playerAudio.duration && isFinite(playerAudio.duration)) { - playerAudio.currentTime = (playerProgress.value / 100) * playerAudio.duration; - } - }); - - playerVolume?.addEventListener('input', () => { - if (playerAudio) { - playerAudio.volume = playerVolume.value; - updateVolIcon(playerVolume.value); - } - }); - - playerVolBtn?.addEventListener('click', () => { - if (!playerAudio) return; - if (playerAudio.volume > 0) { - playerAudio.dataset.prevVol = playerAudio.volume; - playerAudio.volume = 0; - if (playerVolume) playerVolume.value = 0; - } else { - const prev = parseFloat(playerAudio.dataset.prevVol || 0.8); - playerAudio.volume = prev; - if (playerVolume) playerVolume.value = prev; - } - updateVolIcon(playerAudio.volume); - }); - function updateVolIcon(vol) { if (!playerVolIcon) return; if (vol <= 0) { @@ -1299,6 +1710,101 @@ document.addEventListener('DOMContentLoaded', () => { } } + // --- Inline bar controls relay to popup --- + playerPlayBtn?.addEventListener('click', () => { + if (playerPopup && !playerPopup.closed) { + playerPopup.postMessage({ type: 'shaarli-player-toggle' }, '*'); + playerPopup.focus(); + } else { + // If no popup, try to re-open with saved state + const savedUrl = localStorage.getItem('mediaPlayerUrl'); + const savedTitle = localStorage.getItem('mediaPlayerTitle'); + if (savedUrl) { + showPlayer(savedUrl, savedTitle || savedUrl); + } + } + }); + + playerCloseBtn?.addEventListener('click', closePlayer); + + // --- Listen for state updates from popup via postMessage --- + window.addEventListener('message', (e) => { + if (!e.data || !e.data.type) return; + + if (e.data.type === 'shaarli-player-state') { + // Sync state from popup to inline bar + updateInlineBar(e.data.title, e.data.playing); + + if (playerProgress && isFinite(e.data.duration) && e.data.duration > 0) { + playerProgress.value = (e.data.currentTime / e.data.duration) * 100; + playerProgress.style.display = ''; + } else if (playerProgress && !isFinite(e.data.duration)) { + playerProgress.style.display = 'none'; + } + + if (playerTime) { + if (!isFinite(e.data.duration)) { + playerTime.textContent = 'LIVE'; + } else { + playerTime.textContent = formatTime(e.data.currentTime) + ' / ' + formatTime(e.data.duration); + } + } + + if (e.data.volume !== undefined) { + updateVolIcon(e.data.volume); + if (playerVolume) playerVolume.value = e.data.volume; + } + } else if (e.data.type === 'shaarli-player-closed') { + // Popup was closed + if (playerBar) playerBar.classList.remove('show'); + localStorage.removeItem('mediaPlayerUrl'); + localStorage.removeItem('mediaPlayerTitle'); + localStorage.removeItem('mediaPlayerPlaying'); + localStorage.removeItem('mediaPopupState'); + playerPopup = null; + } + }); + + // --- Also listen for localStorage changes (for cross-tab sync) --- + window.addEventListener('storage', (e) => { + if (e.key === 'mediaPopupState' && e.newValue) { + try { + const state = JSON.parse(e.newValue); + updateInlineBar(state.title, state.playing); + } catch (err) { } + } else if (e.key === 'mediaPlayerClosed') { + if (playerBar) playerBar.classList.remove('show'); + playerPopup = null; + } + }); + + // Forward progress/volume changes from inline bar to popup + playerProgress?.addEventListener('input', () => { + if (playerPopup && !playerPopup.closed) { + // Send seek command — popup will handle + playerPopup.postMessage({ + type: 'shaarli-player-seek', + value: playerProgress.value + }, '*'); + } + }); + + playerVolume?.addEventListener('input', () => { + if (playerPopup && !playerPopup.closed) { + playerPopup.postMessage({ + type: 'shaarli-player-volume', + value: playerVolume.value + }, '*'); + } + updateVolIcon(playerVolume.value); + }); + + playerVolBtn?.addEventListener('click', () => { + if (playerPopup && !playerPopup.closed) { + playerPopup.postMessage({ type: 'shaarli-player-mute-toggle' }, '*'); + } + }); + // --- Inject play buttons into bookmark cards with media URLs --- document.querySelectorAll('.link-outer').forEach(card => { const urlEl = card.querySelector('.link-url'); @@ -1317,7 +1823,6 @@ document.addEventListener('DOMContentLoaded', () => { playBtn.title = 'Play media'; playBtn.innerHTML = ''; - // Insert before the "Open Link" button (last child usually) const openLinkBtn = actionsDiv.querySelector('a[title="Open Link"]'); if (openLinkBtn) { actionsDiv.insertBefore(playBtn, openLinkBtn); @@ -1335,31 +1840,25 @@ document.addEventListener('DOMContentLoaded', () => { } }); - // --- Restore player on page load --- - (function restorePlayer() { - const savedUrl = localStorage.getItem('mediaPlayerUrl'); - const savedTitle = localStorage.getItem('mediaPlayerTitle'); - const savedPosition = parseFloat(localStorage.getItem('mediaPlayerPosition') || 0); - const wasPlaying = localStorage.getItem('mediaPlayerPlaying') === 'true'; + // --- Restore "Now Playing" bar if popup is still open --- + (function restorePlayerBar() { + const popupState = localStorage.getItem('mediaPopupState'); + if (popupState) { + try { + const state = JSON.parse(popupState); + if (state.url) { + updateInlineBar(state.title, state.playing); - if (savedUrl && playerBar && playerAudio) { - playerAudio.src = savedUrl; - if (playerTitle) playerTitle.textContent = savedTitle || savedUrl; - playerBar.classList.add('show'); - - playerAudio.addEventListener('loadedmetadata', function onMeta() { - if (savedPosition > 0 && isFinite(playerAudio.duration)) { - playerAudio.currentTime = savedPosition; + // Try to re-attach to existing popup + try { + const existingPopup = window.open('', 'shaarli-media-player'); + if (existingPopup && existingPopup.location.href !== 'about:blank' && !existingPopup.closed) { + playerPopup = existingPopup; + } + } catch (e) { } } - if (wasPlaying) { - playerAudio.play().catch(() => { }); - } - playerAudio.removeEventListener('loadedmetadata', onMeta); - }, { once: true }); - - // Set volume from saved state - const savedVol = playerVolume?.value || 0.8; - playerAudio.volume = parseFloat(savedVol); + } catch (err) { } } })(); }); +