feat: implement theme toggle, mobile sidebar, and spotlight-style search overlay with live tag search.

This commit is contained in:
Bruno Charest 2026-02-11 17:11:28 -05:00
parent 70ebda3079
commit 2ed56d03a8

View File

@ -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 = [ const MEDIA_EXTENSIONS = [
'.mp3', '.mp4', '.ogg', '.webm', '.m3u8', '.flac', '.wav', '.aac', '.mp3', '.mp4', '.ogg', '.webm', '.m3u8', '.flac', '.wav', '.aac',
'.m4a', '.opus', '.wma', '.oga', '.m3u', '.pls' '.m4a', '.opus', '.wma', '.oga', '.m3u', '.pls'
@ -1144,13 +1147,10 @@ document.addEventListener('DOMContentLoaded', () => {
function isMediaUrl(url) { function isMediaUrl(url) {
if (!url) return false; if (!url) return false;
const lower = url.toLowerCase(); const lower = url.toLowerCase();
// Strip query params and fragment for cleaner extension matching
const pathname = lower.split('?')[0].split('#')[0]; const pathname = lower.split('?')[0].split('#')[0];
// Check file extensions at end of path
for (const ext of MEDIA_EXTENSIONS) { for (const ext of MEDIA_EXTENSIONS) {
if (pathname.endsWith(ext)) return true; if (pathname.endsWith(ext)) return true;
} }
// Check streaming URL patterns (path-segment matching)
if (/\/(stream|listen|live|icecast|shoutcast)(\/|$|\?)/i.test(url)) { if (/\/(stream|listen|live|icecast|shoutcast)(\/|$|\?)/i.test(url)) {
return true; return true;
} }
@ -1158,7 +1158,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
const playerBar = document.getElementById('media-player-bar'); const playerBar = document.getElementById('media-player-bar');
const playerAudio = document.getElementById('media-player-audio');
const playerPlayBtn = document.getElementById('media-player-play'); const playerPlayBtn = document.getElementById('media-player-play');
const playerPlayIcon = document.getElementById('media-player-play-icon'); const playerPlayIcon = document.getElementById('media-player-play-icon');
const playerTitle = document.getElementById('media-player-title'); const playerTitle = document.getElementById('media-player-title');
@ -1169,6 +1168,507 @@ document.addEventListener('DOMContentLoaded', () => {
const playerVolIcon = document.getElementById('media-player-vol-icon'); const playerVolIcon = document.getElementById('media-player-vol-icon');
const playerCloseBtn = document.getElementById('media-player-close'); 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 `<!DOCTYPE html>
<html data-theme="${isDark ? 'dark' : 'light'}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> ${escapedTitle}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/7.2.96/css/materialdesignicons.min.css">
<style>
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--primary-light: rgba(37,99,235,0.08);
--bg-body: #f1f5f9;
--bg-card: #ffffff;
--text-main: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--border: #e2e8f0;
--danger: #ef4444;
--shadow: 0 1px 3px rgba(0,0,0,0.1);
}
[data-theme="dark"] {
--primary: #3b82f6;
--primary-hover: #60a5fa;
--primary-light: rgba(59,130,246,0.12);
--bg-body: #0f172a;
--bg-card: #1e293b;
--text-main: #e2e8f0;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border: #334155;
--danger: #f87171;
--shadow: 0 1px 3px rgba(0,0,0,0.3);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
background: var(--bg-body);
color: var(--text-main);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
user-select: none;
overflow: hidden;
}
.player-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 1rem;
box-shadow: var(--shadow);
padding: 1.5rem;
width: 100%;
max-width: 420px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.player-artwork {
display: flex;
align-items: center;
justify-content: center;
background: var(--primary-light);
border-radius: 0.75rem;
padding: 1.5rem;
position: relative;
overflow: hidden;
}
.player-artwork i {
font-size: 3rem;
color: var(--primary);
animation: pulse-icon 2s ease-in-out infinite;
opacity: 0.6;
}
.player-artwork i.playing { animation: pulse-icon 1.5s ease-in-out infinite; opacity: 1; }
@keyframes pulse-icon {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.08); }
}
.live-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: var(--danger);
color: white;
font-size: 0.65rem;
font-weight: 700;
padding: 0.15rem 0.5rem;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.06em;
display: none;
}
.live-badge.show { display: block; }
.player-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-main);
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.player-progress-wrap { width: 100%; }
.player-progress {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 5px;
background: var(--border);
border-radius: 3px;
outline: none;
cursor: pointer;
}
.player-progress:hover { height: 7px; }
.player-progress::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px; height: 14px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
.player-progress::-moz-range-thumb {
width: 14px; height: 14px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
.player-time {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
color: var(--text-muted);
margin-top: 0.2rem;
font-variant-numeric: tabular-nums;
}
.player-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
}
.ctrl-btn {
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
background: transparent;
color: var(--text-secondary);
}
.ctrl-btn:hover { background: var(--primary-light); color: var(--primary); }
.ctrl-btn.sm { width: 36px; height: 36px; font-size: 1.1rem; }
.ctrl-btn.lg {
width: 52px; height: 52px;
background: var(--primary);
color: white;
font-size: 1.5rem;
}
.ctrl-btn.lg:hover { background: var(--primary-hover); transform: scale(1.06); }
.ctrl-btn.close:hover { background: rgba(239,68,68,0.1); color: var(--danger); }
.volume-wrap {
display: flex;
align-items: center;
gap: 0.4rem;
width: 100%;
}
.volume-slider {
-webkit-appearance: none;
appearance: none;
flex: 1;
height: 4px;
background: var(--border);
border-radius: 2px;
outline: none;
cursor: pointer;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px; height: 12px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
}
.volume-slider::-moz-range-thumb {
width: 12px; height: 12px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
}
</style>
</head>
<body>
<div class="player-card">
<div class="player-artwork" id="artwork">
<i class="mdi mdi-music-note" id="artwork-icon"></i>
<span class="live-badge" id="live-badge">LIVE</span>
</div>
<div class="player-title" id="title">${escapedTitle}</div>
<div class="player-progress-wrap" id="progress-wrap">
<input type="range" class="player-progress" id="progress" min="0" max="100" value="0" step="0.1">
<div class="player-time">
<span id="time-current">0:00</span>
<span id="time-total">0:00</span>
</div>
</div>
<div class="player-controls">
<button class="ctrl-btn sm" id="vol-btn" title="Mute/Unmute">
<i class="mdi mdi-volume-high" id="vol-icon"></i>
</button>
<button class="ctrl-btn lg" id="play-btn" title="Play/Pause">
<i class="mdi mdi-play" id="play-icon"></i>
</button>
<button class="ctrl-btn sm close" id="close-btn" title="Stop & Close">
<i class="mdi mdi-close"></i>
</button>
</div>
<div class="volume-wrap">
<i class="mdi mdi-volume-low" style="color:var(--text-muted);font-size:0.85rem;"></i>
<input type="range" class="volume-slider" id="volume" min="0" max="1" value="${volume}" step="0.01">
<i class="mdi mdi-volume-high" style="color:var(--text-muted);font-size:0.85rem;"></i>
</div>
</div>
<audio id="audio" preload="metadata"></audio>
<script>
(function() {
const audio = document.getElementById('audio');
const playBtn = document.getElementById('play-btn');
const playIcon = document.getElementById('play-icon');
const closeBtn = document.getElementById('close-btn');
const progress = document.getElementById('progress');
const progressWrap = document.getElementById('progress-wrap');
const timeCurrent = document.getElementById('time-current');
const timeTotal = document.getElementById('time-total');
const volumeSlider = document.getElementById('volume');
const volBtn = document.getElementById('vol-btn');
const volIcon = document.getElementById('vol-icon');
const artworkIcon = document.getElementById('artwork-icon');
const liveBadge = document.getElementById('live-badge');
const titleEl = document.getElementById('title');
let currentUrl = '${escapedUrl}';
let currentTitle = '${escapedTitle}';
let prevVol = ${volume};
function fmt(s) {
if (!s || isNaN(s) || !isFinite(s)) return '0:00';
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return m + ':' + (sec < 10 ? '0' : '') + sec;
}
function updateVolIcon(v) {
if (v <= 0) volIcon.className = 'mdi mdi-volume-off';
else if (v < 0.5) volIcon.className = 'mdi mdi-volume-medium';
else volIcon.className = 'mdi mdi-volume-high';
}
function syncToParent() {
const state = {
type: 'shaarli-player-state',
url: currentUrl,
title: currentTitle,
playing: !audio.paused,
currentTime: audio.currentTime,
duration: audio.duration,
volume: audio.volume
};
localStorage.setItem('mediaPopupState', JSON.stringify(state));
// Also notify opener if still open
try {
if (window.opener && !window.opener.closed) {
window.opener.postMessage(state, '*');
}
} catch(e) {}
}
// Load and play
function loadAndPlay(url, title) {
currentUrl = url;
currentTitle = title;
titleEl.textContent = title;
document.title = '♪ ' + title;
audio.src = url;
audio.volume = parseFloat(volumeSlider.value);
audio.play().catch(function() {});
}
audio.addEventListener('play', function() {
playIcon.className = 'mdi mdi-pause';
artworkIcon.classList.add('playing');
syncToParent();
});
audio.addEventListener('pause', function() {
playIcon.className = 'mdi mdi-play';
artworkIcon.classList.remove('playing');
syncToParent();
});
audio.addEventListener('timeupdate', function() {
if (!audio.duration) return;
if (isFinite(audio.duration)) {
progress.value = (audio.currentTime / audio.duration) * 100;
timeCurrent.textContent = fmt(audio.currentTime);
}
// Sync every 2s
if (Math.floor(audio.currentTime) % 2 === 0) syncToParent();
});
audio.addEventListener('loadedmetadata', function() {
if (!isFinite(audio.duration)) {
liveBadge.classList.add('show');
progressWrap.style.display = 'none';
artworkIcon.className = 'mdi mdi-radio-tower playing';
} else {
liveBadge.classList.remove('show');
progressWrap.style.display = '';
timeTotal.textContent = fmt(audio.duration);
artworkIcon.className = 'mdi mdi-music-note';
}
});
audio.addEventListener('ended', function() {
playIcon.className = 'mdi mdi-play';
progress.value = 0;
artworkIcon.classList.remove('playing');
syncToParent();
});
playBtn.addEventListener('click', function() {
if (audio.paused) audio.play().catch(function() {});
else audio.pause();
});
closeBtn.addEventListener('click', function() {
audio.pause();
audio.src = '';
// Clear state
localStorage.removeItem('mediaPopupState');
localStorage.setItem('mediaPlayerClosed', Date.now().toString());
try {
if (window.opener && !window.opener.closed) {
window.opener.postMessage({ type: 'shaarli-player-closed' }, '*');
}
} catch(e) {}
window.close();
});
progress.addEventListener('input', function() {
if (audio.duration && isFinite(audio.duration)) {
audio.currentTime = (progress.value / 100) * audio.duration;
}
});
volumeSlider.addEventListener('input', function() {
audio.volume = volumeSlider.value;
updateVolIcon(audio.volume);
syncToParent();
});
volBtn.addEventListener('click', function() {
if (audio.volume > 0) {
prevVol = audio.volume;
audio.volume = 0;
volumeSlider.value = 0;
} else {
audio.volume = prevVol || 0.8;
volumeSlider.value = audio.volume;
}
updateVolIcon(audio.volume);
syncToParent();
});
// Listen for messages from parent window (e.g. play new track)
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'shaarli-player-load') {
loadAndPlay(e.data.url, e.data.title);
} else if (e.data && e.data.type === 'shaarli-player-toggle') {
if (audio.paused) audio.play().catch(function() {});
else audio.pause();
} else if (e.data && e.data.type === 'shaarli-player-stop') {
audio.pause();
audio.src = '';
localStorage.removeItem('mediaPopupState');
window.close();
}
});
// Notify parent that popup is ready
syncToParent();
// Start playing
loadAndPlay(currentUrl, currentTitle);
// On window close, clean up
window.addEventListener('beforeunload', function() {
localStorage.removeItem('mediaPopupState');
localStorage.setItem('mediaPlayerClosed', Date.now().toString());
try {
if (window.opener && !window.opener.closed) {
window.opener.postMessage({ type: 'shaarli-player-closed' }, '*');
}
} catch(e) {}
});
})();
</script>
</body>
</html>`;
}
/**
* 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) { function formatTime(seconds) {
if (!seconds || isNaN(seconds) || !isFinite(seconds)) return '0:00'; if (!seconds || isNaN(seconds) || !isFinite(seconds)) return '0:00';
const m = Math.floor(seconds / 60); const m = Math.floor(seconds / 60);
@ -1176,118 +1676,29 @@ document.addEventListener('DOMContentLoaded', () => {
return m + ':' + (s < 10 ? '0' : '') + s; 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() { 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) { if (playerAudio) {
playerAudio.pause(); playerAudio.pause();
playerAudio.src = ''; playerAudio.src = '';
} }
if (playerBar) playerBar.classList.remove('show'); if (playerBar) playerBar.classList.remove('show');
localStorage.removeItem('mediaPlayerUrl'); localStorage.removeItem('mediaPlayerUrl');
localStorage.removeItem('mediaPlayerTitle'); localStorage.removeItem('mediaPlayerTitle');
localStorage.removeItem('mediaPlayerPosition'); localStorage.removeItem('mediaPlayerPosition');
localStorage.removeItem('mediaPlayerPlaying'); 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) { function updateVolIcon(vol) {
if (!playerVolIcon) return; if (!playerVolIcon) return;
if (vol <= 0) { 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 --- // --- Inject play buttons into bookmark cards with media URLs ---
document.querySelectorAll('.link-outer').forEach(card => { document.querySelectorAll('.link-outer').forEach(card => {
const urlEl = card.querySelector('.link-url'); const urlEl = card.querySelector('.link-url');
@ -1317,7 +1823,6 @@ document.addEventListener('DOMContentLoaded', () => {
playBtn.title = 'Play media'; playBtn.title = 'Play media';
playBtn.innerHTML = '<i class="mdi mdi-play-circle-outline"></i>'; playBtn.innerHTML = '<i class="mdi mdi-play-circle-outline"></i>';
// Insert before the "Open Link" button (last child usually)
const openLinkBtn = actionsDiv.querySelector('a[title="Open Link"]'); const openLinkBtn = actionsDiv.querySelector('a[title="Open Link"]');
if (openLinkBtn) { if (openLinkBtn) {
actionsDiv.insertBefore(playBtn, openLinkBtn); actionsDiv.insertBefore(playBtn, openLinkBtn);
@ -1335,31 +1840,25 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
// --- Restore player on page load --- // --- Restore "Now Playing" bar if popup is still open ---
(function restorePlayer() { (function restorePlayerBar() {
const savedUrl = localStorage.getItem('mediaPlayerUrl'); const popupState = localStorage.getItem('mediaPopupState');
const savedTitle = localStorage.getItem('mediaPlayerTitle'); if (popupState) {
const savedPosition = parseFloat(localStorage.getItem('mediaPlayerPosition') || 0); try {
const wasPlaying = localStorage.getItem('mediaPlayerPlaying') === 'true'; const state = JSON.parse(popupState);
if (state.url) {
updateInlineBar(state.title, state.playing);
if (savedUrl && playerBar && playerAudio) { // Try to re-attach to existing popup
playerAudio.src = savedUrl; try {
if (playerTitle) playerTitle.textContent = savedTitle || savedUrl; const existingPopup = window.open('', 'shaarli-media-player');
playerBar.classList.add('show'); if (existingPopup && existingPopup.location.href !== 'about:blank' && !existingPopup.closed) {
playerPopup = existingPopup;
playerAudio.addEventListener('loadedmetadata', function onMeta() { }
if (savedPosition > 0 && isFinite(playerAudio.duration)) { } catch (e) { }
playerAudio.currentTime = savedPosition;
} }
if (wasPlaying) { } catch (err) { }
playerAudio.play().catch(() => { });
}
playerAudio.removeEventListener('loadedmetadata', onMeta);
}, { once: true });
// Set volume from saved state
const savedVol = playerVolume?.value || 0.8;
playerAudio.volume = parseFloat(savedVol);
} }
})(); })();
}); });