mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-07-26 20:02:14 +00:00
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:
parent
d88e4226da
commit
5cb1bb75db
3 changed files with 222 additions and 0 deletions
128
appleApp/AppleCore/FluxaAppleAddonStoreManager.swift
Normal file
128
appleApp/AppleCore/FluxaAppleAddonStoreManager.swift
Normal 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
|
||||
}
|
||||
}
|
||||
80
appleApp/iOS/FluxaAppleAddonStoreStartup.swift
Normal file
80
appleApp/iOS/FluxaAppleAddonStoreStartup.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue