feat: implement theme toggle, mobile sidebar, and spotlight-style search overlay with live tag search.
This commit is contained in:
parent
70ebda3079
commit
2ed56d03a8
@ -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);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user