feat(macos): move location access to permissions tab
This commit is contained in:
@@ -2,15 +2,12 @@ import AppKit
|
|||||||
import ClawdbotDiscovery
|
import ClawdbotDiscovery
|
||||||
import ClawdbotIPC
|
import ClawdbotIPC
|
||||||
import ClawdbotKit
|
import ClawdbotKit
|
||||||
import CoreLocation
|
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct GeneralSettings: View {
|
struct GeneralSettings: View {
|
||||||
@Bindable var state: AppState
|
@Bindable var state: AppState
|
||||||
@AppStorage(cameraEnabledKey) private var cameraEnabled: Bool = false
|
@AppStorage(cameraEnabledKey) private var cameraEnabled: Bool = false
|
||||||
@AppStorage(locationModeKey) private var locationModeRaw: String = ClawdbotLocationMode.off.rawValue
|
|
||||||
@AppStorage(locationPreciseKey) private var locationPreciseEnabled: Bool = true
|
|
||||||
private let healthStore = HealthStore.shared
|
private let healthStore = HealthStore.shared
|
||||||
private let gatewayManager = GatewayProcessManager.shared
|
private let gatewayManager = GatewayProcessManager.shared
|
||||||
@State private var gatewayDiscovery = GatewayDiscoveryModel(
|
@State private var gatewayDiscovery = GatewayDiscoveryModel(
|
||||||
@@ -20,7 +17,6 @@ struct GeneralSettings: View {
|
|||||||
@State private var showRemoteAdvanced = false
|
@State private var showRemoteAdvanced = false
|
||||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||||
private var isNixMode: Bool { ProcessInfo.processInfo.isNixMode }
|
private var isNixMode: Bool { ProcessInfo.processInfo.isNixMode }
|
||||||
@State private var lastLocationModeRaw: String = ClawdbotLocationMode.off.rawValue
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
@@ -60,27 +56,6 @@ struct GeneralSettings: View {
|
|||||||
subtitle: "Allow the agent to capture a photo or short video via the built-in camera.",
|
subtitle: "Allow the agent to capture a photo or short video via the built-in camera.",
|
||||||
binding: self.$cameraEnabled)
|
binding: self.$cameraEnabled)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
Text("Location Access")
|
|
||||||
.font(.body)
|
|
||||||
|
|
||||||
Picker("", selection: self.$locationModeRaw) {
|
|
||||||
Text("Off").tag(ClawdbotLocationMode.off.rawValue)
|
|
||||||
Text("While Using").tag(ClawdbotLocationMode.whileUsing.rawValue)
|
|
||||||
Text("Always").tag(ClawdbotLocationMode.always.rawValue)
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
.pickerStyle(.menu)
|
|
||||||
|
|
||||||
Toggle("Precise Location", isOn: self.$locationPreciseEnabled)
|
|
||||||
.disabled(self.locationMode == .off)
|
|
||||||
|
|
||||||
Text("Always may require System Settings to approve background location.")
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundStyle(.tertiary)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsToggleRow(
|
SettingsToggleRow(
|
||||||
title: "Enable Peekaboo Bridge",
|
title: "Enable Peekaboo Bridge",
|
||||||
subtitle: "Allow signed tools (e.g. `peekaboo`) to drive UI automation via PeekabooBridge.",
|
subtitle: "Allow signed tools (e.g. `peekaboo`) to drive UI automation via PeekabooBridge.",
|
||||||
@@ -106,27 +81,12 @@ struct GeneralSettings: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
guard !self.isPreview else { return }
|
guard !self.isPreview else { return }
|
||||||
self.refreshGatewayStatus()
|
self.refreshGatewayStatus()
|
||||||
self.lastLocationModeRaw = self.locationModeRaw
|
|
||||||
}
|
}
|
||||||
.onChange(of: self.state.canvasEnabled) { _, enabled in
|
.onChange(of: self.state.canvasEnabled) { _, enabled in
|
||||||
if !enabled {
|
if !enabled {
|
||||||
CanvasManager.shared.hideAll()
|
CanvasManager.shared.hideAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: self.locationModeRaw) { _, newValue in
|
|
||||||
let previous = self.lastLocationModeRaw
|
|
||||||
self.lastLocationModeRaw = newValue
|
|
||||||
guard let mode = ClawdbotLocationMode(rawValue: newValue) else { return }
|
|
||||||
Task {
|
|
||||||
let granted = await self.requestLocationAuthorization(mode: mode)
|
|
||||||
if !granted {
|
|
||||||
await MainActor.run {
|
|
||||||
self.locationModeRaw = previous
|
|
||||||
self.lastLocationModeRaw = previous
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var activeBinding: Binding<Bool> {
|
private var activeBinding: Binding<Bool> {
|
||||||
@@ -135,26 +95,6 @@ struct GeneralSettings: View {
|
|||||||
set: { self.state.isPaused = !$0 })
|
set: { self.state.isPaused = !$0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
private var locationMode: ClawdbotLocationMode {
|
|
||||||
ClawdbotLocationMode(rawValue: self.locationModeRaw) ?? .off
|
|
||||||
}
|
|
||||||
|
|
||||||
private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool {
|
|
||||||
guard mode != .off else { return true }
|
|
||||||
guard CLLocationManager.locationServicesEnabled() else {
|
|
||||||
await MainActor.run { LocationPermissionHelper.openSettings() }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = CLLocationManager().authorizationStatus
|
|
||||||
let requireAlways = mode == .always
|
|
||||||
if PermissionManager.isLocationAuthorized(status: status, requireAlways: requireAlways) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
let updated = await LocationPermissionRequester.shared.request(always: requireAlways)
|
|
||||||
return PermissionManager.isLocationAuthorized(status: updated, requireAlways: requireAlways)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var connectionSection: some View {
|
private var connectionSection: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text("Clawdbot runs")
|
Text("Clawdbot runs")
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import ClawdbotIPC
|
import ClawdbotIPC
|
||||||
|
import ClawdbotKit
|
||||||
|
import CoreLocation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PermissionsSettings: View {
|
struct PermissionsSettings: View {
|
||||||
@@ -17,6 +19,8 @@ struct PermissionsSettings: View {
|
|||||||
.padding(.horizontal, 2)
|
.padding(.horizontal, 2)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
|
|
||||||
|
LocationAccessSettings()
|
||||||
|
|
||||||
Button("Restart onboarding") { self.showOnboarding() }
|
Button("Restart onboarding") { self.showOnboarding() }
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -26,6 +30,72 @@ struct PermissionsSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct LocationAccessSettings: View {
|
||||||
|
@AppStorage(locationModeKey) private var locationModeRaw: String = ClawdbotLocationMode.off.rawValue
|
||||||
|
@AppStorage(locationPreciseKey) private var locationPreciseEnabled: Bool = true
|
||||||
|
@State private var lastLocationModeRaw: String = ClawdbotLocationMode.off.rawValue
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("Location Access")
|
||||||
|
.font(.body)
|
||||||
|
|
||||||
|
Picker("", selection: self.$locationModeRaw) {
|
||||||
|
Text("Off").tag(ClawdbotLocationMode.off.rawValue)
|
||||||
|
Text("While Using").tag(ClawdbotLocationMode.whileUsing.rawValue)
|
||||||
|
Text("Always").tag(ClawdbotLocationMode.always.rawValue)
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
|
||||||
|
Toggle("Precise Location", isOn: self.$locationPreciseEnabled)
|
||||||
|
.disabled(self.locationMode == .off)
|
||||||
|
|
||||||
|
Text("Always may require System Settings to approve background location.")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
self.lastLocationModeRaw = self.locationModeRaw
|
||||||
|
}
|
||||||
|
.onChange(of: self.locationModeRaw) { _, newValue in
|
||||||
|
let previous = self.lastLocationModeRaw
|
||||||
|
self.lastLocationModeRaw = newValue
|
||||||
|
guard let mode = ClawdbotLocationMode(rawValue: newValue) else { return }
|
||||||
|
Task {
|
||||||
|
let granted = await self.requestLocationAuthorization(mode: mode)
|
||||||
|
if !granted {
|
||||||
|
await MainActor.run {
|
||||||
|
self.locationModeRaw = previous
|
||||||
|
self.lastLocationModeRaw = previous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var locationMode: ClawdbotLocationMode {
|
||||||
|
ClawdbotLocationMode(rawValue: self.locationModeRaw) ?? .off
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool {
|
||||||
|
guard mode != .off else { return true }
|
||||||
|
guard CLLocationManager.locationServicesEnabled() else {
|
||||||
|
await MainActor.run { LocationPermissionHelper.openSettings() }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = CLLocationManager().authorizationStatus
|
||||||
|
let requireAlways = mode == .always
|
||||||
|
if PermissionManager.isLocationAuthorized(status: status, requireAlways: requireAlways) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
let updated = await LocationPermissionRequester.shared.request(always: requireAlways)
|
||||||
|
return PermissionManager.isLocationAuthorized(status: updated, requireAlways: requireAlways)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct PermissionStatusList: View {
|
struct PermissionStatusList: View {
|
||||||
let status: [Capability: Bool]
|
let status: [Capability: Bool]
|
||||||
let refresh: () async -> Void
|
let refresh: () async -> Void
|
||||||
|
|||||||
Reference in New Issue
Block a user