| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- import AirAcceptCore
- import AppKit
- import Foundation
- @MainActor
- final class AppDelegate: NSObject, NSApplicationDelegate {
- private let configStore = ConfigStore()
- private let logger = AirAcceptLogger()
- private lazy var watcher = AirPlayPromptWatcher(configStore: configStore, logger: logger)
- private var statusItem: NSStatusItem?
- private let pollIntervalOptions = [2_000, 3_000, 4_000, 5_000]
- func applicationDidFinishLaunching(_ notification: Notification) {
- NSApp.setActivationPolicy(.accessory)
- logger.info("AirAccept launched pid=\(ProcessInfo.processInfo.processIdentifier)")
- ensureConfigExists()
- buildMenu()
- watcher.start()
- }
- func applicationWillTerminate(_ notification: Notification) {
- logger.info("AirAccept terminating pid=\(ProcessInfo.processInfo.processIdentifier)")
- }
- private func ensureConfigExists() {
- do {
- _ = try configStore.loadOrCreate()
- } catch {
- logger.error("failed to create or load config: \(error.localizedDescription)")
- }
- }
- private func buildMenu() {
- let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
- if let image = NSImage(systemSymbolName: "airplayvideo", accessibilityDescription: "AirAccept") {
- image.isTemplate = true
- item.button?.image = image
- } else {
- item.button?.title = "AA"
- }
- item.button?.toolTip = "AirAccept"
- item.menu = makeMenu()
- statusItem = item
- }
- private func makeMenu() -> NSMenu {
- let menu = NSMenu()
- let statusItem = NSMenuItem(
- title: "Status: Running (PID \(ProcessInfo.processInfo.processIdentifier))",
- action: nil,
- keyEquivalent: ""
- )
- statusItem.isEnabled = false
- menu.addItem(statusItem)
- let permissionStateItem = NSMenuItem(
- title: AccessibilityReader.isTrusted(prompt: false)
- ? "Accessibility: Granted"
- : "Accessibility: Not Granted",
- action: nil,
- keyEquivalent: ""
- )
- permissionStateItem.isEnabled = false
- menu.addItem(permissionStateItem)
- menu.addItem(.separator())
- let watcherItem = NSMenuItem(
- title: watcher.isEnabled ? "Disable Watcher" : "Enable Watcher",
- action: #selector(toggleWatcher(_:)),
- keyEquivalent: ""
- )
- watcherItem.target = self
- menu.addItem(watcherItem)
- let intervalMenuItem = NSMenuItem(title: "Poll Interval", action: nil, keyEquivalent: "")
- intervalMenuItem.submenu = makePollIntervalMenu()
- menu.addItem(intervalMenuItem)
- let launchItem = NSMenuItem(
- title: "Launch at Login",
- action: #selector(toggleLaunchAtLogin(_:)),
- keyEquivalent: ""
- )
- launchItem.target = self
- launchItem.state = LoginItemManager.isEnabled ? .on : .off
- menu.addItem(launchItem)
- menu.addItem(.separator())
- let permissionItem = NSMenuItem(
- title: "Request Accessibility Permission",
- action: #selector(requestAccessibilityPermission(_:)),
- keyEquivalent: ""
- )
- permissionItem.target = self
- menu.addItem(permissionItem)
- let openConfigItem = NSMenuItem(
- title: "Open Config",
- action: #selector(openConfig(_:)),
- keyEquivalent: ""
- )
- openConfigItem.target = self
- menu.addItem(openConfigItem)
- let openLogsItem = NSMenuItem(
- title: "Open Logs",
- action: #selector(openLogs(_:)),
- keyEquivalent: ""
- )
- openLogsItem.target = self
- menu.addItem(openLogsItem)
- menu.addItem(.separator())
- let quitItem = NSMenuItem(title: "Quit", action: #selector(quit(_:)), keyEquivalent: "q")
- quitItem.target = self
- menu.addItem(quitItem)
- return menu
- }
- private func makePollIntervalMenu() -> NSMenu {
- let menu = NSMenu()
- let currentInterval = currentConfig().pollIntervalMilliseconds
- for interval in pollIntervalOptions {
- let seconds = interval / 1_000
- let item = NSMenuItem(
- title: "\(seconds) seconds",
- action: #selector(setPollInterval(_:)),
- keyEquivalent: ""
- )
- item.target = self
- item.representedObject = interval
- item.state = currentInterval == interval ? .on : .off
- menu.addItem(item)
- }
- return menu
- }
- private func rebuildMenu() {
- statusItem?.menu = makeMenu()
- }
- private func currentConfig() -> AirAcceptConfig {
- (try? configStore.loadOrCreate()) ?? AirAcceptConfig()
- }
- @objc private func toggleWatcher(_ sender: NSMenuItem) {
- watcher.setEnabled(!watcher.isEnabled)
- rebuildMenu()
- }
- @objc private func setPollInterval(_ sender: NSMenuItem) {
- guard let interval = sender.representedObject as? Int else {
- return
- }
- do {
- try configStore.save(AirAcceptConfig(pollIntervalMilliseconds: interval))
- watcher.reloadConfiguration()
- logger.info("poll interval set to \(Double(interval) / 1_000.0)s")
- } catch {
- logger.error("failed to set poll interval: \(error.localizedDescription)")
- }
- rebuildMenu()
- }
- @objc private func toggleLaunchAtLogin(_ sender: NSMenuItem) {
- do {
- try LoginItemManager.setEnabled(!LoginItemManager.isEnabled)
- logger.info("launch at login set to \(LoginItemManager.isEnabled)")
- } catch {
- logger.error("failed to toggle launch at login: \(error.localizedDescription)")
- }
- rebuildMenu()
- }
- @objc private func requestAccessibilityPermission(_ sender: NSMenuItem) {
- _ = AccessibilityReader.isTrusted(prompt: true)
- }
- @objc private func openConfig(_ sender: NSMenuItem) {
- ensureConfigExists()
- NSWorkspace.shared.activateFileViewerSelecting([configStore.configURL])
- }
- @objc private func openLogs(_ sender: NSMenuItem) {
- NSWorkspace.shared.activateFileViewerSelecting([logger.logURL])
- }
- @objc private func quit(_ sender: NSMenuItem) {
- watcher.stop()
- NSApp.terminate(nil)
- }
- }
|