1. 新增TDashboard翻译类型与多语言文案 2. 完善macOS权限管理与Impeller渲染适配 3. 更新服务器部署配置与协议文件上传脚本 4. 修复翻译导入服务与根类型编译问题
278 lines
12 KiB
Swift
278 lines
12 KiB
Swift
/// ============================================================
|
||
/// 闲言APP — macOS 应用代理
|
||
/// 创建时间: 2026-06-02
|
||
/// 更新时间: 2026-06-26
|
||
/// 作用: 应用启动入口,注册应用级 MethodChannel(Dock 徽章/菜单栏金句/Spotlight 索引/CPU 架构检测/Impeller 开关/应用重启/权限管理)
|
||
/// 上次更新: 1. 新增权限管理 MethodChannel(checkPermission/requestPermission/openPermissionSettings),
|
||
/// 委托给 PermissionManager 处理,实现 macOS 原生权限动态申请
|
||
/// (相机/麦克风/相册/通知,替代 permission_handler_apple 无 macOS 实现的问题)。
|
||
/// 2. 同步 MainFlutterWindow.swift 的 Impeller 修复说明:
|
||
/// setImpellerEnabled 仍写入 UserDefaults,重启后由
|
||
/// MainFlutterWindow.awakeFromNib 中 FlutterDartProject.commandLineArguments 读取
|
||
/// (原 setenv 方式不生效,已废弃)。
|
||
/// 3. 保留 getImpellerRunningStatus 方法,返回 MainFlutterWindow.currentImpellerEnabled。
|
||
/// 4. 保留 restartApp 改进(日志、错误检查、延迟 800ms 终止)。
|
||
/// ============================================================
|
||
|
||
import Cocoa
|
||
import FlutterMacOS
|
||
import CoreSpotlight
|
||
|
||
@main
|
||
class AppDelegate: FlutterAppDelegate {
|
||
// ============================================================
|
||
// MARK: - 属性
|
||
// ============================================================
|
||
|
||
/// 菜单栏金句 NSStatusItem(需强引用,否则会被释放)
|
||
private var statusItem: NSStatusItem?
|
||
|
||
/// 应用级 MethodChannel(apps.xy.xianyan/macos.app)
|
||
private var methodChannel: FlutterMethodChannel?
|
||
|
||
/// 当前菜单栏金句完整内容(用于点击复制,避免从截断标题反解析)
|
||
private var currentSentence: String = ""
|
||
|
||
// ============================================================
|
||
// MARK: - 应用生命周期
|
||
// ============================================================
|
||
|
||
/// 应用启动完成(channel 已在 MainFlutterWindow.awakeFromNib 中提前注册)
|
||
override func applicationDidFinishLaunching(_ notification: Notification) {
|
||
super.applicationDidFinishLaunching(notification)
|
||
}
|
||
|
||
/// 注册应用级 MethodChannel(apps.xy.xianyan/macos.app)
|
||
///
|
||
/// 必须在 Flutter 引擎启动 Dart 代码前调用,否则 Dart 侧调用 getCpuArchitecture
|
||
/// 等方法会触发 MissingPluginException。
|
||
/// 由 MainFlutterWindow.awakeFromNib 在创建 FlutterViewController 后立即调用。
|
||
static func registerAppChannel(controller: FlutterViewController) {
|
||
let appDelegate = NSApp.delegate as? AppDelegate
|
||
|
||
let channel = FlutterMethodChannel(
|
||
name: "apps.xy.xianyan/macos.app",
|
||
binaryMessenger: controller.engine.binaryMessenger
|
||
)
|
||
|
||
channel.setMethodCallHandler { call, result in
|
||
switch call.method {
|
||
// ---------- CPU 架构检测(用于玻璃质量降级) ----------
|
||
case "getCpuArchitecture":
|
||
let arch = getCpuArchitecture()
|
||
result(arch)
|
||
|
||
// ---------- Impeller 渲染引擎开关 ----------
|
||
// 用户可在设置中手动开启/关闭 Impeller,修改后需重启应用生效
|
||
// 默认值:Apple Silicon 开启,Intel Mac 关闭(Metal 驱动有渲染资源累积 bug)
|
||
|
||
// 获取用户设置的 Impeller 开关状态(从 UserDefaults 读取,含默认值逻辑)
|
||
// 注意:此值可能已被用户修改但尚未重启生效
|
||
case "getImpellerEnabled":
|
||
let enabled = getImpellerEnabledWithDefault()
|
||
result(enabled)
|
||
|
||
// 获取当前引擎实际运行的 Impeller 状态
|
||
// 此值在应用启动时固定(MainFlutterWindow.currentImpellerEnabled),
|
||
// 不随用户设置改变,用于在 UI 中显示"当前实际渲染引擎"
|
||
case "getImpellerRunningStatus":
|
||
let running = MainFlutterWindow.currentImpellerEnabled
|
||
result(running)
|
||
|
||
case "setImpellerEnabled":
|
||
let args = call.arguments as? [String: Any]
|
||
let enabled = args?["enabled"] as? Bool ?? false
|
||
UserDefaults.standard.set(enabled, forKey: "xianyan_enable_impeller")
|
||
result(nil)
|
||
|
||
// ---------- 应用重启 ----------
|
||
// 用户修改 Impeller 开关后,可选择立即重启应用使设置生效
|
||
// 改进:添加日志、错误检查、延迟终止确保新应用完全启动
|
||
case "restartApp":
|
||
let url = URL(fileURLWithPath: Bundle.main.bundlePath)
|
||
let config = NSWorkspace.OpenConfiguration()
|
||
config.activates = true
|
||
NSLog("[restartApp] 正在启动新实例: \(url.path)")
|
||
NSWorkspace.shared.openApplication(at: url, configuration: config) { newApp, error in
|
||
if let error = error {
|
||
NSLog("[restartApp] 启动新实例失败: \(error.localizedDescription)")
|
||
return
|
||
}
|
||
NSLog("[restartApp] 新实例已启动,延迟 800ms 后终止当前实例")
|
||
// 延迟 800ms 确保新应用完全启动后再终止当前应用
|
||
// 避免新应用尚未完成初始化就被旧应用的终止流程影响
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||
NSLog("[restartApp] 终止当前实例")
|
||
NSApp.terminate(nil)
|
||
}
|
||
}
|
||
result(nil)
|
||
|
||
// ---------- Dock 徽章(NSDockTile) ----------
|
||
case "setDockBadge":
|
||
let args = call.arguments as? [String: Any]
|
||
let count = args?["count"] as? Int ?? 0
|
||
NSApp.dockTile.badgeLabel = count > 0 ? "\(count)" : nil
|
||
result(nil)
|
||
|
||
// ---------- 菜单栏金句(NSStatusItem) ----------
|
||
case "updateStatusBarSentence":
|
||
let args = call.arguments as? [String: Any]
|
||
let sentence = args?["sentence"] as? String ?? ""
|
||
appDelegate?.updateStatusItem(sentence: sentence)
|
||
result(nil)
|
||
|
||
// ---------- Spotlight 索引(CoreSpotlight) ----------
|
||
case "indexSpotlightItems":
|
||
let args = call.arguments as? [String: Any]
|
||
let items = args?["items"] as? [[String: Any]] ?? []
|
||
appDelegate?.indexSpotlightItems(items: items)
|
||
result(nil)
|
||
|
||
case "clearSpotlightIndex":
|
||
CSSearchableIndex.default().deleteAllSearchableItems { error in
|
||
if let error = error {
|
||
result(FlutterError(code: "SPOTLIGHT_ERROR",
|
||
message: error.localizedDescription,
|
||
details: nil))
|
||
} else {
|
||
result(nil)
|
||
}
|
||
}
|
||
|
||
// ---------- 权限管理(PermissionManager) ----------
|
||
// macOS 原生权限动态申请,替代 permission_handler_apple(无 macOS 实现)
|
||
// 支持的权限:camera / microphone / photos / notification
|
||
// 权限状态字符串:notDetermined / granted / permanentlyDenied / restricted
|
||
|
||
/// 查询权限状态(异步)
|
||
/// 参数: { "permission": "camera" | "microphone" | "photos" | "notification" }
|
||
/// 返回: 权限状态字符串
|
||
case "checkPermission":
|
||
let args = call.arguments as? [String: Any]
|
||
let permission = args?["permission"] as? String ?? ""
|
||
PermissionManager.checkStatus(permission) { status in
|
||
result(status)
|
||
}
|
||
|
||
/// 请求权限(触发系统 TCC 弹窗)
|
||
/// 参数: { "permission": "camera" | "microphone" | "photos" | "notification" }
|
||
/// 返回: 权限状态字符串
|
||
case "requestPermission":
|
||
let args = call.arguments as? [String: Any]
|
||
let permission = args?["permission"] as? String ?? ""
|
||
PermissionManager.requestPermission(permission) { status in
|
||
result(status)
|
||
}
|
||
|
||
/// 打开系统设置 - 隐私与安全性
|
||
/// macOS 无法直接跳转到特定权限的设置页面,统一跳转到隐私与安全性页面
|
||
case "openPermissionSettings":
|
||
let args = call.arguments as? [String: Any]
|
||
let permission = args?["permission"] as? String
|
||
PermissionManager.openSystemSettings(permission)
|
||
result(nil)
|
||
|
||
default:
|
||
result(FlutterMethodNotImplemented)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 窗口关闭后不退出应用,支持托盘常驻
|
||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||
return false
|
||
}
|
||
|
||
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||
return true
|
||
}
|
||
|
||
// ============================================================
|
||
// MARK: - 菜单栏金句(NSStatusItem)
|
||
// ============================================================
|
||
|
||
/// 创建或更新菜单栏金句,截断到 30 字符显示
|
||
func updateStatusItem(sentence: String) {
|
||
if statusItem == nil {
|
||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||
statusItem?.button?.target = self
|
||
statusItem?.button?.action = #selector(copySentence(_:))
|
||
}
|
||
currentSentence = sentence
|
||
let display = sentence.count > 30 ? String(sentence.prefix(30)) + "…" : sentence
|
||
statusItem?.button?.title = "💬 \(display)"
|
||
}
|
||
|
||
/// 点击菜单栏金句复制完整内容到剪贴板
|
||
@objc private func copySentence(_ sender: Any) {
|
||
guard !currentSentence.isEmpty else { return }
|
||
NSPasteboard.general.clearContents()
|
||
NSPasteboard.general.setString(currentSentence, forType: .string)
|
||
}
|
||
|
||
// ============================================================
|
||
// MARK: - CPU 架构检测
|
||
// ============================================================
|
||
|
||
/// 获取 CPU 架构标识,用于 Dart 端判断是否为 Intel Mac
|
||
///
|
||
/// 通过 `uname` 系统调用获取机器硬件标识:
|
||
/// - `arm64` → Apple Silicon (M1/M2/M3+)
|
||
/// - `x86_64` → Intel CPU
|
||
///
|
||
/// Dart 端通过 MethodChannel 调用此方法,在应用启动时缓存结果,
|
||
/// 用于动态调整液态玻璃渲染质量(Intel Mac 降级避免黑屏闪烁)。
|
||
static func getCpuArchitecture() -> String {
|
||
var systemInfo = utsname()
|
||
uname(&systemInfo)
|
||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||
return machineMirror.children.reduce("") { identifier, element in
|
||
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||
return identifier + String(UnicodeScalar(UInt8(value)))
|
||
}
|
||
}
|
||
|
||
/// 获取 Impeller 启用状态(含默认值逻辑)
|
||
///
|
||
/// 与 MainFlutterWindow.shouldEnableImpeller() 逻辑一致:
|
||
/// - 用户已显式设置:返回用户设置的值
|
||
/// - 未设置(首次启动):Apple Silicon 默认开启,Intel Mac 默认关闭
|
||
///
|
||
/// 此方法用于 Dart 端读取开关当前状态,确保 UI 显示与实际引擎行为一致。
|
||
static func getImpellerEnabledWithDefault() -> Bool {
|
||
let defaults = UserDefaults.standard
|
||
if defaults.object(forKey: "xianyan_enable_impeller") != nil {
|
||
return defaults.bool(forKey: "xianyan_enable_impeller")
|
||
}
|
||
// 未设置过:Apple Silicon 默认开启,Intel Mac 默认关闭
|
||
return getCpuArchitecture() != "x86_64"
|
||
}
|
||
|
||
// ============================================================
|
||
// MARK: - Spotlight 索引(CoreSpotlight)
|
||
// ============================================================
|
||
|
||
/// 将条目索引到 Spotlight,支持点击搜索结果跳转
|
||
func indexSpotlightItems(items: [[String: Any]]) {
|
||
let searchableItems = items.map { item in
|
||
let attributeSet = CSSearchableItemAttributeSet(itemContentType: "public.text")
|
||
attributeSet.title = item["title"] as? String
|
||
attributeSet.contentDescription = item["content"] as? String
|
||
attributeSet.keywords = [item["type"] as? String ?? "xianyan"]
|
||
|
||
return CSSearchableItem(
|
||
uniqueIdentifier: item["id"] as? String ?? "",
|
||
domainIdentifier: "apps.xy.xianyan.\(item["type"] as? String ?? "item")",
|
||
attributeSet: attributeSet
|
||
)
|
||
}
|
||
|
||
CSSearchableIndex.default().indexSearchableItems(searchableItems) { error in
|
||
if let error = error {
|
||
print("Spotlight indexing error: \(error)")
|
||
}
|
||
}
|
||
}
|
||
}
|