Initial commit, blog system works
This commit is contained in:
66
static/radio.js
Normal file
66
static/radio.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// radio.js — floating radio player (bottom-left).
|
||||
//
|
||||
// Markup comes from render.lua (only when the radio feature is enabled);
|
||||
// the channel list is lazily fetched by HTMX from /radio/channels on the
|
||||
// first expand. Volume and last-played channel persist in localStorage.
|
||||
|
||||
(function () {
|
||||
function init() {
|
||||
var player = document.getElementById('radio-player');
|
||||
if (!player) return;
|
||||
var audio = document.getElementById('radio-audio');
|
||||
var play = document.getElementById('radio-play');
|
||||
var vol = document.getElementById('radio-vol');
|
||||
var expand = document.getElementById('radio-expand');
|
||||
var channels = document.getElementById('radio-channels');
|
||||
|
||||
function markActive() {
|
||||
channels.querySelectorAll('.radio-ch').forEach(function (b) {
|
||||
b.classList.toggle('radio-ch-active', b.dataset.url === audio.src);
|
||||
});
|
||||
}
|
||||
|
||||
// Restore persisted state.
|
||||
var savedVol = parseFloat(localStorage.getItem('radio_vol'));
|
||||
if (!isNaN(savedVol)) vol.value = Math.round(savedVol * 100);
|
||||
audio.volume = vol.value / 100;
|
||||
var savedSrc = localStorage.getItem('radio_src');
|
||||
if (savedSrc) {
|
||||
audio.src = savedSrc;
|
||||
play.disabled = false;
|
||||
}
|
||||
|
||||
play.addEventListener('click', function () {
|
||||
if (audio.paused) audio.play(); else audio.pause();
|
||||
});
|
||||
audio.addEventListener('play', function () { player.classList.add('radio-playing'); });
|
||||
audio.addEventListener('pause', function () { player.classList.remove('radio-playing'); });
|
||||
|
||||
vol.addEventListener('input', function () {
|
||||
audio.volume = vol.value / 100;
|
||||
localStorage.setItem('radio_vol', String(audio.volume));
|
||||
});
|
||||
|
||||
expand.addEventListener('click', function () {
|
||||
var opening = channels.hidden;
|
||||
channels.hidden = !opening;
|
||||
expand.setAttribute('aria-expanded', opening ? 'true' : 'false');
|
||||
expand.classList.toggle('radio-open', opening);
|
||||
});
|
||||
|
||||
channels.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.radio-ch');
|
||||
if (!btn) return;
|
||||
audio.src = btn.dataset.url;
|
||||
audio.play();
|
||||
play.disabled = false;
|
||||
localStorage.setItem('radio_src', audio.src);
|
||||
markActive();
|
||||
});
|
||||
|
||||
// Highlight the saved channel once HTMX loads the list.
|
||||
channels.addEventListener('htmx:afterSwap', markActive);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
})();
|
||||
1661
static/style.css
Normal file
1661
static/style.css
Normal file
File diff suppressed because it is too large
Load Diff
63
static/theme.js
Normal file
63
static/theme.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// theme.js — theme picker menu + syntax highlighting hooks.
|
||||
//
|
||||
// The critical "read from localStorage" snippet is inlined in the <head>
|
||||
// via render.lua to avoid flash-of-wrong-theme. This file wires the
|
||||
// header theme menu (auto/light/dark/mac8) and highlight.js.
|
||||
|
||||
(function () {
|
||||
var THEMES = ['auto', 'light', 'dark', 'mac8'];
|
||||
|
||||
function currentTheme() {
|
||||
var t = localStorage.getItem('theme');
|
||||
return THEMES.indexOf(t) > 0 ? t : 'auto';
|
||||
}
|
||||
|
||||
function markActive() {
|
||||
var cur = currentTheme();
|
||||
document.querySelectorAll('.theme-option').forEach(function (b) {
|
||||
b.classList.toggle('theme-active', b.dataset.themeChoice === cur);
|
||||
});
|
||||
}
|
||||
|
||||
function applyTheme(t) {
|
||||
if (t === 'auto') {
|
||||
delete document.documentElement.dataset.theme;
|
||||
localStorage.removeItem('theme');
|
||||
} else {
|
||||
document.documentElement.dataset.theme = t;
|
||||
localStorage.setItem('theme', t);
|
||||
}
|
||||
markActive();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
markActive();
|
||||
var menu = document.getElementById('theme-menu');
|
||||
if (!menu) return;
|
||||
menu.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.theme-option');
|
||||
if (!btn) return;
|
||||
applyTheme(btn.dataset.themeChoice);
|
||||
menu.open = false;
|
||||
});
|
||||
// Close the menu when clicking anywhere else.
|
||||
document.addEventListener('click', function (e) {
|
||||
if (menu.open && !menu.contains(e.target)) menu.open = false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// Syntax highlighting: run highlight.js over fenced code blocks on page
|
||||
// load and again after every HTMX swap (boosted nav, editor preview).
|
||||
// highlight.js marks processed elements, so re-running is safe.
|
||||
function highlightCode(root) {
|
||||
if (!window.hljs) return;
|
||||
(root || document).querySelectorAll('pre code').forEach(function (el) {
|
||||
if (el.dataset.highlighted !== 'yes') window.hljs.highlightElement(el);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () { highlightCode(); });
|
||||
document.addEventListener('htmx:afterSwap', function (e) {
|
||||
highlightCode(e.target);
|
||||
});
|
||||
Reference in New Issue
Block a user