Better GUI

This commit is contained in:
2026-08-27 10:49:28 +02:00
parent 26833ad7eb
commit a2dea77eb0
4 changed files with 282 additions and 9 deletions
+8
View File
@@ -92,6 +92,14 @@ in. De naam wordt in dezelfde agent-GUI ingesteld. Na registratie verschijnt
de agent met die naam als uitvoer van type `AGENT`; de server volgt latere de agent met die naam als uitvoer van type `AGENT`; de server volgt latere
naamswijzigingen bij registratie en polling. naamswijzigingen bij registratie en polling.
Onder **Afspelen** toont de agent de huidige track, afspeeltoestand, verstreken
en totale tijd, decoderformaat en samplefrequentie. Op Windows kan de agent via
de optie onderaan actief blijven in het systeemvak wanneer het venster wordt
gesloten. Dubbelklik op het pictogram om het venster terug te halen; via het
contextmenu kan de agent volledig worden afgesloten. Hiervoor gebruikt de
agent de standaard Windows Forms-notificatiezone via de aanwezige Windows
PowerShell. Op andere platforms blijft de optie uitgeschakeld.
De eerste implementatie downloadt een geselecteerde track volledig naar een De eerste implementatie downloadt een geselecteerde track volledig naar een
tijdelijk bestand voordat `racket-audio` de weergave start. Daardoor is de tijdelijk bestand voordat `racket-audio` de weergave start. Daardoor is de
implementatie klein en zijn geen gedeelde mappen nodig, maar het starten van implementatie klein en zijn geen gedeelde mappen nodig, maar het starten van
+172 -8
View File
@@ -6,6 +6,7 @@
racket-audio racket-audio
racket/class racket/class
racket/file racket/file
racket/format
racket/gui/base racket/gui/base
racket/os racket/os
racket/path racket/path
@@ -13,7 +14,8 @@
racket/random racket/random
racket/string racket/string
simple-ini simple-ini
simple-log) simple-log
"windows-tray.rkt")
(provide run-player-agent-gui) (provide run-player-agent-gui)
@@ -49,9 +51,13 @@
(ini-get config 'agent 'name (ini-get config 'agent 'name
(format "~a playback" (gethostname)))) (format "~a playback" (gethostname))))
(define tray-enabled?
(ini-get config 'agent 'system-tray #t))
(define (save-config!) (define (save-config!)
(ini-set! config 'agent 'app-id app-id) (ini-set! config 'agent 'app-id app-id)
(ini-set! config 'agent 'name assigned-name) (ini-set! config 'agent 'name assigned-name)
(ini-set! config 'agent 'system-tray tray-enabled?)
(ini-set! config 'server 'url server-url) (ini-set! config 'server 'url server-url)
(ini->file config config-file #:private? #t)) (ini->file config config-file #:private? #t))
@@ -110,6 +116,7 @@
(define running? #f) (define running? #f)
(define audio #f) (define audio #f)
(define temporary-media #f) (define temporary-media #f)
(define current-track #f)
(define acknowledged-command 0) (define acknowledged-command 0)
(define ended-counter 0) (define ended-counter 0)
(define logical-volume 50) (define logical-volume 50)
@@ -124,6 +131,19 @@
'volume logical-volume 'volume logical-volume
'error 'null)) 'error 'null))
(define (format-time value)
(let* ((seconds
(if (and (number? value) (>= value 0))
(inexact->exact (floor value))
0))
(hours (quotient seconds 3600))
(minutes (quotient (remainder seconds 3600) 60))
(remaining (remainder seconds 60)))
(format "~a:~a:~a"
(~r hours #:min-width 2 #:pad-string "0")
(~r minutes #:min-width 2 #:pad-string "0")
(~r remaining #:min-width 2 #:pad-string "0"))))
(define (with-agent-state proc) (define (with-agent-state proc)
(call-with-semaphore state-lock proc)) (call-with-semaphore state-lock proc))
@@ -214,6 +234,14 @@
(info-player-agent "Executing command ~a" action) (info-player-agent "Executing command ~a" action)
(cond (cond
((string=? action "play") ((string=? action "play")
(set! current-track data)
(with-agent-state
(λ ()
(set! agent-state
(hash-set
(hash-set agent-state 'state "starting")
'error
'null))))
(let ((next-media (let ((next-media
(download-media! (download-media!
(hash-ref data 'mediaToken) (hash-ref data 'mediaToken)
@@ -248,9 +276,15 @@
(define frame #f) (define frame #f)
(define status-message #f) (define status-message #f)
(define playback-message #f)
(define playback-details #f)
(define name-field #f) (define name-field #f)
(define server-field #f) (define server-field #f)
(define connect-button #f) (define connect-button #f)
(define tray-checkbox #f)
(define tray-stop #f)
(define shutting-down? #f)
(define playback-timer #f)
(define (show-status! message) (define (show-status! message)
(queue-callback (queue-callback
@@ -266,6 +300,55 @@
(send name-field set-value name))) (send name-field set-value name)))
#f)) #f))
(define (refresh-playback-status!)
(when (and playback-message playback-details)
(let* ((snapshot (state-snapshot))
(state (hash-ref snapshot 'state "stopped"))
(title
(and current-track
(hash-ref current-track 'title #f)))
(artist
(and current-track
(hash-ref current-track 'artist #f)))
(track-label
(cond
((and artist (not (string=? artist "")) title)
(format "~a — ~a" artist title))
(title title)
(else "Geen track geselecteerd")))
(prefix
(cond
((string=? state "playing") "Speelt")
((string=? state "paused") "Gepauzeerd")
((string=? state "starting") "Laden")
((string=? state "stopped") "Gestopt")
(else state)))
(position (hash-ref snapshot 'position 0))
(duration (hash-ref snapshot 'duration 'null))
(format-name (hash-ref snapshot 'format ""))
(rate (hash-ref snapshot 'rate 'null))
(details
(filter
(λ (value) (not (string=? value "")))
(list
(format "~a / ~a"
(format-time position)
(if (number? duration)
(format-time duration)
"--:--:--"))
(if (string=? format-name "") format-name
(string-upcase format-name))
(if (number? rate)
(format "~a kHz" (~r (/ rate 1000.0)
#:precision '(= 1)))
"")))))
(send playback-message
set-label
(if current-track
(format "~a: ~a" prefix track-label)
"Er wordt niets afgespeeld"))
(send playback-details set-label (string-join details " · ")))))
(define (poll-loop) (define (poll-loop)
(with-handlers (with-handlers
((exn:fail? ((exn:fail?
@@ -357,22 +440,63 @@
(show-status! "Verbinden…") (show-status! "Verbinden…")
(start-worker!)) (start-worker!))
(define (stop-tray!)
(when tray-stop
(tray-stop)
(set! tray-stop #f)))
(define (shutdown!)
(unless shutting-down?
(set! shutting-down? #t)
(stop-worker!)
(stop-tray!)
(when playback-timer
(send playback-timer stop))
(when audio
(with-handlers ((exn:fail? void))
(audio-quit! audio))
(set! audio #f))
(safe-delete-file temporary-media)))
(define (ensure-tray!)
(cond
((and tray-enabled? (not tray-stop))
(set!
tray-stop
(start-windows-tray!
(λ ()
(queue-callback
(λ ()
(send frame show #t)
(send frame focus))
#f))
(λ ()
(queue-callback
(λ ()
(shutdown!)
(send frame show #f))
#f))))
(when (and tray-enabled? (not tray-stop))
(set! tray-enabled? #f)
(save-config!)
(when tray-checkbox
(send tray-checkbox set-value #f))))
((not tray-enabled?)
(stop-tray!))))
(define agent-frame% (define agent-frame%
(class frame% (class frame%
(super-new) (super-new)
(define/augment (on-close) (define/augment (on-close)
(stop-worker!) (unless (and tray-enabled? tray-stop)
(when audio (shutdown!))
(with-handlers ((exn:fail? void))
(audio-quit! audio)))
(safe-delete-file temporary-media)
(inner (void) on-close)))) (inner (void) on-close))))
(set! frame (set! frame
(new agent-frame% (new agent-frame%
(label "RKT Web Player Agent") (label "RKT Web Player Agent")
(width 560) (width 560)
(height 230))) (height 310)))
(define panel (define panel
(new vertical-panel% (new vertical-panel%
(parent frame) (parent frame)
@@ -394,7 +518,28 @@
(parent panel) (parent panel)
(label "Applicatie-ID") (label "Applicatie-ID")
(init-value app-id))) (init-value app-id)))
(send id-field enable #f) ;; Lock just the editor instead of disabling the complete widget. Windows
;; renders disabled native controls in grey, which made the label and ID look
;; as though their glyphs were damaged.
(send (send id-field get-editor) lock #t)
(define playback-panel
(new group-box-panel%
(parent panel)
(label "Afspelen")
(alignment '(left top))
(stretchable-height #f)))
(set! playback-message
(new message%
(parent playback-panel)
(label "Er wordt niets afgespeeld")
(auto-resize #t)))
(set! playback-details
(new message%
(parent playback-panel)
(label "00:00:00 / --:--:--")
(auto-resize #t)))
(define controls (define controls
(new horizontal-panel% (new horizontal-panel%
(parent panel) (parent panel)
@@ -411,6 +556,25 @@
(label "Verbinden…") (label "Verbinden…")
(auto-resize #t))) (auto-resize #t)))
(set! tray-checkbox
(new check-box%
(parent panel)
(label "In het systeemvak actief blijven na sluiten")
(value (and tray-enabled? (windows-tray-available?)))
(enabled (windows-tray-available?))
(callback
(λ (box _event)
(set! tray-enabled? (send box get-value))
(save-config!)
(ensure-tray!)))))
(set! playback-timer
(new timer%
(notify-callback refresh-playback-status!)
(interval 500)))
(refresh-playback-status!)
(send frame show #t) (send frame show #t)
(ensure-tray!)
(start-worker!) (start-worker!)
frame) frame)
+5 -1
View File
@@ -367,7 +367,11 @@
'filename 'filename
(path->string (path->string
(or (file-name-from-path (track-file item)) (or (file-name-from-path (track-file item))
(track-file item))))))) (track-file item)))
'title (track-title item)
'artist (track-artist item)
'album (track-album item)
'duration (or (track-duration item) 'null)))))
(else (else
(dlna-player-play! backend (track-file item)))) (dlna-player-play! backend (track-file item))))
(clear-error! value))) (clear-error! value)))
+97
View File
@@ -0,0 +1,97 @@
#lang racket/base
(require racket/port
racket/system)
(provide windows-tray-available?
start-windows-tray!)
(define (windows-tray-available?)
(and (eq? (system-type 'os) 'windows)
(find-executable-path "powershell.exe")
#t))
(define tray-script
#<<POWERSHELL
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$tray = New-Object System.Windows.Forms.NotifyIcon
$tray.Icon = [System.Drawing.SystemIcons]::Application
$tray.Text = "RKT Web Player Agent"
$tray.Visible = $true
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$open = $menu.Items.Add("Open RKT Web Player Agent")
$quit = $menu.Items.Add("Exit")
$tray.ContextMenuStrip = $menu
$send = {
param([string]$message)
[Console]::Out.WriteLine($message)
[Console]::Out.Flush()
}
$open.add_Click({ & $send "open" })
$tray.add_DoubleClick({ & $send "open" })
$quit.add_Click({
& $send "quit"
$tray.Visible = $false
[System.Windows.Forms.Application]::Exit()
})
[System.Windows.Forms.Application]::Run()
$tray.Visible = $false
$tray.Dispose()
POWERSHELL
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Add a Windows notification-area icon with open and quit actions.
; pre : Called on Windows with PowerShell and a GUI eventspace available.
; post : Callbacks are delivered from a reader thread; the stop thunk removes
; the helper process and its icon.
; result : A stop thunk, or #f when the platform has no supported tray helper.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (start-windows-tray! on-open on-quit)
(cond
((not (windows-tray-available?)) #f)
(else
(let-values (((process output input error-output)
(subprocess
#f #f #f
(find-executable-path "powershell.exe")
"-NoLogo"
"-NoProfile"
"-NonInteractive"
"-WindowStyle"
"Hidden"
"-STA"
"-Command"
tray-script)))
(close-output-port input)
(define stopped? #f)
(define reader
(thread
(λ ()
(let loop ()
(let ((line (read-line output 'any)))
(unless (eof-object? line)
(cond
((string=? line "open") (on-open))
((string=? line "quit") (on-quit)))
(loop)))))))
(thread
(λ ()
;; Drain diagnostics so the helper cannot block on a full pipe.
(copy-port error-output (open-output-nowhere))))
(λ ()
(unless stopped?
(set! stopped? #t)
(when (and reader (not (thread-dead? reader)))
(kill-thread reader))
(close-input-port output)
(close-input-port error-output)
(when (eq? (subprocess-status process) 'running)
(subprocess-kill process #t))))))))