Files
siftlode/frontend/src/features/video/videoWrites.ts
T
peter 1a1e9d9542 fix(playlists): what the first review of bulk actions found
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.
2026-11-02 03:41:08 +01:00

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;
});
}