| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- //
- // AppDelegate.swift
- // google_apps
- //
- // Created by Dev Mac 1 on 27/03/2026.
- //
- import Cocoa
- import Combine
- import SwiftUI
- @main
- class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
- private var statusItem: NSStatusItem?
- private weak var launcherWindowReference: NSWindow?
- private var launcherWindowResignObserver: NSObjectProtocol?
- private var refocusLauncherObserver: NSObjectProtocol?
- private var appearanceChangedObserver: NSObjectProtocol?
- private var effectiveAppearanceObservation: NSKeyValueObservation?
- private var startupPaywallWindow: NSWindow?
- private var hasPresentedStartupPaywall = false
- private var premiumUnlockObservation: NSKeyValueObservation?
- private var premiumStateMonitorTask: Task<Void, Never>?
- private var launcherWindow: NSWindow? {
- if let launcherWindowReference, launcherWindowReference.contentViewController is ViewController {
- return launcherWindowReference
- }
- let resolved =
- NSApp.windows.first(where: { $0.contentViewController is ViewController }) ??
- NSApp.orderedWindows.first(where: { $0.contentViewController is ViewController }) ??
- NSApp.windows.first ??
- NSApp.orderedWindows.first
- launcherWindowReference = resolved
- return resolved
- }
- func applicationDidFinishLaunching(_ aNotification: Notification) {
- applyBundledAppIcon()
- configureMainWindowIfNeeded(centerOnLaunch: true)
- observeRefocusLauncherRequests()
- setupStatusBarItem()
- StatusBarAppIconsController.shared.start()
- presentLauncherAndPaywallOnLaunch()
- }
- private func observeRefocusLauncherRequests() {
- if let refocusLauncherObserver {
- NotificationCenter.default.removeObserver(refocusLauncherObserver)
- self.refocusLauncherObserver = nil
- }
- refocusLauncherObserver = NotificationCenter.default.addObserver(
- forName: .refocusLauncherWindowRequested,
- object: nil,
- queue: .main
- ) { [weak self] _ in
- guard let self else { return }
- // Defer a tick so window-close/miniaturize animations complete cleanly.
- DispatchQueue.main.async {
- self.showMainWindow()
- }
- }
- }
- private func presentLauncherAndPaywallOnLaunch() {
- // Bring the launcher up-front on cold launch, then show the paywall on top
- // for non-premium users. The paywall is presented as an AppKit child window
- // (instead of a SwiftUI .sheet) to avoid the resign-key observer below
- // tearing the window stack down the moment the sheet takes key focus.
- showMainWindow()
- Task { @MainActor in
- await PremiumStore.shared.refreshEntitlements()
- guard !PremiumStore.shared.isPremiumUnlocked else { return }
- self.presentStartupPaywallIfNeeded()
- }
- }
- @MainActor
- private func presentStartupPaywallIfNeeded() {
- guard !hasPresentedStartupPaywall else { return }
- guard !PremiumStore.shared.isPremiumUnlocked else { return }
- guard let parent = launcherWindow else { return }
- presentPremiumPaywall(over: parent, storeAsStartupPaywall: true)
- hasPresentedStartupPaywall = true
- observePremiumUnlockToAutoDismissPaywall()
- }
- /// Presents the premium paywall as a non-draggable, non-resizable child window that exactly covers `parent`.
- @MainActor
- func presentPremiumPaywall(over parent: NSWindow? = nil, storeAsStartupPaywall: Bool = false) {
- if storeAsStartupPaywall {
- // Startup paywall is only for non-premium users.
- guard !PremiumStore.shared.isPremiumUnlocked else { return }
- } else {
- // Allow showing the paywall for the yearly->lifetime upgrade flow.
- if PremiumStore.shared.isPremiumUnlocked && PremiumStore.shared.activePremiumProductID != .yearly {
- return
- }
- }
- guard let parentWindow = parent ?? launcherWindow else { return }
- if let existing = startupPaywallWindow, existing.isVisible {
- // Ensure it stays pinned to the parent’s current frame.
- existing.setFrame(PremiumPaywallWindowSizing.paywallFrame(forParentWindow: parentWindow), display: false)
- existing.makeKeyAndOrderFront(nil)
- NSApp.activate(ignoringOtherApps: true)
- return
- }
- let paywallFrame = PremiumPaywallWindowSizing.paywallFrame(forParentWindow: parentWindow)
- let paywallWindow = NSWindow(
- contentRect: paywallFrame,
- styleMask: [.borderless],
- backing: .buffered,
- defer: false
- )
- paywallWindow.isReleasedWhenClosed = false
- paywallWindow.delegate = self
- paywallWindow.isMovable = false
- paywallWindow.isMovableByWindowBackground = false
- paywallWindow.hasShadow = true
- paywallWindow.isOpaque = false
- paywallWindow.backgroundColor = .clear
- paywallWindow.level = .floating
- paywallWindow.minSize = paywallFrame.size
- paywallWindow.maxSize = paywallFrame.size
- let hosting = NSHostingController(
- rootView: PremiumFeaturesView(onDismissFromAppKitHost: { [weak self] in
- self?.closeStartupPaywall()
- })
- .environmentObject(PremiumStore.shared)
- )
- hosting.view.frame = NSRect(origin: .zero, size: paywallFrame.size)
- hosting.view.autoresizingMask = [.width, .height]
- paywallWindow.contentViewController = hosting
- if let layer = paywallWindow.contentView?.layer {
- paywallWindow.contentView?.wantsLayer = true
- layer.cornerRadius = 16
- if #available(macOS 11.0, *) {
- layer.cornerCurve = .continuous
- }
- layer.masksToBounds = true
- }
- paywallWindow.setFrame(paywallFrame, display: false)
- if storeAsStartupPaywall {
- startupPaywallWindow = paywallWindow
- } else {
- startupPaywallWindow = paywallWindow
- }
- parentWindow.addChildWindow(paywallWindow, ordered: .above)
- paywallWindow.makeKeyAndOrderFront(nil)
- NSApp.activate(ignoringOtherApps: true)
- }
- @MainActor
- private func closeStartupPaywall() {
- guard let window = startupPaywallWindow else { return }
- window.parent?.removeChildWindow(window)
- window.orderOut(nil)
- startupPaywallWindow = nil
- premiumStateMonitorTask?.cancel()
- premiumStateMonitorTask = nil
- }
- private func observePremiumUnlockToAutoDismissPaywall() {
- premiumStateMonitorTask?.cancel()
- premiumStateMonitorTask = Task { @MainActor [weak self] in
- for await unlocked in PremiumStore.shared.$isPremiumUnlocked.values {
- guard let self else { return }
- if unlocked {
- self.closeStartupPaywall()
- return
- }
- }
- }
- }
- private func applyBundledAppIcon() {
- if let assetAppIcon = NSImage(named: "AppIcon") {
- NSApp.applicationIconImage = assetAppIcon
- return
- }
- if let icnsURL = Bundle.main.url(forResource: "AppIcon", withExtension: "icns"),
- let icnsIcon = NSImage(contentsOf: icnsURL) {
- NSApp.applicationIconImage = icnsIcon
- }
- }
- private func setupStatusBarItem() {
- let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
- statusItem = item
- guard let button = item.button else { return }
- updateStatusBarIconForCurrentAppearance()
- observeAppearanceChanges()
- button.imageScaling = .scaleProportionallyDown
- button.imagePosition = .imageOnly
- button.action = #selector(statusBarButtonClicked(_:))
- button.target = self
- button.sendAction(on: [.leftMouseUp, .rightMouseUp])
- }
- private func updateStatusBarIconForCurrentAppearance() {
- guard let button = statusItem?.button else { return }
- // Use a template image so macOS tints it appropriately (white on dark menu bar,
- // black on light menu bar), matching other status bar icons.
- if let menuBarIcon = NSImage(named: "menu_bar_dark_icon") ?? NSImage(named: "menu_bar_icon") {
- menuBarIcon.isTemplate = true
- button.image = menuBarIcon
- button.image?.isTemplate = true
- return
- }
- button.image = NSImage(systemSymbolName: "square.grid.2x2.fill", accessibilityDescription: "My Apps")
- button.image?.isTemplate = true
- }
- private func observeAppearanceChanges() {
- if let appearanceChangedObserver {
- DistributedNotificationCenter.default().removeObserver(appearanceChangedObserver)
- self.appearanceChangedObserver = nil
- }
- effectiveAppearanceObservation?.invalidate()
- effectiveAppearanceObservation = nil
- appearanceChangedObserver = DistributedNotificationCenter.default().addObserver(
- forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"),
- object: nil,
- queue: .main
- ) { [weak self] _ in
- self?.updateStatusBarIconForCurrentAppearance()
- }
- effectiveAppearanceObservation = NSApp.observe(\.effectiveAppearance, options: [.new]) { [weak self] _, _ in
- self?.updateStatusBarIconForCurrentAppearance()
- }
- }
- @objc
- private func statusBarButtonClicked(_ sender: Any?) {
- guard let event = NSApp.currentEvent else {
- toggleMainWindowFromStatusBar()
- return
- }
- if event.type == .rightMouseUp {
- presentStatusBarShortcutsMenu()
- } else {
- toggleMainWindowFromStatusBar()
- }
- }
- private func presentStatusBarShortcutsMenu() {
- guard let button = statusItem?.button else { return }
- let menu = NSMenu()
- let openItem = NSMenuItem(
- title: "Open My Apps",
- action: #selector(openMainWindowFromStatusBar),
- keyEquivalent: ""
- )
- openItem.target = self
- menu.addItem(openItem)
- menu.addItem(.separator())
- let quitItem = NSMenuItem(
- title: "Quit",
- action: #selector(terminateApp),
- keyEquivalent: "q"
- )
- quitItem.target = self
- menu.addItem(quitItem)
- let location = NSPoint(x: 0, y: button.bounds.height)
- menu.popUp(positioning: nil, at: location, in: button)
- }
- @objc
- private func terminateApp() {
- NSApp.terminate(nil)
- }
- @objc
- private func openMainWindowFromStatusBar() {
- showMainWindow()
- }
- @objc
- private func toggleMainWindowFromStatusBar() {
- configureMainWindowIfNeeded()
- guard let window = launcherWindow else { return }
- if window.isVisible && window.isKeyWindow {
- hideMainWindow()
- return
- }
- showMainWindow()
- }
- private func hideMainWindow() {
- guard let window = launcherWindow else { return }
- // Don't auto-hide while a modal-style child window (e.g. the paywall) is
- // attached; otherwise the launcher and its child get torn down together
- // the moment the child takes key focus.
- if windowHasAttachedChild(window) {
- return
- }
- if window.isMiniaturized {
- window.deminiaturize(nil)
- }
- window.orderOut(nil)
- }
- private func windowHasAttachedChild(_ window: NSWindow) -> Bool {
- if window.attachedSheet != nil { return true }
- if let children = window.childWindows, !children.isEmpty { return true }
- return false
- }
- private func showMainWindow() {
- configureMainWindowIfNeeded(centerOnLaunch: false)
- NSApp.setActivationPolicy(.regular)
- NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
- NSApp.activate(ignoringOtherApps: true)
- guard let window = launcherWindow else { return }
- if window.isMiniaturized {
- window.deminiaturize(nil)
- }
- window.orderFrontRegardless()
- window.makeKeyAndOrderFront(nil)
- // Re-assert key/front on next runloop; status-bar activations can race app focus handoff.
- DispatchQueue.main.async {
- window.orderFrontRegardless()
- window.makeKeyAndOrderFront(nil)
- }
- }
- private func configureMainWindowIfNeeded(centerOnLaunch: Bool = false) {
- guard let window = launcherWindow else { return }
- observeLauncherWindowResign(window)
- window.title = "My Apps"
- window.styleMask.insert(.titled)
- window.isOpaque = false
- window.backgroundColor = .clear
- applyRoundedCorners(to: window)
- window.titlebarAppearsTransparent = true
- window.titleVisibility = .hidden
- window.styleMask.insert(.fullSizeContentView)
- window.isMovableByWindowBackground = true
- window.minSize = NSSize(width: 760, height: 520)
- window.standardWindowButton(.closeButton)?.isHidden = true
- window.standardWindowButton(.miniaturizeButton)?.isHidden = true
- window.standardWindowButton(.zoomButton)?.isHidden = true
- let targetContentSize = NSSize(width: 980, height: 700)
- if window.contentRect(forFrameRect: window.frame).size.width < targetContentSize.width ||
- window.contentRect(forFrameRect: window.frame).size.height < targetContentSize.height {
- let currentCenter = NSPoint(x: window.frame.midX, y: window.frame.midY)
- var targetFrame = window.frameRect(forContentRect: NSRect(origin: .zero, size: targetContentSize))
- targetFrame.origin = NSPoint(
- x: currentCenter.x - (targetFrame.width / 2),
- y: currentCenter.y - (targetFrame.height / 2)
- )
- window.setFrame(targetFrame, display: true)
- }
- if centerOnLaunch {
- centerWindowOnCurrentOrPrimaryScreen(window)
- }
- }
- private func centerWindowOnCurrentOrPrimaryScreen(_ window: NSWindow) {
- // Prefer the screen the window is currently on; fall back to the primary screen.
- let screen = window.screen ?? NSScreen.main
- guard let visibleFrame = screen?.visibleFrame else { return }
- let frameSize = window.frame.size
- let unclampedX = visibleFrame.midX - (frameSize.width / 2)
- let unclampedY = visibleFrame.midY - (frameSize.height / 2)
- // Clamp so we don't move the window completely off-screen on small displays.
- let clampedX = min(max(unclampedX, visibleFrame.minX), visibleFrame.maxX - frameSize.width)
- let clampedY = min(max(unclampedY, visibleFrame.minY), visibleFrame.maxY - frameSize.height)
- window.setFrame(NSRect(origin: NSPoint(x: clampedX, y: clampedY), size: frameSize), display: false)
- }
- private func observeLauncherWindowResign(_ window: NSWindow) {
- if let observer = launcherWindowResignObserver {
- NotificationCenter.default.removeObserver(observer)
- launcherWindowResignObserver = nil
- }
- launcherWindowResignObserver = NotificationCenter.default.addObserver(
- forName: NSWindow.didResignKeyNotification,
- object: window,
- queue: .main
- ) { [weak self] _ in
- guard let self else { return }
- // Skip auto-hide while a child window (e.g. paywall) is attached;
- // the parent loses key when the child becomes key, but the user is
- // still interacting with our app's window stack.
- if let win = self.launcherWindow, self.windowHasAttachedChild(win) {
- return
- }
- self.hideMainWindow()
- }
- }
- private func applyRoundedCorners(to window: NSWindow) {
- let cornerRadius: CGFloat = 16
- window.hasShadow = true
- window.contentView?.wantsLayer = true
- window.contentView?.layer?.cornerRadius = cornerRadius
- window.contentView?.layer?.masksToBounds = true
- }
- func applicationWillTerminate(_ aNotification: Notification) {
- if let observer = launcherWindowResignObserver {
- NotificationCenter.default.removeObserver(observer)
- launcherWindowResignObserver = nil
- }
- if let observer = refocusLauncherObserver {
- NotificationCenter.default.removeObserver(observer)
- refocusLauncherObserver = nil
- }
- if let observer = appearanceChangedObserver {
- DistributedNotificationCenter.default().removeObserver(observer)
- appearanceChangedObserver = nil
- }
- effectiveAppearanceObservation?.invalidate()
- effectiveAppearanceObservation = nil
- premiumStateMonitorTask?.cancel()
- premiumStateMonitorTask = nil
- premiumUnlockObservation?.invalidate()
- premiumUnlockObservation = nil
- }
- func applicationDidResignActive(_ notification: Notification) {
- // Same guard as the resign-key observer: don't hide while the user is
- // looking at the paywall (or any other attached child window).
- if let win = launcherWindow, windowHasAttachedChild(win) {
- return
- }
- hideMainWindow()
- }
- func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
- showMainWindow()
- return true
- }
- func windowWillClose(_ notification: Notification) {
- guard let closing = notification.object as? NSWindow else { return }
- if closing === startupPaywallWindow {
- closing.parent?.removeChildWindow(closing)
- startupPaywallWindow = nil
- premiumStateMonitorTask?.cancel()
- premiumStateMonitorTask = nil
- }
- }
- func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
- return true
- }
- }
|