Files
ReelShort/ios/Runner/AppDelegate.swift
T
Vicky a3f3481f4f feat: phase1 UI prep — 设备ID系统传递 + 国家本地化展示 + 继续观看直进播放页
设备标识: Android SSAID/ iOS IDFV 原生通道 → Dart DeviceIdProvider
国家解析: 注册/登录从后端 IP → 国家/语言/时区/币种 自动落库
账号资料: 全部本地化展示 (account_display.dart)
我的页: 继续观看卡片点击直接进播放页
2026-10-27 15:45:20 +08:00

189 lines
6.4 KiB
Swift

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 wirelessDataPrompt = WirelessDataPrompt()
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)
}
override func applicationDidBecomeActive(_ application: UIApplication) {
super.applicationDidBecomeActive(application)
requestWirelessDataPrompt()
}
func requestWirelessDataPrompt() {
wirelessDataPrompt.requestSystemPrompt()
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
registerWirelessDataChannel(engineBridge)
registerDeviceIdChannel(engineBridge)
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)
}
}
}
/// 设备标识:IDFV 免权限,卸载本 App 不变,避免客户端自生成的 id 每次重装都变。
/// 同通道顺带提供设备区域(ISO-2),注册时作为服务端 IP 解析失败后的兜底。
private func registerDeviceIdChannel(_ engineBridge: FlutterImplicitEngineBridge) {
let channel = FlutterMethodChannel(
name: "com.stormg.lumadrama/device",
binaryMessenger: engineBridge.applicationRegistrar.messenger()
)
channel.setMethodCallHandler { call, result in
switch call.method {
case "idfv":
result(UIDevice.current.identifierForVendor?.uuidString)
case "region":
if #available(iOS 16.0, *) {
result(Locale.current.region?.identifier)
} else {
result(Locale.current.regionCode)
}
default:
result(FlutterMethodNotImplemented)
}
}
}
private func registerWirelessDataChannel(_ engineBridge: FlutterImplicitEngineBridge) {
let channel = FlutterMethodChannel(
name: WirelessDataPrompt.channelName,
binaryMessenger: engineBridge.applicationRegistrar.messenger()
)
channel.setMethodCallHandler { [weak self] call, result in
guard call.method == WirelessDataPrompt.awaitDecisionMethod else {
result(FlutterMethodNotImplemented)
return
}
guard let self else {
result(nil)
return
}
self.wirelessDataPrompt.awaitDecision(result)
}
}
}