diff --git a/CLAUDE.md b/CLAUDE.md index 1f7b63f..431dc99 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -659,7 +659,8 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal nobody can answer a prompt. Steps: `ServerCheckWorker` (`ssh test -d`; an existing folder asks before replacing it), `ExportWorker`, then `UploadWorker`, all shown in `gui/web_upload_progress.py`. The web - player has a **shuffle** button (and the S key): a shuffled walk that starts at + player has **previous/next** buttons (the arrow keys, wrapping at either + end, through the shuffle order when on) and a **shuffle** button (and the S key): a shuffled walk that starts at the current song and plays every song once. A mix can carry several images (`plan.images`): the dialog always keeps one empty row to add another, and the page shows them in order under the description, a blank diff --git a/lintunes/__init__.py b/lintunes/__init__.py index 8b8466a..aa06f75 100644 --- a/lintunes/__init__.py +++ b/lintunes/__init__.py @@ -1,3 +1,3 @@ """LinTunes — iTunes-style music library manager and player for Linux.""" -__version__ = "0.50.0" +__version__ = "0.51.0" diff --git a/lintunes/export/templates/player.css b/lintunes/export/templates/player.css index fbac128..e587c4c 100644 --- a/lintunes/export/templates/player.css +++ b/lintunes/export/templates/player.css @@ -27,7 +27,7 @@ .audiojs audio { position: absolute; left: -1px; } .audiojs { - width: 286px; + width: 334px; height: 36px; background: #eee; color: #000; @@ -40,9 +40,9 @@ } .audiojs .play-pause { - width: 40px; + width: 36px; height: 40px; - padding: 4px 6px; + padding: 4px 2px; margin: 0px; float: left; overflow: hidden; @@ -88,6 +88,21 @@ .audiojs .time em { padding: 0px 2px 0px 0px; color: #000; font-style: normal; } .audiojs .time strong { padding: 0px 0px 0px 2px; font-weight: normal; } +/* Previous / next, either side of play-pause: plain black glyphs. */ +.audiojs .skip { + float: left; + width: 24px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + color: #000; + cursor: pointer; +} +.audiojs .prev { margin-left: 4px; } +.audiojs .skip:hover { color: var(--accent, #8c764a); } +.audiojs .skip:focus-visible { outline: 1px dotted #000; } + /* Shuffle: black like the other glyphs while off, the accent while on * (the same fill as the progress bar, with the text color picked for it). */ .audiojs .shuffle { diff --git a/lintunes/export/templates/player.js b/lintunes/export/templates/player.js index 6de5935..b23ae2a 100644 --- a/lintunes/export/templates/player.js +++ b/lintunes/export/templates/player.js @@ -24,10 +24,20 @@ "use strict"; var WRAPPER = + '' + '
' + '

' + '

' + '
' + + '' + '
' + '
00:00/00:00
' + '
' + @@ -62,7 +72,9 @@ played: wrap.querySelector(".played"), duration: wrap.querySelector(".duration"), error: wrap.querySelector(".error-message"), - shuffle: wrap.querySelector(".shuffle") + shuffle: wrap.querySelector(".shuffle"), + prev: wrap.querySelector(".prev"), + next: wrap.querySelector(".next") }; function state(name) { @@ -127,6 +139,8 @@ return { element: audio, shuffleButton: ui.shuffle, + prevButton: ui.prev, + nextButton: ui.next, load: function (src) { ui.progress.style.width = "0%"; ui.loaded.style.width = "0%"; @@ -176,16 +190,30 @@ function first() { return (order || items)[0]; } function last() { var list = order || items; return list[list.length - 1]; } - player.shuffleButton.addEventListener("click", function () { - setShuffle(!order); - }); - player.shuffleButton.addEventListener("keydown", function (e) { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - e.stopPropagation(); - setShuffle(!order); - } - }); + // The previous/next buttons and the arrow keys: wrap around at either + // end, through the shuffled walk when shuffle is on. + function goNext() { + var current = document.querySelector("ol li.playing"); + select((current && step(current, 1)) || first(), true); + } + function goPrev() { + var current = document.querySelector("ol li.playing"); + select((current && step(current, -1)) || last(), true); + } + + function button(el, action) { + el.addEventListener("click", action); + el.addEventListener("keydown", function (e) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + e.stopPropagation(); + action(); + } + }); + } + button(player.shuffleButton, function () { setShuffle(!order); }); + button(player.prevButton, goPrev); + button(player.nextButton, goNext); function select(li, autoplay) { items.forEach(function (other) { other.classList.remove("playing"); }); @@ -215,13 +243,12 @@ var tag = (target.tagName || "").toLowerCase(); if (tag === "input" || tag === "textarea" || target.isContentEditable) return; - var current = document.querySelector("ol li.playing"); if (e.key === "ArrowRight") { e.preventDefault(); - select((current && step(current, 1)) || first(), true); + goNext(); } else if (e.key === "ArrowLeft") { e.preventDefault(); - select((current && step(current, -1)) || last(), true); + goPrev(); } else if ((e.key === "s" || e.key === "S") && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); setShuffle(!order); diff --git a/tests/test_round81.py b/tests/test_round81.py index e8f0e9e..1b9d143 100644 --- a/tests/test_round81.py +++ b/tests/test_round81.py @@ -565,3 +565,11 @@ def test_upload_window_shows_the_size(qapp, window): _wait_for(qapp, lambda: _FakeUpload.runs) from lintunes.gui import web_upload_progress as wp assert "KB" in window._web_progress.status(wp.EXPORT) + + +def test_player_ships_previous_and_next_buttons(): + js = (exporter.templates() / "player.js").read_text() + css = (exporter.templates() / "player.css").read_text() + assert 'class="skip prev"' in js and 'class="skip next"' in js + assert "button(player.prevButton, goPrev)" in js + assert ".audiojs .skip" in css