Add InjectionValues property container
This commit is contained in:
parent
e6d44ac1ea
commit
cc5c836f04
20 changed files with 266 additions and 198 deletions
|
|
@ -52,7 +52,7 @@ struct ContentView: View {
|
||||||
item: item,
|
item: item,
|
||||||
client: client,
|
client: client,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
mediaSourceId: item.Id ?? "",
|
mediaSourceId: item.id ?? "",
|
||||||
playSessionId: "",
|
playSessionId: "",
|
||||||
streamURL: URL(string: "https://example.com/stream")!,
|
streamURL: URL(string: "https://example.com/stream")!,
|
||||||
onClose: { activePlayerItem = nil }
|
onClose: { activePlayerItem = nil }
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ public struct ServerSetupView: View {
|
||||||
do {
|
do {
|
||||||
let client = JellyfinClient(serverURL: url)
|
let client = JellyfinClient(serverURL: url)
|
||||||
let result = try await client.authenticate(username: username, password: password)
|
let result = try await client.authenticate(username: username, password: password)
|
||||||
let userId = result.User?.value1.Id ?? ""
|
let userId = result.user?.value1.id ?? ""
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
onLogin(client, userId)
|
onLogin(client, userId)
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Components.Schemas.BaseItemPerson: Identifiable {
|
extension Components.Schemas.BaseItemPerson: Identifiable { }
|
||||||
public var id: String { Name ?? UUID().uuidString }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Components.Schemas.BaseItemDto: Identifiable {
|
extension Components.Schemas.BaseItemDto: Identifiable { }
|
||||||
public var id: String { Id ?? "" }
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension Components.Schemas.SearchHint {
|
public extension Components.Schemas.SearchHint {
|
||||||
var runtimeString: String {
|
var runtimeString: String {
|
||||||
guard let ticks = RunTimeTicks else { return "" }
|
guard let ticks = runTimeTicks else { return "" }
|
||||||
let totalSeconds = Int(ticks / 10_000_000)
|
let totalSeconds = Int(ticks / 10_000_000)
|
||||||
let hours = totalSeconds / 3600
|
let hours = totalSeconds / 3600
|
||||||
let minutes = (totalSeconds % 3600) / 60
|
let minutes = (totalSeconds % 3600) / 60
|
||||||
|
|
@ -20,14 +16,8 @@ public extension Components.Schemas.SearchHint {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Components.Schemas.BaseItemDto {
|
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 {
|
var runtimeString: String {
|
||||||
guard let ticks = RunTimeTicks else { return "" }
|
guard let ticks = runTimeTicks else { return "" }
|
||||||
let totalSeconds = Int(ticks / 10_000_000)
|
let totalSeconds = Int(ticks / 10_000_000)
|
||||||
let hours = totalSeconds / 3600
|
let hours = totalSeconds / 3600
|
||||||
let minutes = (totalSeconds % 3600) / 60
|
let minutes = (totalSeconds % 3600) / 60
|
||||||
|
|
@ -36,15 +26,15 @@ public extension Components.Schemas.BaseItemDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
var yearString: String {
|
var yearString: String {
|
||||||
guard let year = ProductionYear else { return "" }
|
guard let year = productionYear else { return "" }
|
||||||
return "\(year)"
|
return "\(year)"
|
||||||
}
|
}
|
||||||
|
|
||||||
var primaryImageTag: String? {
|
var primaryImageTag: String? {
|
||||||
ImageTags?.additionalProperties["Primary"]
|
imageTags?.additionalProperties["Primary"]
|
||||||
}
|
}
|
||||||
|
|
||||||
var backdropImageTag: String? {
|
var backdropImageTag: String? {
|
||||||
BackdropImageTags?.first
|
backdropImageTags?.first
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
Sources/LuminateCore/InjectionValues.swift
Normal file
12
Sources/LuminateCore/InjectionValues.swift
Normal 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() {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -95,29 +95,29 @@ public actor JellyfinClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func authenticate(username: String, password: String) async throws -> Components.Schemas.AuthenticationResult {
|
public func authenticate(username: String, password: String) async throws -> Components.Schemas.AuthenticationResult {
|
||||||
let response = try await client.AuthenticateUserByName(Operations.AuthenticateUserByName.Input(
|
let response = try await client.authenticateUserByName(Operations.AuthenticateUserByName.Input(
|
||||||
body: .json(.init(value1: .init(Username: username, Pw: password)))
|
body: .json(.init(value1: .init(username: username, pw: password)))
|
||||||
))
|
))
|
||||||
switch response {
|
switch response {
|
||||||
case .ok(let ok):
|
case .ok(let ok):
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
guard let accessToken = result.AccessToken else { throw JellyfinError.invalidResponse }
|
guard let accessToken = result.accessToken else { throw JellyfinError.invalidResponse }
|
||||||
token = accessToken
|
token = accessToken
|
||||||
client = makeClient(token: accessToken)
|
client = makeClient(token: accessToken)
|
||||||
userId = result.User?.value1.Id
|
userId = result.user?.value1.id
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
guard let accessToken = result.AccessToken else { throw JellyfinError.invalidResponse }
|
guard let accessToken = result.accessToken else { throw JellyfinError.invalidResponse }
|
||||||
token = accessToken
|
token = accessToken
|
||||||
client = makeClient(token: accessToken)
|
client = makeClient(token: accessToken)
|
||||||
userId = result.User?.value1.Id
|
userId = result.user?.value1.id
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
guard let accessToken = result.AccessToken else { throw JellyfinError.invalidResponse }
|
guard let accessToken = result.accessToken else { throw JellyfinError.invalidResponse }
|
||||||
token = accessToken
|
token = accessToken
|
||||||
client = makeClient(token: accessToken)
|
client = makeClient(token: accessToken)
|
||||||
userId = result.User?.value1.Id
|
userId = result.user?.value1.id
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .serviceUnavailable:
|
case .serviceUnavailable:
|
||||||
|
|
@ -154,15 +154,15 @@ public actor JellyfinClient {
|
||||||
query.limit = limit
|
query.limit = limit
|
||||||
query.recursive = recursive
|
query.recursive = recursive
|
||||||
query.isFavorite = isFavorite
|
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 {
|
switch response {
|
||||||
case .ok(let ok):
|
case .ok(let ok):
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .unauthorized:
|
case .unauthorized:
|
||||||
|
|
@ -178,7 +178,7 @@ public actor JellyfinClient {
|
||||||
|
|
||||||
public func getItem(itemId: String, userId: String? = nil) async throws -> Components.Schemas.BaseItemDto {
|
public func getItem(itemId: String, userId: String? = nil) async throws -> Components.Schemas.BaseItemDto {
|
||||||
guard token != nil else { throw JellyfinError.notAuthenticated }
|
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),
|
path: .init(itemId: itemId),
|
||||||
query: .init(userId: userId)
|
query: .init(userId: userId)
|
||||||
))
|
))
|
||||||
|
|
@ -187,9 +187,9 @@ public actor JellyfinClient {
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .unauthorized:
|
case .unauthorized:
|
||||||
|
|
@ -205,7 +205,7 @@ public actor JellyfinClient {
|
||||||
|
|
||||||
public func getUserViews(userId: String) async throws -> Components.Schemas.BaseItemDtoQueryResult {
|
public func getUserViews(userId: String) async throws -> Components.Schemas.BaseItemDtoQueryResult {
|
||||||
guard token != nil else { throw JellyfinError.notAuthenticated }
|
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)
|
query: .init(userId: userId)
|
||||||
))
|
))
|
||||||
switch response {
|
switch response {
|
||||||
|
|
@ -213,9 +213,9 @@ public actor JellyfinClient {
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .unauthorized:
|
case .unauthorized:
|
||||||
|
|
@ -248,15 +248,15 @@ public actor JellyfinClient {
|
||||||
query.parentId = parentId
|
query.parentId = parentId
|
||||||
query.enableResumable = enableResumable
|
query.enableResumable = enableResumable
|
||||||
query.enableRewatching = enableRewatching
|
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 {
|
switch response {
|
||||||
case .ok(let ok):
|
case .ok(let ok):
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .unauthorized:
|
case .unauthorized:
|
||||||
|
|
@ -288,7 +288,7 @@ public actor JellyfinClient {
|
||||||
query.imageTypeLimit = imageTypeLimit
|
query.imageTypeLimit = imageTypeLimit
|
||||||
query.enableImageTypes = enableImageTypes
|
query.enableImageTypes = enableImageTypes
|
||||||
query.enableUserData = enableUserData
|
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),
|
path: .init(seriesId: seriesId),
|
||||||
query: query
|
query: query
|
||||||
))
|
))
|
||||||
|
|
@ -297,9 +297,9 @@ public actor JellyfinClient {
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .notFound:
|
case .notFound:
|
||||||
|
|
@ -339,7 +339,7 @@ public actor JellyfinClient {
|
||||||
query.imageTypeLimit = imageTypeLimit
|
query.imageTypeLimit = imageTypeLimit
|
||||||
query.enableImageTypes = enableImageTypes
|
query.enableImageTypes = enableImageTypes
|
||||||
query.enableUserData = enableUserData
|
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),
|
path: .init(seriesId: seriesId),
|
||||||
query: query
|
query: query
|
||||||
))
|
))
|
||||||
|
|
@ -348,9 +348,9 @@ public actor JellyfinClient {
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .notFound:
|
case .notFound:
|
||||||
|
|
@ -381,15 +381,15 @@ public actor JellyfinClient {
|
||||||
query.limit = limit
|
query.limit = limit
|
||||||
query.includeItemTypes = includeItemTypes
|
query.includeItemTypes = includeItemTypes
|
||||||
query.parentId = parentId
|
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 {
|
switch response {
|
||||||
case .ok(let ok):
|
case .ok(let ok):
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .unauthorized:
|
case .unauthorized:
|
||||||
|
|
@ -405,7 +405,7 @@ public actor JellyfinClient {
|
||||||
|
|
||||||
public func markPlayedItem(itemId: String, userId: String, datePlayed: Date? = nil) async throws -> Components.Schemas.UserItemDataDto {
|
public func markPlayedItem(itemId: String, userId: String, datePlayed: Date? = nil) async throws -> Components.Schemas.UserItemDataDto {
|
||||||
guard token != nil else { throw JellyfinError.notAuthenticated }
|
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),
|
path: .init(itemId: itemId),
|
||||||
query: .init(userId: userId, datePlayed: datePlayed)
|
query: .init(userId: userId, datePlayed: datePlayed)
|
||||||
))
|
))
|
||||||
|
|
@ -414,9 +414,9 @@ public actor JellyfinClient {
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .notFound:
|
case .notFound:
|
||||||
|
|
@ -434,7 +434,7 @@ public actor JellyfinClient {
|
||||||
|
|
||||||
public func markUnplayedItem(itemId: String, userId: String) async throws -> Components.Schemas.UserItemDataDto {
|
public func markUnplayedItem(itemId: String, userId: String) async throws -> Components.Schemas.UserItemDataDto {
|
||||||
guard token != nil else { throw JellyfinError.notAuthenticated }
|
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),
|
path: .init(itemId: itemId),
|
||||||
query: .init(userId: userId)
|
query: .init(userId: userId)
|
||||||
))
|
))
|
||||||
|
|
@ -443,9 +443,9 @@ public actor JellyfinClient {
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .notFound:
|
case .notFound:
|
||||||
|
|
@ -463,7 +463,7 @@ public actor JellyfinClient {
|
||||||
|
|
||||||
public func markFavoriteItem(itemId: String, userId: String) async throws -> Components.Schemas.UserItemDataDto {
|
public func markFavoriteItem(itemId: String, userId: String) async throws -> Components.Schemas.UserItemDataDto {
|
||||||
guard token != nil else { throw JellyfinError.notAuthenticated }
|
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),
|
path: .init(itemId: itemId),
|
||||||
query: .init(userId: userId)
|
query: .init(userId: userId)
|
||||||
))
|
))
|
||||||
|
|
@ -472,9 +472,9 @@ public actor JellyfinClient {
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .unauthorized:
|
case .unauthorized:
|
||||||
|
|
@ -490,7 +490,7 @@ public actor JellyfinClient {
|
||||||
|
|
||||||
public func unmarkFavoriteItem(itemId: String, userId: String) async throws -> Components.Schemas.UserItemDataDto {
|
public func unmarkFavoriteItem(itemId: String, userId: String) async throws -> Components.Schemas.UserItemDataDto {
|
||||||
guard token != nil else { throw JellyfinError.notAuthenticated }
|
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),
|
path: .init(itemId: itemId),
|
||||||
query: .init(userId: userId)
|
query: .init(userId: userId)
|
||||||
))
|
))
|
||||||
|
|
@ -499,9 +499,9 @@ public actor JellyfinClient {
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .unauthorized:
|
case .unauthorized:
|
||||||
|
|
@ -517,7 +517,7 @@ public actor JellyfinClient {
|
||||||
|
|
||||||
public func getPlaybackInfo(itemId: String, userId: String) async throws -> Components.Schemas.PlaybackInfoResponse {
|
public func getPlaybackInfo(itemId: String, userId: String) async throws -> Components.Schemas.PlaybackInfoResponse {
|
||||||
guard token != nil else { throw JellyfinError.notAuthenticated }
|
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),
|
path: .init(itemId: itemId),
|
||||||
query: .init(userId: userId)
|
query: .init(userId: userId)
|
||||||
))
|
))
|
||||||
|
|
@ -526,9 +526,9 @@ public actor JellyfinClient {
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .notFound:
|
case .notFound:
|
||||||
|
|
@ -546,7 +546,7 @@ public actor JellyfinClient {
|
||||||
|
|
||||||
public func reportPlaybackStart(info: Components.Schemas.PlaybackStartInfo) async throws {
|
public func reportPlaybackStart(info: Components.Schemas.PlaybackStartInfo) async throws {
|
||||||
guard token != nil else { throw JellyfinError.notAuthenticated }
|
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))
|
body: .json(.init(value1: info))
|
||||||
))
|
))
|
||||||
switch response {
|
switch response {
|
||||||
|
|
@ -565,7 +565,7 @@ public actor JellyfinClient {
|
||||||
|
|
||||||
public func reportPlaybackProgress(info: Components.Schemas.PlaybackProgressInfo) async throws {
|
public func reportPlaybackProgress(info: Components.Schemas.PlaybackProgressInfo) async throws {
|
||||||
guard token != nil else { throw JellyfinError.notAuthenticated }
|
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))
|
body: .json(.init(value1: info))
|
||||||
))
|
))
|
||||||
switch response {
|
switch response {
|
||||||
|
|
@ -584,7 +584,7 @@ public actor JellyfinClient {
|
||||||
|
|
||||||
public func reportPlaybackStopped(info: Components.Schemas.PlaybackStopInfo) async throws {
|
public func reportPlaybackStopped(info: Components.Schemas.PlaybackStopInfo) async throws {
|
||||||
guard token != nil else { throw JellyfinError.notAuthenticated }
|
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))
|
body: .json(.init(value1: info))
|
||||||
))
|
))
|
||||||
switch response {
|
switch response {
|
||||||
|
|
@ -624,15 +624,15 @@ public actor JellyfinClient {
|
||||||
query.enableImageTypes = enableImageTypes
|
query.enableImageTypes = enableImageTypes
|
||||||
query.enableUserData = enableUserData
|
query.enableUserData = enableUserData
|
||||||
query.groupItems = groupItems
|
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 {
|
switch response {
|
||||||
case .ok(let ok):
|
case .ok(let ok):
|
||||||
switch ok.body {
|
switch ok.body {
|
||||||
case .json(let result):
|
case .json(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_camelcase_quot_(let result):
|
case .applicationJsonProfile_Quot_camelcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
case .application_json_profile__quot_pascalcase_quot_(let result):
|
case .applicationJsonProfile_Quot_pascalcase_quot_(let result):
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case .unauthorized:
|
case .unauthorized:
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ generate:
|
||||||
- types
|
- types
|
||||||
- client
|
- client
|
||||||
accessModifier: public
|
accessModifier: public
|
||||||
|
namingStrategy: idiomatic
|
||||||
|
|
|
||||||
40
Sources/LuminateHome/AnyView+Overflow.swift
Normal file
40
Sources/LuminateHome/AnyView+Overflow.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -12,34 +12,47 @@ struct HomePosterCell: View {
|
||||||
VStack {
|
VStack {
|
||||||
if let data = imageData {
|
if let data = imageData {
|
||||||
Picture()
|
Picture()
|
||||||
|
.contentFit(.cover)
|
||||||
.data(data)
|
.data(data)
|
||||||
.frame(minWidth: 150, minHeight: 225)
|
.frame(minWidth: 200, minHeight: 300)
|
||||||
.frame(maxWidth: 150)
|
.frame(maxWidth: 200)
|
||||||
.frame(maxHeight: 225)
|
.frame(maxHeight: 300)
|
||||||
} else {
|
} else {
|
||||||
Box(spacing: 0) {}
|
Box(spacing: 0) {}
|
||||||
.frame(minWidth: 150, minHeight: 225)
|
.frame(minWidth: 200, minHeight: 300)
|
||||||
.frame(maxWidth: 150)
|
.frame(maxWidth: 200)
|
||||||
.frame(maxHeight: 225)
|
.frame(maxHeight: 300)
|
||||||
.style("card")
|
.card()
|
||||||
}
|
}
|
||||||
Text(item.Name ?? "")
|
VStack(spacing: 0) {
|
||||||
.style("body")
|
Text(item.name ?? "")
|
||||||
|
.ellipsize()
|
||||||
|
.heading()
|
||||||
.halign(.center)
|
.halign(.center)
|
||||||
.frame(maxWidth: 150)
|
.frame(maxWidth: 200)
|
||||||
|
Text(item.yearString)
|
||||||
|
.ellipsize()
|
||||||
|
.caption()
|
||||||
|
.dimLabel()
|
||||||
|
.halign(.center)
|
||||||
|
.frame(maxWidth: 200)
|
||||||
|
}
|
||||||
|
.padding(4)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadImage()
|
loadImage()
|
||||||
}
|
}
|
||||||
|
.overflow(.hidden)
|
||||||
|
.card()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadImage() {
|
private func loadImage() {
|
||||||
guard let tag = item.primaryImageTag,
|
guard let tag = item.primaryImageTag,
|
||||||
let itemId = item.Id else { return }
|
let itemId = item.id else { return }
|
||||||
Task {
|
Task {
|
||||||
guard let url = await client.imageURL(
|
guard let url = await client.imageURL(
|
||||||
itemId: itemId,
|
itemId: itemId,
|
||||||
imageType: .Primary,
|
imageType: .primary,
|
||||||
tag: tag,
|
tag: tag,
|
||||||
maxWidth: 300
|
maxWidth: 300
|
||||||
) else { return }
|
) else { return }
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ public struct HomeView: View {
|
||||||
@State private var latestItems: [Components.Schemas.BaseItemDto] = []
|
@State private var latestItems: [Components.Schemas.BaseItemDto] = []
|
||||||
@State private var libraries: [Components.Schemas.BaseItemDto] = []
|
@State private var libraries: [Components.Schemas.BaseItemDto] = []
|
||||||
@State private var isLoading = true
|
@State private var isLoading = true
|
||||||
|
@State private var isLoadingData = false
|
||||||
|
|
||||||
|
@Environment("client") var apiClient: JellyfinClient?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
app: AdwaitaApp,
|
app: AdwaitaApp,
|
||||||
|
|
@ -27,10 +30,18 @@ public struct HomeView: View {
|
||||||
|
|
||||||
public var view: Body {
|
public var view: Body {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
Clamp()
|
||||||
|
.maximumSize(1550)
|
||||||
|
.tighteningThreshold(550)
|
||||||
|
.child {
|
||||||
VStack {
|
VStack {
|
||||||
if isLoading {
|
if isLoading {
|
||||||
|
|
||||||
Spinner()
|
Spinner()
|
||||||
.padding(50)
|
.halign(.center)
|
||||||
|
.valign(.center)
|
||||||
|
.frame(minWidth: 64)
|
||||||
|
.frame(maxWidth: 64)
|
||||||
} else {
|
} else {
|
||||||
if !resumeItems.isEmpty {
|
if !resumeItems.isEmpty {
|
||||||
MediaRow(
|
MediaRow(
|
||||||
|
|
@ -38,7 +49,7 @@ public struct HomeView: View {
|
||||||
items: resumeItems,
|
items: resumeItems,
|
||||||
client: client
|
client: client
|
||||||
)
|
)
|
||||||
.padding(10, .bottom)
|
.padding(16, .bottom)
|
||||||
}
|
}
|
||||||
if !nextUpItems.isEmpty {
|
if !nextUpItems.isEmpty {
|
||||||
MediaRow(
|
MediaRow(
|
||||||
|
|
@ -46,7 +57,7 @@ public struct HomeView: View {
|
||||||
items: nextUpItems,
|
items: nextUpItems,
|
||||||
client: client
|
client: client
|
||||||
)
|
)
|
||||||
.padding(10, .bottom)
|
.padding(16, .bottom)
|
||||||
}
|
}
|
||||||
if !latestItems.isEmpty {
|
if !latestItems.isEmpty {
|
||||||
MediaRow(
|
MediaRow(
|
||||||
|
|
@ -55,7 +66,7 @@ public struct HomeView: View {
|
||||||
client: client,
|
client: client,
|
||||||
onSeeAll: {}
|
onSeeAll: {}
|
||||||
)
|
)
|
||||||
.padding(10, .bottom)
|
.padding(16, .bottom)
|
||||||
}
|
}
|
||||||
LibraryGrid(
|
LibraryGrid(
|
||||||
libraries: libraries,
|
libraries: libraries,
|
||||||
|
|
@ -63,20 +74,23 @@ public struct HomeView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(8, .horizontal)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.hscrollbarPolicy(.never)
|
||||||
|
.propagateNaturalHeight()
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadHomeData()
|
loadHomeData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadHomeData() {
|
private func loadHomeData() {
|
||||||
isLoading = true
|
|
||||||
Task {
|
Task {
|
||||||
async let resume = client.getItems(
|
async let resume = client.getItems(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
filters: [.IsResumable],
|
filters: [.isResumable],
|
||||||
sortBy: [.DatePlayed],
|
sortBy: [.datePlayed],
|
||||||
sortOrder: [.Descending],
|
sortOrder: [.descending],
|
||||||
limit: 20,
|
limit: 20,
|
||||||
recursive: true
|
recursive: true
|
||||||
)
|
)
|
||||||
|
|
@ -85,16 +99,15 @@ public struct HomeView: View {
|
||||||
async let views = client.getUserViews(userId: userId)
|
async let views = client.getUserViews(userId: userId)
|
||||||
do {
|
do {
|
||||||
let (r, n, l, v) = try await (resume, nextUp, latest, views)
|
let (r, n, l, v) = try await (resume, nextUp, latest, views)
|
||||||
await MainActor.run {
|
resumeItems = r.items ?? []
|
||||||
resumeItems = r.Items ?? []
|
nextUpItems = n.items ?? []
|
||||||
nextUpItems = n.Items ?? []
|
|
||||||
latestItems = l
|
latestItems = l
|
||||||
libraries = v.Items ?? []
|
libraries = v.items ?? []
|
||||||
|
isLoading = false
|
||||||
|
} catch {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
} catch {
|
isLoadingData = false
|
||||||
await MainActor.run { isLoading = false }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
import Adwaita
|
|
||||||
import LuminateCore
|
|
||||||
|
|
@ -10,7 +10,7 @@ struct MediaRow: View {
|
||||||
var onSeeAll: (() -> Void)?
|
var onSeeAll: (() -> Void)?
|
||||||
|
|
||||||
var view: Body {
|
var view: Body {
|
||||||
VStack {
|
VStack(spacing: 8) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(title)
|
Text(title)
|
||||||
.style("title-3")
|
.style("title-3")
|
||||||
|
|
@ -21,14 +21,15 @@ struct MediaRow: View {
|
||||||
.style("flat")
|
.style("flat")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(10, .horizontal)
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
HStack {
|
ForEach(items, horizontal: true) { item in
|
||||||
ForEach(items) { item in
|
|
||||||
HomePosterCell(item: item, client: client)
|
HomePosterCell(item: item, client: client)
|
||||||
|
.padding(16, .trailing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.vscrollbarPolicy(.never)
|
||||||
|
.hscrollbarPolicy(.external)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ struct PersonCell: View {
|
||||||
var view: Body {
|
var view: Body {
|
||||||
VStack {
|
VStack {
|
||||||
Avatar(showInitials: false, size: 60)
|
Avatar(showInitials: false, size: 60)
|
||||||
Text(person.Name ?? "")
|
Text(person.name ?? "")
|
||||||
.style("caption")
|
.style("caption")
|
||||||
if let role = person.Role {
|
if let role = person.role {
|
||||||
Text(role)
|
Text(role)
|
||||||
.style("caption")
|
.style("caption")
|
||||||
.dimLabel()
|
.dimLabel()
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ struct EpisodeList: View {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
seasonId: seasonId
|
seasonId: seasonId
|
||||||
)
|
)
|
||||||
await MainActor.run { episodes = result?.Items ?? [] }
|
await MainActor.run { episodes = result?.items ?? [] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +59,7 @@ struct EpisodeRow: View {
|
||||||
.style("card")
|
.style("card")
|
||||||
}
|
}
|
||||||
VStack {
|
VStack {
|
||||||
Text("\(episode.IndexNumber ?? 0). \(episode.Name ?? "")")
|
Text("\(episode.indexNumber ?? 0). \(episode.name ?? "")")
|
||||||
.style("body")
|
.style("body")
|
||||||
.halign(.start)
|
.halign(.start)
|
||||||
Text(episode.runtimeString)
|
Text(episode.runtimeString)
|
||||||
|
|
@ -79,10 +79,10 @@ struct EpisodeRow: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadImage() {
|
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 {
|
Task {
|
||||||
guard let url = await client.imageURL(
|
guard let url = await client.imageURL(
|
||||||
itemId: itemId, imageType: .Primary, tag: tag, maxWidth: 200
|
itemId: itemId, imageType: .primary, tag: tag, maxWidth: 200
|
||||||
) else { return }
|
) else { return }
|
||||||
let service = ImageService()
|
let service = ImageService()
|
||||||
imageData = try? await service.loadImage(url: url)
|
imageData = try? await service.loadImage(url: url)
|
||||||
|
|
|
||||||
|
|
@ -57,15 +57,15 @@ public struct ItemGrid: View {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
includeItemTypes: includeItemTypes,
|
includeItemTypes: includeItemTypes,
|
||||||
fields: [.Overview, .Genres, .People, .MediaSources],
|
fields: [.overview, .genres, .people, .mediaSources],
|
||||||
sortBy: [.SortName],
|
sortBy: [.sortName],
|
||||||
sortOrder: [.Ascending],
|
sortOrder: [.ascending],
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
recursive: true
|
recursive: true
|
||||||
)
|
)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
items = result.Items ?? []
|
items = result.items ?? []
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ struct MovieDetailView: View {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.client = client
|
self.client = client
|
||||||
self.userId = userId
|
self.userId = userId
|
||||||
_isFavorite = .init(wrappedValue: item.UserData?.value1.IsFavorite ?? false)
|
_isFavorite = .init(wrappedValue: item.userData?.value1.isFavorite ?? false)
|
||||||
_isPlayed = .init(wrappedValue: item.UserData?.value1.Played ?? false)
|
_isPlayed = .init(wrappedValue: item.userData?.value1.played ?? false)
|
||||||
}
|
}
|
||||||
|
|
||||||
var view: Body {
|
var view: Body {
|
||||||
|
|
@ -35,15 +35,15 @@ struct MovieDetailView: View {
|
||||||
.frame(minWidth: 200)
|
.frame(minWidth: 200)
|
||||||
.frame(maxWidth: 200)
|
.frame(maxWidth: 200)
|
||||||
VStack {
|
VStack {
|
||||||
Text(item.Name ?? "")
|
Text(item.name ?? "")
|
||||||
.style("title-1")
|
.style("title-1")
|
||||||
.halign(.start)
|
.halign(.start)
|
||||||
HStack {
|
HStack {
|
||||||
if let year = item.ProductionYear {
|
if let year = item.productionYear {
|
||||||
Text("\(year)")
|
Text("\(year)")
|
||||||
}
|
}
|
||||||
Text(item.runtimeString)
|
Text(item.runtimeString)
|
||||||
if let rating = item.CommunityRating {
|
if let rating = item.communityRating {
|
||||||
RatingBadge(rating: Double(rating))
|
RatingBadge(rating: Double(rating))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +62,7 @@ struct MovieDetailView: View {
|
||||||
.style("flat")
|
.style("flat")
|
||||||
}
|
}
|
||||||
.padding(10, .vertical)
|
.padding(10, .vertical)
|
||||||
if let overview = item.Overview {
|
if let overview = item.overview {
|
||||||
Text(overview)
|
Text(overview)
|
||||||
.style("body")
|
.style("body")
|
||||||
.halign(.start)
|
.halign(.start)
|
||||||
|
|
@ -71,7 +71,7 @@ struct MovieDetailView: View {
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
if let people = item.People, !people.isEmpty {
|
if let people = item.people, !people.isEmpty {
|
||||||
VStack {
|
VStack {
|
||||||
Text("Cast")
|
Text("Cast")
|
||||||
.style("title-3")
|
.style("title-3")
|
||||||
|
|
@ -107,10 +107,10 @@ struct MovieDetailView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadBackdrop() {
|
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 {
|
Task {
|
||||||
guard let url = await client.imageURL(
|
guard let url = await client.imageURL(
|
||||||
itemId: itemId, imageType: .Backdrop, tag: tag, maxWidth: 1920
|
itemId: itemId, imageType: .backdrop, tag: tag, maxWidth: 1920
|
||||||
) else { return }
|
) else { return }
|
||||||
let service = ImageService()
|
let service = ImageService()
|
||||||
backdropData = try? await service.loadImage(url: url)
|
backdropData = try? await service.loadImage(url: url)
|
||||||
|
|
@ -121,22 +121,22 @@ struct MovieDetailView: View {
|
||||||
Task {
|
Task {
|
||||||
let result = try? await client.getItems(
|
let result = try? await client.getItems(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
includeItemTypes: [.Movie],
|
includeItemTypes: [.movie],
|
||||||
fields: [.Overview, .Genres, .MediaSources],
|
fields: [.overview, .genres, .mediaSources],
|
||||||
sortBy: [.SortName],
|
sortBy: [.sortName],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
recursive: true
|
recursive: true
|
||||||
)
|
)
|
||||||
await MainActor.run { similarItems = result?.Items ?? [] }
|
await MainActor.run { similarItems = result?.items ?? [] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleFavorite() {
|
private func toggleFavorite() {
|
||||||
Task {
|
Task {
|
||||||
if isFavorite {
|
if isFavorite {
|
||||||
try? await client.unmarkFavoriteItem(itemId: item.Id ?? "", userId: userId)
|
try? await client.unmarkFavoriteItem(itemId: item.id ?? "", userId: userId)
|
||||||
} else {
|
} else {
|
||||||
try? await client.markFavoriteItem(itemId: item.Id ?? "", userId: userId)
|
try? await client.markFavoriteItem(itemId: item.id ?? "", userId: userId)
|
||||||
}
|
}
|
||||||
await MainActor.run { isFavorite.toggle() }
|
await MainActor.run { isFavorite.toggle() }
|
||||||
}
|
}
|
||||||
|
|
@ -145,9 +145,9 @@ struct MovieDetailView: View {
|
||||||
private func togglePlayed() {
|
private func togglePlayed() {
|
||||||
Task {
|
Task {
|
||||||
if isPlayed {
|
if isPlayed {
|
||||||
try? await client.markUnplayedItem(itemId: item.Id ?? "", userId: userId)
|
try? await client.markUnplayedItem(itemId: item.id ?? "", userId: userId)
|
||||||
} else {
|
} else {
|
||||||
try? await client.markPlayedItem(itemId: item.Id ?? "", userId: userId)
|
try? await client.markPlayedItem(itemId: item.id ?? "", userId: userId)
|
||||||
}
|
}
|
||||||
await MainActor.run { isPlayed.toggle() }
|
await MainActor.run { isPlayed.toggle() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ struct PosterCell: View {
|
||||||
.frame(maxHeight: 225)
|
.frame(maxHeight: 225)
|
||||||
.style("card")
|
.style("card")
|
||||||
}
|
}
|
||||||
Text(item.Name ?? "")
|
Text(item.name ?? "")
|
||||||
.style("body")
|
.style("body")
|
||||||
.halign(.center)
|
.halign(.center)
|
||||||
.frame(maxWidth: 150)
|
.frame(maxWidth: 150)
|
||||||
|
|
@ -33,11 +33,11 @@ struct PosterCell: View {
|
||||||
|
|
||||||
private func loadImage() {
|
private func loadImage() {
|
||||||
guard let tag = item.primaryImageTag,
|
guard let tag = item.primaryImageTag,
|
||||||
let itemId = item.Id else { return }
|
let itemId = item.id else { return }
|
||||||
Task {
|
Task {
|
||||||
let url = await client.imageURL(
|
let url = await client.imageURL(
|
||||||
itemId: itemId,
|
itemId: itemId,
|
||||||
imageType: .Primary,
|
imageType: .primary,
|
||||||
tag: tag,
|
tag: tag,
|
||||||
maxWidth: 300
|
maxWidth: 300
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ struct SearchView: View {
|
||||||
limit: 50
|
limit: 50
|
||||||
)
|
)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
results = result?.SearchHints ?? []
|
results = result?.searchHints ?? []
|
||||||
isSearching = false
|
isSearching = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +52,7 @@ struct SearchView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Components.Schemas.SearchHint: Identifiable {
|
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 {
|
struct SearchResultRow: View {
|
||||||
|
|
@ -78,15 +78,15 @@ struct SearchResultRow: View {
|
||||||
.style("card")
|
.style("card")
|
||||||
}
|
}
|
||||||
VStack {
|
VStack {
|
||||||
Text(hint.Name ?? "")
|
Text(hint.name ?? "")
|
||||||
.style("body")
|
.style("body")
|
||||||
.halign(.start)
|
.halign(.start)
|
||||||
if let type = hint._Type?.value1 {
|
if let type = hint._type?.value1 {
|
||||||
Text("\(type)")
|
Text("\(type)")
|
||||||
.style("caption")
|
.style("caption")
|
||||||
.halign(.start)
|
.halign(.start)
|
||||||
}
|
}
|
||||||
if let year = hint.ProductionYear {
|
if let year = hint.productionYear {
|
||||||
Text("\(year)")
|
Text("\(year)")
|
||||||
.style("caption")
|
.style("caption")
|
||||||
.halign(.start)
|
.halign(.start)
|
||||||
|
|
@ -104,11 +104,11 @@ struct SearchResultRow: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadImage() {
|
private func loadImage() {
|
||||||
guard let tag = hint.PrimaryImageTag,
|
guard let tag = hint.primaryImageTag,
|
||||||
let itemId = hint.Id ?? hint.ItemId else { return }
|
let itemId = hint.id ?? hint.itemId else { return }
|
||||||
Task {
|
Task {
|
||||||
guard let url = await client.imageURL(
|
guard let url = await client.imageURL(
|
||||||
itemId: itemId, imageType: .Primary, tag: tag, maxWidth: 160
|
itemId: itemId, imageType: .primary, tag: tag, maxWidth: 160
|
||||||
) else { return }
|
) else { return }
|
||||||
let service = ImageService()
|
let service = ImageService()
|
||||||
imageData = try? await service.loadImage(url: url)
|
imageData = try? await service.loadImage(url: url)
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,15 @@ struct TVShowView: View {
|
||||||
.frame(minWidth: 200)
|
.frame(minWidth: 200)
|
||||||
.frame(maxWidth: 200)
|
.frame(maxWidth: 200)
|
||||||
VStack {
|
VStack {
|
||||||
Text(item.Name ?? "")
|
Text(item.name ?? "")
|
||||||
.style("title-1")
|
.style("title-1")
|
||||||
.halign(.start)
|
.halign(.start)
|
||||||
if let status = item.Status {
|
if let status = item.status {
|
||||||
Text(status)
|
Text(status)
|
||||||
.style("caption")
|
.style("caption")
|
||||||
.halign(.start)
|
.halign(.start)
|
||||||
}
|
}
|
||||||
if let overview = item.Overview {
|
if let overview = item.overview {
|
||||||
Text(overview)
|
Text(overview)
|
||||||
.style("body")
|
.style("body")
|
||||||
.halign(.start)
|
.halign(.start)
|
||||||
|
|
@ -51,10 +51,10 @@ struct TVShowView: View {
|
||||||
.halign(.start)
|
.halign(.start)
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(seasons) { season in
|
ForEach(seasons) { season in
|
||||||
Button(season.Name ?? "?") {
|
Button(season.name ?? "?") {
|
||||||
selectedSeasonId = season.Id
|
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 {
|
if let seasonId = selectedSeasonId {
|
||||||
EpisodeList(
|
EpisodeList(
|
||||||
seriesId: item.Id ?? "",
|
seriesId: item.id ?? "",
|
||||||
seasonId: seasonId,
|
seasonId: seasonId,
|
||||||
client: client,
|
client: client,
|
||||||
userId: userId
|
userId: userId
|
||||||
|
|
@ -77,10 +77,10 @@ struct TVShowView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadBackdrop() {
|
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 {
|
Task {
|
||||||
guard let url = await client.imageURL(
|
guard let url = await client.imageURL(
|
||||||
itemId: itemId, imageType: .Backdrop, tag: tag, maxWidth: 1920
|
itemId: itemId, imageType: .backdrop, tag: tag, maxWidth: 1920
|
||||||
) else { return }
|
) else { return }
|
||||||
let service = ImageService()
|
let service = ImageService()
|
||||||
backdropData = try? await service.loadImage(url: url)
|
backdropData = try? await service.loadImage(url: url)
|
||||||
|
|
@ -89,10 +89,10 @@ struct TVShowView: View {
|
||||||
|
|
||||||
private func loadSeasons() {
|
private func loadSeasons() {
|
||||||
Task {
|
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 {
|
await MainActor.run {
|
||||||
seasons = result?.Items ?? []
|
seasons = result?.items ?? []
|
||||||
selectedSeasonId = seasons.first?.Id
|
selectedSeasonId = seasons.first?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,9 @@ public struct PlayerView: View {
|
||||||
Task {
|
Task {
|
||||||
try? await client.reportPlaybackStart(
|
try? await client.reportPlaybackStart(
|
||||||
info: .init(
|
info: .init(
|
||||||
ItemId: item.Id,
|
itemId: item.id,
|
||||||
MediaSourceId: mediaSourceId,
|
mediaSourceId: mediaSourceId,
|
||||||
PlaySessionId: playSessionId
|
playSessionId: playSessionId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -78,10 +78,10 @@ public struct PlayerView: View {
|
||||||
Task {
|
Task {
|
||||||
try? await client.reportPlaybackStopped(
|
try? await client.reportPlaybackStopped(
|
||||||
info: .init(
|
info: .init(
|
||||||
ItemId: item.Id,
|
itemId: item.id,
|
||||||
MediaSourceId: mediaSourceId,
|
mediaSourceId: mediaSourceId,
|
||||||
PositionTicks: Int64(position * 10_000_000),
|
positionTicks: Int64(position * 10_000_000),
|
||||||
PlaySessionId: playSessionId
|
playSessionId: playSessionId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"app-id": "dev.bscubed.Luminate",
|
"app-id": "dev.bscubed.Luminate",
|
||||||
"runtime": "org.gnome.Platform",
|
"runtime": "org.gnome.Platform",
|
||||||
"runtime-version": "49",
|
"runtime-version": "50",
|
||||||
"sdk": "org.gnome.Sdk",
|
"sdk": "org.gnome.Sdk",
|
||||||
"sdk-extensions": [
|
"sdk-extensions": [
|
||||||
"org.freedesktop.Sdk.Extension.swift6"
|
"org.freedesktop.Sdk.Extension.swift6"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue