From 33338ac10076c20ad1e8b1406bdf7a38f5895bdc Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 20 Mar 2026 01:18:52 +0100 Subject: [PATCH] feat(frontend): make library sidebar drilldown-aware --- k-tv-backend/api/src/routes/library.rs | 80 +++++++++++++++++++ .../library/components/library-sidebar.tsx | 48 +++++++++-- .../schedule-from-library-dialog.tsx | 54 +++++++++---- 3 files changed, 162 insertions(+), 20 deletions(-) diff --git a/k-tv-backend/api/src/routes/library.rs b/k-tv-backend/api/src/routes/library.rs index 0687040..0bf443c 100644 --- a/k-tv-backend/api/src/routes/library.rs +++ b/k-tv-backend/api/src/routes/library.rs @@ -86,6 +86,24 @@ struct PagedResponse { total: u32, } +#[derive(Debug, Serialize)] +struct ShowSummaryResponse { + series_name: String, + episode_count: u32, + season_count: u32, + #[serde(skip_serializing_if = "Option::is_none")] + thumbnail_url: Option, + genres: Vec, +} + +#[derive(Debug, Serialize)] +struct SeasonSummaryResponse { + season_number: u32, + episode_count: u32, + #[serde(skip_serializing_if = "Option::is_none")] + thumbnail_url: Option, +} + #[derive(Debug, Serialize)] struct SyncLogResponse { id: i64, @@ -133,6 +151,20 @@ struct ItemsQuery { limit: Option, offset: Option, provider: Option, + season: Option, +} + +#[derive(Debug, Default, Deserialize)] +struct ShowsQuery { + q: Option, + provider: Option, + #[serde(default)] + genres: Vec, +} + +#[derive(Debug, Deserialize)] +struct SeasonsQuery { + provider: Option, } // ============================================================================ @@ -207,6 +239,7 @@ async fn search_items( collection_id: params.collection, genres: params.genres, search_term: params.q, + season_number: params.season, offset, limit, ..Default::default() @@ -306,6 +339,53 @@ async fn trigger_sync( .into_response()) } +async fn list_shows( + State(state): State, + CurrentUser(_user): CurrentUser, + Query(params): Query, +) -> Result>, ApiError> { + let shows = state + .library_repo + .list_shows( + params.provider.as_deref(), + params.q.as_deref(), + ¶ms.genres, + ) + .await?; + let resp = shows + .into_iter() + .map(|s| ShowSummaryResponse { + series_name: s.series_name, + episode_count: s.episode_count, + season_count: s.season_count, + thumbnail_url: s.thumbnail_url, + genres: s.genres, + }) + .collect(); + Ok(Json(resp)) +} + +async fn list_seasons( + State(state): State, + CurrentUser(_user): CurrentUser, + Path(name): Path, + Query(params): Query, +) -> Result>, ApiError> { + let seasons = state + .library_repo + .list_seasons(&name, params.provider.as_deref()) + .await?; + let resp = seasons + .into_iter() + .map(|s| SeasonSummaryResponse { + season_number: s.season_number, + episode_count: s.episode_count, + thumbnail_url: s.thumbnail_url, + }) + .collect(); + Ok(Json(resp)) +} + async fn get_settings( State(state): State, AdminUser(_user): AdminUser, diff --git a/k-tv-frontend/app/(main)/library/components/library-sidebar.tsx b/k-tv-frontend/app/(main)/library/components/library-sidebar.tsx index 4952ada..e650b47 100644 --- a/k-tv-frontend/app/(main)/library/components/library-sidebar.tsx +++ b/k-tv-frontend/app/(main)/library/components/library-sidebar.tsx @@ -5,23 +5,61 @@ import type { LibrarySearchParams } from "@/hooks/use-library-search"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; +import { ArrowLeft } from "lucide-react"; interface Props { filter: LibrarySearchParams; onFilterChange: (next: Partial) => void; + viewMode: "grouped" | "flat"; + drilldown: null | { series: string } | { series: string; season: number }; + onBack: () => void; } -const CONTENT_TYPES = [ - { value: "", label: "All types" }, +const ALL = ""; + +const CONTENT_TYPES_ALL = [ + { value: ALL, label: "All types" }, { value: "movie", label: "Movies" }, { value: "episode", label: "Episodes" }, { value: "short", label: "Shorts" }, ]; -export function LibrarySidebar({ filter, onFilterChange }: Props) { +const CONTENT_TYPES_GROUPED = [ + { value: ALL, label: "All types" }, + { value: "movie", label: "Movies" }, + { value: "short", label: "Shorts" }, +]; + +export function LibrarySidebar({ filter, onFilterChange, viewMode, drilldown, onBack }: Props) { const { data: collections } = useCollections(filter.provider); const { data: genres } = useGenres(filter.type, { provider: filter.provider }); + if (drilldown !== null) { + return ( + + ); + } + + const contentTypes = viewMode === "grouped" ? CONTENT_TYPES_GROUPED : CONTENT_TYPES_ALL; + return (