macOS: add settings previews
This commit is contained in:
@@ -5,99 +5,116 @@ import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class AppState: ObservableObject {
|
||||
private let isPreview: Bool
|
||||
|
||||
private func ifNotPreview(_ action: () -> Void) {
|
||||
guard !self.isPreview else { return }
|
||||
action()
|
||||
}
|
||||
|
||||
enum ConnectionMode: String {
|
||||
case local
|
||||
case remote
|
||||
}
|
||||
|
||||
@Published var isPaused: Bool {
|
||||
didSet { UserDefaults.standard.set(self.isPaused, forKey: pauseDefaultsKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.isPaused, forKey: pauseDefaultsKey) } }
|
||||
}
|
||||
|
||||
@Published var launchAtLogin: Bool {
|
||||
didSet { Task { AppStateStore.updateLaunchAtLogin(enabled: self.launchAtLogin) } }
|
||||
didSet { self.ifNotPreview { Task { AppStateStore.updateLaunchAtLogin(enabled: self.launchAtLogin) } } }
|
||||
}
|
||||
|
||||
@Published var onboardingSeen: Bool {
|
||||
didSet { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") } }
|
||||
}
|
||||
|
||||
@Published var debugPaneEnabled: Bool {
|
||||
didSet { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") } }
|
||||
}
|
||||
|
||||
@Published var swabbleEnabled: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(self.swabbleEnabled, forKey: swabbleEnabledKey)
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.swabbleEnabled, forKey: swabbleEnabledKey)
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var swabbleTriggerWords: [String] {
|
||||
didSet {
|
||||
// Preserve the raw editing state; sanitization happens when we actually use the triggers.
|
||||
UserDefaults.standard.set(self.swabbleTriggerWords, forKey: swabbleTriggersKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.swabbleTriggerWords, forKey: swabbleTriggersKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var voiceWakeTriggerChime: VoiceWakeChime {
|
||||
didSet { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) }
|
||||
didSet { self.ifNotPreview { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) } }
|
||||
}
|
||||
|
||||
@Published var voiceWakeSendChime: VoiceWakeChime {
|
||||
didSet { self.storeChime(self.voiceWakeSendChime, key: voiceWakeSendChimeKey) }
|
||||
didSet { self.ifNotPreview { self.storeChime(self.voiceWakeSendChime, key: voiceWakeSendChimeKey) } }
|
||||
}
|
||||
|
||||
@Published var iconAnimationsEnabled: Bool {
|
||||
didSet { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) } }
|
||||
}
|
||||
|
||||
@Published var showDockIcon: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey)
|
||||
AppActivationPolicy.apply(showDockIcon: self.showDockIcon)
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey)
|
||||
AppActivationPolicy.apply(showDockIcon: self.showDockIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var voiceWakeMicID: String {
|
||||
didSet {
|
||||
UserDefaults.standard.set(self.voiceWakeMicID, forKey: voiceWakeMicKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.voiceWakeMicID, forKey: voiceWakeMicKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var voiceWakeLocaleID: String {
|
||||
didSet {
|
||||
UserDefaults.standard.set(self.voiceWakeLocaleID, forKey: voiceWakeLocaleKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.voiceWakeLocaleID, forKey: voiceWakeLocaleKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var voiceWakeAdditionalLocaleIDs: [String] {
|
||||
didSet { UserDefaults.standard.set(self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) } }
|
||||
}
|
||||
|
||||
@Published var voiceWakeForwardEnabled: Bool {
|
||||
didSet { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) } }
|
||||
}
|
||||
|
||||
@Published var voiceWakeForwardCommand: String {
|
||||
didSet { UserDefaults.standard.set(self.voiceWakeForwardCommand, forKey: voiceWakeForwardCommandKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardCommand, forKey: voiceWakeForwardCommandKey) } }
|
||||
}
|
||||
|
||||
@Published var voicePushToTalkEnabled: Bool {
|
||||
didSet { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) } }
|
||||
}
|
||||
|
||||
@Published var iconOverride: IconOverrideSelection {
|
||||
didSet { UserDefaults.standard.set(self.iconOverride.rawValue, forKey: iconOverrideKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.iconOverride.rawValue, forKey: iconOverrideKey) } }
|
||||
}
|
||||
|
||||
@Published var isWorking: Bool = false
|
||||
@@ -106,38 +123,41 @@ final class AppState: ObservableObject {
|
||||
@Published var sendCelebrationTick: Int = 0
|
||||
@Published var heartbeatsEnabled: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(self.heartbeatsEnabled, forKey: heartbeatsEnabledKey)
|
||||
Task { _ = await AgentRPC.shared.setHeartbeatsEnabled(self.heartbeatsEnabled) }
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.heartbeatsEnabled, forKey: heartbeatsEnabledKey)
|
||||
Task { _ = await AgentRPC.shared.setHeartbeatsEnabled(self.heartbeatsEnabled) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var connectionMode: ConnectionMode {
|
||||
didSet { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) } }
|
||||
}
|
||||
|
||||
@Published var webChatEnabled: Bool {
|
||||
didSet { UserDefaults.standard.set(self.webChatEnabled, forKey: webChatEnabledKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatEnabled, forKey: webChatEnabledKey) } }
|
||||
}
|
||||
|
||||
@Published var webChatPort: Int {
|
||||
didSet { UserDefaults.standard.set(self.webChatPort, forKey: webChatPortKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatPort, forKey: webChatPortKey) } }
|
||||
}
|
||||
|
||||
@Published var remoteTarget: String {
|
||||
didSet { UserDefaults.standard.set(self.remoteTarget, forKey: remoteTargetKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteTarget, forKey: remoteTargetKey) } }
|
||||
}
|
||||
|
||||
@Published var remoteIdentity: String {
|
||||
didSet { UserDefaults.standard.set(self.remoteIdentity, forKey: remoteIdentityKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteIdentity, forKey: remoteIdentityKey) } }
|
||||
}
|
||||
|
||||
@Published var remoteProjectRoot: String {
|
||||
didSet { UserDefaults.standard.set(self.remoteProjectRoot, forKey: remoteProjectRootKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteProjectRoot, forKey: remoteProjectRootKey) } }
|
||||
}
|
||||
|
||||
private var earBoostTask: Task<Void, Never>?
|
||||
|
||||
init() {
|
||||
init(preview: Bool = false) {
|
||||
self.isPreview = preview
|
||||
self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
|
||||
self.launchAtLogin = false
|
||||
self.onboardingSeen = UserDefaults.standard.bool(forKey: "clawdis.onboardingSeen")
|
||||
@@ -199,16 +219,20 @@ final class AppState: ObservableObject {
|
||||
let storedPort = UserDefaults.standard.integer(forKey: webChatPortKey)
|
||||
self.webChatPort = storedPort > 0 ? storedPort : 18788
|
||||
|
||||
Task.detached(priority: .utility) { [weak self] in
|
||||
let current = await LaunchAgentManager.status()
|
||||
await MainActor.run { [weak self] in self?.launchAtLogin = current }
|
||||
if !self.isPreview {
|
||||
Task.detached(priority: .utility) { [weak self] in
|
||||
let current = await LaunchAgentManager.status()
|
||||
await MainActor.run { [weak self] in self?.launchAtLogin = current }
|
||||
}
|
||||
}
|
||||
|
||||
if self.swabbleEnabled, !PermissionManager.voiceWakePermissionsGranted() {
|
||||
self.swabbleEnabled = false
|
||||
}
|
||||
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
if !self.isPreview {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
}
|
||||
}
|
||||
|
||||
func triggerVoiceEars(ttl: TimeInterval? = 5) {
|
||||
@@ -243,14 +267,15 @@ final class AppState: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
self.swabbleEnabled = enabled
|
||||
guard !self.isPreview else { return }
|
||||
|
||||
if !enabled {
|
||||
self.swabbleEnabled = false
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
return
|
||||
}
|
||||
|
||||
if PermissionManager.voiceWakePermissionsGranted() {
|
||||
self.swabbleEnabled = true
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
return
|
||||
}
|
||||
@@ -280,6 +305,37 @@ final class AppState: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
extension AppState {
|
||||
static var preview: AppState {
|
||||
let state = AppState(preview: true)
|
||||
state.isPaused = false
|
||||
state.launchAtLogin = true
|
||||
state.onboardingSeen = true
|
||||
state.debugPaneEnabled = true
|
||||
state.swabbleEnabled = true
|
||||
state.swabbleTriggerWords = ["Claude", "Computer", "Jarvis"]
|
||||
state.voiceWakeTriggerChime = .system(name: "Glass")
|
||||
state.voiceWakeSendChime = .system(name: "Ping")
|
||||
state.iconAnimationsEnabled = true
|
||||
state.showDockIcon = true
|
||||
state.voiceWakeMicID = "BuiltInMic"
|
||||
state.voiceWakeLocaleID = Locale.current.identifier
|
||||
state.voiceWakeAdditionalLocaleIDs = ["en-US", "de-DE"]
|
||||
state.voiceWakeForwardEnabled = true
|
||||
state.voiceWakeForwardCommand = defaultVoiceWakeForwardCommand
|
||||
state.voicePushToTalkEnabled = false
|
||||
state.iconOverride = .system
|
||||
state.heartbeatsEnabled = true
|
||||
state.connectionMode = .local
|
||||
state.webChatEnabled = true
|
||||
state.webChatPort = 18788
|
||||
state.remoteTarget = "user@example.com"
|
||||
state.remoteIdentity = "~/.ssh/id_ed25519"
|
||||
state.remoteProjectRoot = "~/Projects/clawdis"
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
enum AppStateStore {
|
||||
static let shared = AppState()
|
||||
|
||||
Reference in New Issue
Block a user