diff --git a/README.md b/README.md index aa7ea93..7db34a5 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,7 @@ Open Settings from the menu bar popover or with **⌘,** - **Hotkey** - Choose from common presets or record a custom activation key directly from your keyboard. The selected key is consumed by VocaMac while the app is running. - **Language** - Auto-detect or specify (English, Spanish, French, German, Chinese, Japanese, and more) - **Launch at login** +- **Menu bar icon** - Show or hide the VocaMac icon in the system menu bar ### Audio - **Max recording duration** - 30s, 60s, 120s, or 300s diff --git a/Sources/VocaMac/App/VocaMacApp.swift b/Sources/VocaMac/App/VocaMacApp.swift index f4a72c4..fde86bd 100644 --- a/Sources/VocaMac/App/VocaMacApp.swift +++ b/Sources/VocaMac/App/VocaMacApp.swift @@ -226,10 +226,11 @@ struct VocaMacApp: App { @StateObject private var settingsManager = SettingsWindowManager() @StateObject private var updateWindowManager = UpdateWindowManager() @StateObject private var onboardingManager = OnboardingWindowManager() + @AppStorage("vocamac.showTrayIcon") private var showTrayIcon = true var body: some Scene { // Menu bar presence — the primary UI for VocaMac - MenuBarExtra { + MenuBarExtra(isInserted: $showTrayIcon) { MenuBarView(settingsManager: settingsManager, updateWindowManager: updateWindowManager) .environmentObject(appState) } label: { @@ -244,6 +245,11 @@ struct VocaMacApp: App { } } .menuBarExtraStyle(.window) + + Settings { + SettingsView() + .environmentObject(appState) + } } @MainActor init() { diff --git a/Sources/VocaMac/Models/AppState.swift b/Sources/VocaMac/Models/AppState.swift index d338896..1d06391 100644 --- a/Sources/VocaMac/Models/AppState.swift +++ b/Sources/VocaMac/Models/AppState.swift @@ -108,6 +108,7 @@ final class AppState: ObservableObject { @AppStorage(PreferenceKey.selectedLanguage) var selectedLanguage: String = "auto" @AppStorage("vocamac.launchAtLogin") var launchAtLogin: Bool = false @AppStorage("vocamac.preserveClipboard") var preserveClipboard: Bool = true + @AppStorage("vocamac.showTrayIcon") var showTrayIcon: Bool = true @AppStorage("vocamac.soundEffectsEnabled") var soundEffectsEnabled: Bool = true @AppStorage("vocamac.overlayStyle") var overlayStyle: OverlayStyle = .minimal @AppStorage("vocamac.overlayPosition") var overlayPosition: OverlayPosition = .bottom diff --git a/Sources/VocaMac/Views/SettingsView.swift b/Sources/VocaMac/Views/SettingsView.swift index d2f6aeb..42efa44 100644 --- a/Sources/VocaMac/Views/SettingsView.swift +++ b/Sources/VocaMac/Views/SettingsView.swift @@ -374,6 +374,12 @@ struct ApplicationSettingsPage: View { Text("When enabled, your clipboard contents are restored after injecting text.") .font(.caption) .foregroundStyle(.secondary) + + Toggle("Show VocaMac in the menu bar", isOn: $appState.showTrayIcon) + + Text("Display the VocaMac icon in the system menu bar. If hidden, use ⌘, while VocaMac is active to reopen Settings.") + .font(.caption) + .foregroundStyle(.secondary) } Section("Recording Overlay") { diff --git a/Tests/VocaMacTests/AppStateTests.swift b/Tests/VocaMacTests/AppStateTests.swift index bbb4e3e..b6cc758 100644 --- a/Tests/VocaMacTests/AppStateTests.swift +++ b/Tests/VocaMacTests/AppStateTests.swift @@ -30,6 +30,31 @@ final class TranslationToggleTests: XCTestCase { } } +// MARK: - Tray Icon Toggle Tests + +final class TrayIconToggleTests: XCTestCase { + + @MainActor + func testTrayIconIsShownByDefault() { + let (appState, _) = AppState.makeTestState() + + XCTAssertTrue(appState.showTrayIcon) + } + + @MainActor + func testTrayIconCanBeToggled() { + let (appState, _) = AppState.makeTestState() + + appState.showTrayIcon = false + XCTAssertFalse(appState.showTrayIcon) + XCTAssertFalse(UserDefaults.standard.bool(forKey: "vocamac.showTrayIcon")) + + appState.showTrayIcon = true + XCTAssertTrue(appState.showTrayIcon) + XCTAssertTrue(UserDefaults.standard.bool(forKey: "vocamac.showTrayIcon")) + } +} + // MARK: - OnboardingStep Tests diff --git a/Tests/VocaMacTests/Mocks/MockServices.swift b/Tests/VocaMacTests/Mocks/MockServices.swift index 56beda0..dd4fea4 100644 --- a/Tests/VocaMacTests/Mocks/MockServices.swift +++ b/Tests/VocaMacTests/Mocks/MockServices.swift @@ -484,6 +484,7 @@ extension AppState { ) -> (appState: AppState, mocks: TestMocks) { UserDefaults.standard.removeObject(forKey: "vocamac.selectedAudioDeviceID") UserDefaults.standard.removeObject(forKey: "vocamac.selectedAudioDeviceName") + UserDefaults.standard.removeObject(forKey: "vocamac.showTrayIcon") let audioEngine = MockAudioEngine() let soundManager = MockSoundManager() diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index f8228c8..6515d6a 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -312,6 +312,7 @@ struct UserSettings { // App Behavior var launchAtLogin: Bool = false var preserveClipboard: Bool = true // Restore clipboard after text injection + var showTrayIcon: Bool = true // Show VocaMac in the system menu bar var playSoundEffects: Bool = false // Sound on start/stop recording } ```