AppDelegate.swift 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import AirAcceptCore
  2. import AppKit
  3. import Foundation
  4. @MainActor
  5. final class AppDelegate: NSObject, NSApplicationDelegate {
  6. private let configStore = ConfigStore()
  7. private let logger = AirAcceptLogger()
  8. private lazy var watcher = AirPlayPromptWatcher(configStore: configStore, logger: logger)
  9. private var statusItem: NSStatusItem?
  10. private let pollIntervalOptions = [2_000, 3_000, 4_000, 5_000]
  11. func applicationDidFinishLaunching(_ notification: Notification) {
  12. NSApp.setActivationPolicy(.accessory)
  13. logger.info("AirAccept launched pid=\(ProcessInfo.processInfo.processIdentifier)")
  14. ensureConfigExists()
  15. buildMenu()
  16. watcher.start()
  17. }
  18. func applicationWillTerminate(_ notification: Notification) {
  19. logger.info("AirAccept terminating pid=\(ProcessInfo.processInfo.processIdentifier)")
  20. }
  21. private func ensureConfigExists() {
  22. do {
  23. _ = try configStore.loadOrCreate()
  24. } catch {
  25. logger.error("failed to create or load config: \(error.localizedDescription)")
  26. }
  27. }
  28. private func buildMenu() {
  29. let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
  30. if let image = NSImage(systemSymbolName: "airplayvideo", accessibilityDescription: "AirAccept") {
  31. image.isTemplate = true
  32. item.button?.image = image
  33. } else {
  34. item.button?.title = "AA"
  35. }
  36. item.button?.toolTip = "AirAccept"
  37. item.menu = makeMenu()
  38. statusItem = item
  39. }
  40. private func makeMenu() -> NSMenu {
  41. let menu = NSMenu()
  42. let statusItem = NSMenuItem(
  43. title: "Status: Running (PID \(ProcessInfo.processInfo.processIdentifier))",
  44. action: nil,
  45. keyEquivalent: ""
  46. )
  47. statusItem.isEnabled = false
  48. menu.addItem(statusItem)
  49. let permissionStateItem = NSMenuItem(
  50. title: AccessibilityReader.isTrusted(prompt: false)
  51. ? "Accessibility: Granted"
  52. : "Accessibility: Not Granted",
  53. action: nil,
  54. keyEquivalent: ""
  55. )
  56. permissionStateItem.isEnabled = false
  57. menu.addItem(permissionStateItem)
  58. menu.addItem(.separator())
  59. let watcherItem = NSMenuItem(
  60. title: watcher.isEnabled ? "Disable Watcher" : "Enable Watcher",
  61. action: #selector(toggleWatcher(_:)),
  62. keyEquivalent: ""
  63. )
  64. watcherItem.target = self
  65. menu.addItem(watcherItem)
  66. let intervalMenuItem = NSMenuItem(title: "Poll Interval", action: nil, keyEquivalent: "")
  67. intervalMenuItem.submenu = makePollIntervalMenu()
  68. menu.addItem(intervalMenuItem)
  69. let launchItem = NSMenuItem(
  70. title: "Launch at Login",
  71. action: #selector(toggleLaunchAtLogin(_:)),
  72. keyEquivalent: ""
  73. )
  74. launchItem.target = self
  75. launchItem.state = LoginItemManager.isEnabled ? .on : .off
  76. menu.addItem(launchItem)
  77. menu.addItem(.separator())
  78. let permissionItem = NSMenuItem(
  79. title: "Request Accessibility Permission",
  80. action: #selector(requestAccessibilityPermission(_:)),
  81. keyEquivalent: ""
  82. )
  83. permissionItem.target = self
  84. menu.addItem(permissionItem)
  85. let openConfigItem = NSMenuItem(
  86. title: "Open Config",
  87. action: #selector(openConfig(_:)),
  88. keyEquivalent: ""
  89. )
  90. openConfigItem.target = self
  91. menu.addItem(openConfigItem)
  92. let openLogsItem = NSMenuItem(
  93. title: "Open Logs",
  94. action: #selector(openLogs(_:)),
  95. keyEquivalent: ""
  96. )
  97. openLogsItem.target = self
  98. menu.addItem(openLogsItem)
  99. menu.addItem(.separator())
  100. let quitItem = NSMenuItem(title: "Quit", action: #selector(quit(_:)), keyEquivalent: "q")
  101. quitItem.target = self
  102. menu.addItem(quitItem)
  103. return menu
  104. }
  105. private func makePollIntervalMenu() -> NSMenu {
  106. let menu = NSMenu()
  107. let currentInterval = currentConfig().pollIntervalMilliseconds
  108. for interval in pollIntervalOptions {
  109. let seconds = interval / 1_000
  110. let item = NSMenuItem(
  111. title: "\(seconds) seconds",
  112. action: #selector(setPollInterval(_:)),
  113. keyEquivalent: ""
  114. )
  115. item.target = self
  116. item.representedObject = interval
  117. item.state = currentInterval == interval ? .on : .off
  118. menu.addItem(item)
  119. }
  120. return menu
  121. }
  122. private func rebuildMenu() {
  123. statusItem?.menu = makeMenu()
  124. }
  125. private func currentConfig() -> AirAcceptConfig {
  126. (try? configStore.loadOrCreate()) ?? AirAcceptConfig()
  127. }
  128. @objc private func toggleWatcher(_ sender: NSMenuItem) {
  129. watcher.setEnabled(!watcher.isEnabled)
  130. rebuildMenu()
  131. }
  132. @objc private func setPollInterval(_ sender: NSMenuItem) {
  133. guard let interval = sender.representedObject as? Int else {
  134. return
  135. }
  136. do {
  137. try configStore.save(AirAcceptConfig(pollIntervalMilliseconds: interval))
  138. watcher.reloadConfiguration()
  139. logger.info("poll interval set to \(Double(interval) / 1_000.0)s")
  140. } catch {
  141. logger.error("failed to set poll interval: \(error.localizedDescription)")
  142. }
  143. rebuildMenu()
  144. }
  145. @objc private func toggleLaunchAtLogin(_ sender: NSMenuItem) {
  146. do {
  147. try LoginItemManager.setEnabled(!LoginItemManager.isEnabled)
  148. logger.info("launch at login set to \(LoginItemManager.isEnabled)")
  149. } catch {
  150. logger.error("failed to toggle launch at login: \(error.localizedDescription)")
  151. }
  152. rebuildMenu()
  153. }
  154. @objc private func requestAccessibilityPermission(_ sender: NSMenuItem) {
  155. _ = AccessibilityReader.isTrusted(prompt: true)
  156. }
  157. @objc private func openConfig(_ sender: NSMenuItem) {
  158. ensureConfigExists()
  159. NSWorkspace.shared.activateFileViewerSelecting([configStore.configURL])
  160. }
  161. @objc private func openLogs(_ sender: NSMenuItem) {
  162. NSWorkspace.shared.activateFileViewerSelecting([logger.logURL])
  163. }
  164. @objc private func quit(_ sender: NSMenuItem) {
  165. watcher.stop()
  166. NSApp.terminate(nil)
  167. }
  168. }