blitztext-app-linux/BlitztextMac/App/BlitztextMacApp.swift
2026-05-23 07:28:46 +02:00

154 lines
4.5 KiB
Swift

import SwiftUI
@main
struct BlitztextMacApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings {
EmptyView()
}
}
}
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
private var statusItem: NSStatusItem!
private var popover: NSPopover!
private let menuBarStatusController = MenuBarStatusController()
let appState = AppState()
func applicationDidFinishLaunching(_ notification: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
if let button = statusItem.button {
menuBarStatusController.attach(to: button)
button.action = #selector(togglePopover)
button.target = self
}
popover = NSPopover()
popover.contentSize = NSSize(width: 340, height: 480)
popover.behavior = .transient
popover.delegate = self
popover.contentViewController = NSHostingController(rootView: MenuBarView(appState: appState))
NSApp.setActivationPolicy(.accessory)
// Hotkey events
appState.hotkeyService.onHotkeyEvent = { [weak self] event in
self?.handleHotkeyEvent(event)
}
appState.onMenuBarStatusChange = { [weak self] status in
self?.menuBarStatusController.update(to: status)
}
appState.hotkeyService.start()
// Listen for popover dismiss requests (from auto-paste)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleDismissPopover),
name: .dismissPopover,
object: nil
)
DispatchQueue.main.async { [weak self] in
self?.showOnboardingIfNeeded()
}
}
@objc private func handleDismissPopover() {
appState.isPopoverShown = false
popover.performClose(nil)
}
private func handleHotkeyEvent(_ event: HotkeyEvent) {
switch event {
case .down(let type):
handleHotkeyDown(type)
case .up(let type):
handleHotkeyUp(type)
case .cancel:
handleHotkeyCancel()
}
}
private func handleHotkeyDown(_ type: WorkflowType) {
guard appState.isConfigured else { return }
let mode = appState.appSettings.hotkeyMode
switch mode {
case .hold:
// Hold mode: start recording on key down
appState.startWorkflow(type, source: .hotkeyBackground)
case .toggle:
// Toggle mode: if already recording same workflow, stop it
if let active = appState.activeWorkflow,
active.type == type,
active.phase.isActive {
active.stop()
} else {
appState.prepareForPopoverPresentation()
appState.startWorkflow(type, source: .manual)
showPopover()
}
}
}
private func handleHotkeyUp(_ type: WorkflowType) {
let mode = appState.appSettings.hotkeyMode
guard mode == .hold else { return }
// Hold mode: stop recording on key release
if let active = appState.activeWorkflow,
active.type == type {
// Only stop if currently recording (running phase)
if case .running = active.phase {
active.stop()
}
}
}
private func handleHotkeyCancel() {
appState.activeWorkflow?.stop()
}
@objc private func togglePopover() {
if popover.isShown {
popover.performClose(nil)
appState.isPopoverShown = false
} else {
appState.prepareForPopoverPresentation()
showPopover()
}
}
private func showOnboardingIfNeeded() {
guard appState.shouldShowOnboarding else { return }
appState.prepareForPopoverPresentation()
showPopover()
}
private func showPopover() {
guard let button = statusItem.button else { return }
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
appState.isPopoverShown = true
NSApp.activate(ignoringOtherApps: true)
}
nonisolated func popoverDidClose(_ notification: Notification) {
Task { @MainActor in
appState.isPopoverShown = false
switch appState.currentPhase {
case .done, .error:
appState.resetCurrentWorkflow()
default:
appState.page = .main
}
}
}
}