Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9608fce
feat(ios): implement Pigeon API bridges for OpenList core
Suyunmeng Oct 19, 2025
c9a52bd
Merge branch 'main' into feat/ios/working
Suyunmeng Oct 19, 2025
05866b3
fix(ios): generate and use correct Pigeon Swift API
Suyunmeng Oct 19, 2025
6be98b9
fix(dart): correct Event.setUp method name (uppercase P)
Suyunmeng Oct 19, 2025
77df49f
fix(ios): add Swift files to Xcode project
Suyunmeng Oct 19, 2025
e959074
fix(ios): correct Swift method signatures and Go Mobile bindings
Suyunmeng Oct 19, 2025
ea7934e
fix(ios): correct Go Mobile binding error handling pattern
Suyunmeng Oct 19, 2025
b874bb1
Merge branch 'main' into feat/ios/working
Oct 20, 2025
3f87aa1
Merge branch 'main' into feat/ios/working
Suyunmeng Oct 22, 2025
78be76a
fix(ios): Fix data directory configuration to prevent Sandbox crash
Suyunmeng Oct 22, 2025
c3ba7c9
fix(ios): Remove NSError parameters from SetConfig functions
Suyunmeng Oct 22, 2025
7a00844
fix(web,ios): 添加应用生命周期观察并在 Info.plist 启用后台模式以保持 WebView 后台运行
Suyunmeng Oct 23, 2025
8192d53
fix(ios): prevent WebView process suspension when switching interfaces
Suyunmeng Oct 25, 2025
b6d2e7e
Merge branch 'main' into feat/ios/working
Dec 11, 2025
7159736
Merge branch 'main' into feat/ios/working
Jan 7, 2026
eea9ec2
fix(ios): add iOS platform support for ServiceManager and WebView err…
Suyunmeng Jan 7, 2026
4eafd24
fix(ios): improve OpenList server initialization and lifecycle manage…
Suyunmeng Jan 7, 2026
31358e4
feat(ios): enhance WebView state preservation and loading management
Suyunmeng Jan 7, 2026
3d8b75e
feat(ios): add WKAppBoundDomains and OpenListVersion to Info.plist
Suyunmeng Jan 7, 2026
8d0f661
feat(ios): auto-start OpenList service on iOS launch
Suyunmeng Jan 7, 2026
af99548
feat(ios): improve data directory creation and logging
Suyunmeng Jan 7, 2026
fb8950e
fix(ios): correct WebView syntax errors from previous broken merge
Suyunmeng Jan 7, 2026
002c14e
feat(ios): hide Android-specific UI elements and improve error handling
Suyunmeng Jan 7, 2026
7411c58
fix(ios): improve log callback persistence and debugging
Suyunmeng Jan 7, 2026
3369e0f
feat(ios): enforce iOS data storage guidelines and security
Suyunmeng Jan 7, 2026
4593a8b
feat(ios): enable file sharing for downloaded files
Suyunmeng Jan 7, 2026
ac3b22b
fix(ios): remove non-existent localization keys
Suyunmeng Jan 7, 2026
6ef5f96
fix(ios): correct Pigeon API callback Result type handling
Suyunmeng Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
AA1234561234567890ABCDE1 /* PigeonApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE0 /* PigeonApi.swift */; };
AA1234561234567890ABCDE3 /* OpenListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE2 /* OpenListManager.swift */; };
AA1234561234567890ABCDE5 /* OpenListBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE4 /* OpenListBridge.swift */; };
AA1234561234567890ABCDE7 /* AppConfigBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE6 /* AppConfigBridge.swift */; };
AA1234561234567890ABCDE9 /* CommonBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE8 /* CommonBridge.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
Expand Down Expand Up @@ -47,6 +52,11 @@
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
AA1234561234567890ABCDE0 /* PigeonApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonApi.swift; sourceTree = "<group>"; };
AA1234561234567890ABCDE2 /* OpenListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenListManager.swift; sourceTree = "<group>"; };
AA1234561234567890ABCDE4 /* OpenListBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenListBridge.swift; sourceTree = "<group>"; };
AA1234561234567890ABCDE6 /* AppConfigBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigBridge.swift; sourceTree = "<group>"; };
AA1234561234567890ABCDE8 /* CommonBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonBridge.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
Expand Down Expand Up @@ -109,6 +119,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
AA1234561234567890ABCDEA /* Bridges */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
Expand All @@ -117,10 +128,22 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
AA1234561234567890ABCDE0 /* PigeonApi.swift */,
AA1234561234567890ABCDE2 /* OpenListManager.swift */,
);
path = Runner;
sourceTree = "<group>";
};
AA1234561234567890ABCDEA /* Bridges */ = {
isa = PBXGroup;
children = (
AA1234561234567890ABCDE4 /* OpenListBridge.swift */,
AA1234561234567890ABCDE6 /* AppConfigBridge.swift */,
AA1234561234567890ABCDE8 /* CommonBridge.swift */,
);
path = Bridges;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -270,6 +293,11 @@
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
AA1234561234567890ABCDE1 /* PigeonApi.swift in Sources */,
AA1234561234567890ABCDE3 /* OpenListManager.swift in Sources */,
AA1234561234567890ABCDE5 /* OpenListBridge.swift in Sources */,
AA1234561234567890ABCDE7 /* AppConfigBridge.swift in Sources */,
AA1234561234567890ABCDE9 /* CommonBridge.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
74 changes: 74 additions & 0 deletions ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,85 @@ import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
var eventAPI: Event?
private var backgroundTask: UIBackgroundTaskIdentifier = .invalid

override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)

// Setup Pigeon APIs
guard let controller = window?.rootViewController as? FlutterViewController else {
print("[AppDelegate] Failed to get FlutterViewController")
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

let messenger = controller.binaryMessenger

// Register Pigeon API implementations
AppConfigSetup.setUp(binaryMessenger: messenger, api: AppConfigBridge())
AndroidSetup.setUp(binaryMessenger: messenger, api: OpenListBridge())
NativeCommonSetup.setUp(binaryMessenger: messenger, api: CommonBridge(viewController: controller))

// Setup Event API for Flutter callbacks
eventAPI = Event(binaryMessenger: messenger)

// Initialize OpenList core (if XCFramework is available)
#if canImport(Openlistlib)
let eventHandler = OpenListEventHandler()
let logCallback = OpenListLogCallback()
eventHandler.eventAPI = eventAPI
logCallback.eventAPI = eventAPI

do {
try OpenListManager.shared.initialize(event: eventHandler, logger: logCallback)
print("[AppDelegate] OpenList core initialized")
} catch {
print("[AppDelegate] OpenList core initialization failed: \(error)")
// Continue without core - will work in Flutter-only mode
}
#else
print("[AppDelegate] OpenList core not available - running in Flutter-only mode")
#endif

print("[AppDelegate] Pigeon APIs registered successfully")

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

// MARK: - Application Lifecycle

override func applicationWillTerminate(_ application: UIApplication) {
// Cleanup OpenList core
OpenListManager.shared.stopServer()

// End background task if still active
endBackgroundTask()

super.applicationWillTerminate(application)
}

override func applicationDidEnterBackground(_ application: UIApplication) {
// Begin background task to prevent WebView process suspension
backgroundTask = application.beginBackgroundTask { [weak self] in
// Background task is about to expire, clean up
self?.endBackgroundTask()
}
}

override func applicationWillEnterForeground(_ application: UIApplication) {
// End background task when returning to foreground
endBackgroundTask()
}

// MARK: - Background Task Management

private func endBackgroundTask() {
if backgroundTask != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
}
}
109 changes: 109 additions & 0 deletions ios/Runner/Bridges/AppConfigBridge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import Flutter
import Foundation

/// Bridge implementation for App Configuration APIs
class AppConfigBridge: NSObject, AppConfig {
private let defaults = UserDefaults.standard

// Keys for UserDefaults
private enum Keys {
static let wakeLock = "app_config_wake_lock"
static let startAtBoot = "app_config_start_at_boot"
static let autoCheckUpdate = "app_config_auto_check_update"
static let autoOpenWebPage = "app_config_auto_open_web_page"
static let dataDir = "app_config_data_dir"
static let silentJumpApp = "app_config_silent_jump_app"
}

func isWakeLockEnabled() throws -> Bool {
return defaults.bool(forKey: Keys.wakeLock)
}

func setWakeLockEnabled(enabled: Bool) throws {
defaults.set(enabled, forKey: Keys.wakeLock)
print("[AppConfigBridge] Wake lock enabled: \(enabled)")
}

func isStartAtBootEnabled() throws -> Bool {
return defaults.bool(forKey: Keys.startAtBoot)
}

func setStartAtBootEnabled(enabled: Bool) throws {
defaults.set(enabled, forKey: Keys.startAtBoot)
print("[AppConfigBridge] Start at boot enabled: \(enabled)")
}

func isAutoCheckUpdateEnabled() throws -> Bool {
return defaults.bool(forKey: Keys.autoCheckUpdate)
}

func setAutoCheckUpdateEnabled(enabled: Bool) throws {
defaults.set(enabled, forKey: Keys.autoCheckUpdate)
print("[AppConfigBridge] Auto check update enabled: \(enabled)")
}

func isAutoOpenWebPageEnabled() throws -> Bool {
return defaults.bool(forKey: Keys.autoOpenWebPage)
}

func setAutoOpenWebPageEnabled(enabled: Bool) throws {
defaults.set(enabled, forKey: Keys.autoOpenWebPage)
print("[AppConfigBridge] Auto open web page enabled: \(enabled)")
}

func getDataDir() throws -> String {
if let customDir = defaults.string(forKey: Keys.dataDir), !customDir.isEmpty {
print("[AppConfigBridge] Using custom data directory: \(customDir)")
return customDir
}

// Default to app's document directory with openlist_data subdirectory
// This follows iOS app data storage guidelines
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
let openlistDataDir = documentsDirectory.appendingPathComponent("openlist_data")

// Create directory if not exists
if !FileManager.default.fileExists(atPath: openlistDataDir.path) {
do {
try FileManager.default.createDirectory(at: openlistDataDir, withIntermediateDirectories: true, attributes: nil)
print("[AppConfigBridge] Created data directory: \(openlistDataDir.path)")
} catch {
print("[AppConfigBridge] Failed to create data directory: \(error)")
throw error
}
}

print("[AppConfigBridge] Data directory: \(openlistDataDir.path)")
return openlistDataDir.path
}

func setDataDir(dir: String) throws {
// On iOS, we should not allow users to change data directory arbitrarily
// But we keep the method for compatibility
if dir.isEmpty {
defaults.removeObject(forKey: Keys.dataDir)
print("[AppConfigBridge] Data directory reset to default")
} else {
// iOS: Only allow setting within app's container
let appDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].path
if dir.hasPrefix(appDir) {
defaults.set(dir, forKey: Keys.dataDir)
print("[AppConfigBridge] Data directory set to: \(dir)")
} else {
print("[AppConfigBridge] Rejected invalid data directory (outside app container): \(dir)")
throw NSError(domain: "AppConfigBridge", code: -2,
userInfo: [NSLocalizedDescriptionKey: "Data directory must be within app container"])
}
}
}

func isSilentJumpAppEnabled() throws -> Bool {
return defaults.bool(forKey: Keys.silentJumpApp)
}

func setSilentJumpAppEnabled(enabled: Bool) throws {
defaults.set(enabled, forKey: Keys.silentJumpApp)
print("[AppConfigBridge] Silent jump app enabled: \(enabled)")
}
}
124 changes: 124 additions & 0 deletions ios/Runner/Bridges/CommonBridge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import Flutter
import Foundation
import UIKit

/// Bridge implementation for common native APIs
class CommonBridge: NSObject, NativeCommon {
private let viewController: UIViewController?

init(viewController: UIViewController? = nil) {
self.viewController = viewController
super.init()
}

func startActivityFromUri(intentUri: String) throws -> Bool {
print("[CommonBridge] startActivityFromUri: \(intentUri)")

guard let url = URL(string: intentUri) else {
print("[CommonBridge] Invalid URL: \(intentUri)")
return false
}

// Check if the URL can be opened
guard UIApplication.shared.canOpenURL(url) else {
print("[CommonBridge] Cannot open URL: \(intentUri)")
return false
}

// Open the URL
UIApplication.shared.open(url, options: [:]) { success in
print("[CommonBridge] Open URL result: \(success)")
}

return true
}

func getDeviceSdkInt() throws -> Int64 {
// iOS doesn't have SDK int like Android, return iOS major version
let systemVersion = UIDevice.current.systemVersion
let majorVersion = systemVersion.components(separatedBy: ".").first ?? "0"
let version = Int64(majorVersion) ?? 0
print("[CommonBridge] Device iOS version: \(version)")
return version
}

func getDeviceCPUABI() throws -> String {
// Get CPU architecture
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}

print("[CommonBridge] Device CPU ABI: \(identifier)")
return identifier
}

func getVersionName() throws -> String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
print("[CommonBridge] Version name: \(version)")
return version
}

func getVersionCode() throws -> Int64 {
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1"
let code = Int64(build) ?? 1
print("[CommonBridge] Version code: \(code)")
return code
}

func toast(msg: String) throws {
print("[CommonBridge] Toast: \(msg)")
showToast(message: msg, duration: 2.0)
}

func longToast(msg: String) throws {
print("[CommonBridge] Long toast: \(msg)")
showToast(message: msg, duration: 4.0)
}

// MARK: - Toast Helper

private func showToast(message: String, duration: TimeInterval) {
DispatchQueue.main.async { [weak self] in
guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
print("[CommonBridge] No key window found for toast")
return
}

let toastLabel = UILabel()
toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.7)
toastLabel.textColor = UIColor.white
toastLabel.textAlignment = .center
toastLabel.font = UIFont.systemFont(ofSize: 14)
toastLabel.text = message
toastLabel.alpha = 0.0
toastLabel.layer.cornerRadius = 10
toastLabel.clipsToBounds = true
toastLabel.numberOfLines = 0

let maxSize = CGSize(width: window.frame.width - 80, height: window.frame.height)
let expectedSize = toastLabel.sizeThatFits(maxSize)
toastLabel.frame = CGRect(
x: (window.frame.width - expectedSize.width - 20) / 2,
y: window.frame.height - 150,
width: expectedSize.width + 20,
height: expectedSize.height + 20
)

window.addSubview(toastLabel)

UIView.animate(withDuration: 0.3, animations: {
toastLabel.alpha = 1.0
}) { _ in
UIView.animate(withDuration: 0.3, delay: duration, options: [], animations: {
toastLabel.alpha = 0.0
}) { _ in
toastLabel.removeFromSuperview()
}
}
}
}
}
Loading
Loading