feat(player): a video's related videos, on demand beside it

The player shows what YouTube lists beside the video, without what the
user has watched or hidden. Wide, a Related tab beside the description;
stacked, a button beside the info opens them in a drawer. A row plays in
the same player (Back returns) and the list stays the opened video's.
A switch picks on-topic or Hungarian-trending lists, kept on the account.

Each related row says whether the user follows its channel; the row is
the video card's narrow form with its whole menu. VideoMenu moves to
features/video, where the player can reach it.
This commit is contained in:
2026-10-24 16:57:52 +02:00
parent 8b37c470ba
commit 686c673fb2
18 changed files with 697 additions and 19 deletions
+30
View File
@@ -35,6 +35,21 @@ export function isResettable(v: Pick<Video, "status" | "position_seconds">): boo
return v.status !== "new" || v.position_seconds > 0;
}
/** A video's related videos, in the locale YouTube fills the list in: on the video's topic, or with
* what trends in Hungary (the server's `RELATED_REGIONS`). */
export type RelatedRegion = "topical" | "hu";
/** A row of a related list: a feed row that also says whether the user follows its channel. */
export interface RelatedVideo extends Video {
followed: boolean;
}
export interface RelatedResponse {
items: RelatedVideo[];
has_more: boolean;
next_cursor: string | null;
}
export interface VideoDetail {
id: string;
description: string | null;
@@ -235,6 +250,21 @@ export const feedApi = {
if (limit) p.set("limit", String(limit));
return req(`/api/search/youtube?${p.toString()}`, {}, { quiet: true });
},
// The videos YouTube shows beside this one, without what the user has watched or hidden. `quiet`
// for the same reason as the search: the list renders its refusal (the day's cap, YouTube) inline.
related: (
videoId: string,
region: RelatedRegion,
cursor: string | null,
): Promise<RelatedResponse> => {
const p = new URLSearchParams({ region });
if (cursor) p.set("cursor", cursor);
return req(
`/api/search/related/${encodeURIComponent(videoId)}?${p.toString()}`,
{},
{ quiet: true },
);
},
// Discard a set of live-search results "as if never added" (drops your search-finds + deletes
// the now-orphaned, un-kept videos). Returns how many videos were removed.
clearSearch: (videoIds: string[]): Promise<{ removed: number }> =>