feat(android): the touch model reaches the other three lists
Plan 019 phases 3 and 4, which finish #63. The queue panel and both playlist detail views get tap-to-play and hold-to-select; the playlist views get swipe-to-queue as well. Phase 3 was not the pure wiring the plan expected, in two places. A tap on a queue row plays that position. Copying track-list's tap -- which sets the queue to the list the row is in -- would rebuild the queue from the queue, discarding its source, its shuffle order and anything inserted by hand. It reads as a no-op and is not one. And the queue panel has no swipe, deliberately. A right swipe means add to the queue everywhere else it exists, and a queue row is already in the queue; the only thing it could mean there is remove, which is the same gesture with the opposite effect one screen away. Removing a queue row is on the row, on its sheet since #60, and now on its selection bar. The assertion is that its rows do not opt in. The reveal became utils/swipe-to-queue.ts rather than being copied into three lists, keyed on a data-swipe attribute so one stylesheet carries the touch-action half of the device fix to rows that are called two different things. Phase 4 was already true and is now asserted: a claimed tap has its click swallowed, so an explore-link inside a row never sees one and tap-to-play wins with no rule of its own. Its test was vacuous when written -- the tap helper sent no click, so there was nothing to swallow -- which also weakened phase 1's. It sends one now. Escape leaves selection mode, from selection-bar rather than from each of the four hosts, since that element exists only while the mode does. The platform's back gesture deliberately does not reach it: the shell owns the history stack and four lists reaching for history is four stacks. That is #200. Verified on the reference phone: a queue row taps to its own index and refuses a swipe, a playlist row queues on a swipe and plays its playlist on a tap, and a hold raises the bar without the menu. Closes #63
This commit is contained in:
@@ -64,9 +64,14 @@ import {
|
||||
} from '@utils/explore-link';
|
||||
import {
|
||||
ICON_NEW,
|
||||
ICON_PLAY,
|
||||
ICON_PLAYLIST,
|
||||
ICON_QUEUE,
|
||||
ICON_REMOVE,
|
||||
} from '@utils/icon-language';
|
||||
import type { GestureEvent } from '@utils/touch-gestures';
|
||||
import '@components/selection-bar/selection-bar';
|
||||
import type { SelectionAction } from '@components/selection-bar/selection-bar';
|
||||
/** Above this many tracks, clearing the queue asks first. */
|
||||
const CLEAR_CONFIRM_THRESHOLD = 20;
|
||||
|
||||
@@ -844,6 +849,8 @@ export class QueuePanel
|
||||
virtEl.addEventListener('dragstart', this.onDelegatedDragStart);
|
||||
virtEl.addEventListener('dragend', this.onTrackDragEnd);
|
||||
virtEl.addEventListener('keydown', this.onDelegatedKeydown);
|
||||
virtEl.addEventListener('yj-tap', this.onRowTap);
|
||||
virtEl.addEventListener('yj-long-press', this.onRowLongPress);
|
||||
this.delegationAttached = true;
|
||||
}
|
||||
|
||||
@@ -962,6 +969,8 @@ export class QueuePanel
|
||||
virtEl.removeEventListener('dragstart', this.onDelegatedDragStart);
|
||||
virtEl.removeEventListener('dragend', this.onTrackDragEnd);
|
||||
virtEl.removeEventListener('keydown', this.onDelegatedKeydown);
|
||||
virtEl.removeEventListener('yj-tap', this.onRowTap);
|
||||
virtEl.removeEventListener('yj-long-press', this.onRowLongPress);
|
||||
}
|
||||
this.delegationAttached = false;
|
||||
}
|
||||
@@ -1247,6 +1256,92 @@ export class QueuePanel
|
||||
this.queue.playAtIndex(index);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// A finger on a queue row (plan 019 phase 3, #63)
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* A tap plays this position in the queue.
|
||||
*
|
||||
* `track-list`'s tap sets the queue to the list it was made in;
|
||||
* copying that here would rebuild the queue from the queue, which
|
||||
* is not the no-op it looks like -- it would discard the queue's
|
||||
* source, its shuffle order and everything a user had inserted by
|
||||
* hand. `playAtIndex` is what a double-click already does, and it
|
||||
* is what a tap means.
|
||||
*/
|
||||
private onRowTap = (e: GestureEvent) => {
|
||||
const idx = this.resolveTrackIndexFromEvent(e);
|
||||
|
||||
if (idx === null) return;
|
||||
|
||||
// A control inside the row owns its own tap -- the same rule
|
||||
// the shortcut service has for a focused control that owns a
|
||||
// key. The remove button is the one here.
|
||||
if ((e.target as HTMLElement).closest('.remove-button')) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// The roving tab stop follows the finger, or Tab returns to
|
||||
// wherever the arrows last were rather than to the row that was
|
||||
// just touched.
|
||||
this.focusedIndex = idx;
|
||||
|
||||
if (this.selection.selectionMode) {
|
||||
this.selection.toggleInMode(String(idx), idx);
|
||||
this.virtualizer?.requestUpdate();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.selection.clear();
|
||||
this.queue.playAtIndex(idx);
|
||||
};
|
||||
|
||||
private onRowLongPress = (e: GestureEvent) => {
|
||||
const idx = this.resolveTrackIndexFromEvent(e);
|
||||
|
||||
if (idx === null) return;
|
||||
|
||||
e.preventDefault();
|
||||
this.focusedIndex = idx;
|
||||
this.selection.enterSelectionMode(String(idx), idx);
|
||||
this.virtualizer?.requestUpdate();
|
||||
};
|
||||
|
||||
/**
|
||||
* The two worth a thumb, and "More" for the rest.
|
||||
*
|
||||
* Remove is here rather than left to the overflow because it is
|
||||
* what a selection in a *queue* is most often made for, and it is
|
||||
* the action the row's own × offers one row at a time.
|
||||
*/
|
||||
private static readonly SELECTION_ACTIONS: SelectionAction[] = [
|
||||
{ id: 'play', label: 'Play', icon: ICON_PLAY },
|
||||
{ id: 'remove', label: 'Remove', icon: ICON_REMOVE, danger: true },
|
||||
];
|
||||
|
||||
private renderSelectionBar() {
|
||||
if (!this.selection.selectionMode) return nothing;
|
||||
|
||||
return html`
|
||||
<selection-bar
|
||||
.count=${this.selection.selectionCount}
|
||||
.actions=${QueuePanel.SELECTION_ACTIONS}
|
||||
@selection-exit=${this.onSelectionExit}
|
||||
@selection-action=${(e: CustomEvent<{ id: string }>) =>
|
||||
this.onContextMenuAction(e.detail.id)}
|
||||
@selection-more=${(e: CustomEvent<{ x: number; y: number }>) =>
|
||||
this.ctxMenu.openAt(e.detail.x, e.detail.y)}
|
||||
></selection-bar>
|
||||
`;
|
||||
}
|
||||
|
||||
private onSelectionExit = () => {
|
||||
this.selection.exitSelectionMode();
|
||||
this.virtualizer?.requestUpdate();
|
||||
};
|
||||
|
||||
private handleTrackContextMenu(
|
||||
e: MouseEvent,
|
||||
index: number,
|
||||
@@ -2157,6 +2252,7 @@ export class QueuePanel
|
||||
></lit-virtualizer>
|
||||
`}
|
||||
</div>
|
||||
${this.renderSelectionBar()}
|
||||
</div>
|
||||
|
||||
<menu-surface
|
||||
|
||||
Reference in New Issue
Block a user