From 5cb1bb75db928014f1ca3f751c05b752f4faf0c5 Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:05:37 +0300 Subject: [PATCH] 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. --- .../FluxaAppleAddonStoreManager.swift | 128 ++++++++++++++++++ .../iOS/FluxaAppleAddonStoreStartup.swift | 80 +++++++++++ appleApp/iOS/FluxaIosApp.swift | 14 ++ 3 files changed, 222 insertions(+) create mode 100644 appleApp/AppleCore/FluxaAppleAddonStoreManager.swift create mode 100644 appleApp/iOS/FluxaAppleAddonStoreStartup.swift diff --git a/appleApp/AppleCore/FluxaAppleAddonStoreManager.swift b/appleApp/AppleCore/FluxaAppleAddonStoreManager.swift new file mode 100644 index 0000000..5df21eb --- /dev/null +++ b/appleApp/AppleCore/FluxaAppleAddonStoreManager.swift @@ -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 + } +} diff --git a/appleApp/iOS/FluxaAppleAddonStoreStartup.swift b/appleApp/iOS/FluxaAppleAddonStoreStartup.swift new file mode 100644 index 0000000..2fb4881 --- /dev/null +++ b/appleApp/iOS/FluxaAppleAddonStoreStartup.swift @@ -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) + } +} diff --git a/appleApp/iOS/FluxaIosApp.swift b/appleApp/iOS/FluxaIosApp.swift index a15d616..e0da50a 100644 --- a/appleApp/iOS/FluxaIosApp.swift +++ b/appleApp/iOS/FluxaIosApp.swift @@ -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 {