This commit is contained in:
2026-02-19 20:29:36 +01:00
parent 111042442f
commit 0fffd544c0
20 changed files with 659 additions and 114 deletions

180
gui.rkt
View File

@@ -2,6 +2,10 @@
(require web-racket
racket/runtime-path
racket/gui
"utils.rkt"
"music-library.rkt"
"translate.rkt"
)
(provide
@@ -9,16 +13,18 @@
rktplayer%
)
(define-runtime-path rktplayer-start "rktplayer.html")
(define-runtime-path rktplayer-start "gui/rktplayer.html")
(define-syntax ww-connect
(syntax-rules (this)
((_ id method)
(send (send this element id) connect 'click (λ (data) (send this method)))
)
)
)
(define player-menu
(λ ()
(menu 'main-menu
(menu-item 'm-file (tr "File")
#:submenu
(menu (menu-item 'm-select-library-dir (tr "Select Music Library Folder"))
(menu-item 'm-set-lang (tr "Set language"))
(menu-item 'm-quit (tr "Quit") #:separator #t)))
)
))
(define rktplayer%
(class ww-webview%
@@ -27,15 +33,132 @@
[html-file rktplayer-start]
)
(define el-seeker #f)
(define el-library #f)
(define music-library (send settings get 'music-library (find-system-path 'home-dir)))
(define current-music-path #f)
(define inner-html-handlers (make-hash))
(define/override (html-loaded)
(super html-loaded)
(ww-connect 'play play)
(ww-connect 'prev previous-track)
(ww-connect 'next next-track)
(ww-connect 'repeat repeat)
(ww-connect 'volume volume)
(set! el-seeker (send this element 'seek))
(displayln (format "el-seeker: ~a" (send el-seeker get)))
(let ((seek-reactor (make-delayed-reactor 0.3 (λ (percentage) (send this seek-to percentage)))))
(send el-seeker on-change! seek-reactor))
(set! el-library (send this element 'library))
(send this set-menu! (player-menu))
(send this connect-menu! 'm-quit (λ () (send this quit)))
(send this connect-menu! 'm-select-library-dir (λ () (send this select-library)))
(send this update-library)
)
(define/public (update-library)
(when (eq? current-music-path #f)
(set! current-music-path music-library))
(let* ((nr 0)
(l (filter (λ (r) (music-lib-relevant? (cadr r)))
(map (λ (e)
(set! nr (+ nr 1))
(list (format "row-~a" nr) (build-path current-music-path e) (format "path-~a" nr)))
(directory-list current-music-path)))))
(displayln current-music-path)
(displayln music-library)
(unless (equal? (format "~a" current-music-path) (format "~a" music-library))
(set! l (cons (list "lib-up" "" "lib-up") l))
)
(let ((html (mktable l 'music-library library-formatter)))
(let ((handle (send el-library set-inner-html! html)))
(hash-set! inner-html-handlers handle
(λ (oke)
(when oke
(send this bind 'dblclick "td.library-entry")
(send this bind 'click "td.library-entry")
(send this bind 'contextmenu "td.library-entry")
(for-each (λ (row)
(let ((path-id (string->symbol (caddr row))))
(let ((el (send this new-element path-id)))
(send el connect 'dblclick
(λ (args)
(send this path-choosen (cadr row))))
(send el connect 'click
(λ (args)
(displayln args)))
(send el connect 'contextmenu
(λ (evt)
(send this context-for-path evt (cadr row))))
)))
l)
))))
))
)
(define/override (inner-html-set handle oke)
(ww-debug "inner-html-set called")
(let ((cb (hash-ref inner-html-handlers handle #f)))
(ww-debug (format "got cb = ~a for handle ~a" cb handle))
(when cb
(hash-remove! inner-html-handlers handle)
(cb oke)))
)
(define/public (path-choosen path)
(let ((path-part (if (equal? path "") ".." (format "~a" path))))
(let ((npath (if (string=? path-part "..")
(build-path current-music-path path-part)
path)))
(when (directory-exists? npath)
(set! current-music-path (normalize-path npath))
(send this update-library)
)
)
)
)
(define/public (context-for-path evt path)
(let* ((mnu (menu 'library-popup
(menu-item 'm-play-this (tr "Play this") #:callback (λ () (send this play-path path)))
(menu-item 'm-booklet (tr "Open booklet") #:callback (λ () (send this open-booklet path)))
(menu-item 'm-folder (tr "Open containing folder") #:callback (λ () (send this open-folder path))
#:submenu
(menu (menu-item 'm-idx (tr "Select Music Library Folder") #:separator #t)
(menu-item 'm-idy (tr "Quit") #:separator #t
#:submenu
(menu (menu-item 'mabd (tr "Ja"))
(menu-item 'mdedjk (tr "No"))
(menu-item 'sdakjfas (tr "akjfhalk"))
))
))
)
)
(js-evt (hash-ref evt 'js_evt (make-hash)))
(clientX (hash-ref js-evt 'clientX 60))
(clientY (hash-ref js-evt 'clientY 60))
)
(send this popup-menu mnu clientX clientY)
)
)
(define/public (play-path path)
(displayln (format "Playing ~a" path)))
(define/public (open-booklet path)
(displayln (format "Open booklet ~a" path)))
(define/public (open-folder path)
(displayln (format "open folder ~a" path)))
(define/public (play)
(displayln "Play button clicked")
)
@@ -48,9 +171,42 @@
(displayln "Previous track")
)
(define/public (repeat)
(displayln "Repeat")
)
(define/public (volume)
(displayln "Volume")
)
(define/public (seek-to percentage)
(displayln (format "Seeking to percentage: ~a" percentage))
)
(define/public (quit)
(displayln (format "Quitting"))
(send this close))
(define/public (select-library)
(let ((handle (send this choose-dir
(tr "Choose the folder containing your music library")
(format "~a" music-library))))
(displayln (format "Selecting Music Library with handle: ~a" handle))
)
)
(define/override (dir-choosen handle choosen dir)
(when choosen
(set! music-library dir)
(send settings set! 'music-library dir)
(set! current-music-path #f)
(send this update-library)))
(begin
(displayln "RktPlayer started")
)
(let ((lang (send settings get 'lang 'en)))
(displayln (format "RktPlayer started, current language: ~a" lang)))
)
)
)

View File

Before

Width:  |  Height:  |  Size: 893 B

After

Width:  |  Height:  |  Size: 893 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 903 B

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

166
gui/rktplayer.html Normal file
View File

@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css" />
<meta charset="UTF-8" />
<title>RktPlayer - A music player</title>
<script>
window._web_wire_popup_menu = function(menu, x = -1, y = -1, kind = 'popup') {
if (menu.id == '#f') { menu.id = null; }
let menu_id = (kind == 'popup') ? '@@popup-menu@@' : '@@menubar@@';
let submenu_els = [];
let triggerMenuItem;
let clearPopupMenu = function() {
if (kind == 'popup') {
let el = document.getElementById(menu_id);
if (el !== null) {
el.innerHTML = '';
el.style.display = 'none';
}
if (menu.id !== null) {
console.log("Sending clear trigger for menu clearance : " + menu.id);
let obj = { evt: 'menu-item-choosen', item: menu.id };
window._web_wire_put_evt(obj);
}
} else {
// hide all submenus
submenu_els.forEach(function (el) { el.style.display = 'none'; });
}
};
triggerMenuItem = function(id) {
console.log("Triggering menu item : " + id);
let obj = { evt: 'menu-item-choosen', item: id };
window._web_wire_put_evt(obj);
//clearPopupMenu();
};
let showSubMenu = function(menu_el, item_el, el, parent_type) {
if (parent_type == 'menu') {
el.style.display = 'flex';
let rect = item_el.getBoundingClientRect();
let r = rect.left;
let t = rect.height;
el.style.left = r + 'px';
el.style.top = t + 'px';
} else {
el.style.display = "flex";
let rect = menu_el.getBoundingClientRect();
let irect =item_el.getBoundingClientRect();
let r = rect.width + 5;
let t = irect.y - rect.y;
el.style.left = r + "px";
el.style.top = t + "px";
}
};
let hideSubMenu = function(el) { el.style.display = "none"; };
let makePopupMenu = function(el, menu, visible, type) {
let i;
let N = menu.length;
for(i = 0; i < N; i++) {
let item = menu[i];
let item_el = document.createElement("div");
item_el.id = item.id;
item_el.classList.add("menu-item");
let item_el_icon = document.createElement('span');
item_el_icon.classList.add("menu-icon");
if (item.icon) {
let icon_img = document.createElement('img');
icon_img.setAttribute('src', item.icon);
item_el_icon.appendChild(icon_img);
}
if (item.separator) {
item_el.classList.add("separator");
}
let item_el_name = document.createElement('span');
item_el_name.classList.add('menu-name');
item_el_name.innerHTML = item.name;
let item_el_submenu = document.createElement('span');
item_el_submenu.classList.add('menu-submenu');
if (item.submenu) {
if (type == 'submenu' || kind == 'popup') {
item_el_submenu.innerHTML = '&gt;';
}
item_el.setAttribute('type', 'submenu');
let submenu_el = document.createElement("div");
submenu_els.push(submenu_el);
submenu_el.classList.add("submenu");
submenu_el.classList.add("menu");
item_el.appendChild(submenu_el);
submenu_el.style.display = 'none';
makePopupMenu(submenu_el, item.submenu.menu, false, 'submenu');
item_el.addEventListener('mouseenter', function () { showSubMenu(el, item_el, submenu_el, type); });
item_el.addEventListener('mouseleave', function () { hideSubMenu(submenu_el); });
} else {
item_el.setAttribute('type', 'item');
item_el.addEventListener('click', function() { triggerMenuItem(item.id); });
}
item_el.appendChild(item_el_icon);
item_el.appendChild(item_el_name);
item_el.appendChild(item_el_submenu);
el.appendChild(item_el);
}
};
let el = document.getElementById(menu_id);
if (el === null) {
el = document.createElement("div");
el.id = menu_id;
el.classList.add((kind == 'popup') ? "popup-menu" : "menubar");
if (kind == 'popup') {
el.classList.add("menu");
document.body.appendChild(el);
} else {
document.body.prepend(el);
}
} else {
el.innerHTML = '';
}
makePopupMenu(el, menu.menu, true, 'menu');
el.style.left = x + "px";
el.style.top = y + "px";
el.style.display = "flex";
let clearer_f = function() {
clearPopupMenu();
document.body.removeEventListener('click', clearer_f);
document.body.removeEventListener('contextmenu', clearer_f);
};
document.body.addEventListener('click', clearer_f);
document.body.addEventListener('contextmenu', clearer_f);
};
window._web_wire_menu = function(menubar) {
window._web_wire_popup_menu(menubar, -1, -1, 'menubar');
};
</script>
</head>
<body>
<div class="pane">
<div class="buttons">
<button id="prev" class="command"><img id="previous-img" src="buttons/previous.svg" /></button>
<button id="play" class="command"><img id="play-img" src="buttons/play.svg" /></button>
<button id="next" class="command"><img id="next-img" src="buttons/next.svg" /></button>
<input type="range" min="0" max="100" value="0" class="h-slider" id="seek" step="0.01" />
<div class="text-block"><span id="time" class="time">0:00</span></div>
<div class="text-block"><span id="totaltime" class="time">0:00</span></div>
<button id="repeat" class="command"><img id="repeat-img" src="buttons/repeat-off.svg" /></button>
<button id="volume" class="command"><img id="volume-img" src="buttons/volume-high.svg" /></button>
</div>
<div class="hpane">
<div class="music-info">
<div class="music-library">
<div id="library" class="content scrolly">
Library
</div>
</div>
<div class="album-art">
<div id="album-art" class="content">
Album art
</div>
</div>
</div>
<div class="music-playing">
<div id="tracks" class="content scrolly">
Music playing
</div>
</div>
</div>
</div>
</body>
</html>

156
gui/styles.css Normal file
View File

@@ -0,0 +1,156 @@
body {
font-family: Arial;
font-size: 11pt;
}
.pane {
height: calc(100vh - 40px - 2em - 10px);
width: calc(100% - 10px);
display: flex;
flex-direction: column;
}
.buttons {
height: 40px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid black;
margin-bottom: 5px;
}
button {
background: #e0e0e0;
border: none;
border-left: 1px solid black;
border-right: 1px solid black;
}
button:hover {
background: #909090;
}
button.command {
margin: 0;
padding: 0;
width: 40px;
height: 40px;
}
button.command img {
height: 32px;
width: 32px;
margin: 0;
padding: 2px;
}
input.h-slider {
flex-grow: 1;
}
.buttons .text-block {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
width: 4em;
border-left: 1px solid black;
}
.buttons span.time {
font-weight: bold;
text-align: center;
}
.hpane {
height: 100%;
width: 100%;
display: flex;
}
.music-info {
border: 1px solid black;
width: 30%;
height: 100%;
}
.music-library {
border-bottom: 1px solid black;
width: 100%;
height: 50%;
}
.album-art {
width: 100%;
height: 50%;
padding: 5px;
}
.music-playing {
border: 1px solid black;
border-left: none;
width: 70%;
height: 100%;
}
.content {
width: 100%;
height: 100%;
overflow: hidden;
}
.scrolly {
overflow-y: auto;
}
table.music-library {
padding: 5px;
}
table.music-library tr td {
border-bottom: 1px solid #f0f0f0;
cursor: default;
height: 1.1em;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
}
table.music-library tr:hover td {
background: #e0e0e0;
}
.popup-menu, .popup-submenu {
display: flex;
flex-direction: column;
margin: 5px;
padding: 5px;
position: absolute;
z-index: 9999;
border: 1px solid black;
background: #e0e0e0;
}
.popup-submenu {
display: none;
}
.menubar .menu-item span.menu-icon, .popup-menu .menu-item span.menu-icon {
min-width: unset;
width: unset;
}
.menubar .menu-item {
min-width: unset;
width: unset;
}
.menu-item span.menu-name {
text-wrap: nowrap;
}

47
music-library.rkt Normal file
View File

@@ -0,0 +1,47 @@
#lang racket
(provide music-lib-relevant?
is-music-dir?
is-music-file?
basename
library-formatter
)
(define (music-lib-relevant? f)
(let ((type (file-or-directory-type f #t)))
(if (eq? type 'directory)
(let ((name (basename f)))
(not (string-prefix? name ".")))
(if (eq? type 'file)
(let* ((fn (string-downcase (format "~a" f)))
(exts (list "flac" "mp3")))
(let ((l (filter (λ (e) (string-suffix? fn (string-append "." e))) exts)))
(not (null? l))))
#f))))
(define (is-music-dir? f)
(and (music-lib-relevant? f)
(directory-exists? f)))
(define (is-music-file? f)
(and (music-lib-relevant? f)
(file-exists? f)))
(define (basename file)
(call-with-values (λ () (split-path file))
(λ (base name is-dir)
(path->string name))))
(define (library-formatter row)
(let ((file-entry (car row))
(file-id (cadr row))
)
(list (list 'td (list (list 'class "library-entry") (list 'id file-id) (list 'file (format "~a" file-entry)))
(if (equal? file-id "lib-up")
file-entry
(basename file-entry))
))
)
)

1
player.rkt Normal file
View File

@@ -0,0 +1 @@
#lang racket

1
playlist.rkt Normal file
View File

@@ -0,0 +1 @@
#lang racket

View File

@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css" />
<meta charset="UTF-8" />
<title>RktPlayer - A music player</title>
</head>
<body>
<div class="pane">
<div class="buttons">
<button id="prev" class="command"><img id="previous-img" src="previous.svg" /></button>
<button id="play" class="command"><img id="play-img" src="play.svg" /></button>
<button id="next" class="command"><img id="next-img" src="next.svg" /></button>
<span id="slider" class="h-slider"></span>
<div class="text-block"><span id="time" class="time">0:00</span></div>
<div class="text-block"><span id="totaltime" class="time">0:00</span></div>
<button id="repeat" class="command"><img id="repeat-img" src="repeat-off.svg" /></button>
<button id="volume" class="command"><img id="volume-img" src="volume-high.svg" /></button>
</div>
<div class="hpane">
<div class="music-info">
<div class="music-library">
Library
</div>
<div class="album-art">
Album art
</div>
</div>
<div class="music-playing">
Music playing
</div>
</div>
</div>
</body>
</html>

View File

@@ -2,13 +2,21 @@
(require "gui.rkt"
simple-ini/class
web-racket
racket-sound
)
(ww-set-custom-webui-wire-command! "/home/hans/src/racket/webui-wire/build/Release/webui-wire")
(ww-set-debug #t)
;(ww-tail-log)
(define (run)
(let* ((ini (new ini% [file 'rktplayer]))
(settings (new ww-simple-ini% [ini ini] [section 'player]))
(window (new rktplayer% [settings settings]))
(window (new rktplayer% [settings settings] [use-browser #t]))
)
window)
)
;(run)

View File

@@ -1,66 +0,0 @@
body {
font-family: Arial;
font-size: 11pt;
}
.pane {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
.buttons {
height: 40px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid black;
}
button {
background: #e0e0e0;
border: none;
border-left: 1px solid black;
border-right: 1px solid black;
}
button:hover {
background: #909090;
}
button.command {
margin: 0;
padding: 0;
width: 40px;
height: 40px;
}
button.command img {
height: 32px;
width: 32px;
margin: 0;
padding: 2px;
}
span.h-slider {
flex-grow: 1;
}
.buttons .text-block {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
width: 4em;
border-left: 1px solid black;
}
.buttons span.time {
font-weight: bold;
text-align: center;
}

53
translate.rkt Normal file
View File

@@ -0,0 +1,53 @@
#lang racket
(provide tr)
(define tr_map (make-hash))
(define (add-tr sentence language translated-sentence)
(let ((lang-hash (hash-ref tr_map language (make-hash))))
(hash-set! lang-hash sentence translated-sentence)
(hash-set! tr_map language lang-hash)))
(define-syntax add2
(syntax-rules ()
((_ s (l ts))
(add-tr s l ts))))
(define-syntax add
(syntax-rules ()
((_ s l1 ...)
(begin
(add2 s l1)
...))))
(define language 'en)
(define (languages)
'((en "English") (nl "Nederlands")))
(define (set-lang! l)
(set! language l))
(define (tr s)
(if (eq? language 'en)
s
(let ((lang-hash (hash-ref tr_map language (make-hash))))
(let ((translated (hash-ref lang-hash s (format "~a:~a" language s))))
translated
)
)
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Translations
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(add "Select Music Library Folder"
('nl "Selecteer map met Muziek Bibliotheek"))
(add "Choose the folder containing your music library"
('nl "Kies de map met de Muziek Bibliotheek"))
(add "Quit"
('nl "Beëindigen"))

58
utils.rkt Normal file
View File

@@ -0,0 +1,58 @@
#lang racket/base
(require racket/gui
xml
xml/xexpr
)
(provide ww-connect
make-delayed-reactor
mktable
simple-row-formatter
)
(define-syntax ww-connect
(syntax-rules (this)
((_ id method)
(send (send this element id) connect 'click (λ (data) (send this method)))
)
)
)
(define (make-delayed-reactor seconds closure)
(let* ((last-val #f)
(last-time -1)
(interval-ms (* seconds 1000))
(timeout-check (λ ()
(let ((ms (current-milliseconds)))
(unless (= last-time -1)
(when (> ms (+ last-time interval-ms))
(set! last-time -1)
(closure last-val))))))
(timer (new timer% [notify-callback timeout-check] [interval 100]))
)
(λ (val)
(set! last-val val)
(set! last-time (current-milliseconds))
)))
(define (simple-row-formatter row)
(map (λ (e) (list 'td (format "~a" e))) row))
(define (mktable l table-class row-formatter)
(xexpr->string
(append
(list 'table (list (list 'class (format "~a" table-class))))
(map (λ (row)
(let ((row-id (car row)))
(append (list 'tr (list (list 'id (format "~a" row-id))))
(row-formatter (cdr row)))))
l)
)
)
)