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}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+ }
+
+ /**
+ * 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) { }
}
})();
});
+