android: reorder from the sheet — a playlist row up, down or to the top; a queue row up or down
Without a drag: a playlist row's sheet moves it up, down or to the top and persists the whole order in one call (playlist_set_order, as the desktop's drop does); a queue row's sheet moves it up or down, never past the track playing. Also src-tauri/android/ship.sh — one round end to end: the mock walk, the embedded build installed, the phone walk, the commit, each step gated on the one before.
This commit is contained in:
1 parent
991d52d0cc
commit
60303c8a95
7 files changed
+127
-7
No files matched your search
+5
-3
@@ -192,8 +192,8 @@ of its network search.
|
||||
|
||||
More long presses: a catalog song in the search results opens the catalog
|
||||
track's sheet (play, save, add to a playlist, the record, the artist); a
|
||||
row of the queue face opens the track's sheet with Play now, Move to next
|
||||
and Remove from queue on top; a raw result's file (and a file on the Browse
|
||||
row of the queue face opens the track's sheet with Play now, Move to next,
|
||||
Move up, Move down and Remove from queue on top; a raw result's file (and a file on the Browse
|
||||
screen) opens its own — play from here, save this one file, browse; a name
|
||||
in a room's list opens the peer sheet. The catalog record page's bar ⋯ is
|
||||
the record's sheet: play, save, add every track to a playlist as streams,
|
||||
@@ -253,7 +253,9 @@ describe / pin / duplicate / delete, `playlist_add_items`,
|
||||
a file (the desktop's repair panel as a sheet: the library searched by
|
||||
the row's name, a pick relinks the row through `playlist_relink_item`;
|
||||
an unknown placeholder adopts the track's identity), Go to artist,
|
||||
Remove from playlist.
|
||||
Remove from playlist; Move up, Move down and Move to the top reorder
|
||||
without a drag (`playlist_set_order`, the whole order in one call, as the
|
||||
desktop's drop does).
|
||||
A removal is undoable for seven seconds from the neutral toast
|
||||
(`notice.svelte.js` — the desktop's undo pill); the stash pins the
|
||||
playlist it came from.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# One round, end to end: the mock walk, then the phone (the embedded build,
|
||||
# installed, walked on the real core), then the commit — each step gated on
|
||||
# the one before, so nothing lands on the phone the mock did not pass and
|
||||
# nothing is committed the phone did not pass.
|
||||
#
|
||||
# bash src-tauri/android/ship.sh [commit-message-file]
|
||||
#
|
||||
# The phone walk needs the phone attached with SoulStream (or the launcher)
|
||||
# on screen; SS_ALBUM / SS_QUERY name the record and the search it uses.
|
||||
set -uo pipefail
|
||||
cd /f/Coding/SoulStream
|
||||
MSG="${1:-}"
|
||||
T="$(cygpath -u "$TEMP")"
|
||||
ADB="$(cygpath -u "$LOCALAPPDATA")/Android/Sdk/platform-tools/adb.exe"
|
||||
stamp=$(date +%H%M%S)
|
||||
ALBUM="${SS_ALBUM:-Spiderman}"
|
||||
QUERY="${SS_QUERY:-daft punk}"
|
||||
|
||||
echo "== mock walk $(date +%T)"
|
||||
if ! node tests/mobile/smoke.mjs > "$T/ss-ship-mock-$stamp.log" 2>&1; then
|
||||
echo "MOCK WALK FAILED ($T/ss-ship-mock-$stamp.log)"
|
||||
grep -n "FAIL" "$T/ss-ship-mock-$stamp.log" | cut -c1-160 | head
|
||||
sed -n '/problem(s):/,$p' "$T/ss-ship-mock-$stamp.log" | cut -c1-300 | head -20
|
||||
exit 1
|
||||
fi
|
||||
echo " green"
|
||||
|
||||
echo "== deploy $(date +%T)"
|
||||
if ! bash src-tauri/android/deploy.sh > "$T/ss-ship-deploy-$stamp.log" 2>&1; then
|
||||
echo "DEPLOY FAILED ($T/ss-ship-deploy-$stamp.log)"
|
||||
tail -25 "$T/ss-ship-deploy-$stamp.log"
|
||||
exit 1
|
||||
fi
|
||||
grep -a "^== install\|Success\|not launched" "$T/ss-ship-deploy-$stamp.log" | tail -3
|
||||
|
||||
echo "== phone walk $(date +%T)"
|
||||
if ! node tests/mobile/smoke.mjs --phone --album "$ALBUM" --query "$QUERY" > "$T/ss-ship-phone-$stamp.log" 2>&1; then
|
||||
echo "PHONE WALK FAILED ($T/ss-ship-phone-$stamp.log)"
|
||||
grep -n "FAIL" "$T/ss-ship-phone-$stamp.log" | cut -c1-160 | head
|
||||
sed -n '/problem(s):/,$p' "$T/ss-ship-phone-$stamp.log" | cut -c1-300 | head -20
|
||||
"$ADB" forward --remove-all
|
||||
exit 1
|
||||
fi
|
||||
grep -n "audio \|no exceptions" "$T/ss-ship-phone-$stamp.log" | cut -c1-120
|
||||
"$ADB" forward --remove-all
|
||||
|
||||
if [ -n "$MSG" ]; then
|
||||
echo "== commit $(date +%T)"
|
||||
git add -A
|
||||
git -c user.name="KeelrLeek" commit -q -F "$MSG" 2>/dev/null
|
||||
git log -1 --format=' %h %s' | cut -c1-140
|
||||
fi
|
||||
echo "== shipped $(date +%T)"
|
||||
@@ -185,6 +185,17 @@ function mockPlaylists(cmd, args) {
|
||||
plTouch(p);
|
||||
return Promise.resolve(rows.map((r) => r.id));
|
||||
}
|
||||
if (cmd === "playlist_set_order") {
|
||||
const p = plOf(args);
|
||||
if (!p) return Promise.reject(new Error("playlist_set_order: no such playlist"));
|
||||
const by = new Map((PL_ITEMS[p.id] || []).map((x) => [x.id, x]));
|
||||
const next = (args?.orderedIds || []).map(Number).map((id) => by.get(id)).filter(Boolean);
|
||||
for (const x of PL_ITEMS[p.id] || []) if (!next.includes(x)) next.push(x);
|
||||
PL_ITEMS[p.id] = next;
|
||||
next.forEach((x, i) => { x.position = i; });
|
||||
plTouch(p);
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (cmd === "playlist_relink_item") {
|
||||
const p = plOf(args);
|
||||
const it = p ? (PL_ITEMS[p.id] || []).find((x) => x.id === Number(args?.itemId)) : null;
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
import { speedLabel, groupEntries } from "./net.svelte.js";
|
||||
import * as api from "../lib/api.js";
|
||||
import { nav, closeHalf, push, top, patch, go, back, openHalf, openSheet } from "./nav.svelte.js";
|
||||
import { player, playNext, enqueue, playEntries, playQueueAt, removeFromQueue, moveToNext, setSleep, sleepLabel } from "./player.svelte.js";
|
||||
import { player, playNext, enqueue, playEntries, playQueueAt, removeFromQueue, moveToNext, moveInQueue, setSleep, sleepLabel } from "./player.svelte.js";
|
||||
import { streamSong, saveSong, streamCatalogAlbum, saveCatalogAlbum } from "./resolve.svelte.js";
|
||||
import { catalog, yearOf } from "./catalog.svelte.js";
|
||||
import { pls, bulk, bulkable, runBulk, coverFor, queueable, entryOf, playFrom, shufflePlay, togglePin, renamePlaylist, describePlaylist, duplicatePlaylist, deletePlaylist, removeItems, addEntries, createPlaylist } from "./playlists.svelte.js";
|
||||
import { pls, bulk, bulkable, runBulk, coverFor, queueable, entryOf, playFrom, shufflePlay, togglePin, renamePlaylist, describePlaylist, duplicatePlaylist, deletePlaylist, removeItems, addEntries, createPlaylist, moveItem } from "./playlists.svelte.js";
|
||||
import { DOORS, start as startImport } from "./importer.svelte.js";
|
||||
import { slsk, refreshRooms, disconnect, openThread, addBuddy, removeBuddy, leaveRoom } from "./slsk.svelte.js";
|
||||
import { lib, albumById, albumsBy, tracksOf, loadLibrary, refreshPlaylists, refreshRecents } from "./lib.svelte.js";
|
||||
@@ -282,6 +282,10 @@
|
||||
{@render row("play", "Play now", "", act(() => playQueueAt(trackArg.queueIndex)))}
|
||||
{#if trackArg.queueIndex > player.qi + 1}
|
||||
{@render row("skip-forward", "Move to next", "right after this one", act(() => moveToNext(trackArg.queueIndex)))}
|
||||
{@render row("chevron-up", "Move up", "", act(() => moveInQueue(trackArg.queueIndex, "up")))}
|
||||
{/if}
|
||||
{#if trackArg.queueIndex < player.queue.length - 1}
|
||||
{@render row("chevron-down", "Move down", "", act(() => moveInQueue(trackArg.queueIndex, "down")))}
|
||||
{/if}
|
||||
{@render row("x", "Remove from queue", "", act(() => removeFromQueue(trackArg.queueIndex)))}
|
||||
<div class="sheet-hair"></div>
|
||||
@@ -408,6 +412,13 @@
|
||||
{#if item.artist}
|
||||
{@render row("users", "Go to artist", item.artist, act(() => push("artist", { name: item.artist })))}
|
||||
{/if}
|
||||
{#if rows.length > 1}
|
||||
{@const at = rows.findIndex((r) => r.id === item.id)}
|
||||
<div class="sheet-hair"></div>
|
||||
{#if at > 0}{@render row("chevron-up", "Move up", "", act(() => moveItem(p, rows, item, "up").catch(oops)))}{/if}
|
||||
{#if at < rows.length - 1}{@render row("chevron-down", "Move down", "", act(() => moveItem(p, rows, item, "down").catch(oops)))}{/if}
|
||||
{#if at > 1}{@render row("skip-back", "Move to the top", "", act(() => moveItem(p, rows, item, "top").catch(oops)))}{/if}
|
||||
{/if}
|
||||
<div class="sheet-hair"></div>
|
||||
{@render row("x", "Remove from playlist", "undo for a few seconds after", act(() => removeItems(p, rows, [item]).catch(oops)))}
|
||||
</div>
|
||||
|
||||
@@ -412,6 +412,15 @@ export function removeFromQueue(i) {
|
||||
armPreload();
|
||||
writeSession();
|
||||
}
|
||||
/** An upcoming row one place up or down, never past the current track. */
|
||||
export function moveInQueue(i, dir) {
|
||||
const j = i + (dir === "up" ? -1 : 1);
|
||||
if (i <= player.qi || j <= player.qi || i >= player.queue.length || j >= player.queue.length) return;
|
||||
const [row] = player.queue.splice(i, 1);
|
||||
player.queue.splice(j, 0, row);
|
||||
armPreload();
|
||||
writeSession();
|
||||
}
|
||||
/** Pull an upcoming row up to right after the current track. */
|
||||
export function moveToNext(i) {
|
||||
if (i <= player.qi + 1 || i >= player.queue.length) return;
|
||||
|
||||
@@ -184,6 +184,21 @@ export async function deletePlaylist(p) {
|
||||
await refreshPlaylists();
|
||||
}
|
||||
|
||||
/* ── reorder: a row up, down or to the top; the complete id order goes
|
||||
in one call, as the desktop's drop does ── */
|
||||
export async function moveItem(p, items, item, where) {
|
||||
const ids = items.map((i) => i.id);
|
||||
const from = ids.indexOf(item.id);
|
||||
if (from < 0) return false;
|
||||
const to = where === "top" ? 0 : where === "up" ? from - 1 : from + 1;
|
||||
if (to < 0 || to >= ids.length || to === from) return false;
|
||||
ids.splice(from, 1);
|
||||
ids.splice(to, 0, item.id);
|
||||
await api.playlistSetOrder(p.id, ids);
|
||||
await refreshPlaylists();
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── removal, undoable ───────────────────────────────────────────────── */
|
||||
let undoTimer = 0;
|
||||
function stashUndo(playlistId, rows, label) {
|
||||
|
||||
+20
-2
@@ -110,6 +110,8 @@ const PROBE = `(function () {
|
||||
pltags: [].map.call(document.querySelectorAll('.pl-rows .tag'), function (t) { return t.textContent.trim(); }).join('|'),
|
||||
plkind: (document.querySelector('.rec-head.pl .kind') || {}).textContent || '',
|
||||
plfilter: !!document.querySelector('.plfilter input'),
|
||||
plfirst: (document.querySelector('.pl-rows .trk .t') || {}).textContent || '',
|
||||
qfirst: (document.querySelector('.q-face .row .t') || {}).textContent || '',
|
||||
plthumbs: document.querySelectorAll('.pl-rows .thumb img').length,
|
||||
imp: document.querySelectorAll('.imp-rows .trk').length,
|
||||
impTags: [].map.call(document.querySelectorAll('.imp-rows .tag'), function (t) { return t.textContent.trim(); }).join('|'),
|
||||
@@ -175,6 +177,9 @@ const MOCK_STEPS = [
|
||||
/* a queue row's sheet: the row removed */
|
||||
{ do: `document.querySelector('.q-face .row').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, name: "queue-row-sheet", wait: 600 },
|
||||
{ do: `__click('.halfsheet .row', 'Remove from queue')`, name: "queue-removed", wait: 600 },
|
||||
/* the second upcoming row moved up */
|
||||
{ do: `(function () { globalThis.__q2 = document.querySelectorAll('.q-face .row')[1].querySelector('.t').textContent; document.querySelectorAll('.q-face .row')[1].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true })); })()`, wait: 600 },
|
||||
{ do: `__click('.halfsheet .row', 'Move up')`, name: "queue-moved-up", wait: 600 },
|
||||
{ do: `document.querySelectorAll('.fbtn')[1].click()`, name: "lyrics", wait: 900 },
|
||||
/* the lyrics tools: nudged half a second, another candidate chosen */
|
||||
{ do: `__click('.lyr-tools', null)`, name: "lyrics-sheet", wait: 700 },
|
||||
@@ -277,6 +282,11 @@ const MOCK_STEPS = [
|
||||
{ do: `document.querySelectorAll('.pl-rows .trk')[0].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, name: "playlist-row-sheet", wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Remove from playlist')`, name: "playlist-removed", wait: 900 },
|
||||
{ do: `__click('.toast.notice .na', 'Undo')`, name: "playlist-undone", wait: 1000 },
|
||||
/* reordered from the sheet: the second row up, then back down */
|
||||
{ do: `document.querySelectorAll('.pl-rows .trk')[1].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, wait: 600 },
|
||||
{ do: `__click('.halfsheet .row', 'Move up')`, name: "playlist-moved-up", wait: 900 },
|
||||
{ do: `document.querySelectorAll('.pl-rows .trk')[0].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, wait: 600 },
|
||||
{ do: `__click('.halfsheet .row', 'Move down')`, name: "playlist-moved-down", wait: 900 },
|
||||
/* the bulk actions from the page's sheet: Add to library files the
|
||||
streamed row (its cache is complete); Download all finds it cached too */
|
||||
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 },
|
||||
@@ -405,6 +415,7 @@ const MOCK_EXPECT = {
|
||||
queue: (i) => { globalThis.__qrows = i.qrows; return i.sheet.includes("up") && i.qrows >= 2; },
|
||||
"queue-row-sheet": (i) => i.half.includes("up") && /Remove from queue/.test(i.halfText) && /Play now/.test(i.halfText),
|
||||
"queue-removed": (i) => !i.half.includes("up") && i.qrows === globalThis.__qrows - 1,
|
||||
"queue-moved-up": (i) => !i.half.includes("up") && !!i.qfirst && i.qrows === globalThis.__qrows - 1,
|
||||
"library-again": (i) => i.screen.includes("tr-pop"),
|
||||
/* after the follow: the feed's lead ("Coming · ALBUM" — the upcoming
|
||||
record leads), the news, the back catalogue, recommendations,
|
||||
@@ -448,6 +459,8 @@ const MOCK_EXPECT = {
|
||||
"playlist-row-sheet": (i) => i.half.includes("up") && /Remove from playlist/.test(i.halfText),
|
||||
"playlist-removed": (i) => i.plrows === 5 && /Removed/.test(i.notice),
|
||||
"playlist-undone": (i) => i.plrows === 6 && !i.notice,
|
||||
"playlist-moved-up": (i) => i.plfirst.startsWith("Track 2"),
|
||||
"playlist-moved-down": (i) => i.plfirst.startsWith("Track 1"),
|
||||
"playlist-bulk-cached": (i) => /1 saved to library/.test(i.bulkline) && !i.half.includes("up"),
|
||||
"playlist-bulk-download": (i) => /1 saved to library/.test(i.bulkline),
|
||||
"playlist-unknown-sheet": (i) => i.half.includes("up") && /Find a local match/.test(i.halfText),
|
||||
@@ -907,8 +920,13 @@ async function main() {
|
||||
const topName = (await cdp.evaluate(HERO)).top;
|
||||
await cdp.evaluate(HELPERS);
|
||||
await cdp.evaluate(`__click('.top .btn-ghost', null)`);
|
||||
await sleep(4500);
|
||||
const hero = await cdp.evaluate(HERO);
|
||||
/* the catalog page lands when the provider answers — polled, since a fixed wait read a slow answer as a failure */
|
||||
let hero = null;
|
||||
for (let i = 0; i < 24; i++) {
|
||||
await sleep(500);
|
||||
hero = await cdp.evaluate(HERO);
|
||||
if (!hero.screen.includes("has-hero") || hero.tabs >= 1 || hero.pop >= 1) break;
|
||||
}
|
||||
await still(path.join(OUT, `${prefix}cat-hero-page.png`));
|
||||
const heroOk = hero.screen.includes("tr-push") && (!hero.screen.includes("has-hero") || hero.tabs >= 1 || hero.pop >= 1);
|
||||
if (!heroOk) problems.push("EXPECT cat-hero-page " + JSON.stringify(hero));
|
||||
|
||||
Reference in new issue
Block a user