GUI: make the Shuffle button on Liked Songs and playlist pages work
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 78/100
- Tipo di issue
- Funzionalità
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- react, typescript
- Ambito
- frontend
Direzione di ricerca
Inizia da gui/src/ListPage.tsx, dal pulsante Shuffle disabilitato (righe 226-233) e dalla lista shownTracks (righe 126-129). Aggiungi in gui/src/sortModel.ts, accanto a shownPlayRequest, un helper puro shufflePlayRequest con una funzione casuale iniettabile, e collegalo all'onClick del pulsante. È completato quando npm test -- sortModel e npm test -- listPage passano con nuovi casi e i controlli completi in gui/ passano.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
What
Open Liked Songs or any playlist in the GUI. Next to the big green Play button there's a Shuffle button. It's always greyed out, even when the list is full of songs. It was added as a placeholder and never wired up.
To see it:
- Start the GUI (see "Done when" for how). From Your Library, open Liked Songs or a playlist. With Local Files, open a folder.
- Hover over the Shuffle icon next to Play, or click it. Nothing happens, because it's disabled.
What should happen: clicking Shuffle plays the songs you see on the page in a random order. If you typed something in the Filter box, it shuffles only the matching rows, the same rows Play would use.
All of this happens in the page. It picks a random order with a Fisher-Yates shuffle and sends the same "play this list of URIs" message that Play already sends after a sort or filter. No Rust change is needed.
Shuffle uses the rows that are loaded when you click. You don't need to fetch anything first. Once the first page of Liked Songs or a Spotify playlist arrives, spotatui fetches the remaining pages in the background by itself, so the list fills up within moments. The header already tells you when it isn't complete yet ("120+ songs", or "500 songs, 120 loaded").
Where to look
gui/src/ListPage.tsx, lines 226-233. This is the button. Line 230 hard-codesdisabled, and there's noonClick:<button type="button" className="list-shuffle" aria-label="Shuffle" disabled > <Glyph name="shuffle" size={28} /> </button>- Same file, the Play button just above it (lines 220-225). Its label is
`Play ${title}`(titleis set at line 182), and it's disabled whenfirst < 0.first(line 205) isshownTracks.findIndex((track) => track.uri), the first shown row that has a URI. - Same file, lines 126-129:
shownTracksholds the rows as the table draws them, after the sort and the filter. That's the list to shuffle. - Same file, lines 132-138:
play(index)builds its request withshownPlayRequestand callssend(action). - Same file, lines 172-177: this is where the page asks for the next page itself, with
send({ LoadMore: ... }), when you step past the last row. You won't need it for Shuffle. gui/src/sortModel.ts,shownPlayRequest(lines 148-157). For a Spotify playlist in its loaded order, it sendsPlayTrackInContext. Once the rows are sorted or filtered, it sendsplayRequest(shown, index)instead.gui/src/libraryModel.ts,playRequest(lines 109-117). Shuffle should send this same shape:
A row counts as playable when it has aconst uris = tracks.flatMap((track) => (track.uri ? [track.uri] : [])); const offset = tracks.slice(0, cursor).filter((track) => track.uri).length; return { PlayUris: { uris, offset } };uri. The page never readsis_playable, so don't add that check here either.gui/src/App.tsx, lines 13-16:sendwraps everyActionas{ type: "action", action }. That'sClientMessage::Actioninsrc/gui/protocol.rs(lines 590-592). SoPlayUrisis an existingAction, not a separate client message, and the page can already send it.src/core/action/mod.rs, lines 95-98:Action::PlayUris { uris, offset }.src/core/action/apply.rsline 68 hands it toApp::start_playback_uris(src/core/app/transport.rs, lines 465-480), which routes it to whichever source the URIs belong to.src/core/action/mod.rs, line 83:Action::SetShuffle(bool). Its arm inapply.rs(lines 49-56) reads the shuffle state of whatever is playing right now and toggles that player. The traps below explain why that matters.src/infra/network/playback.rs, line 32 (MAX_API_PLAYBACK_URIS: usize = 100) andtrim_api_playback_uris(lines 90-115). When Spotify starts from a URI list, it keeps at most 100 URIs around the offset. With offset 0, that's the first 100 of your shuffled order.gui/src/ListPage.css, lines 24-35:.list-shufflealready has a hover style for the enabled state, so you don't need any new CSS.- Tests:
gui/src/sortModel.test.tshas atrack(name, artists, album, duration_ms, uri)helper whereurican benull(lines 16-37). ItsshownPlayRequestblock (lines 204-229) is a good model.gui/src/listPage.test.tsxrendersListPagewithrenderToStaticMarkup, using thepage()helper at lines 55-74.
Possible solution
- Add a pure helper to
gui/src/sortModel.ts, right aftershownPlayRequest. Pass the random function in as a parameter so a test can control it:/** Play the shown rows that have a URI, in a random order; null when none has one. */ export function shufflePlayRequest( shown: TrackInfo[], random: () => number = Math.random, ): Action | null { const uris = shown.flatMap((track) => (track.uri ? [track.uri] : [])); if (uris.length === 0) return null; for (let i = uris.length - 1; i > 0; i--) { const j = Math.floor(random() * (i + 1)); [uris[i], uris[j]] = [uris[j], uris[i]]; } return { PlayUris: { uris, offset: 0 } }; } - In
ListPage.tsx, import it and wire up the button. Give it a label that names the list, the same way Play does:<button type="button" className="list-shuffle" aria-label={`Shuffle ${title}`} disabled={first < 0} onClick={() => { const action = shufflePlayRequest(shownTracks); if (action) send(action); }} > - Add a line to
CHANGELOG.md, under## [Unreleased]>### Added, in the same style as the entries already there:
That section changes often, so if it conflicts, rebase on main and keep both entries.- **The Shuffle button on Liked Songs and playlist pages works**: in the desktop app and the browser GUI, it plays the songs the page shows in a random order, only the matching ones when you filter.
Traps:
- Don't send
SetShuffleorToggleShuffle. They act on whatever is playing when you click, not on the list you're about to start. Say a Local Files song is playing and you shuffle a Spotify playlist. They would flip the Local queue's shuffle and leave Spotify alone. They would also change the shuffle setting the user picked in the player bar. All we want is a random start order, so the shuffledPlayUrisdoes the whole job. - This button isn't a toggle. It doesn't get
aria-pressed. The player bar's Shuffle (gui/src/Transport.tsx, lines 20-29) is the toggle, and its label is just "Shuffle". That's why this one saysShuffle ${title}(for example "Shuffle Liked Songs"): a screen reader user can then tell the two apart. - Don't use
listPlayRequestorPlayTrackInContext. A playlist context plays in the playlist's own order, so only the first song would be random. - Call
Math.randomonly inside the click handler. Don't shuffle during render or in auseMemo. Thereact-hookslint rules flag impure calls in render, and the order would stay frozen until the list changed. - Don't shuffle with
sort(() => Math.random() - 0.5). It's biased, and you can't test it with an injected random function. - Don't send
LoadMorebefore shuffling, and don't wait forhasMoreto turn false. spotatui already fetches the rest of the list in the background after the first page. - Leave the 100-URI cap in
playback.rsalone. On Spotify, a list of more than 100 songs plays 100 random ones. Play has the same limit after a sort or filter. - If shuffle is already on in the player, the player may reorder the list again on its side. That's fine, because the result is still random.
- An Apple Music list plays one random song, because
start_playback_uris(transport.rs, lines 470-478) plays one Music track at a time. Play does the same, so leave it. - Don't touch anything outside
gui/srcandCHANGELOG.md. That means no Rust, nogui/src/bindings/(those files are generated), and noTransport.tsx.
Tests to add:
- In
gui/src/sortModel.test.ts, addshufflePlayRequestto the import list at the top (lines 3-13), next toshownPlayRequest. Then add adescribe("shufflePlayRequest", ...)block. With() => 0, every step swaps with the first slot. With() => 0.999, no step swaps anything. Both orders are easy to work out by hand:
Some of these lines are longer than prettier allows. Run the prettier step in "Done when" and it will reflow them for you.describe("shufflePlayRequest", () => { const shown = [tracks[0], track("gone", [], "", 1, null), tracks[1], tracks[2]]; it("plays every shown song with a URI once, in the order the random picks give", () => { expect(shufflePlayRequest(shown, () => 0)).toEqual({ PlayUris: { uris: ["spotify:track:Apple", "spotify:track:cherry", "spotify:track:banana"], offset: 0, }, }); expect(shufflePlayRequest(shown, () => 0.999)).toEqual({ PlayUris: { uris: ["spotify:track:banana", "spotify:track:Apple", "spotify:track:cherry"], offset: 0, }, }); }); it("sends nothing when no shown row has a URI", () => { expect(shufflePlayRequest([track("gone", [], "", 1, null)], () => 0)).toBeNull(); expect(shufflePlayRequest([], () => 0)).toBeNull(); }); }); - In
gui/src/listPage.test.tsx, check that the button is enabled on a loaded list and disabled while a playlist is still loading. Pull out the button tag and look fordisabled:
For the disabled case, render the playlist directly withconst shuffleButton = (html: string) => html.match(/<button[^>]*aria-label="Shuffle [^"]*"[^>]*>/)?.[0] ?? ""; it("enables Shuffle once the list shows a playable song", () => { const button = shuffleButton(page({ page: "liked" })); expect(button).toContain('aria-label="Shuffle Liked Songs"'); expect(button).not.toContain("disabled"); });table={{ ...table, uri: null, tracks: [], positions: [] }}, as the "ListPage reload" test does at lines 122-148. Then expectdisabledin the tag. - Name tests as behavior sentences, like the ones already in these files.
- Leave tools/gates.count alone. Page tests need no counter change.
Done when
- Run these in
gui/. First runnpx prettier --write .to format your changes (the gate only checks formatting, it doesn't fix it). Then run the same checks.github/workflows/gui.ymlruns. They should all pass:npm ci npm run lint npm run format:check npm run typecheck npm test npm run build - To run only your tests, use
npm test -- sortModelandnpm test -- listPage. Check that the output shows your test ran; a filter that matches nothing runs nothing. - Manual check without Spotify Premium, using Local Files (optional):
- On Linux, install the prerequisites in docs/installation.md (Building from Source > Prerequisites) first.
- In
gui/, runnpm run build. The GUI binary embedsgui/dist, so run this again after every page change. - From the repo root, run:
The page opens in your browser.cargo run --no-default-features --features telemetry,gui,streaming,discord-rpc,self-update,scripting,mcp-server,mpris,local-files,subsonic,internet-radio,youtube,qobuz --bin spotatui-gui - Pick Local Files, either in the first-launch questions or with the Source select in Your Library. Local Files reads the
behavior.local_music_pathsetting, which defaults to your OS Music folder. To use a different folder, change it on the GUI Settings page (in the Sources section) or inconfig.yml. A folder with 10 or more songs works best. - In Your Library, open a folder and click Shuffle a few times. Each click should usually start a different song, and the queue drawer should show the rest in a new order. Type something in the Filter box and shuffle again. Only matching songs should play.
- Screenshots without any music source: after a one-time
npx playwright install chromium firefox webkit, runnpm run shotsingui/. It renders the page against the fake bridge ingui/e2eand writes PNGs togui/shots/<browser>/. Every scene that shows a list page should now have a Shuffle icon that isn't greyed out:library-playlist.png,list-sort-filter.pngandtrack-menu.png. The shots aren't compared against a baseline, so nothing fails there. The fake bridge also doesn't record what the page sends, so the unit tests above are what prove the request is right. - Attach
library-playlist.pngor a short recording of a shuffle to your PR. CHANGELOG.mdhas the new line under### Added.- You don't change any Rust files, so the gate needs no cargo command. CI's cargo legs still run and should pass unchanged.
- Lingua principale
- Rust
- Stelle
- 1.4k
- Fork
- 238
- Merge medio
- 9h 19m
- PR unite (30g)
- 186
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di LargeModGames/spotatui
-
enhancement good first issue javascript
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
LargeModGames/spotatui#838 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
documentation good first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
LargeModGames/spotatui#832 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
bug good first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
LargeModGames/spotatui#829 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
bug good first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
LargeModGames/spotatui#827 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
bug enhancement good first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
LargeModGames/spotatui#825 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di LargeModGames/spotatui
Issue simili
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
I maintainer di solito rispondono entro 1 giorno
-
area:cli bug good first issue priority:medium
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
rtk-ai/rtk#4589 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 85/100
I maintainer di solito rispondono entro 1 giorno
-
dependencies effort:low flow:chore rust
Difficoltà 2/5 1-3 ore Idoneità per principianti 64/100
jaroslawherod/micold-ai-ide#875 ·
I maintainer di solito rispondono entro 1 giorno
-
priority: low
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
storytold/photocraft#6286 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno