mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-06 00:38:59 +00:00
Route Apple home loading through headless effects
This commit is contained in:
parent
221dd8d4ec
commit
a33abcae83
6 changed files with 159 additions and 21 deletions
|
|
@ -19,6 +19,11 @@ final class FluxaAppleCatalogLoader {
|
|||
}
|
||||
|
||||
func loadSnapshot(requests: [FluxaAppleCatalogRequest]) async throws -> String {
|
||||
let rows = try await loadRows(requests: requests)
|
||||
return String(decoding: try encoder.encode(FluxaAppleCatalogSnapshot(rows: rows)), as: UTF8.self)
|
||||
}
|
||||
|
||||
func loadRows(requests: [FluxaAppleCatalogRequest]) async throws -> [FluxaAppleCatalogRow] {
|
||||
var rows = [FluxaAppleCatalogRow]()
|
||||
var lastError: Error?
|
||||
for request in requests {
|
||||
|
|
@ -55,7 +60,7 @@ final class FluxaAppleCatalogLoader {
|
|||
if rows.isEmpty, let lastError {
|
||||
throw lastError
|
||||
}
|
||||
return String(decoding: try encoder.encode(FluxaAppleCatalogSnapshot(rows: rows)), as: UTF8.self)
|
||||
return rows
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,19 +77,19 @@ private struct FluxaAppleStremioMeta: Decodable {
|
|||
let releaseInfo: String?
|
||||
}
|
||||
|
||||
private struct FluxaAppleCatalogSnapshot: Encodable {
|
||||
struct FluxaAppleCatalogSnapshot: Encodable {
|
||||
let rows: [FluxaAppleCatalogRow]
|
||||
let isLoading = false
|
||||
}
|
||||
|
||||
private struct FluxaAppleCatalogRow: Encodable {
|
||||
struct FluxaAppleCatalogRow: Encodable {
|
||||
let id: String
|
||||
let title: String
|
||||
let canLoadMore: Bool
|
||||
let items: [FluxaAppleCatalogItem]
|
||||
}
|
||||
|
||||
private struct FluxaAppleCatalogItem: Encodable {
|
||||
struct FluxaAppleCatalogItem: Encodable {
|
||||
let id: String
|
||||
let type: String
|
||||
let title: String
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ protocol FluxaAppleHeadlessEffectExecutor: AnyObject {
|
|||
|
||||
final class FluxaAppleHeadlessCoordinator {
|
||||
private let runtime: FluxaAppleHeadlessRuntime
|
||||
private weak var executor: (any FluxaAppleHeadlessEffectExecutor)?
|
||||
private let executor: any FluxaAppleHeadlessEffectExecutor
|
||||
private let maxEffectsPerDispatch: Int
|
||||
private let decoder = JSONDecoder()
|
||||
private let encoder = JSONEncoder()
|
||||
|
|
@ -107,13 +107,13 @@ final class FluxaAppleHeadlessCoordinator {
|
|||
|
||||
private func drain(_ initialJson: String) async throws -> FluxaAppleHeadlessResult {
|
||||
var current = try decode(initialJson)
|
||||
var pending = current.effects
|
||||
var remaining = maxEffectsPerDispatch
|
||||
while let effect = current.effects.first, remaining > 0 {
|
||||
guard let executor else {
|
||||
return current
|
||||
}
|
||||
while !pending.isEmpty, remaining > 0 {
|
||||
let effect = pending.removeFirst()
|
||||
let completion = await executor.execute(effect: effect)
|
||||
current = try decode(runtime.completeEffect(resultJson: try encode(completion)))
|
||||
pending.append(contentsOf: current.effects)
|
||||
remaining -= 1
|
||||
}
|
||||
return current
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import FluxaShared
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
|
|
@ -14,13 +13,12 @@ final class FluxaAppleCatalogBootstrap {
|
|||
self.resolver = resolver
|
||||
}
|
||||
|
||||
func refresh(requests: [FluxaAppleCatalogRequest]) async throws {
|
||||
let snapshot = try await loader.loadSnapshot(requests: requests)
|
||||
FluxaApple.shared.updateCatalogHomeJson(homeJson: snapshot)
|
||||
func loadRows(requests: [FluxaAppleCatalogRequest]) async throws -> [FluxaAppleCatalogRow] {
|
||||
try await loader.loadRows(requests: requests)
|
||||
}
|
||||
|
||||
func refresh(localAddonUrls: [String]) async throws {
|
||||
func loadRows(localAddonUrls: [String]) async throws -> [FluxaAppleCatalogRow] {
|
||||
let requests = try await resolver.resolveRequests(localAddonUrls: localAddonUrls)
|
||||
try await refresh(requests: requests)
|
||||
return try await loadRows(requests: requests)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,22 +26,90 @@ final class FluxaAppleAddonConfigurationStore {
|
|||
|
||||
@MainActor
|
||||
final class FluxaAppleCatalogStartup {
|
||||
private let configurationStore: FluxaAppleAddonConfigurationStore
|
||||
private let bootstrap: FluxaAppleCatalogBootstrap
|
||||
private let coordinator: FluxaAppleHeadlessCoordinator
|
||||
private let encoder = JSONEncoder()
|
||||
|
||||
init(
|
||||
runtime: FluxaAppleHeadlessRuntime,
|
||||
configurationStore: FluxaAppleAddonConfigurationStore = FluxaAppleAddonConfigurationStore(),
|
||||
bootstrap: FluxaAppleCatalogBootstrap = FluxaAppleCatalogBootstrap()
|
||||
) {
|
||||
self.configurationStore = configurationStore
|
||||
self.bootstrap = bootstrap
|
||||
let handler = FluxaAppleHomeEffectHandler(
|
||||
configurationStore: configurationStore,
|
||||
catalogBootstrap: bootstrap
|
||||
)
|
||||
coordinator = FluxaAppleHeadlessCoordinator(
|
||||
runtime: runtime,
|
||||
executor: FluxaApplePlatformEffectExecutor(handler: handler)
|
||||
)
|
||||
}
|
||||
|
||||
func refresh() async {
|
||||
do {
|
||||
try await bootstrap.refresh(localAddonUrls: configurationStore.localAddonUrls())
|
||||
let result = try await coordinator.dispatch(
|
||||
actionJson: "{\"type\":\"homeLoadRequested\",\"profile\":{\"id\":\"apple-default\"},\"language\":\"en\",\"force\":true}"
|
||||
)
|
||||
updateSharedHome(result: result)
|
||||
} catch {
|
||||
FluxaApple.shared.updateCatalogHomeJson(homeJson: "{\"rows\":[],\"isLoading\":false}")
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSharedHome(result: FluxaAppleHeadlessResult) {
|
||||
guard case .object(let home)? = result.state["home"],
|
||||
case .array(let categories)? = home["categories"] else {
|
||||
FluxaApple.shared.updateCatalogHomeJson(homeJson: "{\"rows\":[],\"isLoading\":false}")
|
||||
return
|
||||
}
|
||||
let rows = categories.compactMap(sharedRow)
|
||||
let snapshot = FluxaAppleJsonValue.object([
|
||||
"rows": .array(rows),
|
||||
"isLoading": .boolean(false)
|
||||
])
|
||||
guard let data = try? encoder.encode(snapshot) else {
|
||||
return
|
||||
}
|
||||
FluxaApple.shared.updateCatalogHomeJson(homeJson: String(decoding: data, as: UTF8.self))
|
||||
}
|
||||
|
||||
private func sharedRow(_ category: FluxaAppleJsonValue) -> FluxaAppleJsonValue? {
|
||||
guard case .object(let value) = category,
|
||||
let id = string(value["id"]),
|
||||
let title = string(value["name"]),
|
||||
case .array(let items)? = value["items"] else {
|
||||
return nil
|
||||
}
|
||||
return .object([
|
||||
"id": .string(id),
|
||||
"title": .string(title),
|
||||
"canLoadMore": .boolean(false),
|
||||
"items": .array(items.compactMap(sharedItem))
|
||||
])
|
||||
}
|
||||
|
||||
private func sharedItem(_ item: FluxaAppleJsonValue) -> FluxaAppleJsonValue? {
|
||||
guard case .object(let value) = item,
|
||||
let id = string(value["id"]),
|
||||
let type = string(value["type"]),
|
||||
let title = string(value["name"]) else {
|
||||
return nil
|
||||
}
|
||||
return .object([
|
||||
"id": .string(id),
|
||||
"type": .string(type),
|
||||
"title": .string(title),
|
||||
"subtitle": value["releaseInfo"] ?? .string(""),
|
||||
"artworkUrl": value["poster"] ?? .null,
|
||||
"logoUrl": value["logo"] ?? .null,
|
||||
"addonTransportUrl": value["addonTransportUrl"] ?? .null,
|
||||
"catalogType": value["catalogType"] ?? .null
|
||||
])
|
||||
}
|
||||
|
||||
private func string(_ value: FluxaAppleJsonValue?) -> String? {
|
||||
guard case .string(let text)? = value else {
|
||||
return nil
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
|
|
|||
63
appleApp/iOS/FluxaAppleHomeEffectHandler.swift
Normal file
63
appleApp/iOS/FluxaAppleHomeEffectHandler.swift
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import Foundation
|
||||
|
||||
final class FluxaAppleHomeEffectHandler: FluxaApplePlatformEffectHandler {
|
||||
private let configurationStore: FluxaAppleAddonConfigurationStore
|
||||
private let catalogBootstrap: FluxaAppleCatalogBootstrap
|
||||
|
||||
init(
|
||||
configurationStore: FluxaAppleAddonConfigurationStore,
|
||||
catalogBootstrap: FluxaAppleCatalogBootstrap
|
||||
) {
|
||||
self.configurationStore = configurationStore
|
||||
self.catalogBootstrap = catalogBootstrap
|
||||
}
|
||||
|
||||
func execute(effect: FluxaAppleHeadlessEffect) async throws -> FluxaAppleJsonValue {
|
||||
switch effect.type {
|
||||
case "readHomeBootstrap":
|
||||
let rows = try await catalogBootstrap.loadRows(
|
||||
localAddonUrls: configurationStore.localAddonUrls()
|
||||
)
|
||||
return .object([
|
||||
"categories": .array(rows.map(homeCategory)),
|
||||
"continueWatching": .array([]),
|
||||
"watchlist": .array([]),
|
||||
"userAddons": .array(configurationStore.localAddonUrls().map { .string($0) }),
|
||||
"metadataFeeds": .array([]),
|
||||
"billboard": rows.first?.items.first.map(homeMeta) ?? .null
|
||||
])
|
||||
case "refreshContinueWatching":
|
||||
return .object(["continueWatching": .array([])])
|
||||
default:
|
||||
throw NSError(domain: "FluxaAppleUnsupportedEffect", code: 1)
|
||||
}
|
||||
}
|
||||
|
||||
private func homeCategory(_ row: FluxaAppleCatalogRow) -> FluxaAppleJsonValue {
|
||||
.object([
|
||||
"id": .string(row.id),
|
||||
"name": .string(row.title),
|
||||
"semanticName": .string(row.title),
|
||||
"type": .string(row.items.first?.type ?? ""),
|
||||
"catalogId": .string(row.id),
|
||||
"items": .array(row.items.map(homeMeta))
|
||||
])
|
||||
}
|
||||
|
||||
private func homeMeta(_ item: FluxaAppleCatalogItem) -> FluxaAppleJsonValue {
|
||||
.object([
|
||||
"id": .string(item.id),
|
||||
"type": .string(item.type),
|
||||
"name": .string(item.title),
|
||||
"poster": optionalString(item.artworkUrl),
|
||||
"logo": optionalString(item.logoUrl),
|
||||
"releaseInfo": .string(item.subtitle),
|
||||
"addonTransportUrl": optionalString(item.addonTransportUrl),
|
||||
"catalogType": optionalString(item.catalogType)
|
||||
])
|
||||
}
|
||||
|
||||
private func optionalString(_ value: String?) -> FluxaAppleJsonValue {
|
||||
value.map(FluxaAppleJsonValue.string) ?? .null
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,13 @@ import SwiftUI
|
|||
struct FluxaIosApp: App {
|
||||
private let coreVersion = FluxaRustCoreRuntime.version()
|
||||
private let headlessRuntime = requireFluxaAppleHeadlessRuntime()
|
||||
private let catalogStartup = FluxaAppleCatalogStartup()
|
||||
private let catalogStartup: FluxaAppleCatalogStartup
|
||||
@State private var hasStartedCatalog = false
|
||||
|
||||
init() {
|
||||
catalogStartup = FluxaAppleCatalogStartup(runtime: headlessRuntime)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
FluxaRootView()
|
||||
|
|
|
|||
Loading…
Reference in a new issue