mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
feat(v3): playlist shuffle toggle
Crossing-arrows toggle next to Play all / Play album on the playlist detail page. When on, playQueue.start Fisher-Yates-shuffles the queue once at start — on a copy, so the stored playlist order is untouched — swapping per-slot album arrangements in lockstep so each slot keeps its pinned arrangement (#685 contract preserved). Prev-less queue semantics are unchanged: auto-advance simply walks the shuffled order. Preference is global, persisted as localStorage v3PlaylistShuffle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
97a941c45d
commit
425f72b33f
@@ -0,0 +1,86 @@
|
||||
// playQueue.start({ shuffle: true }): the queue is Fisher-Yates-shuffled ONCE
|
||||
// at start. Per-slot arrangements must swap in lockstep with their files
|
||||
// (albums pass arrangements aligned by index, #685), the caller's arrays must
|
||||
// not be mutated, and shuffle:false / absent must preserve order. Extract the
|
||||
// playQueue IIFE from app.js and drive it against a playSong stub.
|
||||
'use strict';
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
function makeQueue() {
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', '..', 'static', 'app.js'), 'utf8');
|
||||
const start = src.indexOf('window.feedBack.playQueue = (function () {');
|
||||
assert.ok(start !== -1, 'playQueue IIFE found in app.js');
|
||||
const end = src.indexOf('})();', start);
|
||||
assert.ok(end !== -1, 'playQueue IIFE terminator found');
|
||||
const iife = src.slice(start, end + 5);
|
||||
const played = [];
|
||||
const sandbox = {
|
||||
window: {
|
||||
feedBack: {},
|
||||
playSong: (fn, arr, opts) => played.push({ fn: decodeURIComponent(fn), arr, opts }),
|
||||
fbNotify: null,
|
||||
},
|
||||
};
|
||||
// eslint-disable-next-line no-new-func
|
||||
new Function('window', 'encodeURIComponent', iife)(sandbox.window, encodeURIComponent);
|
||||
return { q: sandbox.window.feedBack.playQueue, played };
|
||||
}
|
||||
|
||||
function drain(q, played) {
|
||||
while (q.hasNext()) q.advance();
|
||||
return played.map((p) => p.fn);
|
||||
}
|
||||
|
||||
test('shuffle: same multiset, order from the seeded RNG, arrangements follow files', () => {
|
||||
const files = ['a.sloppak', 'b.sloppak', 'c.sloppak', 'd.sloppak'];
|
||||
const arrs = [0, 1, 2, 3]; // arrangement i belongs to files[i]
|
||||
const origRandom = Math.random;
|
||||
try {
|
||||
// Deterministic RNG so the expected order is checkable.
|
||||
let calls = 0;
|
||||
const seq = [0.1, 0.9, 0.5];
|
||||
Math.random = () => seq[calls++ % seq.length];
|
||||
const { q, played } = makeQueue();
|
||||
q.start(files.slice(), { arrangements: arrs.slice(), shuffle: true });
|
||||
const order = drain(q, played);
|
||||
assert.deepStrictEqual(order.slice().sort(), files.slice().sort()); // nothing lost/duplicated
|
||||
// Each played file carries the arrangement it started with.
|
||||
played.forEach((p) => {
|
||||
assert.strictEqual(p.arr, arrs[files.indexOf(p.fn)]);
|
||||
});
|
||||
} finally {
|
||||
Math.random = origRandom;
|
||||
}
|
||||
});
|
||||
|
||||
test('shuffle can change the order', () => {
|
||||
const origRandom = Math.random;
|
||||
try {
|
||||
Math.random = () => 0; // j = 0 every swap → deterministic rotation, ≠ input order
|
||||
const { q, played } = makeQueue();
|
||||
q.start(['a', 'b', 'c'], { shuffle: true });
|
||||
const order = drain(q, played);
|
||||
assert.notDeepStrictEqual(order, ['a', 'b', 'c']);
|
||||
} finally {
|
||||
Math.random = origRandom;
|
||||
}
|
||||
});
|
||||
|
||||
test('no shuffle opt preserves order and caller arrays are never mutated', () => {
|
||||
const files = ['a', 'b', 'c'];
|
||||
const arrs = [2, 0, 1];
|
||||
const { q, played } = makeQueue();
|
||||
q.start(files, { arrangements: arrs });
|
||||
assert.deepStrictEqual(drain(q, played), ['a', 'b', 'c']);
|
||||
assert.deepStrictEqual(files, ['a', 'b', 'c']);
|
||||
assert.deepStrictEqual(arrs, [2, 0, 1]);
|
||||
|
||||
// shuffle:true must also leave the caller's arrays alone (start slices).
|
||||
const { q: q2 } = makeQueue();
|
||||
q2.start(files, { arrangements: arrs, shuffle: true });
|
||||
assert.deepStrictEqual(files, ['a', 'b', 'c']);
|
||||
assert.deepStrictEqual(arrs, [2, 0, 1]);
|
||||
});
|
||||
Reference in New Issue
Block a user