Set up swift-format
This commit is contained in:
parent
2411a220fe
commit
0b478113ac
26 changed files with 461 additions and 203 deletions
79
.swift-format
Normal file
79
.swift-format
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"fileScopedDeclarationPrivacy" : {
|
||||
"accessLevel" : "private"
|
||||
},
|
||||
"indentBlankLines" : false,
|
||||
"indentConditionalCompilationBlocks" : true,
|
||||
"indentSwitchCaseLabels" : false,
|
||||
"indentation" : {
|
||||
"spaces" : 4
|
||||
},
|
||||
"lineBreakAroundMultilineExpressionChainComponents" : false,
|
||||
"lineBreakBeforeControlFlowKeywords" : false,
|
||||
"lineBreakBeforeEachArgument" : false,
|
||||
"lineBreakBeforeEachGenericRequirement" : false,
|
||||
"lineBreakBetweenDeclarationAttributes" : false,
|
||||
"lineLength" : 100,
|
||||
"maximumBlankLines" : 1,
|
||||
"multiElementCollectionTrailingCommas" : true,
|
||||
"noAssignmentInExpressions" : {
|
||||
"allowedFunctions" : [
|
||||
"XCTAssertNoThrow"
|
||||
]
|
||||
},
|
||||
"orderedImports" : {
|
||||
"includeConditionalImports" : false
|
||||
},
|
||||
"prioritizeKeepingFunctionOutputTogether" : false,
|
||||
"reflowMultilineStringLiterals" : "never",
|
||||
"respectsExistingLineBreaks" : true,
|
||||
"rules" : {
|
||||
"AllPublicDeclarationsHaveDocumentation" : false,
|
||||
"AlwaysUseLiteralForEmptyCollectionInit" : false,
|
||||
"AlwaysUseLowerCamelCase" : true,
|
||||
"AmbiguousTrailingClosureOverload" : true,
|
||||
"AvoidRetroactiveConformances" : false,
|
||||
"BeginDocumentationCommentWithOneLineSummary" : false,
|
||||
"DoNotUseSemicolons" : true,
|
||||
"DontRepeatTypeInStaticProperties" : true,
|
||||
"FileScopedDeclarationPrivacy" : true,
|
||||
"FullyIndirectEnum" : true,
|
||||
"GroupNumericLiterals" : true,
|
||||
"IdentifiersMustBeASCII" : true,
|
||||
"NeverForceUnwrap" : false,
|
||||
"NeverUseForceTry" : false,
|
||||
"NeverUseImplicitlyUnwrappedOptionals" : false,
|
||||
"NoAccessLevelOnExtensionDeclaration" : true,
|
||||
"NoAssignmentInExpressions" : true,
|
||||
"NoBlockComments" : true,
|
||||
"NoCasesWithOnlyFallthrough" : true,
|
||||
"NoEmptyLinesOpeningClosingBraces" : false,
|
||||
"NoEmptyTrailingClosureParentheses" : true,
|
||||
"NoLabelsInCasePatterns" : true,
|
||||
"NoLeadingUnderscores" : false,
|
||||
"NoParensAroundConditions" : true,
|
||||
"NoPlaygroundLiterals" : true,
|
||||
"NoVoidReturnOnFunctionSignature" : true,
|
||||
"OmitExplicitReturns" : false,
|
||||
"OneCasePerLine" : true,
|
||||
"OneVariableDeclarationPerLine" : true,
|
||||
"OnlyOneTrailingClosureArgument" : true,
|
||||
"OrderedImports" : true,
|
||||
"ReplaceForEachWithForLoop" : true,
|
||||
"ReturnVoidInsteadOfEmptyTuple" : true,
|
||||
"TypeNamesShouldBeCapitalized" : true,
|
||||
"UseEarlyExits" : false,
|
||||
"UseExplicitNilCheckInConditions" : true,
|
||||
"UseLetInEveryBoundCaseVariable" : true,
|
||||
"UseShorthandTypeNames" : true,
|
||||
"UseSingleLinePropertyGetter" : true,
|
||||
"UseSynthesizedInitializer" : true,
|
||||
"UseTripleSlashForDocumentationComments" : true,
|
||||
"UseWhereClausesInForLoops" : false,
|
||||
"ValidateDocumentationComments" : false
|
||||
},
|
||||
"spacesAroundRangeFormationOperators" : false,
|
||||
"spacesBeforeEndOfLineComments" : 2,
|
||||
"tabWidth" : 4,
|
||||
"version" : 1
|
||||
}
|
||||
|
|
@ -43,12 +43,12 @@ let package = Package(
|
|||
"LuminateLibrary",
|
||||
"LuminatePlayer",
|
||||
.product(name: "Adwaita", package: "adwaita-swift"),
|
||||
.product(name: "Localized", package: "localized")
|
||||
.product(name: "Localized", package: "localized"),
|
||||
],
|
||||
path: "Sources/Luminate",
|
||||
resources: [.process("Localized.yml")],
|
||||
plugins: [.plugin(name: "GenerateLocalized", package: "localized")]
|
||||
)
|
||||
),
|
||||
],
|
||||
swiftLanguageModes: [.v5]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
import LuminateCore
|
||||
import LuminateHome
|
||||
import LuminateLibrary
|
||||
|
|
@ -38,7 +38,7 @@ struct Luminate: App {
|
|||
DIContainer.shared.register(\.client, value: client)
|
||||
DIContainer.shared.register(\.userId, value: id)
|
||||
DIContainer.shared.register(\.imageService, value: ImageService())
|
||||
|
||||
|
||||
self.client = client
|
||||
self.userId = id
|
||||
}
|
||||
|
|
@ -57,19 +57,20 @@ struct Luminate: App {
|
|||
Task {
|
||||
do {
|
||||
defer { isLaunchLoading = false }
|
||||
|
||||
|
||||
let store = try SQLiteStore(dbURL: SQLiteStore.defaultDatabaseURL())
|
||||
let auth = try await store.loadAuth()
|
||||
guard let serverURL = URL(string: auth.serverURL) else { return }
|
||||
|
||||
let client = JellyfinClient(serverURL: serverURL, token: auth.token, userId: auth.userId)
|
||||
|
||||
let client = JellyfinClient(
|
||||
serverURL: serverURL, token: auth.token, userId: auth.userId)
|
||||
let isValid = await client.validateToken()
|
||||
guard isValid else { return }
|
||||
|
||||
|
||||
DIContainer.shared.register(\.client, value: client)
|
||||
DIContainer.shared.register(\.userId, value: auth.userId)
|
||||
DIContainer.shared.register(\.imageService, value: ImageService())
|
||||
|
||||
|
||||
self.client = client
|
||||
self.userId = auth.userId
|
||||
} catch {
|
||||
|
|
@ -83,24 +84,26 @@ struct ContentView: View {
|
|||
|
||||
var client: JellyfinClient
|
||||
var userId: String
|
||||
@State private var activePlayerItem: Components.Schemas.BaseItemDto?
|
||||
@State var stack: NavigationStack<Page> = .init()
|
||||
|
||||
var view: Body {
|
||||
if let item = activePlayerItem {
|
||||
PlayerView(
|
||||
item: item,
|
||||
client: client,
|
||||
userId: userId,
|
||||
mediaSourceId: item.id ?? "",
|
||||
playSessionId: "",
|
||||
streamURL: URL(string: "https://example.com/stream")!,
|
||||
onClose: { activePlayerItem = nil }
|
||||
)
|
||||
} else {
|
||||
HomeView()
|
||||
.topToolbar {
|
||||
ToolbarView()
|
||||
NavigationView($stack, "Luminate") { page in
|
||||
switch page {
|
||||
case .folder(let title, let items):
|
||||
LibraryPage(title: title, items: items, navigation: $stack)
|
||||
.topToolbar {
|
||||
ToolbarView()
|
||||
}
|
||||
.navigationTitle(title)
|
||||
case .library(let item):
|
||||
Text("REPLACE ME")
|
||||
}
|
||||
} initialView: {
|
||||
HomeView(navigation: $stack)
|
||||
.topToolbar {
|
||||
ToolbarView()
|
||||
}
|
||||
.navigationTitle("Luminate")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ struct ToolbarView: View {
|
|||
.shortcutsItem(Loc.keyboardShortcuts, accelerator: "question".ctrl())
|
||||
}
|
||||
}
|
||||
.titleWidget {
|
||||
WindowTitle(subtitle: "", title: "Luminate")
|
||||
}
|
||||
// .titleWidget {
|
||||
// WindowTitle(subtitle: "", title: "Luminate")
|
||||
// }
|
||||
}
|
||||
|
||||
var content: AnyView {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import Foundation
|
||||
|
||||
extension Components.Schemas.BaseItemPerson: Identifiable { }
|
||||
extension Components.Schemas.BaseItemPerson: Identifiable {}
|
||||
|
||||
extension Components.Schemas.BaseItemDto: Identifiable { }
|
||||
extension Components.Schemas.BaseItemDto: Identifiable {}
|
||||
|
||||
public extension Components.Schemas.SearchHint {
|
||||
var runtimeString: String {
|
||||
extension Components.Schemas.SearchHint {
|
||||
public var runtimeString: String {
|
||||
guard let ticks = runTimeTicks else { return "" }
|
||||
let totalSeconds = Int(ticks / 10_000_000)
|
||||
let hours = totalSeconds / 3600
|
||||
|
|
@ -15,8 +15,8 @@ public extension Components.Schemas.SearchHint {
|
|||
}
|
||||
}
|
||||
|
||||
public extension Components.Schemas.BaseItemDto {
|
||||
var runtimeString: String {
|
||||
extension Components.Schemas.BaseItemDto {
|
||||
public var runtimeString: String {
|
||||
guard let ticks = runTimeTicks else { return "" }
|
||||
let totalSeconds = Int(ticks / 10_000_000)
|
||||
let hours = totalSeconds / 3600
|
||||
|
|
@ -25,16 +25,16 @@ public extension Components.Schemas.BaseItemDto {
|
|||
return "\(minutes)m"
|
||||
}
|
||||
|
||||
var yearString: String {
|
||||
public var yearString: String {
|
||||
guard let year = productionYear else { return "" }
|
||||
return "\(year)"
|
||||
}
|
||||
|
||||
var primaryImageTag: String? {
|
||||
public var primaryImageTag: String? {
|
||||
imageTags?.additionalProperties["Primary"]
|
||||
}
|
||||
|
||||
var backdropImageTag: String? {
|
||||
public var backdropImageTag: String? {
|
||||
backdropImageTags?.first
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public final class DIContainer {
|
|||
guard let value = values[keyPath: keyPath] else {
|
||||
fatalError(
|
||||
"DIContainer: No value registered for \(keyPath). "
|
||||
+ "Call DIContainer.shared.register(\\.key, value:) during app startup."
|
||||
+ "Call DIContainer.shared.register(\\.key, value:) during app startup."
|
||||
)
|
||||
}
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
public actor ImageService {
|
||||
|
|
@ -12,7 +13,8 @@ public actor ImageService {
|
|||
for: .cachesDirectory, in: .userDomainMask
|
||||
).first!.appendingPathComponent("luminate/images")
|
||||
self.cacheDir = cacheDir ?? defaultCache
|
||||
try? FileManager.default.createDirectory(at: self.cacheDir, withIntermediateDirectories: true)
|
||||
try? FileManager.default.createDirectory(
|
||||
at: self.cacheDir, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
public func loadImage(url: URL) async throws -> Data {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ public struct Injected<T> {
|
|||
|
||||
private let keyPath: KeyPath<InjectionValues, T?>
|
||||
|
||||
@MainActor
|
||||
public var wrappedValue: T {
|
||||
DIContainer.shared.resolve(keyPath)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
import HTTPTypes
|
||||
import OpenAPIRuntime
|
||||
import OpenAPIURLSession
|
||||
import HTTPTypes
|
||||
|
||||
struct JellyfinDateTranscoder: DateTranscoder {
|
||||
func encode(_ date: Date) throws -> String {
|
||||
|
|
@ -24,7 +24,9 @@ struct JellyfinDateTranscoder: DateTranscoder {
|
|||
return date
|
||||
}
|
||||
throw DecodingError.dataCorrupted(
|
||||
.init(codingPath: [], debugDescription: "Expected date string to be ISO8601-formatted: \(dateString)")
|
||||
.init(
|
||||
codingPath: [],
|
||||
debugDescription: "Expected date string to be ISO8601-formatted: \(dateString)")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +52,9 @@ struct AuthMiddleware: ClientMiddleware {
|
|||
body: OpenAPIRuntime.HTTPBody?,
|
||||
baseURL: URL,
|
||||
operationID: String,
|
||||
next: (HTTPRequest, OpenAPIRuntime.HTTPBody?, URL) async throws -> (HTTPResponse, OpenAPIRuntime.HTTPBody?)
|
||||
next: (HTTPRequest, OpenAPIRuntime.HTTPBody?, URL) async throws -> (
|
||||
HTTPResponse, OpenAPIRuntime.HTTPBody?
|
||||
)
|
||||
) async throws -> (HTTPResponse, OpenAPIRuntime.HTTPBody?) {
|
||||
var request = request
|
||||
request.headerFields[.authorization] = mediaBrowserHeader(token: token)
|
||||
|
|
@ -117,27 +121,36 @@ 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)))
|
||||
))
|
||||
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)))
|
||||
))
|
||||
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
|
||||
return 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
|
||||
client = makeClient(token: accessToken)
|
||||
userId = result.user?.value1.id
|
||||
return 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
|
||||
client = makeClient(token: accessToken)
|
||||
userId = result.user?.value1.id
|
||||
|
|
@ -199,12 +212,15 @@ 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 }
|
||||
let response = try await client.getItem(Operations.GetItem.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
let response = try await client.getItem(
|
||||
Operations.GetItem.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
switch response {
|
||||
case .ok(let ok):
|
||||
switch ok.body {
|
||||
|
|
@ -226,11 +242,14 @@ 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 }
|
||||
let response = try await client.getUserViews(Operations.GetUserViews.Input(
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
let response = try await client.getUserViews(
|
||||
Operations.GetUserViews.Input(
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
switch response {
|
||||
case .ok(let ok):
|
||||
switch ok.body {
|
||||
|
|
@ -311,10 +330,11 @@ public actor JellyfinClient {
|
|||
query.imageTypeLimit = imageTypeLimit
|
||||
query.enableImageTypes = enableImageTypes
|
||||
query.enableUserData = enableUserData
|
||||
let response = try await client.getSeasons(Operations.GetSeasons.Input(
|
||||
path: .init(seriesId: seriesId),
|
||||
query: query
|
||||
))
|
||||
let response = try await client.getSeasons(
|
||||
Operations.GetSeasons.Input(
|
||||
path: .init(seriesId: seriesId),
|
||||
query: query
|
||||
))
|
||||
switch response {
|
||||
case .ok(let ok):
|
||||
switch ok.body {
|
||||
|
|
@ -362,10 +382,11 @@ public actor JellyfinClient {
|
|||
query.imageTypeLimit = imageTypeLimit
|
||||
query.enableImageTypes = enableImageTypes
|
||||
query.enableUserData = enableUserData
|
||||
let response = try await client.getEpisodes(Operations.GetEpisodes.Input(
|
||||
path: .init(seriesId: seriesId),
|
||||
query: query
|
||||
))
|
||||
let response = try await client.getEpisodes(
|
||||
Operations.GetEpisodes.Input(
|
||||
path: .init(seriesId: seriesId),
|
||||
query: query
|
||||
))
|
||||
switch response {
|
||||
case .ok(let ok):
|
||||
switch ok.body {
|
||||
|
|
@ -404,7 +425,8 @@ 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 {
|
||||
|
|
@ -426,12 +448,15 @@ 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 }
|
||||
let response = try await client.markPlayedItem(Operations.MarkPlayedItem.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId, datePlayed: datePlayed)
|
||||
))
|
||||
let response = try await client.markPlayedItem(
|
||||
Operations.MarkPlayedItem.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId, datePlayed: datePlayed)
|
||||
))
|
||||
switch response {
|
||||
case .ok(let ok):
|
||||
switch ok.body {
|
||||
|
|
@ -455,12 +480,15 @@ 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 }
|
||||
let response = try await client.markUnplayedItem(Operations.MarkUnplayedItem.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
let response = try await client.markUnplayedItem(
|
||||
Operations.MarkUnplayedItem.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
switch response {
|
||||
case .ok(let ok):
|
||||
switch ok.body {
|
||||
|
|
@ -484,12 +512,15 @@ 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 }
|
||||
let response = try await client.markFavoriteItem(Operations.MarkFavoriteItem.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
let response = try await client.markFavoriteItem(
|
||||
Operations.MarkFavoriteItem.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
switch response {
|
||||
case .ok(let ok):
|
||||
switch ok.body {
|
||||
|
|
@ -511,12 +542,15 @@ 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 }
|
||||
let response = try await client.unmarkFavoriteItem(Operations.UnmarkFavoriteItem.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
let response = try await client.unmarkFavoriteItem(
|
||||
Operations.UnmarkFavoriteItem.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
switch response {
|
||||
case .ok(let ok):
|
||||
switch ok.body {
|
||||
|
|
@ -538,12 +572,15 @@ 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 }
|
||||
let response = try await client.getPlaybackInfo(Operations.GetPlaybackInfo.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
let response = try await client.getPlaybackInfo(
|
||||
Operations.GetPlaybackInfo.Input(
|
||||
path: .init(itemId: itemId),
|
||||
query: .init(userId: userId)
|
||||
))
|
||||
switch response {
|
||||
case .ok(let ok):
|
||||
switch ok.body {
|
||||
|
|
@ -569,9 +606,10 @@ 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(
|
||||
body: .json(.init(value1: info))
|
||||
))
|
||||
let response = try await client.reportPlaybackStart(
|
||||
Operations.ReportPlaybackStart.Input(
|
||||
body: .json(.init(value1: info))
|
||||
))
|
||||
switch response {
|
||||
case .noContent:
|
||||
return
|
||||
|
|
@ -588,9 +626,10 @@ 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(
|
||||
body: .json(.init(value1: info))
|
||||
))
|
||||
let response = try await client.reportPlaybackProgress(
|
||||
Operations.ReportPlaybackProgress.Input(
|
||||
body: .json(.init(value1: info))
|
||||
))
|
||||
switch response {
|
||||
case .noContent:
|
||||
return
|
||||
|
|
@ -607,9 +646,10 @@ 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(
|
||||
body: .json(.init(value1: info))
|
||||
))
|
||||
let response = try await client.reportPlaybackStopped(
|
||||
Operations.ReportPlaybackStopped.Input(
|
||||
body: .json(.init(value1: info))
|
||||
))
|
||||
switch response {
|
||||
case .noContent:
|
||||
return
|
||||
|
|
@ -647,7 +687,8 @@ 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 {
|
||||
|
|
@ -669,8 +710,13 @@ public actor JellyfinClient {
|
|||
}
|
||||
}
|
||||
|
||||
public func imageURL(itemId: String, imageType: Components.Schemas.ImageType, tag: String? = nil, maxWidth: Int32? = nil, quality: Int32? = 90) -> URL? {
|
||||
guard var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false) else { return nil }
|
||||
public func imageURL(
|
||||
itemId: String, imageType: Components.Schemas.ImageType, tag: String? = nil,
|
||||
maxWidth: Int32? = nil, quality: Int32? = 90
|
||||
) -> URL? {
|
||||
guard var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false) else {
|
||||
return nil
|
||||
}
|
||||
components.path = "/Items/\(itemId)/Images/\(imageType)"
|
||||
var queryItems: [URLQueryItem] = []
|
||||
if let tag { queryItems.append(.init(name: "tag", value: tag)) }
|
||||
|
|
@ -681,7 +727,9 @@ public actor JellyfinClient {
|
|||
}
|
||||
|
||||
public func userImageURL(userId: String, tag: String? = nil) -> URL? {
|
||||
guard var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false) else { return nil }
|
||||
guard var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false) else {
|
||||
return nil
|
||||
}
|
||||
components.path = "/Users/\(userId)/Images/Primary"
|
||||
if let tag { components.queryItems = [.init(name: "tag", value: tag)] }
|
||||
return components.url
|
||||
|
|
|
|||
20
Sources/LuminateCore/Page.swift
Normal file
20
Sources/LuminateCore/Page.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// Page.swift
|
||||
// Luminate
|
||||
//
|
||||
// Created by Brendan Szymanski on 6/11/26.
|
||||
//
|
||||
|
||||
public enum Page: CustomStringConvertible {
|
||||
case library(item: Components.Schemas.BaseItemDto)
|
||||
case folder(title: String, items: [Components.Schemas.BaseItemDto])
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .library(let item):
|
||||
return item.name ?? "Library"
|
||||
case .folder(let title, _):
|
||||
return title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,35 +52,39 @@ public actor SQLiteStore: PersistenceService {
|
|||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!
|
||||
return appSupport
|
||||
return
|
||||
appSupport
|
||||
.appendingPathComponent("dev.bscubed.Luminate")
|
||||
.appendingPathComponent("db.sqlite")
|
||||
} else {
|
||||
#if os(Linux)
|
||||
let xdgData = ProcessInfo.processInfo.environment["XDG_DATA_HOME"]
|
||||
?? "\(FileManager.default.homeDirectoryForCurrentUser.path).local/share"
|
||||
return URL(fileURLWithPath: xdgData)
|
||||
.appendingPathComponent("dev.bscubed.Luminate")
|
||||
.appendingPathComponent("db.sqlite")
|
||||
let xdgData =
|
||||
ProcessInfo.processInfo.environment["XDG_DATA_HOME"]
|
||||
?? "\(FileManager.default.homeDirectoryForCurrentUser.path).local/share"
|
||||
return URL(fileURLWithPath: xdgData)
|
||||
.appendingPathComponent("dev.bscubed.Luminate")
|
||||
.appendingPathComponent("db.sqlite")
|
||||
#else
|
||||
let appSupport = FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!
|
||||
return appSupport
|
||||
.appendingPathComponent("dev.bscubed.Luminate")
|
||||
.appendingPathComponent("db.sqlite")
|
||||
let appSupport = FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!
|
||||
return
|
||||
appSupport
|
||||
.appendingPathComponent("dev.bscubed.Luminate")
|
||||
.appendingPathComponent("db.sqlite")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public func loadAuth() async throws -> AuthData {
|
||||
let stmt = try db.prepare("SELECT server_url, token, user_id, username FROM auth WHERE id = 1")
|
||||
let stmt = try db.prepare(
|
||||
"SELECT server_url, token, user_id, username FROM auth WHERE id = 1")
|
||||
for row in stmt {
|
||||
guard let serverURL = row[0] as? String,
|
||||
let token = row[1] as? String,
|
||||
let userId = row[2] as? String,
|
||||
let username = row[3] as? String
|
||||
let token = row[1] as? String,
|
||||
let userId = row[2] as? String,
|
||||
let username = row[3] as? String
|
||||
else {
|
||||
throw PersistenceError.decodingFailed
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
public actor WebSocketClient {
|
||||
|
|
@ -51,7 +52,8 @@ public actor WebSocketClient {
|
|||
switch message {
|
||||
case .string(let text):
|
||||
guard let data = text.data(using: .utf8),
|
||||
let event = try? JSONDecoder().decode(WebSocketEvent.self, from: data) else { return }
|
||||
let event = try? JSONDecoder().decode(WebSocketEvent.self, from: data)
|
||||
else { return }
|
||||
Task { @MainActor in
|
||||
NotificationCenter.default.post(
|
||||
name: .init(event.messageType),
|
||||
|
|
@ -82,12 +84,21 @@ public struct AnyDecodable: Decodable {
|
|||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let intVal = try? container.decode(Int.self) { value = intVal }
|
||||
else if let doubleVal = try? container.decode(Double.self) { value = doubleVal }
|
||||
else if let boolVal = try? container.decode(Bool.self) { value = boolVal }
|
||||
else if let stringVal = try? container.decode(String.self) { value = stringVal }
|
||||
else if let arrayVal = try? container.decode([AnyDecodable].self) { value = arrayVal.map(\.value) }
|
||||
else if let dictVal = try? container.decode([String: AnyDecodable].self) { value = dictVal.mapValues(\.value) }
|
||||
else { throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "AnyDecodable error")) }
|
||||
if let intVal = try? container.decode(Int.self) {
|
||||
value = intVal
|
||||
} else if let doubleVal = try? container.decode(Double.self) {
|
||||
value = doubleVal
|
||||
} else if let boolVal = try? container.decode(Bool.self) {
|
||||
value = boolVal
|
||||
} else if let stringVal = try? container.decode(String.self) {
|
||||
value = stringVal
|
||||
} else if let arrayVal = try? container.decode([AnyDecodable].self) {
|
||||
value = arrayVal.map(\.value)
|
||||
} else if let dictVal = try? container.decode([String: AnyDecodable].self) {
|
||||
value = dictVal.mapValues(\.value)
|
||||
} else {
|
||||
throw DecodingError.dataCorrupted(
|
||||
.init(codingPath: container.codingPath, debugDescription: "AnyDecodable error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
import LuminateCore
|
||||
|
||||
struct HomePosterCell: View {
|
||||
|
|
@ -54,16 +54,21 @@ struct HomePosterCell: View {
|
|||
|
||||
private func loadImage() {
|
||||
guard let tag = item.primaryImageTag,
|
||||
let itemId = item.seriesId ?? item.id else { return }
|
||||
let itemId = item.seriesId ?? item.id
|
||||
else { return }
|
||||
Task {
|
||||
guard let url = await client.imageURL(
|
||||
itemId: itemId,
|
||||
imageType: .primary,
|
||||
tag: tag,
|
||||
maxWidth: 400
|
||||
) else { return }
|
||||
guard
|
||||
let url = await client.imageURL(
|
||||
itemId: itemId,
|
||||
imageType: .primary,
|
||||
tag: tag,
|
||||
maxWidth: 400
|
||||
)
|
||||
else { return }
|
||||
let service = ImageService()
|
||||
imageData = try? await service.loadImage(url: url)
|
||||
await MainActor.run {
|
||||
imageData = try? await service.loadImage(url: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@ import LuminateCore
|
|||
|
||||
public struct HomeView: View {
|
||||
|
||||
nonisolated public init() {}
|
||||
nonisolated public init(navigation: Binding<NavigationStack<Page>>) {
|
||||
_navigation = navigation
|
||||
}
|
||||
|
||||
@Injected(\.client) var client
|
||||
@Injected(\.userId) var userId
|
||||
@Binding var navigation: NavigationStack<Page>
|
||||
@State private var resumeItems: [Components.Schemas.BaseItemDto] = []
|
||||
@State private var nextUpItems: [Components.Schemas.BaseItemDto] = []
|
||||
@State private var latestItems: [Components.Schemas.BaseItemDto] = []
|
||||
|
|
@ -22,7 +25,7 @@ public struct HomeView: View {
|
|||
.child {
|
||||
VStack {
|
||||
if isLoading {
|
||||
|
||||
|
||||
Spinner()
|
||||
.halign(.center)
|
||||
.valign(.center)
|
||||
|
|
@ -30,29 +33,44 @@ public struct HomeView: View {
|
|||
.frame(maxWidth: 64)
|
||||
} else {
|
||||
if !resumeItems.isEmpty {
|
||||
let title = "Continue Watching"
|
||||
MediaRow(
|
||||
title: "Continue Watching",
|
||||
items: resumeItems
|
||||
title: title,
|
||||
items: resumeItems,
|
||||
navigation: $navigation,
|
||||
onSeeAll: {
|
||||
navigation.push(.folder(title: title, items: resumeItems))
|
||||
}
|
||||
)
|
||||
.padding(32, .bottom)
|
||||
}
|
||||
if !nextUpItems.isEmpty {
|
||||
let title = "Next Up"
|
||||
MediaRow(
|
||||
title: "Next Up",
|
||||
items: nextUpItems
|
||||
title: title,
|
||||
items: nextUpItems,
|
||||
navigation: $navigation,
|
||||
onSeeAll: {
|
||||
navigation.push(.folder(title: title, items: nextUpItems))
|
||||
}
|
||||
)
|
||||
.padding(32, .bottom)
|
||||
}
|
||||
if !latestItems.isEmpty {
|
||||
let title = "Recently Added"
|
||||
MediaRow(
|
||||
title: "Recently Added",
|
||||
title: title,
|
||||
items: latestItems,
|
||||
onSeeAll: {}
|
||||
navigation: $navigation,
|
||||
onSeeAll: {
|
||||
navigation.push(.folder(title: title, items: latestItems))
|
||||
}
|
||||
)
|
||||
.padding(32, .bottom)
|
||||
}
|
||||
LibraryGrid(
|
||||
libraries: libraries
|
||||
libraries: libraries,
|
||||
navigation: $navigation
|
||||
)
|
||||
.padding(32, .bottom)
|
||||
}
|
||||
|
|
@ -63,7 +81,6 @@ public struct HomeView: View {
|
|||
}
|
||||
.hscrollbarPolicy(.never)
|
||||
.propagateNaturalHeight()
|
||||
.navigationTitle("Luminate")
|
||||
.onAppear {
|
||||
loadHomeData()
|
||||
}
|
||||
|
|
@ -83,11 +100,11 @@ public struct HomeView: View {
|
|||
async let latest = client.getLatestMedia(userId: userId, limit: 20)
|
||||
async let views = client.getUserViews(userId: userId)
|
||||
do {
|
||||
let (r, n, l, v) = try await (resume, nextUp, latest, views)
|
||||
resumeItems = r.items ?? []
|
||||
nextUpItems = n.items ?? []
|
||||
latestItems = l
|
||||
libraries = v.items ?? []
|
||||
let (resume, nextUp, latest, views) = try await (resume, nextUp, latest, views)
|
||||
resumeItems = resume.items ?? []
|
||||
nextUpItems = nextUp.items ?? []
|
||||
latestItems = latest
|
||||
libraries = views.items ?? []
|
||||
isLoading = false
|
||||
} catch {
|
||||
isLoading = false
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
import LuminateCore
|
||||
|
||||
struct LibraryGrid: View {
|
||||
|
||||
var libraries: [Components.Schemas.BaseItemDto]
|
||||
@Binding var navigation: NavigationStack<Page>
|
||||
var title: String?
|
||||
|
||||
var view: Body {
|
||||
VStack {
|
||||
Text("Libraries")
|
||||
VStack(spacing: 16) {
|
||||
Text(title ?? "Libraries")
|
||||
.style("title-3")
|
||||
.halign(.start)
|
||||
.padding(10, .horizontal)
|
||||
FlowBox(libraries) { library in
|
||||
HomePosterCell(item: library)
|
||||
}
|
||||
.columnSpacing(16)
|
||||
.rowSpacing(16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
Sources/LuminateHome/LibraryPage.swift
Normal file
43
Sources/LuminateHome/LibraryPage.swift
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// LibraryPage.swift
|
||||
// Luminate
|
||||
//
|
||||
// Created by Brendan Szymanski on 6/11/26.
|
||||
//
|
||||
|
||||
import Adwaita
|
||||
import LuminateCore
|
||||
|
||||
public struct LibraryPage: View {
|
||||
|
||||
nonisolated public init(
|
||||
title: String, items: [Components.Schemas.BaseItemDto],
|
||||
navigation: Binding<NavigationStack<Page>>
|
||||
) {
|
||||
self.title = title
|
||||
self.items = items
|
||||
_navigation = navigation
|
||||
}
|
||||
|
||||
private var items: [Components.Schemas.BaseItemDto]
|
||||
private var title: String
|
||||
@Binding var navigation: NavigationStack<Page>
|
||||
|
||||
public var view: Body {
|
||||
ScrollView {
|
||||
Clamp()
|
||||
.maximumSize(1550)
|
||||
.tighteningThreshold(550)
|
||||
.child {
|
||||
LibraryGrid(
|
||||
libraries: items,
|
||||
navigation: $navigation,
|
||||
title: title
|
||||
)
|
||||
.padding(32, .bottom)
|
||||
}
|
||||
}
|
||||
.hscrollbarPolicy(.never)
|
||||
.propagateNaturalHeight()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +1,39 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
import LuminateCore
|
||||
|
||||
struct MediaRow: View {
|
||||
|
||||
var title: String
|
||||
var items: [Components.Schemas.BaseItemDto]
|
||||
@Binding var navigation: NavigationStack<Page>
|
||||
var onSeeAll: (() -> Void)?
|
||||
|
||||
var view: Body {
|
||||
VStack(spacing: 16) {
|
||||
HStack(spacing: 16) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.style("title-3")
|
||||
if onSeeAll != nil {
|
||||
|
||||
/// Poor man's `Spacer()`
|
||||
Bin()
|
||||
.hexpand()
|
||||
|
||||
if let onSeeAll {
|
||||
Button("See All") {
|
||||
onSeeAll?()
|
||||
onSeeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
ForEach(items, horizontal: true) { item in
|
||||
HomePosterCell(item: item)
|
||||
ForEach(items, horizontal: true) { item in
|
||||
HomePosterCell(item: item)
|
||||
.padding(16, .trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.vscrollbarPolicy(.never)
|
||||
.hscrollbarPolicy(.external)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
import LuminateCore
|
||||
|
||||
struct EpisodeList: View {
|
||||
|
|
@ -48,14 +48,14 @@ struct EpisodeRow: View {
|
|||
Picture()
|
||||
.data(data)
|
||||
.frame(minWidth: 100, minHeight: 56)
|
||||
.frame(maxWidth: 100)
|
||||
.frame(maxHeight: 56)
|
||||
.frame(maxWidth: 100)
|
||||
.frame(maxHeight: 56)
|
||||
.style("card")
|
||||
} else {
|
||||
Box(spacing: 0) {}
|
||||
.frame(minWidth: 100, minHeight: 56)
|
||||
.frame(maxWidth: 100)
|
||||
.frame(maxHeight: 56)
|
||||
.frame(maxWidth: 100)
|
||||
.frame(maxHeight: 56)
|
||||
.style("card")
|
||||
}
|
||||
VStack {
|
||||
|
|
@ -79,11 +79,18 @@ 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
|
||||
) else { return }
|
||||
guard
|
||||
let url = await client.imageURL(
|
||||
itemId: itemId,
|
||||
imageType: .primary,
|
||||
tag: tag,
|
||||
maxWidth: 200
|
||||
)
|
||||
else { return }
|
||||
let service = ImageService()
|
||||
imageData = try? await service.loadImage(url: url)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Adwaita
|
||||
import LuminateCore
|
||||
|
||||
public struct LuminateLibrary {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
import LuminateCore
|
||||
|
||||
struct MovieDetailView: View {
|
||||
|
|
@ -27,13 +27,13 @@ struct MovieDetailView: View {
|
|||
Picture()
|
||||
.data(data)
|
||||
.frame(minHeight: 300)
|
||||
.frame(maxHeight: 300)
|
||||
.frame(maxHeight: 300)
|
||||
.hexpand(true)
|
||||
}
|
||||
HStack {
|
||||
PosterCell(item: item, client: client)
|
||||
.frame(minWidth: 200)
|
||||
.frame(maxWidth: 200)
|
||||
.frame(maxWidth: 200)
|
||||
VStack {
|
||||
Text(item.name ?? "")
|
||||
.style("title-1")
|
||||
|
|
@ -109,9 +109,11 @@ struct MovieDetailView: View {
|
|||
private func loadBackdrop() {
|
||||
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
|
||||
) else { return }
|
||||
guard
|
||||
let url = await client.imageURL(
|
||||
itemId: itemId, imageType: .backdrop, tag: tag, maxWidth: 1920
|
||||
)
|
||||
else { return }
|
||||
let service = ImageService()
|
||||
backdropData = try? await service.loadImage(url: url)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
import LuminateCore
|
||||
|
||||
struct PosterCell: View {
|
||||
|
|
@ -33,7 +33,8 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
import LuminateCore
|
||||
|
||||
struct SearchView: View {
|
||||
|
|
@ -67,14 +67,14 @@ struct SearchResultRow: View {
|
|||
Picture()
|
||||
.data(data)
|
||||
.frame(minWidth: 80, minHeight: 120)
|
||||
.frame(maxWidth: 80)
|
||||
.frame(maxHeight: 120)
|
||||
.frame(maxWidth: 80)
|
||||
.frame(maxHeight: 120)
|
||||
.style("card")
|
||||
} else {
|
||||
Box(spacing: 0) {}
|
||||
.frame(minWidth: 80, minHeight: 120)
|
||||
.frame(maxWidth: 80)
|
||||
.frame(maxHeight: 120)
|
||||
.frame(maxWidth: 80)
|
||||
.frame(maxHeight: 120)
|
||||
.style("card")
|
||||
}
|
||||
VStack {
|
||||
|
|
@ -105,11 +105,14 @@ struct SearchResultRow: View {
|
|||
|
||||
private func loadImage() {
|
||||
guard let tag = hint.primaryImageTag,
|
||||
let itemId = hint.id ?? hint.itemId else { return }
|
||||
let itemId = hint.id ?? hint.itemId
|
||||
else { return }
|
||||
Task {
|
||||
guard let url = await client.imageURL(
|
||||
itemId: itemId, imageType: .primary, tag: tag, maxWidth: 160
|
||||
) else { return }
|
||||
guard
|
||||
let url = await client.imageURL(
|
||||
itemId: itemId, imageType: .primary, tag: tag, maxWidth: 160
|
||||
)
|
||||
else { return }
|
||||
let service = ImageService()
|
||||
imageData = try? await service.loadImage(url: url)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
import LuminateCore
|
||||
|
||||
struct TVShowView: View {
|
||||
|
|
@ -18,13 +18,13 @@ struct TVShowView: View {
|
|||
Picture()
|
||||
.data(data)
|
||||
.frame(minHeight: 300)
|
||||
.frame(maxHeight: 300)
|
||||
.frame(maxHeight: 300)
|
||||
.hexpand(true)
|
||||
}
|
||||
HStack {
|
||||
PosterCell(item: item, client: client)
|
||||
.frame(minWidth: 200)
|
||||
.frame(maxWidth: 200)
|
||||
.frame(maxWidth: 200)
|
||||
VStack {
|
||||
Text(item.name ?? "")
|
||||
.style("title-1")
|
||||
|
|
@ -79,9 +79,11 @@ struct TVShowView: View {
|
|||
private func loadBackdrop() {
|
||||
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
|
||||
) else { return }
|
||||
guard
|
||||
let url = await client.imageURL(
|
||||
itemId: itemId, imageType: .backdrop, tag: tag, maxWidth: 1920
|
||||
)
|
||||
else { return }
|
||||
let service = ImageService()
|
||||
backdropData = try? await service.loadImage(url: url)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Adwaita
|
||||
import LuminateCore
|
||||
|
||||
public struct LuminatePlayer {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
import LuminateCore
|
||||
|
||||
public struct PlayerView: View {
|
||||
|
|
@ -52,7 +52,7 @@ public struct PlayerView: View {
|
|||
onTogglePlay: { isPlaying.toggle() },
|
||||
onSeekBack: { position = max(0, position - 10) },
|
||||
onSeekForward: { position = min(duration, position + 10) },
|
||||
onFullscreen: { },
|
||||
onFullscreen: {},
|
||||
onClose: {
|
||||
stopPlayback()
|
||||
onClose()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import Foundation
|
||||
|
||||
struct VideoPlayerWidget: View {
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue