audio player at the bottom
This commit is contained in:
@@ -302,10 +302,85 @@ body {
|
||||
.term-prose a { color: oklch(var(--in)); }
|
||||
|
||||
/* --- 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 .btn { flex: none; }
|
||||
.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) --------------------- */
|
||||
.term-statusline {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<span>✗ access denied — invalid email or password.</span>
|
||||
</div>
|
||||
{% 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">
|
||||
<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">
|
||||
|
||||
@@ -60,13 +60,12 @@
|
||||
{% if tracks | length > 0 %}
|
||||
{% for track in tracks %}
|
||||
<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 }}">▶ play</button>
|
||||
<span class="term-track-name">
|
||||
<span class="t-dim">{% if track.track_number %}{{ track.track_number }}{% else %}-{% endif %}</span>
|
||||
<span class="t-green">▸</span> {{ track.title }}
|
||||
</p>
|
||||
<audio controls preload="metadata">
|
||||
<source src="/audio/tracks/{{ track.id }}/stream">
|
||||
</audio>
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
||||
@@ -30,10 +30,9 @@
|
||||
{% if tracks | length > 0 %}
|
||||
{% for track in tracks %}
|
||||
<div class="term-track">
|
||||
<p class="term-track-name"><span class="t-green">▸</span> {{ track.title }}</p>
|
||||
<audio controls preload="metadata">
|
||||
<source src="/audio/tracks/{{ track.id }}/stream">
|
||||
</audio>
|
||||
<button type="button" class="uw-play btn btn-primary btn-sm"
|
||||
data-src="/audio/tracks/{{ track.id }}/stream" data-title="{{ track.title }}">▶ play</button>
|
||||
<span class="term-track-name"><span class="t-green">▸</span> {{ track.title }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
||||
@@ -27,13 +27,43 @@
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
|
||||
if ((localStorage.getItem('theme') || 'dark') === 'system') applyTheme('system');
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
highlightTheme(localStorage.getItem('theme') || 'dark');
|
||||
function markActiveNav() {
|
||||
var path = location.pathname;
|
||||
document.querySelectorAll('.term-navlinks a[data-nav]').forEach(function (a) {
|
||||
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>
|
||||
<link href="/static/css/app.css" rel="stylesheet" type="text/css">
|
||||
@@ -63,7 +93,7 @@
|
||||
}
|
||||
</style>
|
||||
</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">
|
||||
<nav class="term-nav">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
@@ -81,7 +111,7 @@
|
||||
{% if logged_in_admin %}
|
||||
<li><a href="/admin/dashboard" class="t-yellow" data-nav="/admin">admin</a></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>
|
||||
</form>
|
||||
</li>
|
||||
@@ -148,5 +178,13 @@
|
||||
<span class="term-seg is-alt">gruvbox-dark</span>
|
||||
<span class="term-seg is-mode">100%</span>
|
||||
</footer>
|
||||
<div id="uw-player" hx-preserve="true">
|
||||
<div class="uw-player-inner">
|
||||
<span class="uw-player-tag">▶ now playing</span>
|
||||
<span id="uw-now" class="uw-player-title">—</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">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user