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,
|
||||
client: client,
|
||||
userId: userId,
|
||||
mediaSourceId: item.Id ?? "",
|
||||
mediaSourceId: item.id ?? "",
|
||||
playSessionId: "",
|
||||
streamURL: URL(string: "https://example.com/stream")!,
|
||||
onClose: { activePlayerItem = nil }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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 {
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ generate:
|
|||
- types
|
||||
- client
|
||||
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 {
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
import Adwaita
|
||||
import LuminateCore
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue