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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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)
}
}