feat(playlists): unify Saved into a built-in Watch later playlist
The old per-video status='saved' becomes membership in a built-in, undeletable 'watch_later' playlist. Migration 0012 moves existing saved videos into each user's Watch later list and demotes the states to 'new'. The feed/playlist serializers now expose a 'saved' boolean (EXISTS in watch_later); the card bookmark toggles watch_later via new /api/playlists/watch-later add/remove endpoints (create-on-demand) instead of setting a status. Removed the 'saved' show-filter and status. Watch later shows a localized name and hides rename/delete in the Playlists page and add-to-playlist popover.
This commit is contained in:
parent
86844b0bdd
commit
2f66196816
12 changed files with 260 additions and 36 deletions
|
|
@ -15,8 +15,6 @@ function matchesView(status: string, show: string): boolean {
|
|||
return status === "hidden";
|
||||
case "watched":
|
||||
return status === "watched";
|
||||
case "saved":
|
||||
return status === "saved";
|
||||
case "unwatched":
|
||||
case "in_progress":
|
||||
// (in_progress is further narrowed server-side by resume position; here we only
|
||||
|
|
@ -42,6 +40,7 @@ export default function Feed({
|
|||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [overrides, setOverrides] = useState<Record<string, string>>({});
|
||||
const [savedOverrides, setSavedOverrides] = useState<Record<string, boolean>>({});
|
||||
// The open player: which video and where to start (null = resume from saved position).
|
||||
const [activeVideo, setActiveVideo] = useState<{ video: Video; startAt: number | null } | null>(
|
||||
null
|
||||
|
|
@ -64,8 +63,14 @@ export default function Feed({
|
|||
// arrives — after a refetch the server is authoritative, so a stale override
|
||||
// (e.g. a video reverted to "new" from the notification center) won't keep it
|
||||
// filtered out of the current view.
|
||||
useEffect(() => setOverrides({}), [filters]);
|
||||
useEffect(() => setOverrides({}), [query.dataUpdatedAt]);
|
||||
useEffect(() => {
|
||||
setOverrides({});
|
||||
setSavedOverrides({});
|
||||
}, [filters]);
|
||||
useEffect(() => {
|
||||
setOverrides({});
|
||||
setSavedOverrides({});
|
||||
}, [query.dataUpdatedAt]);
|
||||
|
||||
const countQuery = useQuery({
|
||||
queryKey: ["feed-count", filters],
|
||||
|
|
@ -139,10 +144,24 @@ export default function Feed({
|
|||
[filters, setFilters]
|
||||
);
|
||||
|
||||
const onToggleSave = useCallback(
|
||||
(id: string, saved: boolean) => {
|
||||
setSavedOverrides((o) => ({ ...o, [id]: saved }));
|
||||
(saved ? api.watchLaterAdd(id) : api.watchLaterRemove(id))
|
||||
.then(() => {
|
||||
qc.invalidateQueries({ queryKey: ["playlists"] });
|
||||
qc.invalidateQueries({ queryKey: ["playlist"] });
|
||||
})
|
||||
.catch(() => setSavedOverrides((o) => ({ ...o, [id]: !saved })));
|
||||
},
|
||||
[qc]
|
||||
);
|
||||
|
||||
const loaded: Video[] = (query.data?.pages ?? []).flatMap((p) => p.items);
|
||||
loadedRef.current = loaded;
|
||||
const items: Video[] = loaded
|
||||
.map((v) => (overrides[v.id] ? { ...v, status: overrides[v.id] } : v))
|
||||
.map((v) => (savedOverrides[v.id] !== undefined ? { ...v, saved: savedOverrides[v.id] } : v))
|
||||
.filter((v) => matchesView(v.status, filters.show));
|
||||
|
||||
if (query.isLoading) return <div className="p-8 text-muted">{t("feed.loading")}</div>;
|
||||
|
|
@ -192,6 +211,7 @@ export default function Feed({
|
|||
video={v}
|
||||
view="grid"
|
||||
onState={onState}
|
||||
onToggleSave={onToggleSave}
|
||||
onChannelFilter={onChannelFilter}
|
||||
onOpen={openVideo}
|
||||
/>
|
||||
|
|
@ -205,6 +225,7 @@ export default function Feed({
|
|||
video={v}
|
||||
view="list"
|
||||
onState={onState}
|
||||
onToggleSave={onToggleSave}
|
||||
onChannelFilter={onChannelFilter}
|
||||
onOpen={openVideo}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue