Files
universal_web_loco_rewrite/assets/views/base.html
Priec 30db21f4af
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
mobile phone resolution media player
2026-05-20 21:44:34 +02:00

367 lines
19 KiB
HTML

<!doctype html>
<html lang="{{ lang | default(value='sk') }}" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ t(key="brand", lang=lang | default(value='sk')) }}{% endblock title %}</title>
<meta name="description" content="{% block meta_description %}{{ t(key="meta-description", lang=lang | default(value='sk')) }}{% endblock meta_description %}">
<link rel="icon" type="image/x-icon" href="/static/favicon/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png">
<link rel="manifest" href="/static/favicon/site.webmanifest">
<script>
function applyTheme(t) {
var dark = t === 'dark'
|| (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
}
function highlightTheme(t) {
document.querySelectorAll('[data-theme-opt]').forEach(function (b) {
var on = b.getAttribute('data-theme-opt') === t;
b.classList.toggle('active', on);
var chk = b.querySelector('.opt-check');
if (chk) chk.classList.toggle('hidden', !on);
});
}
function setTheme(t) {
localStorage.setItem('theme', t);
applyTheme(t);
highlightTheme(t);
}
applyTheme(localStorage.getItem('theme') || 'dark');
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
if ((localStorage.getItem('theme') || 'dark') === 'system') applyTheme('system');
});
function markActiveNav() {
var path = location.pathname;
document.querySelectorAll('.term-navlinks a[data-nav]').forEach(function (a) {
var h = a.getAttribute('data-nav');
a.classList.toggle('is-active', h === path || (h !== '/' && path.indexOf(h) === 0));
});
}
function initPage() {
highlightTheme(localStorage.getItem('theme') || 'dark');
markActiveNav();
}
// --- persistent audio player with playlist queue ----------
// Survives htmx-boosted navigation: window state persists and
// #uw-player carries hx-preserve so <audio> keeps playing.
var uwQueue = []; // [{ src, title }]
var uwIndex = -1; // index of the current track, -1 when empty
function uwSave() {
try {
sessionStorage.setItem('uwQueue', JSON.stringify({ q: uwQueue, i: uwIndex }));
} catch (e) {}
}
function uwRestore() {
try {
var d = JSON.parse(sessionStorage.getItem('uwQueue') || 'null');
if (d && d.q) { uwQueue = d.q; uwIndex = (typeof d.i === 'number' ? d.i : -1); }
} catch (e) {}
}
function uwRenderQueue() {
var player = document.getElementById('uw-player');
if (player) player.classList.toggle('uw-has-queue', uwQueue.length > 0);
var badge = document.getElementById('uw-queue-badge');
if (badge) badge.textContent = uwQueue.length;
var count = document.getElementById('uw-queue-count');
if (count) count.textContent = uwQueue.length + (uwQueue.length === 1 ? ' track' : ' tracks');
var list = document.getElementById('uw-queue-list');
if (!list) return;
list.innerHTML = '';
uwQueue.forEach(function (t, idx) {
var li = document.createElement('li');
li.className = 'uw-queue-item' + (idx === uwIndex ? ' is-current' : '');
var jump = document.createElement('button');
jump.type = 'button';
jump.className = 'uw-queue-jump';
jump.setAttribute('data-uw-jump', idx);
jump.textContent = (idx === uwIndex ? '▸' : (idx + 1));
var name = document.createElement('span');
name.className = 'uw-queue-name';
name.setAttribute('data-uw-jump', idx);
name.textContent = t.title || 'unknown track';
var rm = document.createElement('button');
rm.type = 'button';
rm.className = 'uw-queue-remove';
rm.setAttribute('data-uw-remove', idx);
rm.setAttribute('aria-label', 'Remove from playlist');
rm.textContent = '✕';
li.appendChild(jump);
li.appendChild(name);
li.appendChild(rm);
list.appendChild(li);
});
}
// Point <audio> at the current queue entry; play it when asked.
function uwLoad(autoplay) {
var audio = document.getElementById('uw-audio');
var now = document.getElementById('uw-now');
if (!audio) return;
var t = uwQueue[uwIndex];
if (!t) {
if (now) now.textContent = '—';
audio.pause();
audio.removeAttribute('src');
document.documentElement.classList.remove('uw-playing');
uwRenderQueue();
uwSave();
return;
}
document.documentElement.classList.add('uw-playing');
if (now) now.textContent = t.title || 'unknown track';
if (audio.getAttribute('src') !== t.src) {
audio.setAttribute('src', t.src);
audio.load();
}
if (autoplay) {
var p = audio.play();
if (p && p.catch) p.catch(function () {});
}
uwRenderQueue();
uwSave();
}
// Replace the whole queue with a fresh set of tracks and play.
function uwPlayList(tracks) {
if (!tracks || !tracks.length) return;
uwQueue = tracks.slice();
uwIndex = 0;
uwLoad(true);
}
// Add one track: play it now if idle, otherwise queue it.
function uwAdd(src, title) {
var audio = document.getElementById('uw-audio');
var idle = uwIndex < 0 || !audio || audio.ended || !audio.getAttribute('src');
uwQueue.push({ src: src, title: title });
if (idle) { uwIndex = uwQueue.length - 1; uwLoad(true); }
else { uwRenderQueue(); uwSave(); }
}
function uwNext() {
if (uwIndex >= 0 && uwIndex < uwQueue.length - 1) { uwIndex++; uwLoad(true); }
}
function uwPrev() {
var audio = document.getElementById('uw-audio');
if (audio && audio.currentTime > 3) { audio.currentTime = 0; return; }
if (uwIndex > 0) { uwIndex--; uwLoad(true); }
else if (audio) audio.currentTime = 0;
}
function uwJump(idx) {
if (idx >= 0 && idx < uwQueue.length) { uwIndex = idx; uwLoad(true); }
}
function uwRemove(idx) {
if (idx < 0 || idx >= uwQueue.length) return;
var playing = document.documentElement.classList.contains('uw-playing');
uwQueue.splice(idx, 1);
if (idx < uwIndex) { uwIndex--; uwRenderQueue(); uwSave(); }
else if (idx > uwIndex) { uwRenderQueue(); uwSave(); }
else {
if (uwIndex >= uwQueue.length) uwIndex = uwQueue.length - 1;
uwLoad(playing);
}
}
function uwClear() {
uwQueue = [];
uwIndex = -1;
uwLoad(false);
var panel = document.getElementById('uw-queue');
if (panel) panel.hidden = true;
}
function uwInit() {
var audio = document.getElementById('uw-audio');
if (!audio || audio.dataset.uwBound) return;
audio.dataset.uwBound = '1';
uwRestore();
audio.addEventListener('ended', uwNext);
uwRenderQueue();
if (uwIndex >= 0 && uwQueue[uwIndex]) uwLoad(false);
}
document.addEventListener('DOMContentLoaded', function () { initPage(); uwInit(); });
document.addEventListener('htmx:afterSwap', initPage);
document.addEventListener('click', function (e) {
if (!e.target.closest) return;
var albumBtn = e.target.closest('.uw-play-album');
if (albumBtn) {
var sel = albumBtn.getAttribute('data-tracks-from');
var scope = (sel && document.querySelector(sel)) || document;
var tracks = [];
scope.querySelectorAll('.uw-play').forEach(function (b) {
tracks.push({ src: b.getAttribute('data-src'), title: b.getAttribute('data-title') });
});
uwPlayList(tracks);
return;
}
// Play an album straight from the listing: fetch its tracks first.
var remoteBtn = e.target.closest('.uw-play-album-remote');
if (remoteBtn) {
var rurl = remoteBtn.getAttribute('data-album-tracks-url');
if (rurl) {
remoteBtn.disabled = true;
fetch(rurl, { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.json(); })
.then(function (d) { if (d && d.tracks) uwPlayList(d.tracks); })
.catch(function () {})
.then(function () { remoteBtn.disabled = false; });
}
return;
}
var playBtn = e.target.closest('.uw-play');
if (playBtn) {
uwAdd(playBtn.getAttribute('data-src'), playBtn.getAttribute('data-title'));
return;
}
var jumpEl = e.target.closest('[data-uw-jump]');
if (jumpEl) { uwJump(parseInt(jumpEl.getAttribute('data-uw-jump'), 10)); return; }
var rmEl = e.target.closest('[data-uw-remove]');
if (rmEl) { uwRemove(parseInt(rmEl.getAttribute('data-uw-remove'), 10)); return; }
if (e.target.closest('#uw-next')) { uwNext(); return; }
if (e.target.closest('#uw-prev')) { uwPrev(); return; }
if (e.target.closest('#uw-queue-clear')) { uwClear(); return; }
if (e.target.closest('#uw-queue-toggle')) {
var panel = document.getElementById('uw-queue');
if (panel) panel.hidden = !panel.hidden;
return;
}
if (e.target.closest('#uw-close')) { uwClear(); return; }
});
</script>
<link href="/static/css/app.css?v=2026-05-20b" rel="stylesheet" type="text/css">
<link href="/static/css/theme.css?v=2026-05-20b" rel="stylesheet" type="text/css">
<script src="/static/vendor/htmx/htmx-1.9.12.min.js"></script>
<style>
@media (min-width: 768px) {
.nav-menu { flex-direction: row; }
}
#nav-backdrop { display: none; }
@media (max-width: 767px) {
#nav-backdrop {
display: block;
position: fixed;
inset: 0;
z-index: 40;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease, visibility 0s linear 0.2s;
}
.term-titlebar:has(.dropdown:focus-within) ~ #nav-backdrop {
opacity: 1;
visibility: visible;
transition: opacity 0.15s ease, visibility 0s;
}
}
</style>
</head>
<body hx-boost="true" class="flex min-h-screen flex-col bg-base-100 text-base-content antialiased">
<header class="term-titlebar">
<nav class="term-nav">
<a href="/" class="term-brand">{{ t(key="brand", lang=lang | default(value='sk')) }}</a>
<ul class="nav-menu term-navlinks menu menu-sm hidden items-center md:flex">
<li><a href="/" data-nav="/">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/blog" data-nav="/blog">{{ t(key="nav-blog", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/audio/albums" data-nav="/audio/albums">{{ t(key="nav-audio", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/audio/tracks" data-nav="/audio/tracks">{{ t(key="nav-songs", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/about" data-nav="/about">{{ t(key="nav-about", lang=lang | default(value='sk')) }}</a></li>
{% if logged_in_admin %}
<li><a href="/admin/dashboard" hx-boost="false" class="t-yellow" data-nav="/admin">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li>
<li>
<form method="post" action="/admin/logout" hx-boost="false">
<button type="submit" class="t-red w-full">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form>
</li>
{% else %}
<li><a href="/admin/login" data-nav="/admin/login">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li>
{% endif %}
</ul>
<div class="term-nav-right">
<div class="dropdown dropdown-end md:hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm btn-circle" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</div>
<ul tabindex="0"
class="menu dropdown-content z-50 mt-3 w-52 border border-base-300 bg-base-200 p-2 shadow-lg">
<li><a href="/">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/blog">{{ t(key="nav-blog", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/audio/albums">{{ t(key="nav-audio", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/audio/tracks">{{ t(key="nav-songs", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/about">{{ t(key="nav-about", lang=lang | default(value='sk')) }}</a></li>
{% if logged_in_admin %}
<li><a href="/admin/dashboard" hx-boost="false" class="t-yellow">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li>
<li>
<form method="post" action="/admin/logout">
<button type="submit" class="t-red w-full">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form>
</li>
{% else %}
<li><a href="/admin/login">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li>
{% endif %}
</ul>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm btn-circle" aria-label="{{ t(key='settings', lang=lang | default(value='sk')) }}" title="{{ t(key='settings', lang=lang | default(value='sk')) }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</div>
<form method="post" action="/lang" hx-boost="false">
<ul tabindex="0" class="menu dropdown-content z-50 mt-3 w-56 border border-base-300 bg-base-200 p-2 shadow-lg">
<li class="menu-title">{{ t(key="settings-language", lang=lang | default(value='sk')) }}</li>
<li>
<button type="submit" name="lang" value="en" class="{% if lang | default(value='sk') == 'en' %}active{% endif %}">
English
{% if lang | default(value='sk') == 'en' %}
<span class="ml-auto"></span>
{% endif %}
</button>
</li>
<li>
<button type="submit" name="lang" value="sk" class="{% if lang | default(value='sk') == 'sk' %}active{% endif %}">
Slovenčina
{% if lang | default(value='sk') == 'sk' %}
<span class="ml-auto"></span>
{% endif %}
</button>
</li>
<li class="menu-title">{{ t(key="settings-theme", lang=lang | default(value='sk')) }}</li>
<li><button type="button" data-theme-opt="system" onclick="setTheme('system')">{{ t(key="theme-system", lang=lang | default(value='sk')) }} <span class="opt-check ml-auto hidden"></span></button></li>
<li><button type="button" data-theme-opt="light" onclick="setTheme('light')">{{ t(key="theme-light", lang=lang | default(value='sk')) }} <span class="opt-check ml-auto hidden"></span></button></li>
<li><button type="button" data-theme-opt="dark" onclick="setTheme('dark')">{{ t(key="theme-dark", lang=lang | default(value='sk')) }} <span class="opt-check ml-auto hidden"></span></button></li>
</ul>
</form>
</div>
</div>
</nav>
</header>
<div id="nav-backdrop" aria-hidden="true"></div>
<main class="term-main">
{% block content %}{% endblock content %}
</main>
<div id="uw-player" hx-preserve="true">
<div id="uw-queue" class="uw-queue" hidden>
<div class="uw-queue-head">
<span class="uw-queue-title">&#9776; playlist</span>
<span id="uw-queue-count" class="uw-queue-meta">0 tracks</span>
<button type="button" id="uw-queue-clear" class="uw-queue-clear">clear</button>
</div>
<ol id="uw-queue-list" class="uw-queue-list"></ol>
</div>
<div class="uw-player-inner">
<span class="uw-player-tag">&#9654; now playing</span>
<span id="uw-now" class="uw-player-title">&mdash;</span>
<button type="button" id="uw-prev" class="uw-player-btn" aria-label="Previous track" title="Previous">&#9198;</button>
<audio id="uw-audio" controls preload="none"></audio>
<button type="button" id="uw-next" class="uw-player-btn" aria-label="Next track" title="Next">&#9197;</button>
<button type="button" id="uw-queue-toggle" class="uw-player-btn" aria-label="Toggle playlist" title="Playlist">&#9776;<span id="uw-queue-badge" class="uw-queue-badge">0</span></button>
<button type="button" id="uw-close" class="uw-player-close" aria-label="Stop playback" title="Stop">&#10005;</button>
</div>
</div>
</body>
</html>