CI / build (push) Failing after 6m2s
Introduce a Node build system for minified CSS/JS with content hashes, add PWA support, i18n helpers, accessibility improvements, and more translatable strings. Move theme configuration from bookmarks to localStorage, add Playwright tests, and set up CI workflows.
526 lines
16 KiB
HTML
526 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>♪ Shaarli Player</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;
|
|
}
|
|
.no-media {
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
padding: 2rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
.no-media i {
|
|
font-size: 3rem;
|
|
display: block;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.4;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="player-card" id="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'|t}</span>
|
|
</div>
|
|
<div class="player-title" id="title">{'Loading...'|t}</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'|t}">
|
|
<i class="mdi mdi-volume-high" id="vol-icon"></i>
|
|
</button>
|
|
<button class="ctrl-btn lg" id="play-btn" title="{'Play/Pause'|t}">
|
|
<i class="mdi mdi-play" id="play-icon"></i>
|
|
</button>
|
|
<button class="ctrl-btn sm close" id="close-btn" title="{'Stop & Close'|t}">
|
|
<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="0.8" step="0.01">
|
|
<i class="mdi mdi-volume-high" style="color:var(--text-muted);font-size:0.85rem;"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="no-media" id="no-media" style="display:none;">
|
|
<i class="mdi mdi-music-note-off"></i>
|
|
No media selected. Click play on a bookmark to start.
|
|
</div>
|
|
|
|
<audio id="audio" preload="metadata"></audio>
|
|
|
|
<script>
|
|
(function() {
|
|
// --- DOM refs ---
|
|
var audio = document.getElementById('audio');
|
|
var playBtn = document.getElementById('play-btn');
|
|
var playIcon = document.getElementById('play-icon');
|
|
var closeBtn = document.getElementById('close-btn');
|
|
var progress = document.getElementById('progress');
|
|
var progressWrap = document.getElementById('progress-wrap');
|
|
var timeCurrent = document.getElementById('time-current');
|
|
var timeTotal = document.getElementById('time-total');
|
|
var volumeSlider = document.getElementById('volume');
|
|
var volBtn = document.getElementById('vol-btn');
|
|
var volIcon = document.getElementById('vol-icon');
|
|
var artworkIcon = document.getElementById('artwork-icon');
|
|
var liveBadge = document.getElementById('live-badge');
|
|
var titleEl = document.getElementById('title');
|
|
var playerCard = document.getElementById('player-card');
|
|
var noMediaEl = document.getElementById('no-media');
|
|
|
|
var currentUrl = '';
|
|
var currentTitle = '';
|
|
var prevVol = 0.8;
|
|
|
|
// --- Apply theme ---
|
|
var savedTheme = localStorage.getItem('shaarliTheme') || 'light';
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
|
|
// --- BroadcastChannel for communication with main page ---
|
|
var channel = null;
|
|
try {
|
|
channel = new BroadcastChannel('shaarli-media-player');
|
|
} catch(e) {
|
|
// BroadcastChannel not supported, fallback to localStorage events
|
|
}
|
|
|
|
// --- Helpers ---
|
|
function fmt(s) {
|
|
if (!s || isNaN(s) || !isFinite(s)) return '0:00';
|
|
var m = Math.floor(s / 60);
|
|
var 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 broadcastState() {
|
|
var state = {
|
|
type: 'player-state',
|
|
url: currentUrl,
|
|
title: currentTitle,
|
|
playing: !audio.paused,
|
|
currentTime: audio.currentTime,
|
|
duration: audio.duration,
|
|
volume: audio.volume
|
|
};
|
|
// Save to localStorage for page reloads
|
|
localStorage.setItem('mediaPopupState', JSON.stringify(state));
|
|
// Broadcast via channel
|
|
if (channel) {
|
|
try { channel.postMessage(state); } catch(e) {}
|
|
}
|
|
}
|
|
|
|
function loadAndPlay(url, title) {
|
|
if (!url) return;
|
|
currentUrl = url;
|
|
currentTitle = title || url;
|
|
titleEl.textContent = currentTitle;
|
|
document.title = '\u266A ' + currentTitle;
|
|
audio.src = url;
|
|
audio.volume = parseFloat(volumeSlider.value);
|
|
audio.play().catch(function() {});
|
|
|
|
playerCard.style.display = '';
|
|
noMediaEl.style.display = 'none';
|
|
|
|
// Update localStorage keys (for the main page bar)
|
|
localStorage.setItem('mediaPlayerUrl', url);
|
|
localStorage.setItem('mediaPlayerTitle', currentTitle);
|
|
localStorage.setItem('mediaPlayerPlaying', 'true');
|
|
}
|
|
|
|
// --- Audio events ---
|
|
audio.addEventListener('play', function() {
|
|
playIcon.className = 'mdi mdi-pause';
|
|
artworkIcon.classList.add('playing');
|
|
broadcastState();
|
|
});
|
|
|
|
audio.addEventListener('pause', function() {
|
|
playIcon.className = 'mdi mdi-play';
|
|
artworkIcon.classList.remove('playing');
|
|
broadcastState();
|
|
});
|
|
|
|
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 2 seconds
|
|
if (Math.floor(audio.currentTime) % 2 === 0) broadcastState();
|
|
});
|
|
|
|
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';
|
|
if (!audio.paused) artworkIcon.classList.add('playing');
|
|
}
|
|
});
|
|
|
|
audio.addEventListener('ended', function() {
|
|
playIcon.className = 'mdi mdi-play';
|
|
progress.value = 0;
|
|
artworkIcon.classList.remove('playing');
|
|
broadcastState();
|
|
});
|
|
|
|
audio.addEventListener('error', function() {
|
|
titleEl.textContent = 'Error loading media';
|
|
playIcon.className = 'mdi mdi-play';
|
|
artworkIcon.classList.remove('playing');
|
|
});
|
|
|
|
// --- Player controls ---
|
|
playBtn.addEventListener('click', function() {
|
|
if (audio.paused) audio.play().catch(function() {});
|
|
else audio.pause();
|
|
});
|
|
|
|
closeBtn.addEventListener('click', function() {
|
|
audio.pause();
|
|
audio.src = '';
|
|
localStorage.removeItem('mediaPopupState');
|
|
localStorage.removeItem('mediaPlayerUrl');
|
|
localStorage.removeItem('mediaPlayerTitle');
|
|
localStorage.removeItem('mediaPlayerPlaying');
|
|
if (channel) {
|
|
try { channel.postMessage({ type: '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 = parseFloat(volumeSlider.value);
|
|
updateVolIcon(audio.volume);
|
|
broadcastState();
|
|
});
|
|
|
|
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);
|
|
broadcastState();
|
|
});
|
|
|
|
// --- Listen for commands from main page via BroadcastChannel ---
|
|
if (channel) {
|
|
channel.addEventListener('message', function(e) {
|
|
var msg = e.data;
|
|
if (!msg || !msg.type) return;
|
|
|
|
if (msg.type === 'player-load') {
|
|
loadAndPlay(msg.url, msg.title);
|
|
} else if (msg.type === 'player-toggle') {
|
|
if (audio.paused) audio.play().catch(function() {});
|
|
else audio.pause();
|
|
} else if (msg.type === 'player-stop') {
|
|
audio.pause();
|
|
audio.src = '';
|
|
localStorage.removeItem('mediaPopupState');
|
|
window.close();
|
|
} else if (msg.type === 'player-seek') {
|
|
if (audio.duration && isFinite(audio.duration)) {
|
|
audio.currentTime = (msg.value / 100) * audio.duration;
|
|
}
|
|
} else if (msg.type === 'player-volume') {
|
|
audio.volume = parseFloat(msg.value);
|
|
volumeSlider.value = msg.value;
|
|
updateVolIcon(audio.volume);
|
|
} else if (msg.type === 'player-mute-toggle') {
|
|
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);
|
|
broadcastState();
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Also listen for localStorage changes (fallback for no BroadcastChannel) ---
|
|
window.addEventListener('storage', function(e) {
|
|
if (e.key === 'mediaPlayerCommand' && e.newValue) {
|
|
try {
|
|
var cmd = JSON.parse(e.newValue);
|
|
if (cmd.action === 'load') {
|
|
loadAndPlay(cmd.url, cmd.title);
|
|
} else if (cmd.action === 'toggle') {
|
|
if (audio.paused) audio.play().catch(function() {});
|
|
else audio.pause();
|
|
} else if (cmd.action === 'stop') {
|
|
audio.pause();
|
|
audio.src = '';
|
|
localStorage.removeItem('mediaPopupState');
|
|
window.close();
|
|
}
|
|
// Clear the command
|
|
localStorage.removeItem('mediaPlayerCommand');
|
|
} catch(err) {}
|
|
}
|
|
});
|
|
|
|
// --- Clean up on window close ---
|
|
window.addEventListener('beforeunload', function() {
|
|
localStorage.removeItem('mediaPopupState');
|
|
if (channel) {
|
|
try { channel.postMessage({ type: 'player-closed' }); } catch(e) {}
|
|
}
|
|
});
|
|
|
|
// --- Initial load: read config from localStorage ---
|
|
var initUrl = localStorage.getItem('mediaPlayerUrl');
|
|
var initTitle = localStorage.getItem('mediaPlayerTitle');
|
|
|
|
if (initUrl) {
|
|
loadAndPlay(initUrl, initTitle);
|
|
} else {
|
|
playerCard.style.display = 'none';
|
|
noMediaEl.style.display = '';
|
|
}
|
|
|
|
// Announce that the popup is ready
|
|
if (channel) {
|
|
try { channel.postMessage({ type: 'player-ready' }); } catch(e) {}
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|