From c0ae12df6d7ecdb3381b34ed598858c0a77c5b1e Mon Sep 17 00:00:00 2001 From: andyhtran Date: Fri, 3 Apr 2026 00:03:16 -0400 Subject: [PATCH] Refactor launch-at-login toggle to shared service manager --- Sources/MiniWhisper/AppState.swift | 24 ---------- .../Services/LaunchAtLoginManager.swift | 46 +++++++++++++++++++ Sources/MiniWhisper/Views/MenuBarView.swift | 36 +++++++-------- 3 files changed, 64 insertions(+), 42 deletions(-) create mode 100644 Sources/MiniWhisper/Services/LaunchAtLoginManager.swift diff --git a/Sources/MiniWhisper/AppState.swift b/Sources/MiniWhisper/AppState.swift index ec15f44..089ed93 100644 --- a/Sources/MiniWhisper/AppState.swift +++ b/Sources/MiniWhisper/AppState.swift @@ -1,7 +1,6 @@ import Foundation import Observation import AppKit -import ServiceManagement import UserNotifications @Observable @@ -38,8 +37,6 @@ final class AppState: Sendable { var isModelDownloading: Bool { whisper.isDownloading } var modelDownloadProgress: Double { whisper.downloadProgress } - var launchAtLoginEnabled: Bool { SMAppService.mainApp.status == .enabled } - var launchAtLoginSupported: Bool { SMAppService.mainApp.status != .notFound } // MARK: - Initialization @@ -212,27 +209,6 @@ final class AppState: Sendable { } } - func setLaunchAtLogin(_ enabled: Bool) { - let service = SMAppService.mainApp - guard service.status != .notFound else { - toast.showError( - title: "Start on Login Unavailable", - message: "This is available only when running the bundled app." - ) - return - } - - do { - if enabled { - try service.register() - } else { - try service.unregister() - } - } catch { - toast.showError(title: "Start on Login Failed", message: error.localizedDescription) - } - } - // MARK: - Transcription private func transcribe(audioURL: URL, recordingId: String, duration: TimeInterval, sampleRate: Double) async { diff --git a/Sources/MiniWhisper/Services/LaunchAtLoginManager.swift b/Sources/MiniWhisper/Services/LaunchAtLoginManager.swift new file mode 100644 index 0000000..5398e35 --- /dev/null +++ b/Sources/MiniWhisper/Services/LaunchAtLoginManager.swift @@ -0,0 +1,46 @@ +import Foundation +import ServiceManagement + +@MainActor +final class LaunchAtLoginManager: ObservableObject { + static let shared = LaunchAtLoginManager() + + @Published var isEnabled: Bool { + didSet { + if isEnabled { + enableLaunchAtLogin() + } else { + disableLaunchAtLogin() + } + } + } + + private init() { + isEnabled = SMAppService.mainApp.status == .enabled + } + + private func enableLaunchAtLogin() { + do { + try SMAppService.mainApp.register() + } catch { + Task { @MainActor in + self.isEnabled = false + } + } + } + + private func disableLaunchAtLogin() { + do { + try SMAppService.mainApp.unregister() + } catch { + return + } + } + + func refresh() { + let newStatus = SMAppService.mainApp.status == .enabled + if newStatus != isEnabled { + _isEnabled = Published(wrappedValue: newStatus) + } + } +} diff --git a/Sources/MiniWhisper/Views/MenuBarView.swift b/Sources/MiniWhisper/Views/MenuBarView.swift index ee5fc69..0e8b9de 100644 --- a/Sources/MiniWhisper/Views/MenuBarView.swift +++ b/Sources/MiniWhisper/Views/MenuBarView.swift @@ -361,6 +361,7 @@ private struct PermissionRow: View { private struct FooterBarView: View { @Environment(AppState.self) private var appState + @StateObject private var launchManager = LaunchAtLoginManager.shared @State private var showHistory = false @State private var showReplacements = false @State private var showModelPicker = false @@ -432,9 +433,9 @@ private struct FooterBarView: View { Button { showLaunchAtLogin.toggle() } label: { - Image(systemName: appState.launchAtLoginEnabled ? "power.circle.fill" : "power.circle") + Image(systemName: launchManager.isEnabled ? "power.circle.fill" : "power.circle") .font(.system(size: 14)) - .foregroundColor(appState.launchAtLoginEnabled ? .accentColor : .secondary) + .foregroundColor(launchManager.isEnabled ? .accentColor : .secondary) .frame(width: 28, height: 28) } .buttonStyle(.plain) @@ -456,11 +457,14 @@ private struct FooterBarView: View { } .padding(.horizontal, 16) .padding(.vertical, 8) + .onAppear { + launchManager.refresh() + } } } private struct LaunchAtLoginPopoverView: View { - @Environment(AppState.self) private var appState + @StateObject private var launchManager = LaunchAtLoginManager.shared var body: some View { VStack(alignment: .leading, spacing: 10) { @@ -470,25 +474,21 @@ private struct LaunchAtLoginPopoverView: View { .textCase(.uppercase) .tracking(0.5) - if appState.launchAtLoginSupported { - Toggle( - "Start MiniWhisper when you log in", - isOn: Binding( - get: { appState.launchAtLoginEnabled }, - set: { appState.setLaunchAtLogin($0) } - ) + Toggle( + "Start MiniWhisper when you log in", + isOn: Binding( + get: { launchManager.isEnabled }, + set: { launchManager.isEnabled = $0 } ) - .toggleStyle(.switch) - .font(.system(size: 13)) - } else { - Text("Unavailable in this runtime. Build/run the bundled app to enable login item registration.") - .font(.system(size: 12)) - .foregroundColor(.secondary) - .fixedSize(horizontal: false, vertical: true) - } + ) + .toggleStyle(.switch) + .font(.system(size: 13)) } .padding(12) .frame(width: 280) + .onAppear { + launchManager.refresh() + } } }