Wire Addon Store screen to real networking on iOS

FluxaAppleAddonStoreManager fetches manifests over URLSession and
persists the local addon list plus enabled/disabled state via
FluxaAppleAddonConfigurationStore. FluxaAppleAddonStoreStartup routes
AppleAddonStoreActionSnapshot actions from the shared UI to it and
pushes merged state back to Kotlin. FluxaIosApp constructs and
registers it, restoring/refreshing on launch. Users can now actually
add, toggle, reorder, and remove Stremio addons on iOS.
This commit is contained in:
KhooLy 2026-07-20 15:05:37 +03:00
parent d88e4226da
commit 5cb1bb75db
3 changed files with 222 additions and 0 deletions

View file

@ -0,0 +1,128 @@
import FluxaShared
import Foundation
struct FluxaAppleInstalledAddon {
let name: String
let description: String
let url: String
let logoUrl: String?
let version: String?
let configurable: Bool
let isEnabled: Bool
let canMoveUp: Bool
let canMoveDown: Bool
}
final class FluxaAppleAddonStoreManager {
private let configurationStore: FluxaAppleAddonConfigurationStore
private let session: URLSession
init(
configurationStore: FluxaAppleAddonConfigurationStore = FluxaAppleAddonConfigurationStore(),
session: URLSession = .shared
) {
self.configurationStore = configurationStore
self.session = session
}
func currentAddons() async -> [FluxaAppleInstalledAddon] {
let urls = configurationStore.localAddonUrls()
let disabled = configurationStore.disabledAddonUrls()
var results = [FluxaAppleInstalledAddon]()
for (index, url) in urls.enumerated() {
let manifest = await fetchManifest(rawUrl: url)
results.append(
FluxaAppleInstalledAddon(
name: manifest?.name ?? fallbackName(for: url),
description: manifest?.description ?? "",
url: url,
logoUrl: manifest?.logo,
version: manifest?.version,
configurable: manifest?.configurable ?? false,
isEnabled: !disabled.contains(url),
canMoveUp: index > 0,
canMoveDown: index < urls.count - 1
)
)
}
return results
}
func submitManifestUrl(_ raw: String) async -> (addons: [FluxaAppleInstalledAddon], addedName: String?, failed: Bool) {
let normalized = normalizeManifestUrl(raw)
guard let manifest = await fetchManifest(rawUrl: normalized) else {
return (await currentAddons(), nil, true)
}
var urls = configurationStore.localAddonUrls()
if !urls.contains(where: { normalizeManifestUrl($0) == normalized }) {
urls.append(normalized)
configurationStore.save(localAddonUrls: urls)
}
return (await currentAddons(), manifest.name, false)
}
func toggleAddon(url: String, enabled: Bool) async -> [FluxaAppleInstalledAddon] {
var disabled = configurationStore.disabledAddonUrls()
if enabled {
disabled.remove(url)
} else {
disabled.insert(url)
}
configurationStore.save(disabledAddonUrls: disabled)
return await currentAddons()
}
func removeAddon(url: String) async -> [FluxaAppleInstalledAddon] {
let normalized = normalizeManifestUrl(url)
var urls = configurationStore.localAddonUrls()
urls.removeAll { normalizeManifestUrl($0) == normalized }
configurationStore.save(localAddonUrls: urls)
var disabled = configurationStore.disabledAddonUrls()
disabled.remove(url)
configurationStore.save(disabledAddonUrls: disabled)
return await currentAddons()
}
func moveAddon(url: String, direction: Int) async -> [FluxaAppleInstalledAddon] {
let normalized = normalizeManifestUrl(url)
var urls = configurationStore.localAddonUrls()
guard let index = urls.firstIndex(where: { normalizeManifestUrl($0) == normalized }) else {
return await currentAddons()
}
let newIndex = index + direction
guard urls.indices.contains(newIndex) else {
return await currentAddons()
}
urls.swapAt(index, newIndex)
configurationStore.save(localAddonUrls: urls)
return await currentAddons()
}
func refreshAddon(url: String) async -> [FluxaAppleInstalledAddon] {
await currentAddons()
}
private func fetchManifest(rawUrl: String) async -> AppleAddonManifestSnapshot? {
guard let manifestUrl = URL(string: normalizeManifestUrl(rawUrl)) else {
return nil
}
do {
let (data, response) = try await session.data(from: manifestUrl)
guard let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode) else {
return nil
}
return FluxaApple.shared.parseAddonManifest(body: String(decoding: data, as: UTF8.self))
} catch {
return nil
}
}
private func normalizeManifestUrl(_ raw: String) -> String {
FluxaApple.shared.normalizeAddonManifestUrl(rawUrl: raw)
}
private func fallbackName(for url: String) -> String {
URL(string: url)?.host ?? url
}
}

View file

@ -0,0 +1,80 @@
import FluxaShared
import Foundation
@MainActor
final class FluxaAppleAddonStoreStartup {
private let manager: FluxaAppleAddonStoreManager
init(manager: FluxaAppleAddonStoreManager) {
self.manager = manager
}
func start() async {
push(addons: await manager.currentAddons())
}
func handle(_ action: AppleAddonStoreActionSnapshot) async {
switch action.type {
case "refresh":
push(addons: await manager.currentAddons())
case "submitInput":
guard let text = action.text, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return
}
let result = await manager.submitManifestUrl(text)
push(
addons: result.addons,
addedAddonName: result.addedName,
inputFailed: result.failed,
clearInputOnSuccess: !result.failed
)
case "toggleAddon":
guard let url = action.url, let enabled = action.enabled?.boolValue else { return }
push(addons: await manager.toggleAddon(url: url, enabled: enabled))
case "removeAddon":
guard let url = action.url else { return }
push(addons: await manager.removeAddon(url: url))
case "moveAddon":
guard let url = action.url, let direction = action.direction?.intValue else { return }
push(addons: await manager.moveAddon(url: url, direction: direction))
case "refreshAddon":
guard let url = action.url else { return }
push(addons: await manager.currentAddons(), refreshingUrl: url)
push(addons: await manager.refreshAddon(url: url))
default:
break
}
}
private func push(
addons: [FluxaAppleInstalledAddon],
refreshingUrl: String? = nil,
isLoading: Bool = false,
addedAddonName: String? = nil,
inputFailed: Bool = false,
clearInputOnSuccess: Bool = false
) {
let snapshot = AppleAddonStoreSnapshot(
installedAddons: addons.map { addon in
AppleInstalledAddonSnapshot(
name: addon.name,
description: addon.description,
url: addon.url,
logoUrl: addon.logoUrl,
version: addon.version,
configurable: addon.configurable,
isEnabled: addon.isEnabled,
canMoveUp: addon.canMoveUp,
canMoveDown: addon.canMoveDown,
isRefreshing: addon.url == refreshingUrl
)
},
isLoading: isLoading,
isSubmittingInput: false,
inputFailed: inputFailed,
addedAddonName: addedAddonName,
clearInputOnSuccess: clearInputOnSuccess
)
FluxaApple.shared.updateAddonStore(snapshot: snapshot)
}
}

View file

@ -9,6 +9,8 @@ struct FluxaIosApp: App {
private let catalogStartup: FluxaAppleCatalogStartup
private let pluginsManager: FluxaApplePluginRepositoryManager
private let pluginsStartup: FluxaApplePluginsStartup
private let addonStoreManager: FluxaAppleAddonStoreManager
private let addonStoreStartup: FluxaAppleAddonStoreStartup
init() {
let runtime = requireFluxaAppleHeadlessRuntime()
@ -28,6 +30,10 @@ struct FluxaIosApp: App {
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()
@ -83,6 +89,14 @@ struct FluxaIosApp: App {
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 {