vOOice/VoiceInk/Views/PermissionsView.swift
2025-02-22 11:52:41 +05:45

295 lines
12 KiB
Swift

import SwiftUI
import AVFoundation
import Cocoa
import KeyboardShortcuts
class PermissionManager: ObservableObject {
@Published var audioPermissionStatus = AVCaptureDevice.authorizationStatus(for: .audio)
@Published var isAccessibilityEnabled = false
@Published var isScreenRecordingEnabled = false
@Published var isKeyboardShortcutSet = false
init() {
// Start observing system events that might indicate permission changes
setupNotificationObservers()
// Initial permission checks
checkAllPermissions()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func setupNotificationObservers() {
// Only observe when app becomes active, as this is a likely time for permissions to have changed
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidBecomeActive),
name: NSApplication.didBecomeActiveNotification,
object: nil
)
}
@objc private func applicationDidBecomeActive() {
checkAllPermissions()
}
func checkAllPermissions() {
checkAccessibilityPermissions()
checkScreenRecordingPermission()
checkAudioPermissionStatus()
checkKeyboardShortcut()
}
func checkAccessibilityPermissions() {
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: false]
let accessibilityEnabled = AXIsProcessTrustedWithOptions(options)
DispatchQueue.main.async {
self.isAccessibilityEnabled = accessibilityEnabled
}
}
func checkScreenRecordingPermission() {
DispatchQueue.main.async {
self.isScreenRecordingEnabled = CGPreflightScreenCaptureAccess()
}
}
func requestScreenRecordingPermission() {
CGRequestScreenCaptureAccess()
}
func checkAudioPermissionStatus() {
DispatchQueue.main.async {
self.audioPermissionStatus = AVCaptureDevice.authorizationStatus(for: .audio)
}
}
func requestAudioPermission() {
AVCaptureDevice.requestAccess(for: .audio) { granted in
DispatchQueue.main.async {
self.audioPermissionStatus = granted ? .authorized : .denied
}
}
}
func checkKeyboardShortcut() {
DispatchQueue.main.async {
self.isKeyboardShortcutSet = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) != nil
}
}
}
struct PermissionCard: View {
let icon: String
let title: String
let description: String
let isGranted: Bool
let buttonTitle: String
let buttonAction: () -> Void
let checkPermission: () -> Void
@State private var isRefreshing = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 16) {
// Icon with background
ZStack {
Circle()
.fill(isGranted ? Color.green.opacity(0.15) : Color.orange.opacity(0.15))
.frame(width: 44, height: 44)
Image(systemName: isGranted ? "\(icon).fill" : icon)
.font(.system(size: 20, weight: .semibold))
.foregroundColor(isGranted ? .green : .orange)
.symbolRenderingMode(.hierarchical)
}
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
Text(description)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
// Status indicator with refresh
HStack(spacing: 12) {
Button(action: {
withAnimation(.easeInOut(duration: 0.5)) {
isRefreshing = true
}
checkPermission()
// Reset the animation after a delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isRefreshing = false
}
}) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.secondary)
.rotationEffect(.degrees(isRefreshing ? 360 : 0))
}
.buttonStyle(.plain)
.contentShape(Rectangle())
if isGranted {
Image(systemName: "checkmark.seal.fill")
.font(.system(size: 20))
.foregroundColor(.green)
.symbolRenderingMode(.hierarchical)
} else {
Image(systemName: "xmark.seal.fill")
.font(.system(size: 20))
.foregroundColor(.orange)
.symbolRenderingMode(.hierarchical)
}
}
}
if !isGranted {
Button(action: buttonAction) {
HStack {
Text(buttonTitle)
Spacer()
Image(systemName: "arrow.right")
}
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(
LinearGradient(
colors: [Color.accentColor, Color.accentColor.opacity(0.8)],
startPoint: .leading,
endPoint: .trailing
)
)
.cornerRadius(10)
}
.buttonStyle(.plain)
}
}
.padding()
.background(Color(.windowBackgroundColor).opacity(0.9))
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.05), radius: 5, y: 2)
}
}
struct PermissionsView: View {
@StateObject private var permissionManager = PermissionManager()
var body: some View {
ScrollView {
VStack(spacing: 32) {
// Header
VStack(spacing: 24) {
Image(systemName: "shield.lefthalf.filled")
.font(.system(size: 40))
.foregroundStyle(.blue)
.padding(20)
.background(Circle()
.fill(Color(.windowBackgroundColor).opacity(0.9))
.shadow(color: .black.opacity(0.1), radius: 10, y: 5))
VStack(spacing: 8) {
Text("App Permissions")
.font(.system(size: 28, weight: .bold))
Text("VoiceInk requires the following permissions to function properly")
.font(.system(size: 15))
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 40)
.frame(maxWidth: .infinity)
// Permission Cards
VStack(spacing: 16) {
// Keyboard Shortcut Permission
PermissionCard(
icon: "keyboard",
title: "Keyboard Shortcut",
description: "Set up a keyboard shortcut to use VoiceInk anywhere",
isGranted: permissionManager.isKeyboardShortcutSet,
buttonTitle: "Configure Shortcut",
buttonAction: {
NotificationCenter.default.post(
name: .navigateToDestination,
object: nil,
userInfo: ["destination": "Settings"]
)
},
checkPermission: { permissionManager.checkKeyboardShortcut() }
)
// Audio Permission
PermissionCard(
icon: "mic",
title: "Microphone Access",
description: "Allow VoiceInk to record your voice for transcription",
isGranted: permissionManager.audioPermissionStatus == .authorized,
buttonTitle: permissionManager.audioPermissionStatus == .notDetermined ? "Request Permission" : "Open System Settings",
buttonAction: {
if permissionManager.audioPermissionStatus == .notDetermined {
permissionManager.requestAudioPermission()
} else {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone") {
NSWorkspace.shared.open(url)
}
}
},
checkPermission: { permissionManager.checkAudioPermissionStatus() }
)
// Accessibility Permission
PermissionCard(
icon: "hand.raised",
title: "Accessibility Access",
description: "Allow VoiceInk to paste transcribed text directly at your cursor position",
isGranted: permissionManager.isAccessibilityEnabled,
buttonTitle: "Open System Settings",
buttonAction: {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
NSWorkspace.shared.open(url)
}
},
checkPermission: { permissionManager.checkAccessibilityPermissions() }
)
// Screen Recording Permission
PermissionCard(
icon: "rectangle.on.rectangle",
title: "Screen Recording Access",
description: "Allow VoiceInk to understand context from your screen for transcript Enhancement",
isGranted: permissionManager.isScreenRecordingEnabled,
buttonTitle: "Request Permission",
buttonAction: {
permissionManager.requestScreenRecordingPermission()
// After requesting, open system preferences as fallback
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") {
NSWorkspace.shared.open(url)
}
},
checkPermission: { permissionManager.checkScreenRecordingPermission() }
)
}
}
.padding(24)
}
.background(Color(NSColor.controlBackgroundColor))
.onAppear {
permissionManager.checkAllPermissions()
}
}
}
#Preview {
PermissionsView()
}