feat(macos): add Camera permission to onboarding flow

- Add 'camera' case to Capability enum
- Add UI strings (title, subtitle, icon) in PermissionsSettings
- Add ensureCamera() and camera status check in PermissionManager
- Add CameraPermissionHelper for opening System Settings

🦞 Clawd's first code contribution!
This commit is contained in:
Peter Steinberger
2026-01-02 15:27:33 +00:00
parent 9b3aef3567
commit 8de40e0c10
3 changed files with 42 additions and 0 deletions

View File

@@ -31,6 +31,8 @@ enum PermissionManager {
await self.ensureMicrophone(interactive: interactive) await self.ensureMicrophone(interactive: interactive)
case .speechRecognition: case .speechRecognition:
await self.ensureSpeechRecognition(interactive: interactive) await self.ensureSpeechRecognition(interactive: interactive)
case .camera:
await self.ensureCamera(interactive: interactive)
} }
} }
@@ -114,6 +116,24 @@ enum PermissionManager {
return SFSpeechRecognizer.authorizationStatus() == .authorized return SFSpeechRecognizer.authorizationStatus() == .authorized
} }
private static func ensureCamera(interactive: Bool) async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
return true
case .notDetermined:
guard interactive else { return false }
return await AVCaptureDevice.requestAccess(for: .video)
case .denied, .restricted:
if interactive {
CameraPermissionHelper.openSettings()
}
return false
@unknown default:
return false
}
}
static func voiceWakePermissionsGranted() -> Bool { static func voiceWakePermissionsGranted() -> Bool {
let mic = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized let mic = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
let speech = SFSpeechRecognizer.authorizationStatus() == .authorized let speech = SFSpeechRecognizer.authorizationStatus() == .authorized
@@ -153,6 +173,9 @@ enum PermissionManager {
case .speechRecognition: case .speechRecognition:
results[cap] = SFSpeechRecognizer.authorizationStatus() == .authorized results[cap] = SFSpeechRecognizer.authorizationStatus() == .authorized
case .camera:
results[cap] = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
} }
} }
return results return results
@@ -189,6 +212,21 @@ enum MicrophonePermissionHelper {
} }
} }
enum CameraPermissionHelper {
static func openSettings() {
let candidates = [
"x-apple.systempreferences:com.apple.preference.security?Privacy_Camera",
"x-apple.systempreferences:com.apple.preference.security",
]
for candidate in candidates {
if let url = URL(string: candidate), NSWorkspace.shared.open(url) {
return
}
}
}
}
enum AppleScriptPermission { enum AppleScriptPermission {
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "AppleScriptPermission") private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "AppleScriptPermission")

View File

@@ -120,6 +120,7 @@ struct PermissionRow: View {
case .screenRecording: "Screen Recording" case .screenRecording: "Screen Recording"
case .microphone: "Microphone" case .microphone: "Microphone"
case .speechRecognition: "Speech Recognition" case .speechRecognition: "Speech Recognition"
case .camera: "Camera"
} }
} }
@@ -132,6 +133,7 @@ struct PermissionRow: View {
case .screenRecording: "Capture the screen for context or screenshots" case .screenRecording: "Capture the screen for context or screenshots"
case .microphone: "Allow Voice Wake and audio capture" case .microphone: "Allow Voice Wake and audio capture"
case .speechRecognition: "Transcribe Voice Wake trigger phrases on-device" case .speechRecognition: "Transcribe Voice Wake trigger phrases on-device"
case .camera: "Capture photos and video from the camera"
} }
} }
@@ -143,6 +145,7 @@ struct PermissionRow: View {
case .screenRecording: "display" case .screenRecording: "display"
case .microphone: "mic" case .microphone: "mic"
case .speechRecognition: "waveform" case .speechRecognition: "waveform"
case .camera: "camera"
} }
} }
} }

View File

@@ -11,6 +11,7 @@ public enum Capability: String, Codable, CaseIterable, Sendable {
case screenRecording case screenRecording
case microphone case microphone
case speechRecognition case speechRecognition
case camera
} }
public enum CameraFacing: String, Codable, Sendable { public enum CameraFacing: String, Codable, Sendable {