Add InjectionValues property container

This commit is contained in:
Brendan Szymanski 2026-06-10 17:02:19 -04:00
parent e6d44ac1ea
commit cc5c836f04
20 changed files with 266 additions and 198 deletions

View file

@ -52,7 +52,7 @@ struct ContentView: View {
item: item,
client: client,
userId: userId,
mediaSourceId: item.Id ?? "",
mediaSourceId: item.id ?? "",
playSessionId: "",
streamURL: URL(string: "https://example.com/stream")!,
onClose: { activePlayerItem = nil }

View file

@ -61,7 +61,7 @@ public struct ServerSetupView: View {
do {
let client = JellyfinClient(serverURL: url)
let result = try await client.authenticate(username: username, password: password)
let userId = result.User?.value1.Id ?? ""
let userId = result.user?.value1.id ?? ""
isLoading = false
onLogin(client, userId)

View file

@ -1,16 +1,12 @@
import Foundation
extension Components.Schemas.BaseItemPerson: Identifiable {
public var id: String { Name ?? UUID().uuidString }
}
extension Components.Schemas.BaseItemPerson: Identifiable { }
extension Components.Schemas.BaseItemDto: Identifiable {
public var id: String { Id ?? "" }
}
extension Components.Schemas.BaseItemDto: Identifiable { }
public extension Components.Schemas.SearchHint {
var runtimeString: String {
guard let ticks = RunTimeTicks else { return "" }
guard let ticks = runTimeTicks else { return "" }
let totalSeconds = Int(ticks / 10_000_000)
let hours = totalSeconds / 3600
let minutes = (totalSeconds % 3600) / 60
@ -20,14 +16,8 @@ public extension Components.Schemas.SearchHint {
}
public extension Components.Schemas.BaseItemDto {
var isMovie: Bool { _Type?.value1 == .Movie }
var isSeries: Bool { _Type?.value1 == .Series }
var isEpisode: Bool { _Type?.value1 == .Episode }
var isSeason: Bool { _Type?.value1 == .Season }
var isFolder: Bool { IsFolder ?? false }
var runtimeString: String {
guard let ticks = RunTimeTicks else { return "" }
guard let ticks = runTimeTicks else { return "" }
let totalSeconds = Int(ticks / 10_000_000)
let hours = totalSeconds / 3600
let minutes = (totalSeconds % 3600) / 60
@ -36,15 +26,15 @@ public extension Components.Schemas.BaseItemDto {
}
var yearString: String {
guard let year = ProductionYear else { return "" }
guard let year = productionYear else { return "" }
return "\(year)"
}
var primaryImageTag: String? {
ImageTags?.additionalProperties["Primary"]
imageTags?.additionalProperties["Primary"]
}
var backdropImageTag: String? {
BackdropImageTags?.first
backdropImageTags?.first
}
}

View file

@ -0,0 +1,12 @@
import Foundation
public struct InjectionValues {
public var client: JellyfinClient?
public var userId: String?
public var imageService: ImageService?
public var webSocketClient: WebSocketClient?
public init() {}
}

View file

@ -95,29 +95,29 @@ public actor JellyfinClient {
}
public func authenticate(username: String, password: String) async throws -> Components.Schemas.AuthenticationResult {
let response = try await client.AuthenticateUserByName(Operations.AuthenticateUserByName.Input(
body: .json(.init(value1: .init(Username: username, Pw: password)))
let response = try await client.authenticateUserByName(Operations.AuthenticateUserByName.Input(
body: .json(.init(value1: .init(username: username, pw: password)))
))
switch response {
case .ok(let ok):
switch ok.body {
case .json(let result):
guard let accessToken = result.AccessToken else { throw JellyfinError.invalidResponse }
guard let accessToken = result.accessToken else { throw JellyfinError.invalidResponse }
token = accessToken
client = makeClient(token: accessToken)
userId = result.User?.value1.Id
userId = result.user?.value1.id
return result
case .application_json_profile__quot_camelcase_quot_(let result):
guard let accessToken = result.AccessToken else { throw JellyfinError.invalidResponse }
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
guard let accessToken = result.accessToken else { throw JellyfinError.invalidResponse }
token = accessToken
client = makeClient(token: accessToken)
userId = result.User?.value1.Id
userId = result.user?.value1.id
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
guard let accessToken = result.AccessToken else { throw JellyfinError.invalidResponse }
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
guard let accessToken = result.accessToken else { throw JellyfinError.invalidResponse }
token = accessToken
client = makeClient(token: accessToken)
userId = result.User?.value1.Id
userId = result.user?.value1.id
return result
}
case .serviceUnavailable:
@ -154,15 +154,15 @@ public actor JellyfinClient {
query.limit = limit
query.recursive = recursive
query.isFavorite = isFavorite
let response = try await client.GetItems(Operations.GetItems.Input(query: query))
let response = try await client.getItems(Operations.GetItems.Input(query: query))
switch response {
case .ok(let ok):
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .unauthorized:
@ -178,7 +178,7 @@ public actor JellyfinClient {
public func getItem(itemId: String, userId: String? = nil) async throws -> Components.Schemas.BaseItemDto {
guard token != nil else { throw JellyfinError.notAuthenticated }
let response = try await client.GetItem(Operations.GetItem.Input(
let response = try await client.getItem(Operations.GetItem.Input(
path: .init(itemId: itemId),
query: .init(userId: userId)
))
@ -187,9 +187,9 @@ public actor JellyfinClient {
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .unauthorized:
@ -205,7 +205,7 @@ public actor JellyfinClient {
public func getUserViews(userId: String) async throws -> Components.Schemas.BaseItemDtoQueryResult {
guard token != nil else { throw JellyfinError.notAuthenticated }
let response = try await client.GetUserViews(Operations.GetUserViews.Input(
let response = try await client.getUserViews(Operations.GetUserViews.Input(
query: .init(userId: userId)
))
switch response {
@ -213,9 +213,9 @@ public actor JellyfinClient {
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .unauthorized:
@ -248,15 +248,15 @@ public actor JellyfinClient {
query.parentId = parentId
query.enableResumable = enableResumable
query.enableRewatching = enableRewatching
let response = try await client.GetNextUp(Operations.GetNextUp.Input(query: query))
let response = try await client.getNextUp(Operations.GetNextUp.Input(query: query))
switch response {
case .ok(let ok):
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .unauthorized:
@ -288,7 +288,7 @@ public actor JellyfinClient {
query.imageTypeLimit = imageTypeLimit
query.enableImageTypes = enableImageTypes
query.enableUserData = enableUserData
let response = try await client.GetSeasons(Operations.GetSeasons.Input(
let response = try await client.getSeasons(Operations.GetSeasons.Input(
path: .init(seriesId: seriesId),
query: query
))
@ -297,9 +297,9 @@ public actor JellyfinClient {
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .notFound:
@ -339,7 +339,7 @@ public actor JellyfinClient {
query.imageTypeLimit = imageTypeLimit
query.enableImageTypes = enableImageTypes
query.enableUserData = enableUserData
let response = try await client.GetEpisodes(Operations.GetEpisodes.Input(
let response = try await client.getEpisodes(Operations.GetEpisodes.Input(
path: .init(seriesId: seriesId),
query: query
))
@ -348,9 +348,9 @@ public actor JellyfinClient {
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .notFound:
@ -381,15 +381,15 @@ public actor JellyfinClient {
query.limit = limit
query.includeItemTypes = includeItemTypes
query.parentId = parentId
let response = try await client.GetSearchHints(Operations.GetSearchHints.Input(query: query))
let response = try await client.getSearchHints(Operations.GetSearchHints.Input(query: query))
switch response {
case .ok(let ok):
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .unauthorized:
@ -405,7 +405,7 @@ public actor JellyfinClient {
public func markPlayedItem(itemId: String, userId: String, datePlayed: Date? = nil) async throws -> Components.Schemas.UserItemDataDto {
guard token != nil else { throw JellyfinError.notAuthenticated }
let response = try await client.MarkPlayedItem(Operations.MarkPlayedItem.Input(
let response = try await client.markPlayedItem(Operations.MarkPlayedItem.Input(
path: .init(itemId: itemId),
query: .init(userId: userId, datePlayed: datePlayed)
))
@ -414,9 +414,9 @@ public actor JellyfinClient {
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .notFound:
@ -434,7 +434,7 @@ public actor JellyfinClient {
public func markUnplayedItem(itemId: String, userId: String) async throws -> Components.Schemas.UserItemDataDto {
guard token != nil else { throw JellyfinError.notAuthenticated }
let response = try await client.MarkUnplayedItem(Operations.MarkUnplayedItem.Input(
let response = try await client.markUnplayedItem(Operations.MarkUnplayedItem.Input(
path: .init(itemId: itemId),
query: .init(userId: userId)
))
@ -443,9 +443,9 @@ public actor JellyfinClient {
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .notFound:
@ -463,7 +463,7 @@ public actor JellyfinClient {
public func markFavoriteItem(itemId: String, userId: String) async throws -> Components.Schemas.UserItemDataDto {
guard token != nil else { throw JellyfinError.notAuthenticated }
let response = try await client.MarkFavoriteItem(Operations.MarkFavoriteItem.Input(
let response = try await client.markFavoriteItem(Operations.MarkFavoriteItem.Input(
path: .init(itemId: itemId),
query: .init(userId: userId)
))
@ -472,9 +472,9 @@ public actor JellyfinClient {
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .unauthorized:
@ -490,7 +490,7 @@ public actor JellyfinClient {
public func unmarkFavoriteItem(itemId: String, userId: String) async throws -> Components.Schemas.UserItemDataDto {
guard token != nil else { throw JellyfinError.notAuthenticated }
let response = try await client.UnmarkFavoriteItem(Operations.UnmarkFavoriteItem.Input(
let response = try await client.unmarkFavoriteItem(Operations.UnmarkFavoriteItem.Input(
path: .init(itemId: itemId),
query: .init(userId: userId)
))
@ -499,9 +499,9 @@ public actor JellyfinClient {
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .unauthorized:
@ -517,7 +517,7 @@ public actor JellyfinClient {
public func getPlaybackInfo(itemId: String, userId: String) async throws -> Components.Schemas.PlaybackInfoResponse {
guard token != nil else { throw JellyfinError.notAuthenticated }
let response = try await client.GetPlaybackInfo(Operations.GetPlaybackInfo.Input(
let response = try await client.getPlaybackInfo(Operations.GetPlaybackInfo.Input(
path: .init(itemId: itemId),
query: .init(userId: userId)
))
@ -526,9 +526,9 @@ public actor JellyfinClient {
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .notFound:
@ -546,7 +546,7 @@ public actor JellyfinClient {
public func reportPlaybackStart(info: Components.Schemas.PlaybackStartInfo) async throws {
guard token != nil else { throw JellyfinError.notAuthenticated }
let response = try await client.ReportPlaybackStart(Operations.ReportPlaybackStart.Input(
let response = try await client.reportPlaybackStart(Operations.ReportPlaybackStart.Input(
body: .json(.init(value1: info))
))
switch response {
@ -565,7 +565,7 @@ public actor JellyfinClient {
public func reportPlaybackProgress(info: Components.Schemas.PlaybackProgressInfo) async throws {
guard token != nil else { throw JellyfinError.notAuthenticated }
let response = try await client.ReportPlaybackProgress(Operations.ReportPlaybackProgress.Input(
let response = try await client.reportPlaybackProgress(Operations.ReportPlaybackProgress.Input(
body: .json(.init(value1: info))
))
switch response {
@ -584,7 +584,7 @@ public actor JellyfinClient {
public func reportPlaybackStopped(info: Components.Schemas.PlaybackStopInfo) async throws {
guard token != nil else { throw JellyfinError.notAuthenticated }
let response = try await client.ReportPlaybackStopped(Operations.ReportPlaybackStopped.Input(
let response = try await client.reportPlaybackStopped(Operations.ReportPlaybackStopped.Input(
body: .json(.init(value1: info))
))
switch response {
@ -624,15 +624,15 @@ public actor JellyfinClient {
query.enableImageTypes = enableImageTypes
query.enableUserData = enableUserData
query.groupItems = groupItems
let response = try await client.GetLatestMedia(Operations.GetLatestMedia.Input(query: query))
let response = try await client.getLatestMedia(Operations.GetLatestMedia.Input(query: query))
switch response {
case .ok(let ok):
switch ok.body {
case .json(let result):
return result
case .application_json_profile__quot_camelcase_quot_(let result):
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
return result
case .application_json_profile__quot_pascalcase_quot_(let result):
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
return result
}
case .unauthorized:

View file

@ -2,3 +2,4 @@ generate:
- types
- client
accessModifier: public
namingStrategy: idiomatic

View file

@ -0,0 +1,40 @@
//
// AnyView+Overflow.swift
// Luminate
//
// Created by Brendan Szymanski on 6/8/26.
//
import Adwaita
import CAdw
/// The overflow behavior for a widget.
public enum Overflow: Int {
/// Content is not clipped.
case visible
/// Content is clipped to the widget's bounds.
case hidden
/// Get the GtkOverflow value.
public var cValue: GtkOverflow {
switch self {
case .visible:
CAdw.GTK_OVERFLOW_VISIBLE
case .hidden:
CAdw.GTK_OVERFLOW_HIDDEN
}
}
}
extension AnyView {
/// Set the overflow behavior of the widget.
public func overflow(_ overflow: Overflow) -> AnyView {
wrapModifier(properties: [overflow]) { storage in
gtk_widget_set_overflow(storage.opaquePointer?.cast(), overflow.cValue)
}
}
}

View file

@ -12,34 +12,47 @@ struct HomePosterCell: View {
VStack {
if let data = imageData {
Picture()
.contentFit(.cover)
.data(data)
.frame(minWidth: 150, minHeight: 225)
.frame(maxWidth: 150)
.frame(maxHeight: 225)
.frame(minWidth: 200, minHeight: 300)
.frame(maxWidth: 200)
.frame(maxHeight: 300)
} else {
Box(spacing: 0) {}
.frame(minWidth: 150, minHeight: 225)
.frame(maxWidth: 150)
.frame(maxHeight: 225)
.style("card")
.frame(minWidth: 200, minHeight: 300)
.frame(maxWidth: 200)
.frame(maxHeight: 300)
.card()
}
Text(item.Name ?? "")
.style("body")
VStack(spacing: 0) {
Text(item.name ?? "")
.ellipsize()
.heading()
.halign(.center)
.frame(maxWidth: 150)
.frame(maxWidth: 200)
Text(item.yearString)
.ellipsize()
.caption()
.dimLabel()
.halign(.center)
.frame(maxWidth: 200)
}
.padding(4)
}
.onAppear {
loadImage()
}
.overflow(.hidden)
.card()
}
private func loadImage() {
guard let tag = item.primaryImageTag,
let itemId = item.Id else { return }
let itemId = item.id else { return }
Task {
guard let url = await client.imageURL(
itemId: itemId,
imageType: .Primary,
imageType: .primary,
tag: tag,
maxWidth: 300
) else { return }

View file

@ -12,6 +12,9 @@ public struct HomeView: View {
@State private var latestItems: [Components.Schemas.BaseItemDto] = []
@State private var libraries: [Components.Schemas.BaseItemDto] = []
@State private var isLoading = true
@State private var isLoadingData = false
@Environment("client") var apiClient: JellyfinClient?
public init(
app: AdwaitaApp,
@ -27,10 +30,18 @@ public struct HomeView: View {
public var view: Body {
ScrollView {
Clamp()
.maximumSize(1550)
.tighteningThreshold(550)
.child {
VStack {
if isLoading {
Spinner()
.padding(50)
.halign(.center)
.valign(.center)
.frame(minWidth: 64)
.frame(maxWidth: 64)
} else {
if !resumeItems.isEmpty {
MediaRow(
@ -38,7 +49,7 @@ public struct HomeView: View {
items: resumeItems,
client: client
)
.padding(10, .bottom)
.padding(16, .bottom)
}
if !nextUpItems.isEmpty {
MediaRow(
@ -46,7 +57,7 @@ public struct HomeView: View {
items: nextUpItems,
client: client
)
.padding(10, .bottom)
.padding(16, .bottom)
}
if !latestItems.isEmpty {
MediaRow(
@ -55,7 +66,7 @@ public struct HomeView: View {
client: client,
onSeeAll: {}
)
.padding(10, .bottom)
.padding(16, .bottom)
}
LibraryGrid(
libraries: libraries,
@ -63,20 +74,23 @@ public struct HomeView: View {
)
}
}
.padding(8, .horizontal)
}
}
.hscrollbarPolicy(.never)
.propagateNaturalHeight()
.onAppear {
loadHomeData()
}
}
private func loadHomeData() {
isLoading = true
Task {
async let resume = client.getItems(
userId: userId,
filters: [.IsResumable],
sortBy: [.DatePlayed],
sortOrder: [.Descending],
filters: [.isResumable],
sortBy: [.datePlayed],
sortOrder: [.descending],
limit: 20,
recursive: true
)
@ -85,16 +99,15 @@ public struct HomeView: View {
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 ?? []
resumeItems = r.items ?? []
nextUpItems = n.items ?? []
latestItems = l
libraries = v.Items ?? []
libraries = v.items ?? []
isLoading = false
} catch {
isLoading = false
}
} catch {
await MainActor.run { isLoading = false }
}
isLoadingData = false
}
}
}

View file

@ -1,2 +0,0 @@
import Adwaita
import LuminateCore

View file

@ -10,7 +10,7 @@ struct MediaRow: View {
var onSeeAll: (() -> Void)?
var view: Body {
VStack {
VStack(spacing: 8) {
HStack {
Text(title)
.style("title-3")
@ -21,14 +21,15 @@ struct MediaRow: View {
.style("flat")
}
}
.padding(10, .horizontal)
ScrollView {
HStack {
ForEach(items) { item in
ForEach(items, horizontal: true) { item in
HomePosterCell(item: item, client: client)
.padding(16, .trailing)
}
}
}
.vscrollbarPolicy(.never)
.hscrollbarPolicy(.external)
}
}
}

View file

@ -6,9 +6,9 @@ struct PersonCell: View {
var view: Body {
VStack {
Avatar(showInitials: false, size: 60)
Text(person.Name ?? "")
Text(person.name ?? "")
.style("caption")
if let role = person.Role {
if let role = person.role {
Text(role)
.style("caption")
.dimLabel()

View file

@ -31,7 +31,7 @@ struct EpisodeList: View {
userId: userId,
seasonId: seasonId
)
await MainActor.run { episodes = result?.Items ?? [] }
await MainActor.run { episodes = result?.items ?? [] }
}
}
}
@ -59,7 +59,7 @@ struct EpisodeRow: View {
.style("card")
}
VStack {
Text("\(episode.IndexNumber ?? 0). \(episode.Name ?? "")")
Text("\(episode.indexNumber ?? 0). \(episode.name ?? "")")
.style("body")
.halign(.start)
Text(episode.runtimeString)
@ -79,10 +79,10 @@ struct EpisodeRow: View {
}
private func loadImage() {
guard let tag = episode.primaryImageTag, let itemId = episode.Id else { return }
guard let tag = episode.primaryImageTag, let itemId = episode.id else { return }
Task {
guard let url = await client.imageURL(
itemId: itemId, imageType: .Primary, tag: tag, maxWidth: 200
itemId: itemId, imageType: .primary, tag: tag, maxWidth: 200
) else { return }
let service = ImageService()
imageData = try? await service.loadImage(url: url)

View file

@ -57,15 +57,15 @@ public struct ItemGrid: View {
userId: userId,
parentId: parentId,
includeItemTypes: includeItemTypes,
fields: [.Overview, .Genres, .People, .MediaSources],
sortBy: [.SortName],
sortOrder: [.Ascending],
fields: [.overview, .genres, .people, .mediaSources],
sortBy: [.sortName],
sortOrder: [.ascending],
startIndex: 0,
limit: pageSize,
recursive: true
)
await MainActor.run {
items = result.Items ?? []
items = result.items ?? []
isLoading = false
}
} catch {

View file

@ -16,8 +16,8 @@ struct MovieDetailView: View {
self.item = item
self.client = client
self.userId = userId
_isFavorite = .init(wrappedValue: item.UserData?.value1.IsFavorite ?? false)
_isPlayed = .init(wrappedValue: item.UserData?.value1.Played ?? false)
_isFavorite = .init(wrappedValue: item.userData?.value1.isFavorite ?? false)
_isPlayed = .init(wrappedValue: item.userData?.value1.played ?? false)
}
var view: Body {
@ -35,15 +35,15 @@ struct MovieDetailView: View {
.frame(minWidth: 200)
.frame(maxWidth: 200)
VStack {
Text(item.Name ?? "")
Text(item.name ?? "")
.style("title-1")
.halign(.start)
HStack {
if let year = item.ProductionYear {
if let year = item.productionYear {
Text("\(year)")
}
Text(item.runtimeString)
if let rating = item.CommunityRating {
if let rating = item.communityRating {
RatingBadge(rating: Double(rating))
}
}
@ -62,7 +62,7 @@ struct MovieDetailView: View {
.style("flat")
}
.padding(10, .vertical)
if let overview = item.Overview {
if let overview = item.overview {
Text(overview)
.style("body")
.halign(.start)
@ -71,7 +71,7 @@ struct MovieDetailView: View {
.hexpand(true)
}
.padding()
if let people = item.People, !people.isEmpty {
if let people = item.people, !people.isEmpty {
VStack {
Text("Cast")
.style("title-3")
@ -107,10 +107,10 @@ struct MovieDetailView: View {
}
private func loadBackdrop() {
guard let tag = item.backdropImageTag, let itemId = item.Id else { return }
guard let tag = item.backdropImageTag, let itemId = item.id else { return }
Task {
guard let url = await client.imageURL(
itemId: itemId, imageType: .Backdrop, tag: tag, maxWidth: 1920
itemId: itemId, imageType: .backdrop, tag: tag, maxWidth: 1920
) else { return }
let service = ImageService()
backdropData = try? await service.loadImage(url: url)
@ -121,22 +121,22 @@ struct MovieDetailView: View {
Task {
let result = try? await client.getItems(
userId: userId,
includeItemTypes: [.Movie],
fields: [.Overview, .Genres, .MediaSources],
sortBy: [.SortName],
includeItemTypes: [.movie],
fields: [.overview, .genres, .mediaSources],
sortBy: [.sortName],
limit: 10,
recursive: true
)
await MainActor.run { similarItems = result?.Items ?? [] }
await MainActor.run { similarItems = result?.items ?? [] }
}
}
private func toggleFavorite() {
Task {
if isFavorite {
try? await client.unmarkFavoriteItem(itemId: item.Id ?? "", userId: userId)
try? await client.unmarkFavoriteItem(itemId: item.id ?? "", userId: userId)
} else {
try? await client.markFavoriteItem(itemId: item.Id ?? "", userId: userId)
try? await client.markFavoriteItem(itemId: item.id ?? "", userId: userId)
}
await MainActor.run { isFavorite.toggle() }
}
@ -145,9 +145,9 @@ struct MovieDetailView: View {
private func togglePlayed() {
Task {
if isPlayed {
try? await client.markUnplayedItem(itemId: item.Id ?? "", userId: userId)
try? await client.markUnplayedItem(itemId: item.id ?? "", userId: userId)
} else {
try? await client.markPlayedItem(itemId: item.Id ?? "", userId: userId)
try? await client.markPlayedItem(itemId: item.id ?? "", userId: userId)
}
await MainActor.run { isPlayed.toggle() }
}

View file

@ -21,7 +21,7 @@ struct PosterCell: View {
.frame(maxHeight: 225)
.style("card")
}
Text(item.Name ?? "")
Text(item.name ?? "")
.style("body")
.halign(.center)
.frame(maxWidth: 150)
@ -33,11 +33,11 @@ struct PosterCell: View {
private func loadImage() {
guard let tag = item.primaryImageTag,
let itemId = item.Id else { return }
let itemId = item.id else { return }
Task {
let url = await client.imageURL(
itemId: itemId,
imageType: .Primary,
imageType: .primary,
tag: tag,
maxWidth: 300
)

View file

@ -44,7 +44,7 @@ struct SearchView: View {
limit: 50
)
await MainActor.run {
results = result?.SearchHints ?? []
results = result?.searchHints ?? []
isSearching = false
}
}
@ -52,7 +52,7 @@ struct SearchView: View {
}
extension Components.Schemas.SearchHint: Identifiable {
public var id: String { Id ?? ItemId ?? String(describing: self) }
public var id: String { id ?? itemId ?? String(describing: self) }
}
struct SearchResultRow: View {
@ -78,15 +78,15 @@ struct SearchResultRow: View {
.style("card")
}
VStack {
Text(hint.Name ?? "")
Text(hint.name ?? "")
.style("body")
.halign(.start)
if let type = hint._Type?.value1 {
if let type = hint._type?.value1 {
Text("\(type)")
.style("caption")
.halign(.start)
}
if let year = hint.ProductionYear {
if let year = hint.productionYear {
Text("\(year)")
.style("caption")
.halign(.start)
@ -104,11 +104,11 @@ struct SearchResultRow: View {
}
private func loadImage() {
guard let tag = hint.PrimaryImageTag,
let itemId = hint.Id ?? hint.ItemId else { return }
guard let tag = hint.primaryImageTag,
let itemId = hint.id ?? hint.itemId else { return }
Task {
guard let url = await client.imageURL(
itemId: itemId, imageType: .Primary, tag: tag, maxWidth: 160
itemId: itemId, imageType: .primary, tag: tag, maxWidth: 160
) else { return }
let service = ImageService()
imageData = try? await service.loadImage(url: url)

View file

@ -26,15 +26,15 @@ struct TVShowView: View {
.frame(minWidth: 200)
.frame(maxWidth: 200)
VStack {
Text(item.Name ?? "")
Text(item.name ?? "")
.style("title-1")
.halign(.start)
if let status = item.Status {
if let status = item.status {
Text(status)
.style("caption")
.halign(.start)
}
if let overview = item.Overview {
if let overview = item.overview {
Text(overview)
.style("body")
.halign(.start)
@ -51,10 +51,10 @@ struct TVShowView: View {
.halign(.start)
HStack {
ForEach(seasons) { season in
Button(season.Name ?? "?") {
selectedSeasonId = season.Id
Button(season.name ?? "?") {
selectedSeasonId = season.id
}
.style(selectedSeasonId == season.Id ? "suggested-action" : "flat")
.style(selectedSeasonId == season.id ? "suggested-action" : "flat")
}
}
}
@ -62,7 +62,7 @@ struct TVShowView: View {
}
if let seasonId = selectedSeasonId {
EpisodeList(
seriesId: item.Id ?? "",
seriesId: item.id ?? "",
seasonId: seasonId,
client: client,
userId: userId
@ -77,10 +77,10 @@ struct TVShowView: View {
}
private func loadBackdrop() {
guard let tag = item.backdropImageTag, let itemId = item.Id else { return }
guard let tag = item.backdropImageTag, let itemId = item.id else { return }
Task {
guard let url = await client.imageURL(
itemId: itemId, imageType: .Backdrop, tag: tag, maxWidth: 1920
itemId: itemId, imageType: .backdrop, tag: tag, maxWidth: 1920
) else { return }
let service = ImageService()
backdropData = try? await service.loadImage(url: url)
@ -89,10 +89,10 @@ struct TVShowView: View {
private func loadSeasons() {
Task {
let result = try? await client.getSeasons(seriesId: item.Id ?? "", userId: userId)
let result = try? await client.getSeasons(seriesId: item.id ?? "", userId: userId)
await MainActor.run {
seasons = result?.Items ?? []
selectedSeasonId = seasons.first?.Id
seasons = result?.items ?? []
selectedSeasonId = seasons.first?.id
}
}
}

View file

@ -66,9 +66,9 @@ public struct PlayerView: View {
Task {
try? await client.reportPlaybackStart(
info: .init(
ItemId: item.Id,
MediaSourceId: mediaSourceId,
PlaySessionId: playSessionId
itemId: item.id,
mediaSourceId: mediaSourceId,
playSessionId: playSessionId
)
)
}
@ -78,10 +78,10 @@ public struct PlayerView: View {
Task {
try? await client.reportPlaybackStopped(
info: .init(
ItemId: item.Id,
MediaSourceId: mediaSourceId,
PositionTicks: Int64(position * 10_000_000),
PlaySessionId: playSessionId
itemId: item.id,
mediaSourceId: mediaSourceId,
positionTicks: Int64(position * 10_000_000),
playSessionId: playSessionId
)
)
}

View file

@ -1,7 +1,7 @@
{
"app-id": "dev.bscubed.Luminate",
"runtime": "org.gnome.Platform",
"runtime-version": "49",
"runtime-version": "50",
"sdk": "org.gnome.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.swift6"