// // 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? 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 } }