Fix player page now playing updates and spectrum analyzer MUTED indicator

- Execute scripts after AJAX navigation to enable dynamic content updates
- Fix spectrum analyzer audio element reference across navigation
- Stop and restart spectrum analyzer when re-initializing after AJAX load
- Find audio element in parent frame for frameset mode
- Clear all intervals/timeouts before navigation to prevent errors
- Add status-content.ctml placeholder page
- Remove debug logging from spectrum analyzer

Fixes:
- Now playing panel updates on player page after navigation
- MUTED indicator appears correctly across all pages
- No console errors from abandoned intervals
- Clean resource cleanup on page transitions
This commit is contained in:
Glenn Thompson 2025-12-07 08:21:00 +03:00
parent 3a8827f442
commit ca07b6e670
8 changed files with 313 additions and 66 deletions

View File

@ -607,6 +607,13 @@
:top-artist-3 ""
:top-artist-3-plays ""))
;; Status content frame (for frameset mode)
(define-page status-content #@"/status-content" ()
"Status page content (displayed in content frame)"
(clip:process-to-string
(load-template "status-content")
:title "📡 Asteroid Radio - Status"))
;; Configure static file serving for other files
;; BUT exclude ParenScript-compiled JS files
(define-page static #@"/static/(.*)" (:uri-groups (path))

View File

@ -43,24 +43,22 @@
(let ((audio-element nil)
(canvas-element (ps:chain document (get-element-by-id "spectrum-canvas"))))
;; Try to find audio element in current frame first
;; Try current document first
(setf audio-element (or (ps:chain document (get-element-by-id "live-audio"))
(ps:chain document (get-element-by-id "persistent-audio"))))
;; If not found and we're in a frame, try to access from parent frameset
;; If not found and we're in a frame, try parent frame (frameset mode)
(when (and (not audio-element)
(ps:@ window parent)
(not (eq (ps:@ window parent) window)))
(ps:chain console (log "Trying to access audio from parent frame..."))
(ps:try
(progn
;; Try accessing via parent.frames
(let ((player-frame (ps:getprop (ps:@ window parent) "player-frame")))
(when player-frame
(setf audio-element (ps:chain player-frame document (get-element-by-id "persistent-audio")))
(ps:chain console (log "Found audio in player-frame:" audio-element)))))
(let ((player-frame (ps:getprop (ps:@ window parent frames) "player-frame")))
(when player-frame
(setf audio-element (ps:chain player-frame document (get-element-by-id "persistent-audio")))
(when audio-element
(ps:chain console (log "Found persistent-audio in player-frame")))))
(:catch (e)
(ps:chain console (log "Cross-frame access error:" e)))))
(ps:chain console (log "Could not access parent frame:" e)))))
(when (and audio-element canvas-element)
;; Store current audio element
@ -218,49 +216,72 @@
"Return array of available visualization styles"
(array "bars" "wave" "dots"))
;; Initialize when audio starts playing
(ps:chain document (add-event-listener "DOMContentLoaded"
(lambda ()
;; Load saved theme and style preferences
(let ((saved-theme (ps:chain local-storage (get-item "spectrum-theme")))
(saved-style (ps:chain local-storage (get-item "spectrum-style"))))
(when (and saved-theme (ps:getprop *themes* saved-theme))
(setf *current-theme* saved-theme))
(when (and saved-style (or (= saved-style "bars") (= saved-style "wave") (= saved-style "dots")))
(setf *current-style* saved-style))
;; Update UI selectors, canvas border, and dropdown colors
(let ((theme-selector (ps:chain document (get-element-by-id "spectrum-theme-selector")))
(style-selector (ps:chain document (get-element-by-id "spectrum-style-selector")))
(canvas (ps:chain document (get-element-by-id "spectrum-canvas")))
(theme (ps:getprop *themes* *current-theme*)))
(when theme-selector
(setf (ps:@ theme-selector value) *current-theme*)
(setf (ps:@ theme-selector style color) (ps:@ theme top))
(setf (ps:@ theme-selector style border-color) (ps:@ theme top)))
(when style-selector
(setf (ps:@ style-selector value) *current-style*)
(setf (ps:@ style-selector style color) (ps:@ theme top))
(setf (ps:@ style-selector style border-color) (ps:@ theme top)))
;; Set initial canvas border color
(when canvas
(setf (ps:@ canvas style border-color) (ps:@ theme top)))))
(defun initialize-spectrum-analyzer ()
"Initialize or re-initialize the spectrum analyzer (can be called after AJAX navigation)"
;; Stop existing analyzer if running
(when *animation-id*
(ps:chain window (cancel-animation-frame *animation-id*))
(setf *animation-id* nil))
;; Load saved theme and style preferences
(let ((saved-theme (ps:chain local-storage (get-item "spectrum-theme")))
(saved-style (ps:chain local-storage (get-item "spectrum-style"))))
(when (and saved-theme (ps:getprop *themes* saved-theme))
(setf *current-theme* saved-theme))
(when (and saved-style (or (= saved-style "bars") (= saved-style "wave") (= saved-style "dots")))
(setf *current-style* saved-style))
(let ((audio-element (or (ps:chain document (get-element-by-id "live-audio"))
(ps:chain document (get-element-by-id "persistent-audio")))))
;; Update UI selectors, canvas border, and dropdown colors
(let ((theme-selector (ps:chain document (get-element-by-id "spectrum-theme-selector")))
(style-selector (ps:chain document (get-element-by-id "spectrum-style-selector")))
(canvas (ps:chain document (get-element-by-id "spectrum-canvas")))
(theme (ps:getprop *themes* *current-theme*)))
(when theme-selector
(setf (ps:@ theme-selector value) *current-theme*)
(setf (ps:@ theme-selector style color) (ps:@ theme top))
(setf (ps:@ theme-selector style border-color) (ps:@ theme top)))
(when style-selector
(setf (ps:@ style-selector value) *current-style*)
(setf (ps:@ style-selector style color) (ps:@ theme top))
(setf (ps:@ style-selector style border-color) (ps:@ theme top)))
;; If not found and we're in a frame, try parent
(when (and (not audio-element)
(ps:@ window parent)
(not (eq (ps:@ window parent) window)))
(ps:try
(let ((player-frame (ps:getprop (ps:@ window parent) "player-frame")))
(when player-frame
(setf audio-element (ps:chain player-frame document (get-element-by-id "persistent-audio")))))
(:catch (e)
(ps:chain console (log "Event listener cross-frame error:" e)))))
;; Set initial canvas border color
(when canvas
(setf (ps:@ canvas style border-color) (ps:@ theme top)))))
(let ((audio-element nil))
;; Try current document first
(setf audio-element (or (ps:chain document (get-element-by-id "live-audio"))
(ps:chain document (get-element-by-id "persistent-audio"))))
;; If not found and we're in a frame, try parent frame (frameset mode)
(when (and (not audio-element)
(ps:@ window parent)
(not (eq (ps:@ window parent) window)))
(ps:try
(let ((player-frame (ps:getprop (ps:@ window parent frames) "player-frame")))
(when player-frame
(setf audio-element (ps:chain player-frame document (get-element-by-id "persistent-audio")))
(when audio-element
(ps:chain console (log "Found persistent-audio in player-frame")))))
(:catch (e)
(ps:chain console (log "Could not access parent frame:" e)))))
(when audio-element
;; Store reference for muted detection
(setf *current-audio-element* audio-element)
(ps:chain audio-element (add-event-listener "play" init-spectrum-analyzer))
(ps:chain audio-element (add-event-listener "pause" stop-spectrum-analyzer))
(when audio-element
(ps:chain audio-element (add-event-listener "play" init-spectrum-analyzer))
(ps:chain audio-element (add-event-listener "pause" stop-spectrum-analyzer)))))))))
;; If audio is already playing, restart the analyzer with new reference
(when (and (not (ps:@ audio-element paused))
(ps:chain document (get-element-by-id "spectrum-canvas")))
(ps:chain console (log "Audio already playing, restarting spectrum analyzer"))
(init-spectrum-analyzer)))))
;; Make initialization function globally accessible
(setf (ps:@ window |initializeSpectrumAnalyzer|) initialize-spectrum-analyzer)
;; Initialize when audio starts playing
(ps:chain document (add-event-listener "DOMContentLoaded" initialize-spectrum-analyzer))))

View File

@ -34,6 +34,13 @@
const url = link.href;
console.log('Loading via AJAX:', url);
// Clear all intervals to prevent old page scripts from running
const highestId = window.setTimeout(() => {}, 0);
for (let i = 0; i < highestId; i++) {
window.clearInterval(i);
window.clearTimeout(i);
}
fetch(url)
.then(response => response.text())
.then(html => {
@ -41,6 +48,23 @@
document.write(html);
document.close();
// Execute scripts in the new content
const scripts = document.querySelectorAll('script');
scripts.forEach(oldScript => {
const newScript = document.createElement('script');
if (oldScript.src) {
newScript.src = oldScript.src;
} else {
newScript.textContent = oldScript.textContent;
}
oldScript.parentNode.replaceChild(newScript, oldScript);
});
// Re-initialize spectrum analyzer after navigation
if (window.initializeSpectrumAnalyzer) {
setTimeout(() => window.initializeSpectrumAnalyzer(), 100);
}
if (window.history && window.history.pushState) {
window.history.pushState({}, '', url);
}

View File

@ -39,6 +39,13 @@
const url = link.href;
console.log('Loading via AJAX:', url);
// Clear all intervals to prevent old page scripts from running
const highestId = window.setTimeout(() => {}, 0);
for (let i = 0; i < highestId; i++) {
window.clearInterval(i);
window.clearTimeout(i);
}
fetch(url)
.then(response => response.text())
.then(html => {
@ -47,6 +54,23 @@
document.write(html);
document.close();
// Execute scripts in the new content
const scripts = document.querySelectorAll('script');
scripts.forEach(oldScript => {
const newScript = document.createElement('script');
if (oldScript.src) {
newScript.src = oldScript.src;
} else {
newScript.textContent = oldScript.textContent;
}
oldScript.parentNode.replaceChild(newScript, oldScript);
});
// Re-initialize spectrum analyzer after navigation
if (window.initializeSpectrumAnalyzer) {
setTimeout(() => window.initializeSpectrumAnalyzer(), 100);
}
// Update browser history
if (window.history && window.history.pushState) {
window.history.pushState({}, '', url);
@ -101,13 +125,13 @@
</div>
<nav class="nav">
<a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/player-content" target="content-frame">Player</a>
<a href="/asteroid/status" target="content-frame">Status</a>
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
<a href="/asteroid/content" target="content-frame" onclick="return loadInFrame(this)">Home</a>
<a href="/asteroid/player-content" target="content-frame" onclick="return loadInFrame(this)">Player</a>
<a href="/asteroid/status-content" target="content-frame" onclick="return loadInFrame(this)">Status</a>
<a href="/asteroid/profile-content" target="content-frame" data-show-if-logged-in onclick="return loadInFrame(this)">Profile</a>
<a href="/asteroid/admin-content" target="content-frame" data-show-if-admin onclick="return loadInFrame(this)">Admin</a>
<a href="/asteroid/login-content" target="content-frame" data-show-if-logged-out onclick="return loadInFrame(this)">Login</a>
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
<a href="/asteroid/register-content" target="content-frame" data-show-if-logged-out onclick="return loadInFrame(this)">Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout" onclick="handleLogout(); return false;">Logout</a>
</nav>
</header>

View File

@ -14,6 +14,13 @@
const url = link.href;
console.log('Loading via AJAX:', url);
// Clear all intervals to prevent old page scripts from running
const highestId = window.setTimeout(() => {}, 0);
for (let i = 0; i < highestId; i++) {
window.clearInterval(i);
window.clearTimeout(i);
}
fetch(url)
.then(response => response.text())
.then(html => {
@ -21,6 +28,23 @@
document.write(html);
document.close();
// Execute scripts in the new content
const scripts = document.querySelectorAll('script');
scripts.forEach(oldScript => {
const newScript = document.createElement('script');
if (oldScript.src) {
newScript.src = oldScript.src;
} else {
newScript.textContent = oldScript.textContent;
}
oldScript.parentNode.replaceChild(newScript, oldScript);
});
// Re-initialize spectrum analyzer after navigation
if (window.initializeSpectrumAnalyzer) {
setTimeout(() => window.initializeSpectrumAnalyzer(), 100);
}
if (window.history && window.history.pushState) {
window.history.pushState({}, '', url);
}
@ -74,9 +98,9 @@
<span>ASTEROID RADIO - LOGIN</span>
</h1>
<nav class="nav">
<a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/status" target="content-frame">Status</a>
<a href="/asteroid/register-content" target="content-frame">Register</a>
<a href="/asteroid/content" target="content-frame" onclick="return loadInFrame(this)">Home</a>
<a href="/asteroid/status-content" target="content-frame" onclick="return loadInFrame(this)">Status</a>
<a href="/asteroid/register-content" target="content-frame" onclick="return loadInFrame(this)">Register</a>
</nav>
</header>

View File

@ -35,6 +35,13 @@
const url = link.href;
console.log('Loading via AJAX:', url);
// Clear all intervals to prevent old page scripts from running
const highestId = window.setTimeout(() => {}, 0);
for (let i = 0; i < highestId; i++) {
window.clearInterval(i);
window.clearTimeout(i);
}
fetch(url)
.then(response => response.text())
.then(html => {
@ -43,6 +50,23 @@
document.write(html);
document.close();
// Execute scripts in the new content
const scripts = document.querySelectorAll('script');
scripts.forEach(oldScript => {
const newScript = document.createElement('script');
if (oldScript.src) {
newScript.src = oldScript.src;
} else {
newScript.textContent = oldScript.textContent;
}
oldScript.parentNode.replaceChild(newScript, oldScript);
});
// Re-initialize spectrum analyzer after navigation
if (window.initializeSpectrumAnalyzer) {
setTimeout(() => window.initializeSpectrumAnalyzer(), 100);
}
// Update browser history
if (window.history && window.history.pushState) {
window.history.pushState({}, '', url);
@ -95,11 +119,11 @@
</div>
<div class="nav">
<a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
<a href="/asteroid/content" target="content-frame" onclick="return loadInFrame(this)">Home</a>
<a href="/asteroid/profile-content" target="content-frame" data-show-if-logged-in onclick="return loadInFrame(this)">Profile</a>
<a href="/asteroid/admin-content" target="content-frame" data-show-if-admin onclick="return loadInFrame(this)">Admin</a>
<a href="/asteroid/login-content" target="content-frame" data-show-if-logged-out onclick="return loadInFrame(this)">Login</a>
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
<a href="/asteroid/register-content" target="content-frame" data-show-if-logged-out onclick="return loadInFrame(this)">Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout" onclick="handleLogout(); return false;">Logout</a>
</div>

View File

@ -34,6 +34,13 @@
const url = link.href;
console.log('Loading via AJAX:', url);
// Clear all intervals to prevent old page scripts from running
const highestId = window.setTimeout(() => {}, 0);
for (let i = 0; i < highestId; i++) {
window.clearInterval(i);
window.clearTimeout(i);
}
fetch(url)
.then(response => response.text())
.then(html => {
@ -41,6 +48,23 @@
document.write(html);
document.close();
// Execute scripts in the new content
const scripts = document.querySelectorAll('script');
scripts.forEach(oldScript => {
const newScript = document.createElement('script');
if (oldScript.src) {
newScript.src = oldScript.src;
} else {
newScript.textContent = oldScript.textContent;
}
oldScript.parentNode.replaceChild(newScript, oldScript);
});
// Re-initialize spectrum analyzer after navigation
if (window.initializeSpectrumAnalyzer) {
setTimeout(() => window.initializeSpectrumAnalyzer(), 100);
}
if (window.history && window.history.pushState) {
window.history.pushState({}, '', url);
}

View File

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Asteroid Radio - Status</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
<script src="/asteroid/static/js/auth-ui.js"></script>
<script>
// Handle logout without navigation
function handleLogout() {
fetch('/asteroid/logout', {
method: 'GET',
redirect: 'manual'
})
.then(() => {
// Reload the current page content to show logged-out state
fetch(window.location.href)
.then(response => response.text())
.then(html => {
document.open();
document.write(html);
document.close();
});
})
.catch(error => {
console.error('Logout failed:', error);
});
}
// Load content via AJAX to prevent audio interruption
function loadInFrame(link) {
const url = link.href;
console.log('Loading via AJAX:', url);
// Clear all intervals to prevent old page scripts from running
const highestId = window.setTimeout(() => {}, 0);
for (let i = 0; i < highestId; i++) {
window.clearInterval(i);
window.clearTimeout(i);
}
fetch(url)
.then(response => response.text())
.then(html => {
document.open();
document.write(html);
document.close();
// Execute scripts in the new content
const scripts = document.querySelectorAll('script');
scripts.forEach(oldScript => {
const newScript = document.createElement('script');
if (oldScript.src) {
newScript.src = oldScript.src;
} else {
newScript.textContent = oldScript.textContent;
}
oldScript.parentNode.replaceChild(newScript, oldScript);
});
// Re-initialize spectrum analyzer after navigation
if (window.initializeSpectrumAnalyzer) {
setTimeout(() => window.initializeSpectrumAnalyzer(), 100);
}
if (window.history && window.history.pushState) {
window.history.pushState({}, '', url);
}
})
.catch(error => {
console.error('Failed to load content:', error);
return true;
});
return false;
}
</script>
</head>
<body>
<div class="container">
<h1>📡 SYSTEM STATUS</h1>
<div class="nav">
<a href="/asteroid/content" target="content-frame" onclick="return loadInFrame(this)">Home</a>
<a href="/asteroid/player-content" target="content-frame" onclick="return loadInFrame(this)">Player</a>
<a href="/asteroid/profile-content" target="content-frame" data-show-if-logged-in onclick="return loadInFrame(this)">Profile</a>
<a href="/asteroid/admin-content" target="content-frame" data-show-if-admin onclick="return loadInFrame(this)">Admin</a>
<a href="/asteroid/login-content" target="content-frame" data-show-if-logged-out onclick="return loadInFrame(this)">Login</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout" onclick="handleLogout(); return false;">Logout</a>
</div>
<div class="admin-section">
<h2>Server Status</h2>
<p>Status page coming soon...</p>
<p>For now, administrators can view detailed status information in the <a href="/asteroid/admin-content" target="content-frame" onclick="return loadInFrame(this)">Admin Dashboard</a>.</p>
</div>
</div>
</body>
</html>