audio player at the bottom

This commit is contained in:
Priec
2026-05-19 18:03:32 +02:00
parent e9439382cc
commit 4597b120f4
5 changed files with 128 additions and 17 deletions

View File

@@ -302,10 +302,85 @@ body {
.term-prose a { color: oklch(var(--in)); } .term-prose a { color: oklch(var(--in)); }
/* --- audio rows -------------------------------------------- */ /* --- audio rows -------------------------------------------- */
.term-track { padding: 0.6rem 0; border-top: 1px solid oklch(var(--b3)); } .term-track {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.5rem 0;
border-top: 1px solid oklch(var(--b3));
}
.term-track:first-child { border-top: 0; } .term-track:first-child { border-top: 0; }
.term-track .btn { flex: none; }
.term-track-name { font-size: 0.9rem; } .term-track-name { font-size: 0.9rem; }
audio { width: 100%; margin-top: 0.45rem; }
/* --- persistent audio player bar --------------------------- */
/* Hidden until the first song plays; shown by adding `uw-playing`
* to <html>. The bar itself carries `hx-preserve` so the <audio>
* keeps playing while htmx swaps the page around it. */
#uw-player { display: none; }
.uw-playing #uw-player {
display: block;
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 80;
background: oklch(var(--b2));
border-top: 3px solid oklch(var(--p));
box-shadow: 0 -12px 32px rgba(0, 0, 0, 0.6);
}
.uw-playing body { padding-bottom: 6.75rem; }
.uw-player-inner {
display: flex;
align-items: center;
gap: 1.15rem;
width: 100%;
max-width: 72rem;
margin: 0 auto;
padding: 1rem 1.5rem;
}
.uw-player-tag {
flex: none;
font-size: 0.98rem;
font-weight: 700;
letter-spacing: 0.02em;
color: oklch(var(--p));
white-space: nowrap;
}
.uw-player-title {
flex: none;
max-width: 24rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.1rem;
color: oklch(var(--bc));
}
#uw-audio {
flex: 1;
min-width: 9rem;
width: auto;
height: 3.25rem;
margin: 0;
}
.uw-player-close {
flex: none;
width: 2.85rem;
height: 2.85rem;
font-size: 1.05rem;
background: transparent;
border: 1px solid oklch(var(--b3));
color: oklch(var(--bc) / 0.7);
cursor: pointer;
line-height: 1;
}
.uw-player-close:hover { color: oklch(var(--er)); border-color: oklch(var(--er)); }
@media (max-width: 640px) {
.uw-player-tag { display: none; }
.uw-player-title { max-width: 9rem; font-size: 0.95rem; }
.uw-player-inner { padding: 0.75rem 0.95rem; gap: 0.75rem; }
.uw-playing body { padding-bottom: 5.75rem; }
}
/* --- vim-style statusline (the footer) --------------------- */ /* --- vim-style statusline (the footer) --------------------- */
.term-statusline { .term-statusline {

View File

@@ -23,7 +23,7 @@
<span>✗ access denied — invalid email or password.</span> <span>✗ access denied — invalid email or password.</span>
</div> </div>
{% endif %} {% endif %}
<form method="post" action="/admin/login" class="space-y-2"> <form method="post" action="/admin/login" hx-boost="false" class="space-y-2">
<div class="form-control"> <div class="form-control">
<label class="label"><span class="label-text t-green">email:</span></label> <label class="label"><span class="label-text t-green">email:</span></label>
<input type="email" name="email" required autofocus class="input input-bordered w-full"> <input type="email" name="email" required autofocus class="input input-bordered w-full">

View File

@@ -60,13 +60,12 @@
{% if tracks | length > 0 %} {% if tracks | length > 0 %}
{% for track in tracks %} {% for track in tracks %}
<div class="term-track"> <div class="term-track">
<p class="term-track-name"> <button type="button" class="uw-play btn btn-primary btn-sm"
data-src="/audio/tracks/{{ track.id }}/stream" data-title="{{ track.title }}">&#9654; play</button>
<span class="term-track-name">
<span class="t-dim">{% if track.track_number %}{{ track.track_number }}{% else %}-{% endif %}</span> <span class="t-dim">{% if track.track_number %}{{ track.track_number }}{% else %}-{% endif %}</span>
<span class="t-green"></span> {{ track.title }} <span class="t-green"></span> {{ track.title }}
</p> </span>
<audio controls preload="metadata">
<source src="/audio/tracks/{{ track.id }}/stream">
</audio>
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}

View File

@@ -30,10 +30,9 @@
{% if tracks | length > 0 %} {% if tracks | length > 0 %}
{% for track in tracks %} {% for track in tracks %}
<div class="term-track"> <div class="term-track">
<p class="term-track-name"><span class="t-green"></span> {{ track.title }}</p> <button type="button" class="uw-play btn btn-primary btn-sm"
<audio controls preload="metadata"> data-src="/audio/tracks/{{ track.id }}/stream" data-title="{{ track.title }}">&#9654; play</button>
<source src="/audio/tracks/{{ track.id }}/stream"> <span class="term-track-name"><span class="t-green"></span> {{ track.title }}</span>
</audio>
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}

View File

@@ -27,13 +27,43 @@
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
if ((localStorage.getItem('theme') || 'dark') === 'system') applyTheme('system'); if ((localStorage.getItem('theme') || 'dark') === 'system') applyTheme('system');
}); });
document.addEventListener('DOMContentLoaded', function () { function markActiveNav() {
highlightTheme(localStorage.getItem('theme') || 'dark');
var path = location.pathname; var path = location.pathname;
document.querySelectorAll('.term-navlinks a[data-nav]').forEach(function (a) { document.querySelectorAll('.term-navlinks a[data-nav]').forEach(function (a) {
var h = a.getAttribute('data-nav'); var h = a.getAttribute('data-nav');
if (h === path || (h !== '/' && path.indexOf(h) === 0)) a.classList.add('is-active'); a.classList.toggle('is-active', h === path || (h !== '/' && path.indexOf(h) === 0));
}); });
}
function initPage() {
highlightTheme(localStorage.getItem('theme') || 'dark');
markActiveNav();
}
// --- persistent audio player (survives boosted page navigation) ---
function uwPlay(src, title) {
var audio = document.getElementById('uw-audio');
if (!audio) return;
var now = document.getElementById('uw-now');
if (now) now.textContent = title || 'unknown track';
if (audio.getAttribute('src') !== src) audio.setAttribute('src', src);
document.documentElement.classList.add('uw-playing');
var played = audio.play();
if (played && played.catch) played.catch(function () {});
}
function uwStop() {
var audio = document.getElementById('uw-audio');
if (audio) audio.pause();
document.documentElement.classList.remove('uw-playing');
}
document.addEventListener('DOMContentLoaded', initPage);
document.addEventListener('htmx:afterSwap', initPage);
document.addEventListener('click', function (e) {
if (!e.target.closest) return;
var play = e.target.closest('.uw-play');
if (play) {
uwPlay(play.getAttribute('data-src'), play.getAttribute('data-title'));
} else if (e.target.closest('#uw-close')) {
uwStop();
}
}); });
</script> </script>
<link href="/static/css/app.css" rel="stylesheet" type="text/css"> <link href="/static/css/app.css" rel="stylesheet" type="text/css">
@@ -63,7 +93,7 @@
} }
</style> </style>
</head> </head>
<body class="flex min-h-screen flex-col bg-base-100 text-base-content antialiased"> <body hx-boost="true" class="flex min-h-screen flex-col bg-base-100 text-base-content antialiased">
<header class="term-titlebar"> <header class="term-titlebar">
<nav class="term-nav"> <nav class="term-nav">
<span class="term-dots" aria-hidden="true"> <span class="term-dots" aria-hidden="true">
@@ -81,7 +111,7 @@
{% if logged_in_admin %} {% if logged_in_admin %}
<li><a href="/admin/dashboard" class="t-yellow" data-nav="/admin">admin</a></li> <li><a href="/admin/dashboard" class="t-yellow" data-nav="/admin">admin</a></li>
<li> <li>
<form method="post" action="/admin/logout"> <form method="post" action="/admin/logout" hx-boost="false">
<button type="submit" class="t-red w-full">logout</button> <button type="submit" class="t-red w-full">logout</button>
</form> </form>
</li> </li>
@@ -148,5 +178,13 @@
<span class="term-seg is-alt">gruvbox-dark</span> <span class="term-seg is-alt">gruvbox-dark</span>
<span class="term-seg is-mode">100%</span> <span class="term-seg is-mode">100%</span>
</footer> </footer>
<div id="uw-player" hx-preserve="true">
<div class="uw-player-inner">
<span class="uw-player-tag">&#9654; now playing</span>
<span id="uw-now" class="uw-player-title">&mdash;</span>
<audio id="uw-audio" controls preload="none"></audio>
<button type="button" id="uw-close" class="uw-player-close" aria-label="Stop playback" title="Stop">&#10005;</button>
</div>
</div>
</body> </body>
</html> </html>