import Flutter import UIKit import AVFoundation private let thumbnailHTTPHeaderFieldsKey = "AVURLAssetHTTPHeaderFieldsKey" private final class ThumbnailResponseGate { private let lock = NSLock() private var completed = false func send(_ result: @escaping FlutterResult, data: Data?) { lock.lock() if completed { lock.unlock() return } completed = true lock.unlock() DispatchQueue.main.async { result(data) } } } /// Keeps the system LaunchScreen as the only splash. Flutter would otherwise /// instantiate that storyboard again and the icon appears a second time. final class RunnerFlutterViewController: FlutterViewController { override func loadDefaultSplashScreenView() -> Bool { false } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black } } @main @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { private let thumbnailQueue = DispatchQueue( label: "com.stormg.lumadrama.thumbnail", qos: .userInitiated ) override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { UIApplication.shared.isIdleTimerDisabled = true return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) let channel = FlutterMethodChannel( name: "com.stormg.lumadrama/share", binaryMessenger: engineBridge.applicationRegistrar.messenger() ) channel.setMethodCallHandler { [weak self] call, result in guard call.method == "shareText", let arguments = call.arguments as? [String: Any], let text = arguments["text"] as? String, !text.isEmpty, let controller = self?.window?.rootViewController else { result(FlutterError(code: "share_unavailable", message: "Share sheet is unavailable", details: nil)) return } let activity = UIActivityViewController(activityItems: [text], applicationActivities: nil) activity.popoverPresentationController?.sourceView = controller.view activity.completionWithItemsHandler = { _, _, _, _ in result(nil) } controller.present(activity, animated: true) } let thumbnailChannel = FlutterMethodChannel( name: "com.stormg.lumadrama/thumbnail", binaryMessenger: engineBridge.applicationRegistrar.messenger() ) thumbnailChannel.setMethodCallHandler { [weak self] call, result in guard call.method == "frame" else { result(FlutterMethodNotImplemented) return } guard let self, let arguments = call.arguments as? [String: Any], let timeNumber = arguments["timeMs"] as? NSNumber else { result(nil) return } let localPath = arguments["localPath"] as? String ?? "" let remoteUrl = arguments["url"] as? String ?? "" let headers = arguments["headers"] as? [String: String] ?? [:] let assetUrl: URL? if !localPath.isEmpty { assetUrl = localPath.hasPrefix("file:") ? URL(string: localPath) : URL(fileURLWithPath: localPath) } else if !remoteUrl.isEmpty { assetUrl = URL(string: remoteUrl) } else { assetUrl = nil } guard let assetUrl else { result(nil) return } let options: [String: Any]? = headers.isEmpty ? nil : [thumbnailHTTPHeaderFieldsKey: headers] let asset = AVURLAsset(url: assetUrl, options: options) let generator = AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform = true generator.maximumSize = CGSize(width: 240, height: 426) generator.requestedTimeToleranceBefore = CMTime(seconds: 0.5, preferredTimescale: 600) generator.requestedTimeToleranceAfter = CMTime(seconds: 0.5, preferredTimescale: 600) let time = CMTime(value: timeNumber.int64Value, timescale: 1000) let gate = ThumbnailResponseGate() self.thumbnailQueue.async { do { let image = try generator.copyCGImage(at: time, actualTime: nil) let data = UIImage(cgImage: image).jpegData(compressionQuality: 0.7) gate.send(result, data: data) } catch { gate.send(result, data: nil) } } DispatchQueue.global(qos: .userInitiated).asyncAfter( deadline: .now() + 1.5 ) { gate.send(result, data: nil) } } } }