fluxa/appleApp/iOS/FluxaIosApp.swift
KhooLy 0227d8697a Implement iOS offline downloads matching Android's actual feature set
Android's download engine turned out to be simpler than "full parity"
implied: enqueue, progress, cancel - no pause/resume, no WiFi-only
gating, no auto-delete (none of those exist on Android either, despite
a dead "paused" status branch). Mirror that scope on iOS:

- FluxaAppleDownloadManager: background URLSession-based download
  engine, UserDefaults+JSON persisted (same pattern as other Apple*
  managers in this codebase), pushes state into Kotlin via a new
  updateDownloads bridge call.
- AppleLibraryDataSource now reuses the existing commonMain
  OfflineDownloadItem model and OfflineDownloadGrouping/formatting
  logic directly (same code Android uses) instead of reimplementing
  grouping/status-label/size-label logic in Swift.
- FluxaAppleDetailStartup.downloadEpisode/downloadSeason resolve a
  playable stream through the existing addon resource loader, then
  hand off to the download manager.

Known gap: no application(_:handleEventsForBackgroundURLSession:)
AppDelegate hook, so downloads won't reliably resume/report after the
app is fully killed by the OS - only while backgrounded, not
terminated. Also unverified like the rest of the iOS work this
session: no Xcode/Swift toolchain in this environment to compile
against.
2026-08-12 01:44:21 +03:00

138 lines
5.1 KiB
Swift

import FluxaCore
import FluxaShared
import SwiftUI
@main
struct FluxaIosApp: App {
private let coreVersion: String
private let headlessRuntime: FluxaAppleHeadlessRuntime
private let appRuntime: FluxaAppleAppRuntime
private let catalogStartup: FluxaAppleCatalogStartup
private let pluginsManager: FluxaApplePluginRepositoryManager
private let pluginsStartup: FluxaApplePluginsStartup
private let addonStoreManager: FluxaAppleAddonStoreManager
private let addonStoreStartup: FluxaAppleAddonStoreStartup
init() {
let runtime = requireFluxaAppleHeadlessRuntime()
coreVersion = FluxaRustCoreRuntime.version()
headlessRuntime = runtime
let runtimeApp = FluxaAppleAppRuntime(runtime: runtime)
appRuntime = runtimeApp
let catalogStartup = FluxaAppleCatalogStartup(coordinator: runtimeApp.coordinator)
self.catalogStartup = catalogStartup
let detailStartup = FluxaAppleDetailStartup(coordinator: runtimeApp.coordinator)
let searchStartup = FluxaAppleSearchStartup(coordinator: runtimeApp.coordinator)
let discoverStartup = FluxaAppleDiscoverStartup(coordinator: runtimeApp.coordinator)
let calendarStartup = FluxaAppleCalendarStartup(coordinator: runtimeApp.coordinator)
let libraryStartup = FluxaAppleLibraryStartup(coordinator: runtimeApp.coordinator)
let authStartup = FluxaAppleAuthStartup()
let pluginsManager = FluxaApplePluginRepositoryManager()
self.pluginsManager = pluginsManager
let pluginsStartup = FluxaApplePluginsStartup(manager: pluginsManager)
self.pluginsStartup = pluginsStartup
let addonStoreManager = FluxaAppleAddonStoreManager()
self.addonStoreManager = addonStoreManager
let addonStoreStartup = FluxaAppleAddonStoreStartup(manager: addonStoreManager)
self.addonStoreStartup = addonStoreStartup
FluxaApple.shared.setCatalogHomeRefreshHandler {
Task { @MainActor in
await catalogStartup.refresh()
}
}
FluxaApple.shared.setSearchHandler { query in
Task { @MainActor in
await searchStartup.search(query: query)
}
}
FluxaApple.shared.setDiscoverHandler { request in
Task { @MainActor in
await discoverStartup.discover(request: request)
}
}
FluxaApple.shared.setCalendarMonthHandler { year, month in
Task { @MainActor in
await calendarStartup.load(year: year.intValue, month: month.intValue)
}
}
FluxaApple.shared.setAuthSubmitHandler { request in
Task { @MainActor in
await authStartup.submit(request: request)
}
}
FluxaApple.shared.setLibraryRefreshHandler {
Task { @MainActor in
await libraryStartup.refresh()
}
}
FluxaApple.shared.setDetailHandlers(
load: { request in
Task { @MainActor in
await detailStartup.load(request: request)
}
},
watchlist: { request in
Task { @MainActor in
await detailStartup.toggleWatchlist(request: request)
}
},
season: { request in
Task { @MainActor in
detailStartup.selectSeason(request: request)
}
},
streams: { request in
Task { @MainActor in
await detailStartup.loadSources(request: request)
}
},
addonFilter: { _ in },
downloadEpisode: { request in
Task { @MainActor in
await detailStartup.downloadEpisode(request: request)
}
},
downloadSeason: { request in
Task { @MainActor in
await detailStartup.downloadSeason(request: request)
}
}
)
FluxaApple.shared.setCancelDownloadHandler { id in
Task { @MainActor in
FluxaAppleDownloadManager.shared.cancel(id: id)
}
}
FluxaApple.shared.setPlaybackHandler { request in
Task { @MainActor in
FluxaApplePlaybackPresenter.shared.present(request: request)
}
}
FluxaApple.shared.setPluginsActionHandler { action in
Task { @MainActor in
await pluginsStartup.handle(action)
}
}
Task { @MainActor in
await pluginsStartup.start()
}
FluxaApple.shared.setAddonStoreActionHandler { action in
Task { @MainActor in
await addonStoreStartup.handle(action)
}
}
Task { @MainActor in
await addonStoreStartup.start()
}
}
var body: some Scene {
WindowGroup {
FluxaRootView()
.ignoresSafeArea()
.onOpenURL { url in
FluxaApplePlaybackPresenter.shared.handleOpenURL(url)
}
}
}
}