1. 新增TDashboard翻译类型与多语言文案 2. 完善macOS权限管理与Impeller渲染适配 3. 更新服务器部署配置与协议文件上传脚本 4. 修复翻译导入服务与根类型编译问题
275 lines
9.9 KiB
Swift
275 lines
9.9 KiB
Swift
/// ============================================================
|
||
/// 闲言APP — macOS 原生权限管理器
|
||
/// 创建时间: 2026-06-26
|
||
/// 更新时间: 2026-06-26
|
||
/// 作用: macOS 原生权限动态申请,替代 permission_handler_apple(无 macOS 实现)
|
||
/// 支持的权限:camera / microphone / photos / notification
|
||
/// 权限状态字符串:notDetermined / granted / permanentlyDenied / restricted
|
||
/// 上次更新: 初次创建(从 AppDelegate.swift 使用方式重建,原文件因恢复损坏)
|
||
/// ============================================================
|
||
|
||
import Cocoa
|
||
import AVFoundation
|
||
import Photos
|
||
import UserNotifications
|
||
|
||
/// macOS 原生权限管理器
|
||
///
|
||
/// 提供 TCC(透明度、许可与同意)权限的查询与申请能力。
|
||
/// 通过 AppDelegate 注册的 MethodChannel(apps.xy.xianyan/macos.app)
|
||
/// 暴露给 Dart 端使用,替代 permission_handler_apple 在 macOS 上的空实现。
|
||
///
|
||
/// 权限名称映射(与 Dart 侧 `AppPermission.macosPermissionName` 一致):
|
||
/// - `camera` → AVCaptureDevice(视频)
|
||
/// - `microphone` → AVCaptureDevice(音频)
|
||
/// - `photos` → PHPhotoLibrary(读写)
|
||
/// - `notification` → UNUserNotificationCenter
|
||
///
|
||
/// 状态字符串(与 Dart 侧 `AppPermissionStatus` 枚举一致):
|
||
/// - `notDetermined` → 用户尚未选择
|
||
/// - `granted` → 已授权
|
||
/// - `permanentlyDenied` → 已拒绝且无法再弹窗(需引导用户去系统设置)
|
||
/// - `restricted` → 受限(家长控制/MDM 管理)
|
||
class PermissionManager {
|
||
// ============================================================
|
||
// MARK: - 常量
|
||
// ============================================================
|
||
|
||
/// 支持的权限名称集合
|
||
private static let supportedPermissions: Set<String> = [
|
||
"camera", "microphone", "photos", "notification"
|
||
]
|
||
|
||
// ============================================================
|
||
// MARK: - 查询权限状态
|
||
// ============================================================
|
||
|
||
/// 查询指定权限的当前状态(异步)
|
||
///
|
||
/// - Parameters:
|
||
/// - permission: 权限名称(camera / microphone / photos / notification)
|
||
/// - completion: 完成回调,返回状态字符串(notDetermined / granted / permanentlyDenied / restricted)
|
||
static func checkStatus(_ permission: String, completion: @escaping (String) -> Void) {
|
||
guard supportedPermissions.contains(permission) else {
|
||
NSLog("[PermissionManager] checkStatus: 不支持的权限 '\(permission)',返回 granted")
|
||
completion("granted")
|
||
return
|
||
}
|
||
switch permission {
|
||
case "camera":
|
||
completion(avAuthorizationStatus(for: .video))
|
||
case "microphone":
|
||
completion(avAuthorizationStatus(for: .audio))
|
||
case "photos":
|
||
completion(photosAuthorizationStatus())
|
||
case "notification":
|
||
// 通知权限需异步查询
|
||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||
DispatchQueue.main.async {
|
||
completion(notificationAuthorizationStatus(from: settings.authorizationStatus))
|
||
}
|
||
}
|
||
default:
|
||
completion("granted")
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// MARK: - 请求权限
|
||
// ============================================================
|
||
|
||
/// 请求指定权限(触发系统 TCC 弹窗)
|
||
///
|
||
/// - Parameters:
|
||
/// - permission: 权限名称(camera / microphone / photos / notification)
|
||
/// - completion: 完成回调,返回请求后的最新状态字符串
|
||
static func requestPermission(_ permission: String, completion: @escaping (String) -> Void) {
|
||
guard supportedPermissions.contains(permission) else {
|
||
NSLog("[PermissionManager] requestPermission: 不支持的权限 '\(permission)',返回 granted")
|
||
completion("granted")
|
||
return
|
||
}
|
||
switch permission {
|
||
case "camera":
|
||
requestAVAuthorization(for: .video, completion: completion)
|
||
case "microphone":
|
||
requestAVAuthorization(for: .audio, completion: completion)
|
||
case "photos":
|
||
requestPhotosAuthorization(completion: completion)
|
||
case "notification":
|
||
requestNotificationAuthorization(completion: completion)
|
||
default:
|
||
completion("granted")
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// MARK: - 打开系统设置
|
||
// ============================================================
|
||
|
||
/// 打开系统设置 - 隐私与安全性
|
||
///
|
||
/// macOS 无法直接跳转到特定权限的设置子页面(与 iOS 不同),
|
||
/// 统一跳转到「隐私与安全性」根页面。
|
||
/// - Parameter permission: 权限名称(用于日志记录,可为 nil)
|
||
static func openSystemSettings(_ permission: String?) {
|
||
// macOS Ventura(13)+ 使用新的 URL scheme
|
||
// 隐私与安全性:x-apple.systempreferences:com.apple.preference.security?Privacy
|
||
let urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy"
|
||
if let url = URL(string: urlString) {
|
||
NSWorkspace.shared.open(url)
|
||
NSLog("[PermissionManager] openSystemSettings: 已打开隐私设置(权限: \(permission ?? "nil"))")
|
||
} else {
|
||
NSLog("[PermissionManager] openSystemSettings: URL 构建失败")
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// MARK: - AVFoundation 权限(camera / microphone)
|
||
// ============================================================
|
||
|
||
/// 将 AVAuthorizationStatus 映射为统一的权限状态字符串
|
||
private static func avAuthorizationStatus(for mediaType: AVMediaType) -> String {
|
||
switch AVCaptureDevice.authorizationStatus(for: mediaType) {
|
||
case .authorized:
|
||
return "granted"
|
||
case .notDetermined:
|
||
return "notDetermined"
|
||
case .denied:
|
||
return "permanentlyDenied"
|
||
case .restricted:
|
||
return "restricted"
|
||
@unknown default:
|
||
return "permanentlyDenied"
|
||
}
|
||
}
|
||
|
||
/// 申请 AVFoundation 权限(camera / microphone)
|
||
private static func requestAVAuthorization(
|
||
for mediaType: AVMediaType,
|
||
completion: @escaping (String) -> Void
|
||
) {
|
||
// 已授权或受限时直接返回当前状态
|
||
let currentStatus = AVCaptureDevice.authorizationStatus(for: mediaType)
|
||
if currentStatus == .authorized {
|
||
completion("granted")
|
||
return
|
||
}
|
||
if currentStatus == .restricted {
|
||
completion("restricted")
|
||
return
|
||
}
|
||
if currentStatus == .denied {
|
||
completion("permanentlyDenied")
|
||
return
|
||
}
|
||
// notDetermined:触发系统 TCC 弹窗
|
||
AVCaptureDevice.requestAccess(for: mediaType) { granted in
|
||
DispatchQueue.main.async {
|
||
completion(granted ? "granted" : "permanentlyDenied")
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// MARK: - Photos 权限
|
||
// ============================================================
|
||
|
||
/// 将 PHAuthorizationStatus 映射为统一的权限状态字符串
|
||
private static func photosAuthorizationStatus() -> String {
|
||
switch PHPhotoLibrary.authorizationStatus(for: .readWrite) {
|
||
case .authorized:
|
||
return "granted"
|
||
case .notDetermined:
|
||
return "notDetermined"
|
||
case .denied:
|
||
return "permanentlyDenied"
|
||
case .restricted:
|
||
return "restricted"
|
||
case .limited:
|
||
// limited:用户仅授权部分照片,视为已授权
|
||
return "granted"
|
||
@unknown default:
|
||
return "permanentlyDenied"
|
||
}
|
||
}
|
||
|
||
/// 申请 Photos 权限(读写)
|
||
private static func requestPhotosAuthorization(completion: @escaping (String) -> Void) {
|
||
let currentStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
||
if currentStatus == .authorized || currentStatus == .limited {
|
||
completion("granted")
|
||
return
|
||
}
|
||
if currentStatus == .restricted {
|
||
completion("restricted")
|
||
return
|
||
}
|
||
if currentStatus == .denied {
|
||
completion("permanentlyDenied")
|
||
return
|
||
}
|
||
// notDetermined:触发系统 TCC 弹窗
|
||
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
|
||
DispatchQueue.main.async {
|
||
switch status {
|
||
case .authorized, .limited:
|
||
completion("granted")
|
||
case .denied:
|
||
completion("permanentlyDenied")
|
||
case .restricted:
|
||
completion("restricted")
|
||
case .notDetermined:
|
||
completion("notDetermined")
|
||
@unknown default:
|
||
completion("permanentlyDenied")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// MARK: - 通知权限
|
||
// ============================================================
|
||
|
||
/// 将 UNAuthorizationStatus 映射为统一的权限状态字符串
|
||
private static func notificationAuthorizationStatus(from status: UNAuthorizationStatus) -> String {
|
||
switch status {
|
||
case .authorized:
|
||
return "granted"
|
||
case .notDetermined:
|
||
return "notDetermined"
|
||
case .denied:
|
||
return "permanentlyDenied"
|
||
case .provisional:
|
||
// provisional:临时授权(仅通知可降级展示),视为已授权
|
||
return "granted"
|
||
@unknown default:
|
||
return "permanentlyDenied"
|
||
}
|
||
}
|
||
|
||
/// 申请通知权限
|
||
private static func requestNotificationAuthorization(completion: @escaping (String) -> Void) {
|
||
let center = UNUserNotificationCenter.current()
|
||
center.getNotificationSettings { settings in
|
||
// 已授权或临时授权时直接返回
|
||
if settings.authorizationStatus == .authorized
|
||
|| settings.authorizationStatus == .provisional {
|
||
DispatchQueue.main.async { completion("granted") }
|
||
return
|
||
}
|
||
if settings.authorizationStatus == .denied {
|
||
DispatchQueue.main.async { completion("permanentlyDenied") }
|
||
return
|
||
}
|
||
// notDetermined:触发系统通知授权弹窗
|
||
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
|
||
DispatchQueue.main.async {
|
||
completion(granted ? "granted" : "permanentlyDenied")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|