feat(design): a playlist's one action list (C1) — the row's "…", Delete + undo, the header's "…" (E28 S3e2)
- Playlist rows: the X column became the row's "…" (play, pin, copy title/link, move, order,
remove). Remove runs at once, with no confirm; its toast's Undo puts the row back at its index.
The Delete key removes a keyboard-walked row, and focus moves on to the next.
- POST /api/playlists/{id}/items takes an optional `position`, so an undo is one request that
renumbers the list around the row.
- A new `listDelete` key scope. Only a list with `onDelete` registers it, so the help sheet does not
offer Delete where nothing handles it.
- A playlist has one list, `usePlaylistActions.items`: rename, pin, YouTube open/push/revert,
delete. The rail row's "…" and the page header's "…" both open it. Push and revert moved into
the hook; the header keeps Play all.
- Phone-width playlist table: "#" and Channel are hidden below sm (`Column.phoneHidden`). Their
fixed widths had left the title no width.
This commit is contained in:
@@ -308,14 +308,14 @@ export default function PlayerModal({
|
||||
if (q && i < q.length - 1) setPlayingId(q[i + 1]!.id);
|
||||
};
|
||||
// "Remove from this playlist", offered right where you are watching the thing you want gone. The
|
||||
// parent owns the removal (confirm + API + caches); the player owns what happens NEXT, because it
|
||||
// parent owns the removal (API + caches + the toast whose Undo puts it back); the player owns what happens NEXT, because it
|
||||
// is the only side that knows which item is playing — its queue is frozen at mount, so the parent
|
||||
// cannot tell. Step to the item that followed (clamped when the last one goes), and close when the
|
||||
// list runs out.
|
||||
//
|
||||
// Guarded against a double press by a REF, not by disabling the button: a trigger disabled for
|
||||
// the confirm's lifetime was still disabled when the dialog closed and restored focus to it — a
|
||||
// no-op — so a keyboard user who cancelled was left on <body>, outside this aria-modal player.
|
||||
// Guarded against a double press by a REF, not by disabling the button: a trigger disabled for the
|
||||
// removal's lifetime was still disabled when focus came back to it (it once waited on a confirm) —
|
||||
// a no-op — so a keyboard user was left on <body>, outside this aria-modal player.
|
||||
const removingRef = useRef(false);
|
||||
const removeFromList = async () => {
|
||||
if (!onRemoveFromList || removingRef.current) return;
|
||||
@@ -327,9 +327,9 @@ export default function PlayerModal({
|
||||
} finally {
|
||||
removingRef.current = false;
|
||||
}
|
||||
if (!done) return; // cancelled at the confirm, or the request failed — leave playback alone
|
||||
if (!done) return; // the request failed — leave playback alone
|
||||
// Decide from where the player is NOW, not where it was when the button was pressed: prev/next
|
||||
// and auto-advance keep working under the confirm dialog, and a cursor from before the await
|
||||
// and auto-advance keep working while the request runs, and a cursor from before the await
|
||||
// threw the viewer back onto a video they had already moved past.
|
||||
const { queue: q, index: i } = navRef.current;
|
||||
if ((q?.[i] ?? video).id !== victim.id) {
|
||||
@@ -967,7 +967,7 @@ export default function PlayerModal({
|
||||
variant="outline-danger"
|
||||
size="md"
|
||||
onClick={() => void removeFromList()}
|
||||
// Named with its object, like the row control: a bare "Remove" among Fullscreen /
|
||||
// Named with its object, like the list's menu row: a bare "Remove" among Fullscreen /
|
||||
// Download / Mark watched reads as "delete this video".
|
||||
label={t("playlists.removeItemOf", { title: active.title })}
|
||||
title={t("playlists.removeItem")}
|
||||
|
||||
Reference in New Issue
Block a user