From a2ee8d946baf391e095d289168e0d2b47eb5f608 Mon Sep 17 00:00:00 2001 From: Brendan Szymanski Date: Fri, 5 Jun 2026 01:28:08 -0400 Subject: [PATCH] Add home screen with continue watching, next up, recently added, library grid --- Package.swift | 2 +- Sources/LuminateHome/HomePosterCell.swift | 44 ++++++++++++ Sources/LuminateHome/HomeView.swift | 88 +++++++++++++++++++++++ Sources/LuminateHome/LibraryGrid.swift | 22 ++++++ Sources/LuminateHome/LuminateHome.swift | 24 ------- Sources/LuminateHome/MediaRow.swift | 33 +++++++++ 6 files changed, 188 insertions(+), 25 deletions(-) create mode 100644 Sources/LuminateHome/HomePosterCell.swift create mode 100644 Sources/LuminateHome/HomeView.swift create mode 100644 Sources/LuminateHome/LibraryGrid.swift create mode 100644 Sources/LuminateHome/MediaRow.swift diff --git a/Package.swift b/Package.swift index 0e832d3..194f86d 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ let package = Package( ), .target( name: "LuminateHome", - dependencies: ["LuminateCore", "LuminateLibrary", .product(name: "Adwaita", package: "adwaita-swift")] + dependencies: ["LuminateCore", .product(name: "Adwaita", package: "adwaita-swift")] ), .target( name: "LuminateLibrary", diff --git a/Sources/LuminateHome/HomePosterCell.swift b/Sources/LuminateHome/HomePosterCell.swift new file mode 100644 index 0000000..bccceed --- /dev/null +++ b/Sources/LuminateHome/HomePosterCell.swift @@ -0,0 +1,44 @@ +import Adwaita +import LuminateCore + +struct HomePosterCell: View { + + var item: Components.Schemas.BaseItemDto + var client: JellyfinClient + @State private var imageData: Data? + + var view: Body { + VStack { + if let data = imageData { + Picture(data) + .frame(minWidth: 150, maxWidth: 150, minHeight: 225, maxHeight: 225) + } else { + Box() + .frame(minWidth: 150, maxWidth: 150, minHeight: 225, maxHeight: 225) + .style("card") + } + Text(item.Name ?? "") + .style("body") + .halign(.center) + .maxWidth(150) + } + .onAppear { + loadImage() + } + } + + private func loadImage() { + guard let tag = item.primaryImageTag, + let itemId = item.Id, + let url = client.imageURL( + itemId: itemId, + imageType: .Primary, + tag: tag, + maxWidth: 300 + ) else { return } + Task { + let service = ImageService() + imageData = try? await service.loadImage(url: url) + } + } +} diff --git a/Sources/LuminateHome/HomeView.swift b/Sources/LuminateHome/HomeView.swift new file mode 100644 index 0000000..92c960a --- /dev/null +++ b/Sources/LuminateHome/HomeView.swift @@ -0,0 +1,88 @@ +import Adwaita +import LuminateCore + +struct HomeView: View { + + var app: AdwaitaApp + var window: AdwaitaWindow + var client: JellyfinClient + var userId: String + @State private var resumeItems: [Components.Schemas.BaseItemDto] = [] + @State private var nextUpItems: [Components.Schemas.BaseItemDto] = [] + @State private var latestItems: [Components.Schemas.BaseItemDto] = [] + @State private var libraries: [Components.Schemas.BaseItemDto] = [] + @State private var isLoading = true + + var view: Body { + ScrollView { + VStack { + if isLoading { + Spinner() + .padding(50) + } else { + if !resumeItems.isEmpty { + MediaRow( + title: "Continue Watching", + items: resumeItems, + client: client + ) + .padding(10, .bottom) + } + if !nextUpItems.isEmpty { + MediaRow( + title: "Next Up", + items: nextUpItems, + client: client + ) + .padding(10, .bottom) + } + if !latestItems.isEmpty { + MediaRow( + title: "Recently Added", + items: latestItems, + client: client, + onSeeAll: {} + ) + .padding(10, .bottom) + } + LibraryGrid( + libraries: libraries, + client: client + ) + } + } + } + .onAppear { + loadHomeData() + } + } + + private func loadHomeData() { + isLoading = true + Task { + async let resume = client.getItems( + userId: userId, + filters: [.IsResumable], + sortBy: [.DatePlayed], + sortOrder: [.Descending], + limit: 20, + recursive: true + ) + async let nextUp = client.getNextUp(userId: userId, limit: 20) + async let latest = client.getLatestMedia(userId: userId, limit: 20) + async let views = client.getUserViews(userId: userId) + do { + let (r, n, l, v) = try await (resume, nextUp, latest, views) + await MainActor.run { + resumeItems = r.Items ?? [] + nextUpItems = n.Items ?? [] + latestItems = l + libraries = v.Items ?? [] + isLoading = false + } + } catch { + await MainActor.run { isLoading = false } + } + } + } +} diff --git a/Sources/LuminateHome/LibraryGrid.swift b/Sources/LuminateHome/LibraryGrid.swift new file mode 100644 index 0000000..178fb37 --- /dev/null +++ b/Sources/LuminateHome/LibraryGrid.swift @@ -0,0 +1,22 @@ +import Adwaita +import LuminateCore + +struct LibraryGrid: View { + + var libraries: [Components.Schemas.BaseItemDto] + var client: JellyfinClient + + var view: Body { + VStack { + Text("Libraries") + .style("title-3") + .halign(.start) + .padding(10, .horizontal) + FlowBox { + ForEach(libraries) { library in + HomePosterCell(item: library, client: client) + } + } + } + } +} diff --git a/Sources/LuminateHome/LuminateHome.swift b/Sources/LuminateHome/LuminateHome.swift index 77f9dda..11fdee6 100644 --- a/Sources/LuminateHome/LuminateHome.swift +++ b/Sources/LuminateHome/LuminateHome.swift @@ -1,26 +1,2 @@ import Adwaita import LuminateCore -import LuminateLibrary - -public struct HomeView: View { - - public var app: AdwaitaApp - public var window: AdwaitaWindow - public var client: JellyfinClient - public var userId: String - - public init(app: AdwaitaApp, window: AdwaitaWindow, client: JellyfinClient, userId: String) { - self.app = app - self.window = window - self.client = client - self.userId = userId - } - - public var view: Body { - ItemGrid( - client: client, - userId: userId, - title: "Library" - ) - } -} diff --git a/Sources/LuminateHome/MediaRow.swift b/Sources/LuminateHome/MediaRow.swift new file mode 100644 index 0000000..2fcd89e --- /dev/null +++ b/Sources/LuminateHome/MediaRow.swift @@ -0,0 +1,33 @@ +import Adwaita +import LuminateCore + +struct MediaRow: View { + + var title: String + var items: [Components.Schemas.BaseItemDto] + var client: JellyfinClient + var onSeeAll: (() -> Void)? + + var view: Body { + VStack { + HStack { + Text(title) + .style("title-3") + if onSeeAll != nil { + Button("See All") { + onSeeAll?() + } + .style("flat") + } + } + .padding(10, .horizontal) + ScrollView(.horizontal) { + HStack { + ForEach(items) { item in + HomePosterCell(item: item, client: client) + } + } + } + } + } +}