feat(playlists): select rows with a box, Shift for a range, x and Esc
A box first in every row and one over the column for the rows shown, the selected row tinted. A filter keeps what it hides selected; another list starts over; a row that left the list drops out. Watch later has none. A box no longer counts as a typing target: a click on one left the focus there and the page's keys went dead.
This commit is contained in:
@@ -41,6 +41,7 @@ import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { notifyYouTubeActionError } from "../../lib/youtubeErrors";
|
||||
import { playlistName, playlistNameText } from "../../lib/playlistName";
|
||||
import { reinsertAt } from "../../lib/reinsert";
|
||||
import { useRowSelection } from "../../lib/useRowSelection";
|
||||
import { useUndoable } from "../../lib/useUndoable";
|
||||
import { cycleSort, type SortState } from "../../lib/columnSort";
|
||||
import { useUrlMirror } from "../../lib/useUrlMirror";
|
||||
@@ -79,7 +80,7 @@ import {
|
||||
type Column,
|
||||
type FilterMap,
|
||||
} from "../../lib/tableColumns";
|
||||
import { GRIP_KEY, playlistColumns } from "./playlistColumns";
|
||||
import { GRIP_KEY, playlistColumns, selectColumn } from "./playlistColumns";
|
||||
import { useSortableA11y, useSortableItem } from "../../components/ui/sortableA11y";
|
||||
import { DragHandle } from "../../components/ui/DragHandle";
|
||||
import { useOrderItems } from "../../components/ui/useOrderItems";
|
||||
@@ -119,6 +120,7 @@ function Row({
|
||||
video,
|
||||
index,
|
||||
columns,
|
||||
selected,
|
||||
dragDisabled,
|
||||
dragHint,
|
||||
menuProps,
|
||||
@@ -127,6 +129,7 @@ function Row({
|
||||
/** Its position among the rows shown — the row is a keyboard item (lib/listNav). */
|
||||
index: number;
|
||||
columns: Column<Video>[];
|
||||
selected: boolean;
|
||||
dragDisabled: boolean;
|
||||
dragHint: string;
|
||||
/** useContextMenu's trigger props. On the WHOLE row, not a handle: a right-click anywhere in the
|
||||
@@ -152,7 +155,8 @@ function Row({
|
||||
// transition-COLORS, not `transition` — the bare utility animates transform too, which is
|
||||
// the same lag by another route (150ms behind the pointer, on every row).
|
||||
// `group`: the row's "…" reveals on its hover or focus (`.reveal`), and stays on touch.
|
||||
className="group border-b border-border/50 state-layer transition-colors"
|
||||
className={`group border-b border-border/50 transition-colors ${selected ? "bg-accent/12 hover:bg-accent/16" : "state-layer"}`}
|
||||
data-selected={selected ? "" : undefined}
|
||||
{...navItemAttrs(index)}
|
||||
{...menuProps}
|
||||
>
|
||||
@@ -445,6 +449,25 @@ function PlaylistDetail() {
|
||||
[items, columns, filters, viewSort, groupBy, lang],
|
||||
);
|
||||
|
||||
// The rows the bulk actions cover. Watch later offers no edit, so it has nothing to select them for.
|
||||
const selection = useRowSelection(
|
||||
items.map((v) => v.id),
|
||||
shown.map((v) => v.id),
|
||||
selectedId,
|
||||
);
|
||||
const tableColumns = canEdit
|
||||
? [
|
||||
selectColumn({
|
||||
t,
|
||||
state: selection.headerState,
|
||||
onToggleAll: selection.toggleAllShown,
|
||||
isSelected: selection.isSelected,
|
||||
onToggle: selection.toggle,
|
||||
}),
|
||||
...columns,
|
||||
]
|
||||
: columns;
|
||||
|
||||
const viewActive = !isStoredSort(viewSort) || groupBy || filterActive;
|
||||
// Saving a FILTERED view would drop every hidden item from the playlist, so the button only
|
||||
// offers itself when the view is a pure reordering of the whole list.
|
||||
@@ -1106,7 +1129,7 @@ function PlaylistDetail() {
|
||||
min-content, and a nowrap title cell's min-content is the entire title. */}
|
||||
<table className="w-full table-fixed border-collapse text-sm">
|
||||
<ColumnHead
|
||||
columns={columns}
|
||||
columns={tableColumns}
|
||||
sort={viewSort}
|
||||
onToggleSort={toggleSort}
|
||||
filters={filters}
|
||||
@@ -1130,13 +1153,25 @@ function PlaylistDetail() {
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
selection={
|
||||
canEdit
|
||||
? {
|
||||
toggle: (i) => {
|
||||
const v = shownRef.current[i];
|
||||
if (v) selection.toggle(v.id);
|
||||
},
|
||||
clear: selection.clear,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{shown.map((v, i) => (
|
||||
<Row
|
||||
key={v.id}
|
||||
index={i}
|
||||
video={v}
|
||||
columns={columns}
|
||||
columns={tableColumns}
|
||||
selected={selection.isSelected(v.id)}
|
||||
dragDisabled={!canEdit || viewActive}
|
||||
dragHint={dragHint}
|
||||
menuProps={rowMenu.triggerProps(v)}
|
||||
|
||||
Reference in New Issue
Block a user