AppDelegate.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. //
  2. // AppDelegate.swift
  3. // App for Indeed
  4. //
  5. // Created by Mql Mac 2 on 11/05/2026.
  6. //
  7. import Cocoa
  8. enum AppWindowConfiguration {
  9. static let defaultContentSize = NSSize(width: 1120, height: 800)
  10. static let minimumContentSize = NSSize(width: 1120, height: 800)
  11. @MainActor
  12. static func apply(to window: NSWindow) {
  13. MainWindowCloseBehavior.install(on: window)
  14. window.minSize = minimumContentSize
  15. window.isRestorable = false
  16. window.title = AppMarketingLinks.appDisplayName
  17. window.styleMask.insert(.fullSizeContentView)
  18. window.titlebarAppearsTransparent = true
  19. window.titleVisibility = .hidden
  20. window.isMovableByWindowBackground = true
  21. // Same as dashboard chrome — avoids a halo outside the frame with fullSizeContentView.
  22. window.backgroundColor = AppAppearanceManager.shared.windowChromeColor
  23. let targetContent = NSRect(origin: .zero, size: defaultContentSize)
  24. let targetFrame = window.frameRect(forContentRect: targetContent)
  25. var frame = targetFrame
  26. let current = window.frame
  27. frame.origin.x = current.midX - frame.width / 2
  28. frame.origin.y = current.midY - frame.height / 2
  29. window.setFrame(frame, display: false, animate: false)
  30. window.setContentSize(defaultContentSize)
  31. }
  32. @MainActor
  33. static func mainWindow(in app: NSApplication = .shared) -> NSWindow? {
  34. app.mainWindow
  35. ?? app.keyWindow
  36. ?? app.windows.first(where: { $0.isVisible && $0.canBecomeKey })
  37. ?? app.windows.first(where: { $0.canBecomeKey })
  38. }
  39. }
  40. @main
  41. class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
  42. /// Avoids hammering StoreKit when `didBecomeActive` fires in quick succession (e.g. after system sheets).
  43. private var lastSubscriptionRefreshAt: Date?
  44. /// Window menu item that re-opens the main window after the user closes it with the red button.
  45. private var showMainWindowMenuItem: NSMenuItem?
  46. func applicationWillFinishLaunching(_ notification: Notification) {
  47. AppLanguageManager.shared.applyStoredPreferenceOnLaunch()
  48. AppAppearanceManager.shared.apply()
  49. }
  50. func applicationDidFinishLaunching(_ aNotification: Notification) {
  51. AppRatingCoordinator.shared.start()
  52. NotificationCenter.default.addObserver(
  53. forName: NSApplication.didBecomeActiveNotification,
  54. object: nil,
  55. queue: .main
  56. ) { [weak self] _ in
  57. Task { @MainActor in
  58. guard let self else { return }
  59. let now = Date()
  60. if let last = self.lastSubscriptionRefreshAt, now.timeIntervalSince(last) < 2.5 {
  61. return
  62. }
  63. self.lastSubscriptionRefreshAt = now
  64. await SubscriptionStore.shared.refreshEntitlements(deep: true)
  65. }
  66. }
  67. // Initial StoreKit refresh runs on `LoadingViewController` before the dashboard is shown.
  68. installMainWindowMenuItem()
  69. AppMenuLocalizer.apply()
  70. NotificationCenter.default.addObserver(
  71. forName: AppLanguageManager.didChangeNotification,
  72. object: nil,
  73. queue: .main
  74. ) { _ in
  75. AppMenuLocalizer.apply()
  76. }
  77. NSApp.activate(ignoringOtherApps: true)
  78. applyDefaultWindowSize()
  79. }
  80. func refreshMainWindowMenuItemTitle() {
  81. showMainWindowMenuItem?.title = AppMarketingLinks.appDisplayName
  82. }
  83. @MainActor
  84. private func applyDefaultWindowSize() {
  85. DispatchQueue.main.async { [weak self] in
  86. guard let self, let window = AppWindowConfiguration.mainWindow() else { return }
  87. AppWindowConfiguration.apply(to: window)
  88. window.center()
  89. window.makeKeyAndOrderFront(nil)
  90. // Layout can run after the first pass; enforce default size once more.
  91. DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
  92. guard self != nil, let window = AppWindowConfiguration.mainWindow() else { return }
  93. AppWindowConfiguration.apply(to: window)
  94. window.center()
  95. }
  96. }
  97. }
  98. func applicationWillTerminate(_ aNotification: Notification) {
  99. // Insert code here to tear down your application
  100. }
  101. func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
  102. // Opt out until real `NSWindowRestoration` is implemented (avoids null className restore logs).
  103. return false
  104. }
  105. /// Keep running in the Dock when the user closes the last window (standard single-window macOS apps).
  106. func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
  107. false
  108. }
  109. /// Clicking the Dock icon after closing the window should bring it back.
  110. func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
  111. guard !flag else { return true }
  112. showMainWindow(self)
  113. return true
  114. }
  115. /// Adds a persistent Window menu entry so reviewers and users can reopen the app after closing its window.
  116. private func installMainWindowMenuItem() {
  117. guard let windowMenu = NSApp.windowsMenu else { return }
  118. guard showMainWindowMenuItem == nil else { return }
  119. let item = NSMenuItem(
  120. title: AppMarketingLinks.appDisplayName,
  121. action: #selector(showMainWindow(_:)),
  122. keyEquivalent: ""
  123. )
  124. item.target = self
  125. showMainWindowMenuItem = item
  126. windowMenu.insertItem(item, at: 0)
  127. windowMenu.insertItem(NSMenuItem.separator(), at: 1)
  128. windowMenu.delegate = self
  129. }
  130. @objc func showMainWindow(_ sender: Any?) {
  131. guard let window = AppWindowConfiguration.mainWindow() else { return }
  132. window.makeKeyAndOrderFront(sender)
  133. NSApp.activate(ignoringOtherApps: true)
  134. }
  135. func menuNeedsUpdate(_ menu: NSMenu) {
  136. guard menu === NSApp.windowsMenu, let item = showMainWindowMenuItem else { return }
  137. let window = AppWindowConfiguration.mainWindow()
  138. let isVisible = window?.isVisible == true
  139. item.isEnabled = !isVisible
  140. item.state = isVisible ? .on : .off
  141. }
  142. }
  143. // MARK: - Main window close (hide on red button, do not quit)
  144. @MainActor
  145. private enum MainWindowCloseBehavior {
  146. private final class WindowDelegate: NSObject, NSWindowDelegate {
  147. static let shared = WindowDelegate()
  148. func windowShouldClose(_ sender: NSWindow) -> Bool {
  149. sender.orderOut(nil)
  150. return false
  151. }
  152. }
  153. static func install(on window: NSWindow) {
  154. window.isReleasedWhenClosed = false
  155. if window.delegate !== WindowDelegate.shared {
  156. window.delegate = WindowDelegate.shared
  157. }
  158. }
  159. }