Compare commits
2 Commits
ca07b6e670
...
d39b155df3
| Author | SHA1 | Date |
|---|---|---|
|
|
d39b155df3 | |
|
|
b54af08eeb |
|
|
@ -577,35 +577,53 @@
|
|||
(define-page profile-content #@"/profile-content" ()
|
||||
"User profile content (displayed in content frame)"
|
||||
(require-authentication)
|
||||
(clip:process-to-string
|
||||
(load-template "profile-content")
|
||||
:title "🎧 admin - Profile | Asteroid Radio"
|
||||
:username "admin"
|
||||
:user-role "admin"
|
||||
:join-date "Unknown"
|
||||
:last-active "Unknown"
|
||||
:total-listen-time "0h 0m"
|
||||
:tracks-played "0"
|
||||
:session-count "0"
|
||||
:favorite-genre "Unknown"
|
||||
:recent-track-1-title ""
|
||||
:recent-track-1-artist ""
|
||||
:recent-track-1-duration ""
|
||||
:recent-track-1-played-at ""
|
||||
:recent-track-2-title ""
|
||||
:recent-track-2-artist ""
|
||||
:recent-track-2-duration ""
|
||||
:recent-track-2-played-at ""
|
||||
:recent-track-3-title ""
|
||||
:recent-track-3-artist ""
|
||||
:recent-track-3-duration ""
|
||||
:recent-track-3-played-at ""
|
||||
:top-artist-1 ""
|
||||
:top-artist-1-plays ""
|
||||
:top-artist-2 ""
|
||||
:top-artist-2-plays ""
|
||||
:top-artist-3 ""
|
||||
:top-artist-3-plays ""))
|
||||
(handler-case
|
||||
(let* ((user-id (session:field "user-id"))
|
||||
(current-user (when user-id (find-user-by-id user-id)))
|
||||
(username (if current-user
|
||||
(let ((uname (dm:field current-user "username")))
|
||||
(format nil "~a" (or uname "Unknown")))
|
||||
"Unknown"))
|
||||
(user-role (if current-user
|
||||
(let ((role (dm:field current-user "role")))
|
||||
(format nil "~a" (or role "user")))
|
||||
"user")))
|
||||
(clip:process-to-string
|
||||
(load-template "profile-content")
|
||||
:title (format nil "🎧 ~a - Profile | Asteroid Radio" username)
|
||||
:username username
|
||||
:user-role user-role
|
||||
:join-date "Unknown"
|
||||
:last-active "Unknown"
|
||||
:total-listen-time "0h 0m"
|
||||
:tracks-played "0"
|
||||
:session-count "0"
|
||||
:favorite-genre "Unknown"
|
||||
:recent-track-1-title ""
|
||||
:recent-track-1-artist ""
|
||||
:recent-track-1-duration ""
|
||||
:recent-track-1-played-at ""
|
||||
:recent-track-2-title ""
|
||||
:recent-track-2-artist ""
|
||||
:recent-track-2-duration ""
|
||||
:recent-track-2-played-at ""
|
||||
:recent-track-3-title ""
|
||||
:recent-track-3-artist ""
|
||||
:recent-track-3-duration ""
|
||||
:recent-track-3-played-at ""
|
||||
:top-artist-1 ""
|
||||
:top-artist-1-plays ""
|
||||
:top-artist-2 ""
|
||||
:top-artist-2-plays ""
|
||||
:top-artist-3 ""
|
||||
:top-artist-3-plays ""
|
||||
:top-artist-4 ""
|
||||
:top-artist-4-plays ""
|
||||
:top-artist-5 ""
|
||||
:top-artist-5-plays ""))
|
||||
(error (e)
|
||||
(format t "ERROR in profile-content: ~a~%" e)
|
||||
(format nil "<html><body><h1>Error loading profile</h1><pre>~a</pre></body></html>" e))))
|
||||
|
||||
;; Status content frame (for frameset mode)
|
||||
(define-page status-content #@"/status-content" ()
|
||||
|
|
@ -617,7 +635,8 @@
|
|||
;; Configure static file serving for other files
|
||||
;; BUT exclude ParenScript-compiled JS files
|
||||
(define-page static #@"/static/(.*)" (:uri-groups (path))
|
||||
(cond
|
||||
(handler-case
|
||||
(cond
|
||||
;; Serve ParenScript-compiled auth-ui.js
|
||||
((string= path "js/auth-ui.js")
|
||||
(setf (content-type *response*) "application/javascript")
|
||||
|
|
@ -671,9 +690,19 @@
|
|||
;; Serve ParenScript-compiled player.js
|
||||
((string= path "js/player.js")
|
||||
(setf (content-type *response*) "application/javascript")
|
||||
(setf (radiance:header "Cache-Control") "no-cache, no-store, must-revalidate")
|
||||
(setf (radiance:header "Pragma") "no-cache")
|
||||
(setf (radiance:header "Expires") "0")
|
||||
(handler-case
|
||||
(let ((js (generate-player-js)))
|
||||
(if js js "// Error: No JavaScript generated"))
|
||||
(progn
|
||||
(format t "Attempting to generate player.js...~%")
|
||||
(let ((js (generate-player-js)))
|
||||
(format t "Generated player.js: ~a bytes~%" (if js (length js) 0))
|
||||
(if (and js (> (length js) 0))
|
||||
js
|
||||
(progn
|
||||
(format t "ERROR: player.js is empty or nil~%")
|
||||
"// Error: No JavaScript generated"))))
|
||||
(error (e)
|
||||
(format t "ERROR generating player.js: ~a~%" e)
|
||||
(format nil "// Error generating JavaScript: ~a~%" e))))
|
||||
|
|
@ -691,7 +720,11 @@
|
|||
;; Serve regular static file
|
||||
(t
|
||||
(serve-file (merge-pathnames (format nil "static/~a" path)
|
||||
(asdf:system-source-directory :asteroid))))))
|
||||
(asdf:system-source-directory :asteroid)))))
|
||||
(error (e)
|
||||
(format t "ERROR in static file handler for path ~a: ~a~%" path e)
|
||||
(setf (return-code *response*) 500)
|
||||
(format nil "Error serving file: ~a" e))))
|
||||
|
||||
;; Status check functions
|
||||
(defun check-icecast-status ()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
# Resolution for Issue #57: Persistent Stream Playback Halts on Login Page Navigation
|
||||
|
||||
## Summary
|
||||
This issue has been resolved. The persistent audio stream now continues playing uninterrupted when navigating to the login page and throughout the authentication flow in frameset mode.
|
||||
|
||||
## Original Problem
|
||||
When using the frameset mode with the persistent audio player, navigating to the login page would halt the audio stream. This occurred because:
|
||||
1. The login page was not frameset-aware (no `-content` version existed)
|
||||
2. Navigation to `/login` would load the full page, replacing the frameset
|
||||
3. This destroyed the persistent player frame, stopping the audio
|
||||
|
||||
## Solution Implemented
|
||||
|
||||
### 1. Created Frameset-Aware Login Page
|
||||
Created `template/login-content.ctml` to be loaded within the content frame while preserving the persistent player frame.
|
||||
|
||||
### 2. Implemented AJAX Navigation
|
||||
Modified all navigation links in frameset-aware pages to use AJAX loading via the `loadInFrame()` function:
|
||||
```javascript
|
||||
function loadInFrame(link) {
|
||||
const url = link.href;
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Implemented AJAX Logout
|
||||
Created `handleLogout()` function to perform logout without navigation:
|
||||
```javascript
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Updated Server-Side Logout Handler
|
||||
Modified the logout route to detect frameset mode and redirect appropriately:
|
||||
```lisp
|
||||
(define-page logout #@"/logout" ()
|
||||
"Handle user logout"
|
||||
(setf (session:field "user-id") nil)
|
||||
;; Check if we're in a frameset by looking at the Referer header
|
||||
(let* ((referer (radiance:header "Referer"))
|
||||
(in-frameset (and referer
|
||||
(or (search "/frameset" referer)
|
||||
(search "/content" referer)
|
||||
(search "-content" referer)))))
|
||||
(radiance:redirect (if in-frameset "/content" "/"))))
|
||||
```
|
||||
|
||||
### 5. Created Additional Frameset-Aware Pages
|
||||
- `template/admin-content.ctml` - Admin page for frameset mode
|
||||
- `template/profile-content.ctml` - Profile page for frameset mode
|
||||
- `template/status-content.ctml` - Status page placeholder for frameset mode
|
||||
|
||||
### 6. Added Route Handlers
|
||||
Added corresponding route handlers in `asteroid.lisp`:
|
||||
```lisp
|
||||
(define-page login-content #@"/login-content" ()
|
||||
"Login page content (displayed in content frame)"
|
||||
(clip:process-to-string
|
||||
(load-template "login-content")
|
||||
:title "🔐 Asteroid Radio - Login"))
|
||||
|
||||
(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"))
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
- `auth-routes.lisp` - Updated logout handler to detect frameset mode
|
||||
- `asteroid.lisp` - Added routes for login-content and status-content
|
||||
- `template/login-content.ctml` - Created frameset-aware login page
|
||||
- `template/admin-content.ctml` - Added AJAX navigation and logout handlers
|
||||
- `template/profile-content.ctml` - Added AJAX navigation and logout handlers
|
||||
- `template/front-page-content.ctml` - Added AJAX navigation and logout handlers
|
||||
- `template/player-content.ctml` - Added AJAX navigation and logout handlers
|
||||
- `template/status-content.ctml` - Created placeholder status page
|
||||
|
||||
## Testing Results
|
||||
✅ Stream continues playing when clicking Login link
|
||||
✅ Stream continues playing during login form submission
|
||||
✅ Stream continues playing after successful login
|
||||
✅ Stream continues playing when clicking Logout
|
||||
✅ Stream continues playing when navigating between all pages (Home, Player, Status, Profile, Admin)
|
||||
✅ Login form works correctly with AJAX submission
|
||||
✅ Logout updates UI to show logged-out state
|
||||
✅ All navigation links maintain persistent audio
|
||||
|
||||
## Additional Improvements
|
||||
While fixing this issue, we also resolved:
|
||||
1. **Now Playing panel not updating** - Scripts now execute after AJAX navigation
|
||||
2. **MUTED indicator not appearing** - Spectrum analyzer properly re-initializes after navigation
|
||||
3. **Console errors from abandoned intervals** - All intervals/timeouts are cleared before navigation
|
||||
4. **Faster Now Playing updates** - Reduced initial update delay from 1s to 200ms
|
||||
|
||||
## Conclusion
|
||||
The persistent player with frameset mode is now fully functional. Users can navigate throughout the entire application, including login/logout flows, without any interruption to the audio stream.
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
# Issue: MUTED Indicator Not Appearing in Spectrum Analyzer After Navigation
|
||||
|
||||
## Description
|
||||
The "MUTED" notification in the spectrum analyzer does not appear after navigating between pages using AJAX in frameset mode. The indicator shows correctly on initial page load, but after navigating to another page and back, muting the audio stream does not display the "MUTED" text overlay on the spectrum analyzer canvas.
|
||||
|
||||
## Steps to Reproduce
|
||||
1. Navigate to `/asteroid/frameset`
|
||||
2. Mute the audio stream - "MUTED" indicator appears correctly ✓
|
||||
3. Click "Player" to navigate to the player page
|
||||
4. Mute the audio stream - "MUTED" indicator does NOT appear ✗
|
||||
5. Navigate back to the home page
|
||||
6. Mute the audio stream - "MUTED" indicator does NOT appear ✗
|
||||
|
||||
## Expected Behavior
|
||||
The "MUTED" indicator should:
|
||||
- Display prominently on the spectrum analyzer canvas when audio is muted
|
||||
- Work consistently across all pages with a spectrum analyzer
|
||||
- Continue working after AJAX navigation
|
||||
- Update in real-time when mute state changes
|
||||
|
||||
## Root Cause
|
||||
Multiple issues are causing this problem:
|
||||
|
||||
### 1. Stale Audio Element Reference
|
||||
The spectrum analyzer stores a reference to the audio element in `*current-audio-element*` during initialization. After AJAX navigation:
|
||||
- The DOM is replaced with new content
|
||||
- The old audio element reference becomes stale
|
||||
- The spectrum analyzer is still checking the muted property of the old (no longer valid) element
|
||||
- The animation loop continues running but with outdated references
|
||||
|
||||
### 2. Frame Access Method
|
||||
In frameset mode, the audio element lives in the `player-frame` while the spectrum analyzer canvas is in the `content-frame`. The code is using `window.parent.frames.namedItem()` which is not a standard method and causes errors:
|
||||
```
|
||||
TypeError: window.parent.frames.namedItem is not a function
|
||||
```
|
||||
|
||||
### 3. No Re-initialization After Navigation
|
||||
When content is loaded via AJAX, the spectrum analyzer initialization function runs but:
|
||||
- The animation loop is already running from the previous page
|
||||
- The new audio element reference is stored but the running loop doesn't pick it up
|
||||
- The analyzer needs to be stopped and restarted with the fresh reference
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
### 1. Fix Frame Access
|
||||
Change from `namedItem()` to standard property access:
|
||||
```lisp
|
||||
;; Before (broken):
|
||||
(let ((player-frame (ps:chain (ps:@ window parent frames) (named-item "player-frame"))))
|
||||
|
||||
;; After (working):
|
||||
(let ((player-frame (ps:getprop (ps:@ window parent frames) "player-frame")))
|
||||
```
|
||||
|
||||
### 2. Proper Initialization Order
|
||||
Update the audio element lookup to check the current document first, then fall back to parent frame:
|
||||
```lisp
|
||||
;; 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")))))))
|
||||
```
|
||||
|
||||
### 3. Restart Analyzer After Re-initialization
|
||||
Modify `initialize-spectrum-analyzer()` to:
|
||||
1. Stop the existing animation loop
|
||||
2. Update the audio element reference
|
||||
3. Restart the analyzer if audio is already playing
|
||||
|
||||
```lisp
|
||||
(defun initialize-spectrum-analyzer ()
|
||||
;; Stop existing analyzer if running
|
||||
(when *animation-id*
|
||||
(ps:chain window (cancel-animation-frame *animation-id*))
|
||||
(setf *animation-id* nil))
|
||||
|
||||
;; ... find and store audio element ...
|
||||
|
||||
;; 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")))
|
||||
(init-spectrum-analyzer)))
|
||||
```
|
||||
|
||||
### 4. Call Re-initialization After AJAX Navigation
|
||||
Add call to `initializeSpectrumAnalyzer()` in the `loadInFrame()` function:
|
||||
```javascript
|
||||
// Re-initialize spectrum analyzer after navigation
|
||||
if (window.initializeSpectrumAnalyzer) {
|
||||
setTimeout(() => window.initializeSpectrumAnalyzer(), 100);
|
||||
}
|
||||
```
|
||||
|
||||
## Files to Modify
|
||||
- `parenscript/spectrum-analyzer.lisp` - Fix frame access, initialization order, and restart logic
|
||||
- `template/front-page-content.ctml` - Add re-initialization call
|
||||
- `template/player-content.ctml` - Add re-initialization call
|
||||
- `template/admin-content.ctml` - Add re-initialization call
|
||||
- `template/profile-content.ctml` - Add re-initialization call
|
||||
- `template/login-content.ctml` - Add re-initialization call
|
||||
- `template/status-content.ctml` - Add re-initialization call
|
||||
|
||||
## Testing Plan
|
||||
After implementing the fix:
|
||||
1. Navigate to `/asteroid/frameset`
|
||||
2. Mute audio - verify "MUTED" appears
|
||||
3. Navigate to player page
|
||||
4. Mute audio - verify "MUTED" appears
|
||||
5. Navigate to any other page
|
||||
6. Mute audio - verify "MUTED" appears
|
||||
7. Unmute and remute - verify indicator updates in real-time
|
||||
|
||||
## Additional Benefits
|
||||
This fix will also ensure:
|
||||
- Spectrum analyzer theme/style preferences persist across navigation
|
||||
- Canvas border colors update correctly
|
||||
- All spectrum analyzer features work consistently across pages
|
||||
- Proper resource cleanup when switching pages
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# Issue: Now Playing Panel Not Updating on Player Page After Navigation
|
||||
|
||||
## Description
|
||||
After implementing AJAX navigation for the frameset mode, the "Now Playing" panel on the player page (`/asteroid/player-content`) does not update with current track information. The panel remains empty or shows stale data even though the audio stream is playing correctly.
|
||||
|
||||
## Steps to Reproduce
|
||||
1. Navigate to the frameset mode at `/asteroid/frameset`
|
||||
2. Click on the "Player" navigation link to load the player page
|
||||
3. Observe that the "Now Playing" panel does not populate with current track information
|
||||
4. Navigate to another page and back to the player page
|
||||
5. The panel still does not update
|
||||
|
||||
## Expected Behavior
|
||||
The "Now Playing" panel should:
|
||||
- Populate with current track information shortly after page load
|
||||
- Update every 10 seconds with fresh data from the Icecast server
|
||||
- Continue working after AJAX navigation between pages
|
||||
|
||||
## Root Cause
|
||||
When content pages are loaded via AJAX using `document.write()`, the JavaScript files (including `player.js`) are being inserted into the DOM but not executed. The `<script>` tags are present in the HTML but the browser does not run them because they are added after the initial page load.
|
||||
|
||||
This means that:
|
||||
- The `updateNowPlaying()` function is never called
|
||||
- The `setInterval()` for periodic updates is never started
|
||||
- Event listeners are not attached
|
||||
|
||||
## Proposed Solution
|
||||
Modify the `loadInFrame()` function in all `-content.ctml` template files to manually re-execute scripts after loading new content via AJAX:
|
||||
|
||||
```javascript
|
||||
// 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);
|
||||
});
|
||||
```
|
||||
|
||||
This approach:
|
||||
1. Finds all `<script>` tags in the newly loaded content
|
||||
2. Creates fresh script elements
|
||||
3. Copies the source URL or inline content
|
||||
4. Replaces the old (non-executed) scripts with new ones that the browser will execute
|
||||
|
||||
## Files to Modify
|
||||
- `template/front-page-content.ctml`
|
||||
- `template/player-content.ctml`
|
||||
- `template/admin-content.ctml`
|
||||
- `template/profile-content.ctml`
|
||||
- `template/login-content.ctml`
|
||||
- `template/status-content.ctml`
|
||||
|
||||
## Testing Plan
|
||||
After implementing the fix:
|
||||
1. Navigate to `/asteroid/frameset`
|
||||
2. Click "Player" - verify the Now Playing panel populates within 1 second
|
||||
3. Navigate to other pages and back - verify the panel continues to update correctly
|
||||
4. Verify all dynamic content on all pages works as expected
|
||||
|
||||
## Related Issues
|
||||
This fix should also resolve similar issues with other dynamic content that relies on JavaScript execution after page load.
|
||||
|
|
@ -94,21 +94,21 @@
|
|||
;; Restore user quality preference
|
||||
(let ((selector (ps:chain document (get-element-by-id "live-stream-quality")))
|
||||
(stream-quality (ps:chain (ps:@ local-storage (get-item "stream-quality")) "aac")))
|
||||
(when (and selector (not (== (ps:@ selector value) stream-quality)))
|
||||
(when (and selector (not (equal (ps:@ selector value) stream-quality)))
|
||||
(setf (ps:@ selector value) stream-quality)
|
||||
(ps:chain selector (dispatch-event (new "Event" "change"))))))))
|
||||
|
||||
;; Frame redirection logic
|
||||
(defun redirect-when-frame ()
|
||||
(let ((path (ps:@ window location pathname))
|
||||
(is-frameset-page (not (== (ps:@ window parent) (ps:@ window self))))
|
||||
(is-content-frame (ps:chain path (includes "player-content"))))
|
||||
|
||||
(when (and is-frameset-page (not is-content-frame))
|
||||
(setf (ps:@ window location href) "/asteroid/player-content"))
|
||||
|
||||
(when (and (not is-frameset-page) is-content-frame)
|
||||
(setf (ps:@ window location href) "/asteroid/player"))))
|
||||
(defvar path (ps:@ window location pathname))
|
||||
(defvar is-frameset-page (not (equal (ps:@ window parent) (ps:@ window self))))
|
||||
(defvar is-content-frame (ps:chain path (includes "player-content")))
|
||||
|
||||
(when (and is-frameset-page (not is-content-frame))
|
||||
(setf (ps:@ window location href) "/asteroid/player-content"))
|
||||
|
||||
(when (and (not is-frameset-page) is-content-frame)
|
||||
(setf (ps:@ window location href) "/asteroid/player")))
|
||||
|
||||
;; Setup all event listeners
|
||||
(defun setup-event-listeners ()
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
(then (lambda (result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (== (ps:@ data status) "success")
|
||||
(if (equal (ps:@ data status) "success")
|
||||
(progn
|
||||
(setf *tracks* (or (ps:@ data tracks) (array)))
|
||||
(display-tracks *tracks*))
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
(let ((container (ps:chain document (get-element-by-id "track-list")))
|
||||
(pagination-controls (ps:chain document (get-element-by-id "library-pagination-controls"))))
|
||||
|
||||
(if (== (ps:@ *filtered-library-tracks* length) 0)
|
||||
(if (equal (ps:@ *filtered-library-tracks* length) 0)
|
||||
(progn
|
||||
(setf (ps:@ container inner-html) "<div class=\"no-tracks\">No tracks found</div>")
|
||||
(setf (ps:@ pagination-controls style display) "none")
|
||||
|
|
@ -203,7 +203,7 @@
|
|||
(map (lambda (track page-index)
|
||||
;; Find the actual index in the full tracks array
|
||||
(let ((actual-index (ps:chain *tracks*
|
||||
(find-index (lambda (trk) (== (ps:@ trk id) (ps:@ track id)))))))
|
||||
(find-index (lambda (trk) (equal (ps:@ trk id) (ps:@ track id)))))))
|
||||
(+ "<div class=\"track-item\" data-track-id=\"" (ps:@ track id) "\" data-index=\"" actual-index "\">"
|
||||
"<div class=\"track-info\">"
|
||||
"<div class=\"track-title\">" (or (ps:@ track title 0) "Unknown Title") "</div>"
|
||||
|
|
@ -312,7 +312,7 @@
|
|||
;; Play from queue
|
||||
(let ((next-track (ps:chain *play-queue* (shift))))
|
||||
(play-track (ps:chain *tracks*
|
||||
(find-index (lambda (trk) (== (ps:@ trk id) (ps:@ next-track id))))))
|
||||
(find-index (lambda (trk) (equal (ps:@ trk id) (ps:@ next-track id))))))
|
||||
(update-queue-display))
|
||||
;; Play next track in library
|
||||
(let ((next-index (if *is-shuffled*
|
||||
|
|
@ -386,7 +386,7 @@
|
|||
;; Update queue display
|
||||
(defun update-queue-display ()
|
||||
(let ((container (ps:chain document (get-element-by-id "play-queue"))))
|
||||
(if (== (ps:@ *play-queue* length) 0)
|
||||
(if (equal (ps:@ *play-queue* length) 0)
|
||||
(setf (ps:@ container inner-html) "<div class=\"empty-queue\">Queue is empty</div>")
|
||||
(let ((queue-html (ps:chain *play-queue*
|
||||
(map (lambda (track index)
|
||||
|
|
@ -413,7 +413,7 @@
|
|||
;; Create playlist
|
||||
(defun create-playlist ()
|
||||
(let ((name (ps:chain (ps:chain document (get-element-by-id "new-playlist-name")) value (trim))))
|
||||
(when (not (== name ""))
|
||||
(when (not (equal name ""))
|
||||
(let ((form-data (new "FormData")))
|
||||
(ps:chain form-data (append "name" name))
|
||||
(ps:chain form-data (append "description" ""))
|
||||
|
|
@ -424,7 +424,7 @@
|
|||
(then (lambda (result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (== (ps:@ data status) "success")
|
||||
(if (equal (ps:@ data status) "success")
|
||||
(progn
|
||||
(alert (+ "Playlist \"" name "\" created successfully!"))
|
||||
(setf (ps:chain (ps:chain document (get-element-by-id "new-playlist-name")) value) "")
|
||||
|
|
@ -453,7 +453,7 @@
|
|||
(then (lambda (create-result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((create-data (or (ps:@ create-result data) create-result)))
|
||||
(if (== (ps:@ create-data status) "success")
|
||||
(if (equal (ps:@ create-data status) "success")
|
||||
(progn
|
||||
;; Wait a moment for database to update
|
||||
(ps:chain (new "Promise" (lambda (resolve) (setTimeout resolve 500)))
|
||||
|
|
@ -464,12 +464,12 @@
|
|||
(then (lambda (playlists-result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((playlist-result-data (or (ps:@ playlists-result data) playlists-result)))
|
||||
(if (and (== (ps:@ playlist-result-data status) "success")
|
||||
(if (and (equal (ps:@ playlist-result-data status) "success")
|
||||
(> (ps:@ playlist-result-data playlists length) 0))
|
||||
(progn
|
||||
;; Find the playlist with matching name (most recent)
|
||||
(let ((new-playlist (or (ps:chain (ps:@ playlist-result-data playlists)
|
||||
(find (lambda (p) (== (ps:@ p name) name))))
|
||||
(find (lambda (p) (equal (ps:@ p name) name))))
|
||||
(aref (ps:@ playlist-result-data playlists)
|
||||
(- (ps:@ playlist-result-data playlists length) 1)))))
|
||||
|
||||
|
|
@ -487,7 +487,7 @@
|
|||
(ps:create :method "POST" :body add-form-data))
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (add-result)
|
||||
(when (== (ps:@ add-result data status) "success")
|
||||
(when (equal (ps:@ add-result data status) "success")
|
||||
(setf added-count (+ added-count 1)))))
|
||||
(catch (lambda (err)
|
||||
(ps:chain console (log "Error adding track:" err)))))))))))
|
||||
|
|
@ -514,9 +514,9 @@
|
|||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
(let ((playlists (cond
|
||||
((and (ps:@ result data) (== (ps:@ result data status) "success"))
|
||||
((and (ps:@ result data) (equal (ps:@ result data status) "success"))
|
||||
(or (ps:@ result data playlists) (array)))
|
||||
((== (ps:@ result status) "success")
|
||||
((equal (ps:@ result status) "success")
|
||||
(or (ps:@ result playlists) (array)))
|
||||
(t
|
||||
(array)))))
|
||||
|
|
@ -529,7 +529,7 @@
|
|||
(defun display-playlists (playlists)
|
||||
(let ((container (ps:chain document (get-element-by-id "playlists-container"))))
|
||||
|
||||
(if (or (not playlists) (== (ps:@ playlists length) 0))
|
||||
(if (or (not playlists) (equal (ps:@ playlists length) 0))
|
||||
(setf (ps:@ container inner-html) "<div class=\"no-playlists\">No playlists created yet.</div>")
|
||||
(let ((playlists-html (ps:chain playlists
|
||||
(map (lambda (playlist)
|
||||
|
|
@ -554,7 +554,7 @@
|
|||
(then (lambda (result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (and (== (ps:@ data status) "success") (ps:@ data playlist))
|
||||
(if (and (equal (ps:@ data status) "success") (ps:@ data playlist))
|
||||
(let ((playlist (ps:@ data playlist)))
|
||||
|
||||
;; Clear current queue
|
||||
|
|
@ -566,7 +566,7 @@
|
|||
(for-each (lambda (track)
|
||||
;; Find the full track object from our tracks array
|
||||
(let ((full-track (ps:chain *tracks*
|
||||
(find (lambda (trk) (== (ps:@ trk id) (ps:@ track id)))))))
|
||||
(find (lambda (trk) (equal (ps:@ trk id) (ps:@ track id)))))))
|
||||
(when full-track
|
||||
(setf (aref *play-queue* (ps:@ *play-queue* length)) full-track)))))
|
||||
|
||||
|
|
@ -577,11 +577,11 @@
|
|||
(when (> (ps:@ *play-queue* length) 0)
|
||||
(let ((first-track (ps:chain *play-queue* (shift)))
|
||||
(track-index (ps:chain *tracks*
|
||||
(find-index (lambda (trk) (== (ps:@ trk id) (ps:@ first-track id))))))
|
||||
(find-index (lambda (trk) (equal (ps:@ trk id) (ps:@ first-track id))))))
|
||||
)
|
||||
(when (>= track-index 0)
|
||||
(play-track track-index))))))
|
||||
(when (or (not (ps:@ playlist tracks)) (== (ps:@ playlist tracks length) 0))
|
||||
(when (or (not (ps:@ playlist tracks)) (equal (ps:@ playlist tracks length) 0))
|
||||
(alert (+ "Playlist \"" (ps:@ playlist name) "\" is empty"))))
|
||||
(alert (+ "Error loading playlist: " (or (ps:@ data message) "Unknown error")))))))
|
||||
(catch (lambda (error)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,33 @@
|
|||
<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 src="/asteroid/static/js/player.js"></script>
|
||||
<script src="/api/asteroid/spectrum-analyzer.js"></script>
|
||||
<script>
|
||||
// Simple Now Playing updater
|
||||
function updateNowPlaying() {
|
||||
fetch('/api/asteroid/partial/now-playing')
|
||||
.then(response => {
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('text/html')) {
|
||||
return response.text();
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.then(data => {
|
||||
const nowPlayingDiv = document.getElementById('now-playing');
|
||||
if (nowPlayingDiv) {
|
||||
nowPlayingDiv.innerHTML = data;
|
||||
}
|
||||
})
|
||||
.catch(error => console.log('Could not fetch now playing:', error));
|
||||
}
|
||||
|
||||
// Update on load and every 10 seconds
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setTimeout(updateNowPlaying, 200);
|
||||
setInterval(updateNowPlaying, 10000);
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Handle logout without navigation
|
||||
function handleLogout() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue