diff --git a/parenscript/stream-player.lisp b/parenscript/stream-player.lisp
index 31f5086..52cea01 100644
--- a/parenscript/stream-player.lisp
+++ b/parenscript/stream-player.lisp
@@ -337,9 +337,117 @@
;; Track the last recorded title to avoid duplicate history entries
(defvar *last-recorded-title* nil)
-
- ;; Cache of user's favorite track titles for quick lookup (mini player)
- (defvar *user-favorites-cache-mini* (array))
+
+ ;; Track last notified title to avoid duplicate notifications
+ (defvar *last-notified-title* nil)
+
+ ;; Check if notifications are enabled in localStorage
+ (defun notifications-enabled-p ()
+ (= (ps:chain local-storage (get-item "notifications-enabled")) "true"))
+
+ ;; Check if browser supports notifications
+ (defun notifications-supported-p ()
+ (not (= (typeof (ps:@ window -notification)) "undefined")))
+
+ ;; Get notification permission status
+ (defun get-notification-permission ()
+ (if (notifications-supported-p)
+ (ps:@ -notification permission)
+ "denied"))
+
+ ;; Request notification permission from user
+ (defun request-notification-permission ()
+ (when (notifications-supported-p)
+ (ps:chain -notification (request-permission)
+ (then (lambda (permission)
+ (if (= permission "granted")
+ (progn
+ (ps:chain local-storage (set-item "notifications-enabled" "true"))
+ (update-notification-toggle-ui)
+ (show-track-notification "Notifications Enabled" "You'll now receive track change notifications"))
+ (progn
+ (ps:chain local-storage (set-item "notifications-enabled" "false"))
+ (update-notification-toggle-ui))))))))
+
+ ;; Toggle notifications on/off
+ (defun toggle-notifications ()
+ (let ((permission (get-notification-permission)))
+ (cond
+ ;; Not supported
+ ((not (notifications-supported-p))
+ (alert "Your browser does not support notifications"))
+ ;; Permission denied - can't do anything
+ ((= permission "denied")
+ (alert "Notifications are blocked. Please enable them in your browser settings."))
+ ;; Permission not yet requested
+ ((= permission "default")
+ (request-notification-permission))
+ ;; Permission granted - toggle the setting
+ ((= permission "granted")
+ (if (notifications-enabled-p)
+ (progn
+ (ps:chain local-storage (set-item "notifications-enabled" "false"))
+ (update-notification-toggle-ui))
+ (progn
+ (ps:chain local-storage (set-item "notifications-enabled" "true"))
+ (update-notification-toggle-ui)
+ (show-track-notification "Notifications Enabled" "You'll now receive track change notifications")))))))
+
+ ;; Update the notification toggle button UI
+ (defun update-notification-toggle-ui ()
+ (let ((btn (ps:chain document (get-element-by-id "notification-toggle"))))
+ (when btn
+ (let ((permission (get-notification-permission))
+ (enabled (notifications-enabled-p)))
+ (cond
+ ((not (notifications-supported-p))
+ (setf (ps:@ btn text-content) "🔕")
+ (setf (ps:@ btn title) "Notifications not supported"))
+ ((= permission "denied")
+ (setf (ps:@ btn text-content) "🔕")
+ (setf (ps:@ btn title) "Notifications blocked - enable in browser settings"))
+ ((and (= permission "granted") enabled)
+ (setf (ps:@ btn text-content) "🔔")
+ (setf (ps:@ btn title) "Track notifications ON - click to disable"))
+ (t
+ (setf (ps:@ btn text-content) "🔕")
+ (setf (ps:@ btn title) "Track notifications OFF - click to enable")))))))
+
+ ;; Show a system notification for track change
+ (defun show-track-notification (title body)
+ (when (and (notifications-supported-p)
+ (= (get-notification-permission) "granted")
+ (notifications-enabled-p)
+ (not (= title *last-notified-title*)))
+ (setf *last-notified-title* title)
+ (ps:try
+ (let ((notification (ps:new (-notification title
+ (ps:create :body body
+ :icon "/asteroid/static/asteroid-icon.png"
+ :tag "asteroid-track-change"
+ :renotify true
+ :silent false)))))
+ ;; Auto-close after 5 seconds
+ (set-timeout (lambda () (ps:chain notification (close))) 5000)
+ ;; Click to focus the window
+ (setf (ps:@ notification onclick)
+ (lambda ()
+ (ps:chain window (focus))
+ (ps:chain notification (close)))))
+ (:catch (e)
+ (ps:chain console (log "Notification error:" e))))))
+
+ ;; Notify track change (called from update-mini-now-playing)
+ (defun notify-track-change (title)
+ (when (and title
+ (not (= title ""))
+ (not (= title "Loading..."))
+ (not (= title *last-notified-title*)))
+ ;; Show full "Artist - Track" in title, "Now Playing" as body
+ (show-track-notification title "Now Playing on Asteroid Radio")))
+
+ ;; Cache of user's favorite track titles for quick lookup (mini player)
+ (defvar *user-favorites-cache-mini* (array))
;; Load user's favorites into cache (mini player - only if logged in)
(defun load-favorites-cache-mini ()
@@ -409,9 +517,10 @@
(track-id-el (ps:chain document (get-element-by-id "current-track-id-mini")))
(title (or (ps:@ data data title) (ps:@ data title) "Loading...")))
(when el
- ;; Check if track changed and record to history
+ ;; Check if track changed and record to history + notify
(when (not (= (ps:@ el text-content) title))
- (record-track-listen title))
+ (record-track-listen title)
+ (notify-track-change title))
(setf (ps:@ el text-content) title)
;; Check if this track is in user's favorites
(check-favorite-status-mini))
@@ -856,6 +965,9 @@
;; Update quality selector state based on channel
(update-quality-selector-state)
+ ;; Initialize notification toggle UI
+ (update-notification-toggle-ui)
+
;; Poll server for channel name changes (works across all listeners)
(let ((last-channel-name nil))
(set-interval
@@ -931,17 +1043,21 @@
(ps:chain window (add-event-listener "beforeunload" notify-popout-closing)))))
;; Make functions globally accessible
- (setf (ps:@ window get-stream-config) get-stream-config)
- (setf (ps:@ window change-channel) change-channel)
- (setf (ps:@ window sync-channel-from-storage) sync-channel-from-storage)
- (setf (ps:@ window change-stream-quality) change-stream-quality)
- (setf (ps:@ window reconnect-stream) reconnect-stream)
- (setf (ps:@ window disable-frameset-mode) disable-frameset-mode)
- (setf (ps:@ window init-persistent-player) init-persistent-player)
- (setf (ps:@ window init-popout-player) init-popout-player)
- (setf (ps:@ window update-mini-now-playing) update-mini-now-playing)
- (setf (ps:@ window update-popout-now-playing) update-popout-now-playing)
- (setf (ps:@ window toggle-favorite-mini) toggle-favorite-mini)
+ (setf (ps:@ window get-stream-config) get-stream-config)
+ (setf (ps:@ window change-channel) change-channel)
+ (setf (ps:@ window sync-channel-from-storage) sync-channel-from-storage)
+ (setf (ps:@ window change-stream-quality) change-stream-quality)
+ (setf (ps:@ window reconnect-stream) reconnect-stream)
+ (setf (ps:@ window disable-frameset-mode) disable-frameset-mode)
+ (setf (ps:@ window init-persistent-player) init-persistent-player)
+ (setf (ps:@ window init-popout-player) init-popout-player)
+ (setf (ps:@ window update-mini-now-playing) update-mini-now-playing)
+ (setf (ps:@ window update-popout-now-playing) update-popout-now-playing)
+ (setf (ps:@ window toggle-favorite-mini) toggle-favorite-mini)
+ (setf (ps:@ window toggle-notifications) toggle-notifications)
+ (setf (ps:@ window update-notification-toggle-ui) update-notification-toggle-ui)
+ (setf (ps:@ window notify-popout-opened) notify-popout-opened)
+ (setf (ps:@ window notify-popout-closing) notify-popout-closing)
;; Auto-initialize on DOMContentLoaded based on which elements exist
(ps:chain document
diff --git a/static/asteroid.css b/static/asteroid.css
index c55aca0..621a5e6 100644
--- a/static/asteroid.css
+++ b/static/asteroid.css
@@ -1388,6 +1388,21 @@ body.persistent-player-container .persistent-reconnect-btn:hover{
background: #2a3441;
}
+body.persistent-player-container .persistent-notification-btn{
+ background: transparent;
+ color: #00ff00;
+ border: 1px solid #00ff00;
+ padding: 5px 8px;
+ cursor: pointer;
+ font-size: 1em;
+ white-space: nowrap;
+ margin-right: 5px;
+}
+
+body.persistent-player-container .persistent-notification-btn:hover{
+ background: #2a3441;
+}
+
body.persistent-player-container .persistent-disable-btn{
background: transparent;
color: #00ff00;
diff --git a/static/asteroid.lass b/static/asteroid.lass
index 7fdd73f..e817f48 100644
--- a/static/asteroid.lass
+++ b/static/asteroid.lass
@@ -1122,6 +1122,19 @@
((:and .persistent-reconnect-btn :hover)
:background "#2a3441")
+ (.persistent-notification-btn
+ :background transparent
+ :color "#00ff00"
+ :border "1px solid #00ff00"
+ :padding "5px 8px"
+ :cursor "pointer"
+ :font-size "1em"
+ :white-space nowrap
+ :margin-right "5px")
+
+ ((:and .persistent-notification-btn :hover)
+ :background "#2a3441")
+
(.persistent-disable-btn
:background transparent
:color "#00ff00"
diff --git a/template/audio-player-frame.ctml b/template/audio-player-frame.ctml
index 4811cdd..10f8637 100644
--- a/template/audio-player-frame.ctml
+++ b/template/audio-player-frame.ctml
@@ -46,6 +46,8 @@
+
+