You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

448 lines
16 KiB

import SwiftUI
import AVFoundation
import Photos
struct AppPermissionsView: View {
@EnvironmentObject private var languageManager: LanguageManager
@State private var cameraPermissionStatus: AVAuthorizationStatus = .notDetermined
@State private var photoPermissionStatus: PHAuthorizationStatus = .notDetermined
@State private var isRequestingCameraPermission = false
@State private var isRequestingPhotoPermission = false
@State private var showPermissionDeniedAlert = false
@State private var deniedPermissionType = ""
var body: some View {
ZStack {
//
LinearGradient(
gradient: Gradient(colors: [
Color(.systemBackground),
Color(.systemGray6).opacity(0.2)
]),
startPoint: .top,
endPoint: .bottom
)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 24) {
//
VStack(spacing: 16) {
ZStack {
Circle()
.fill(
LinearGradient(
gradient: Gradient(colors: [
Color.blue.opacity(0.1),
Color.blue.opacity(0.05)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 80, height: 80)
Image(systemName: "lock.shield")
.font(.system(size: 36, weight: .light))
.foregroundColor(.blue)
}
}
.padding(.top, 20)
//
VStack(alignment: .leading, spacing: 16) {
HStack {
Image(systemName: "info.circle")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.blue)
.frame(width: 32)
Text("permissions_info".localized)
.font(.system(size: 18, weight: .semibold))
Spacer()
}
Text("permissions_description".localized)
.font(.system(size: 14))
.foregroundColor(.secondary)
.lineLimit(nil)
}
.padding(20)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemBackground))
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
)
.padding(.horizontal, 20)
//
PermissionCard(
icon: "camera.fill",
iconColor: .blue,
title: "camera_permission".localized,
description: "camera_permission_description".localized,
status: cameraPermissionStatus.displayText,
statusColor: cameraPermissionStatus.statusColor,
action: {
requestCameraPermission()
},
actionTitle: cameraPermissionStatus.actionTitle,
isLoading: isRequestingCameraPermission
)
//
PermissionCard(
icon: "photo.fill",
iconColor: .green,
title: "photo_permission".localized,
description: "photo_permission_description".localized,
status: photoPermissionStatus.displayText,
statusColor: photoPermissionStatus.statusColor,
action: {
requestPhotoPermission()
},
actionTitle: photoPermissionStatus.actionTitle,
isLoading: isRequestingPhotoPermission
)
Spacer(minLength: 30)
}
}
}
.navigationTitle("app_permissions".localized)
.navigationBarTitleDisplayMode(.inline)
.onAppear {
checkPermissions()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
//
checkPermissions()
}
.alert("permission_denied_title".localized, isPresented: $showPermissionDeniedAlert) {
Button("cancel".localized, role: .cancel) { }
Button("open_settings".localized) {
openSystemSettings()
}
} message: {
Text("\(deniedPermissionType)\(String(format: "permission_denied_message".localized, deniedPermissionType))")
}
}
// MARK: -
private func checkPermissions() {
cameraPermissionStatus = AVCaptureDevice.authorizationStatus(for: .video)
photoPermissionStatus = PHPhotoLibrary.authorizationStatus()
}
// MARK: -
private func requestCameraPermission() {
logInfo("🔐 请求相机权限", className: "AppPermissionsView")
//
isRequestingCameraPermission = true
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
//
self.isRequestingCameraPermission = false
if granted {
logInfo("✅ 相机权限请求成功", className: "AppPermissionsView")
self.cameraPermissionStatus = .authorized
//
let successFeedback = UINotificationFeedbackGenerator()
successFeedback.notificationOccurred(.success)
} else {
logWarning("❌ 相机权限请求被拒绝", className: "AppPermissionsView")
self.cameraPermissionStatus = .denied
//
self.deniedPermissionType = "camera".localized
self.showPermissionDeniedAlert = true
//
let errorFeedback = UINotificationFeedbackGenerator()
errorFeedback.notificationOccurred(.error)
}
}
}
}
// MARK: -
private func requestPhotoPermission() {
logInfo("🔐 请求相册权限", className: "AppPermissionsView")
//
isRequestingPhotoPermission = true
PHPhotoLibrary.requestAuthorization { status in
DispatchQueue.main.async {
//
self.isRequestingPhotoPermission = false
logInfo("📸 相册权限状态更新: \(status.rawValue)", className: "AppPermissionsView")
self.photoPermissionStatus = status
//
switch status {
case .authorized, .limited:
//
let successFeedback = UINotificationFeedbackGenerator()
successFeedback.notificationOccurred(.success)
case .denied, .restricted:
//
self.deniedPermissionType = "photo".localized
self.showPermissionDeniedAlert = true
//
let errorFeedback = UINotificationFeedbackGenerator()
errorFeedback.notificationOccurred(.error)
case .notDetermined:
//
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
@unknown default:
break
}
}
}
}
// MARK: -
private func openSystemSettings() {
logInfo("⚙️ 打开系统设置", className: "AppPermissionsView")
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsUrl) { success in
if success {
logInfo("✅ 成功打开系统设置", className: "AppPermissionsView")
} else {
logWarning("⚠️ 打开系统设置失败", className: "AppPermissionsView")
}
}
} else {
logError("❌ 无法创建系统设置URL", className: "AppPermissionsView")
}
}
}
// MARK: -
struct PermissionCard: View {
let icon: String
let iconColor: Color
let title: String
let description: String
let status: String
let statusColor: Color
let action: () -> Void
let actionTitle: String
let isLoading: Bool
@State private var isButtonPressed = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Image(systemName: icon)
.font(.system(size: 20, weight: .medium))
.foregroundColor(iconColor)
.frame(width: 32)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: 18, weight: .semibold))
Text(description)
.font(.system(size: 14))
.foregroundColor(.secondary)
}
Spacer()
}
HStack {
Text(status)
.font(.system(size: 14, weight: .medium))
.foregroundColor(statusColor)
Spacer()
Button(action: {
//
withAnimation(.easeInOut(duration: 0.1)) {
isButtonPressed = true
}
//
action()
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation(.easeInOut(duration: 0.1)) {
isButtonPressed = false
}
}
}) {
HStack(spacing: 6) {
if isLoading {
ProgressView()
.scaleEffect(0.8)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else if actionTitle == "request_permission".localized {
Image(systemName: "hand.raised.fill")
.font(.system(size: 12, weight: .medium))
} else if actionTitle == "open_settings".localized {
Image(systemName: "gear")
.font(.system(size: 12, weight: .medium))
} else if actionTitle == "permission_granted".localized {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 12, weight: .medium))
} else {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 12, weight: .medium))
}
Text(isLoading ? "requesting_permission".localized : actionTitle)
.font(.system(size: 14, weight: .medium))
}
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(statusColor)
.scaleEffect(isButtonPressed ? 0.95 : 1.0)
)
}
.disabled(actionTitle == "permission_granted".localized || isLoading)
.opacity((actionTitle == "permission_granted".localized || isLoading) ? 0.6 : 1.0)
}
}
.padding(20)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemBackground))
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
)
.padding(.horizontal, 20)
}
}
// MARK: -
extension AVAuthorizationStatus {
var displayText: String {
switch self {
case .notDetermined:
return "not_determined".localized
case .restricted:
return "restricted".localized
case .denied:
return "denied".localized
case .authorized:
return "authorized".localized
@unknown default:
return "unknown".localized
}
}
var statusColor: Color {
switch self {
case .notDetermined:
return .orange
case .restricted, .denied:
return .red
case .authorized:
return .green
@unknown default:
return .gray
}
}
var actionTitle: String {
switch self {
case .notDetermined:
return "request_permission".localized
case .restricted, .denied:
return "open_settings".localized
case .authorized:
return "permission_granted".localized
@unknown default:
return "unknown".localized
}
}
var canRequestPermission: Bool {
switch self {
case .notDetermined:
return true
case .restricted, .denied, .authorized:
return false
@unknown default:
return false
}
}
}
extension PHAuthorizationStatus {
var displayText: String {
switch self {
case .notDetermined:
return "not_determined".localized
case .restricted:
return "restricted".localized
case .denied:
return "denied".localized
case .authorized:
return "authorized".localized
case .limited:
return "limited".localized
@unknown default:
return "unknown".localized
}
}
var statusColor: Color {
switch self {
case .notDetermined:
return .orange
case .restricted, .denied:
return .red
case .authorized, .limited:
return .green
@unknown default:
return .gray
}
}
var actionTitle: String {
switch self {
case .notDetermined:
return "request_permission".localized
case .restricted, .denied:
return "open_settings".localized
case .authorized, .limited:
return "permission_granted".localized
@unknown default:
return "unknown".localized
}
}
var canRequestPermission: Bool {
switch self {
case .notDetermined:
return true
case .restricted, .denied, .authorized, .limited:
return false
@unknown default:
return false
}
}
}
#Preview {
NavigationView {
AppPermissionsView()
.environmentObject(LanguageManager.shared)
}
}