An Esc during a pointer drag is spent on the drag and leaves the selection. A wheel over the rail re-reads the target each frame, so a drop lands on the row under the pointer. After a bulk removal the focus goes to the next row left, and to the list's menu when the list is emptied. remove-bulk names the videos that went, and its Undo brings back only those. An Undo of a move keeps each row (and when it was saved), leaves out a video that moved on from the target, closes up its place, and says how many came back. Delete with every tick hidden removes nothing and says why. A list gone before the lock answers 404, and a body without its videos 422. One removal path now: the single row goes through the bulk one, the same toast and undo, which also retires the add-at-position route. railDrop reuses the store and scrollParent, the drag image the overlay and the z-index token, and the sensors keep their number. The count is read out from a region that outlives the band. Tests fill the gaps the review named, each seen failing.
109 lines
4.3 KiB
TypeScript
109 lines
4.3 KiB
TypeScript
import type { QueryClient } from "@tanstack/react-query";
|
|
|
|
import { api, type Playlist, type VideoStatus } from "../../lib/api";
|
|
import { markVideo } from "../../lib/progressCache";
|
|
|
|
// A video's user state, its Watch-later mark and its playlist items, written to the server and —
|
|
// once it has the change — into the lists that are never refetched (`markVideo`: every related
|
|
// list, every live search). These are the ONE way to make those writes, and
|
|
// `videoWrites.contract.test.ts` holds it: four writes that called the api directly (the
|
|
// Notification Center's undo, the feed's clear, the feed's and Home's Watch-later toggles) left a
|
|
// related list hiding a video the user had un-hidden, or showing it unsaved after a save. A
|
|
// playlist item is one of them because the built-in Watch later list IS the mark (`saved`): ticked
|
|
// in Add to playlist or taken out on the Playlists page, it left the bookmark as it was. Each
|
|
// returns the server's answer, so a caller keeps its own optimistic layer, notices and rollback.
|
|
|
|
export function writeVideoState(qc: QueryClient, id: string, status: VideoStatus) {
|
|
return api.setState(id, status).then((r) => {
|
|
markVideo(qc, id, { status });
|
|
return r;
|
|
});
|
|
}
|
|
|
|
/** Forget where the user left it: the resume point goes, the status stays. */
|
|
export function clearVideoProgress(qc: QueryClient, id: string) {
|
|
return api.clearProgress(id).then((r) => {
|
|
markVideo(qc, id, { position_seconds: 0 });
|
|
return r;
|
|
});
|
|
}
|
|
|
|
export function writeWatchLater(qc: QueryClient, id: string, saved: boolean) {
|
|
return (saved ? api.watchLaterAdd(id) : api.watchLaterRemove(id)).then((r) => {
|
|
markVideo(qc, id, { saved });
|
|
return r;
|
|
});
|
|
}
|
|
|
|
/** The list a playlist item write goes to: on the built-in Watch later list (`kind`) the item IS
|
|
* the video's Watch-later mark. A kind the caller cannot tell (undefined) marks nothing. */
|
|
type ListRef = { id: number; kind: Playlist["kind"] | undefined };
|
|
|
|
const markIfWatchLater = (qc: QueryClient, list: ListRef, id: string, saved: boolean) => {
|
|
if (list.kind === "watch_later") markVideo(qc, id, { saved });
|
|
};
|
|
|
|
export function addPlaylistItem(qc: QueryClient, list: ListRef, id: string) {
|
|
return api.addToPlaylist(list.id, id).then((r) => {
|
|
markIfWatchLater(qc, list, id, true);
|
|
return r;
|
|
});
|
|
}
|
|
|
|
export function removePlaylistItem(qc: QueryClient, list: ListRef, id: string) {
|
|
return api.removeFromPlaylist(list.id, id).then((r) => {
|
|
markIfWatchLater(qc, list, id, false);
|
|
return r;
|
|
});
|
|
}
|
|
|
|
/** Take videos out of a playlist, in one request. Marks what the server says went. */
|
|
export function removePlaylistItems(qc: QueryClient, list: ListRef, ids: string[]) {
|
|
return api.removeManyFromPlaylist(list.id, ids).then((r) => {
|
|
for (const id of r.removed) markIfWatchLater(qc, list, id, false);
|
|
return r;
|
|
});
|
|
}
|
|
|
|
/** Put removed videos back at their places, in one request. */
|
|
export function restorePlaylistItems(
|
|
qc: QueryClient,
|
|
list: ListRef,
|
|
ids: string[],
|
|
positions: number[],
|
|
) {
|
|
return api.restoreToPlaylist(list.id, ids, positions).then((r) => {
|
|
for (const id of ids) markIfWatchLater(qc, list, id, true);
|
|
return r;
|
|
});
|
|
}
|
|
|
|
/** Move videos from one playlist to another, in one request (see `api.moveManyToPlaylist`). Marks
|
|
* what the server says it moved: a video a concurrent edit already took out is in neither answer. */
|
|
export function movePlaylistItems(qc: QueryClient, from: ListRef, ids: string[], to: ListRef) {
|
|
return api.moveManyToPlaylist(from.id, ids, to.id).then((r) => {
|
|
for (const id of [...r.moved, ...r.already_there]) {
|
|
markIfWatchLater(qc, from, id, false);
|
|
markIfWatchLater(qc, to, id, true);
|
|
}
|
|
return r;
|
|
});
|
|
}
|
|
|
|
/** Take a move back: every video returns to `from`, and only the ones that landed in `to` leave it;
|
|
* it held the others before the move. */
|
|
export function undoPlaylistMove(
|
|
qc: QueryClient,
|
|
from: ListRef,
|
|
to: ListRef,
|
|
back: { moved: string[]; already_there: string[]; positions: number[] },
|
|
) {
|
|
return api
|
|
.undoPlaylistMove(from.id, to.id, back.moved, back.already_there, back.positions)
|
|
.then((r) => {
|
|
for (const id of back.moved) markIfWatchLater(qc, to, id, false);
|
|
for (const id of [...back.moved, ...back.already_there]) markIfWatchLater(qc, from, id, true);
|
|
return r;
|
|
});
|
|
}
|