import AppKit import Carbon import SwiftUI @MainActor final class AppDelegate: NSObject, NSApplicationDelegate { let appState = AppState() let accountStore = AccountStore() var floatingPanel: FloatingPanel? var settingsWindow: NSWindow? private var hotKeyRef: EventHotKeyRef? func applicationDidFinishLaunching(_ notification: Notification) { registerGlobalHotKey() } func application(_ application: NSApplication, open urls: [URL]) { for url in urls { handleURL(url) } } // MARK: - Floating Panel func showPanel() { if floatingPanel == nil { let panelContent = InputPanelView( appState: appState, accountStore: accountStore, onPost: { [weak self] in self?.performPost() }, onDismiss: { [weak self] in self?.hidePanel() } ) floatingPanel = FloatingPanel(contentView: panelContent) } floatingPanel?.center() floatingPanel?.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) appState.isPanelVisible = true } func hidePanel() { floatingPanel?.orderOut(nil) appState.isPanelVisible = false } func togglePanel() { if appState.isPanelVisible { hidePanel() } else { showPanel() } } // MARK: - Settings Window func showSettings() { if settingsWindow == nil { let settingsView = SettingsView(accountStore: accountStore) let hostingView = NSHostingView(rootView: settingsView) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 360, height: 300), styleMask: [.titled, .closable], backing: .buffered, defer: false ) window.contentView = hostingView window.title = "qStatus Settings" window.center() window.isReleasedWhenClosed = false settingsWindow = window } settingsWindow?.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } // MARK: - Posting func performPost() { guard appState.canPost else { return } let selectedAccounts = accountStore.accounts.filter { appState.selectedAccountIDs.contains($0.id) } do { try InputValidator.validatePost( text: appState.inputText, images: appState.attachedImages, accounts: selectedAccounts ) } catch { appState.statusMessage = StatusMessage(text: error.localizedDescription, isError: true) return } appState.isSubmitting = true appState.statusMessage = nil Task { @MainActor in let results = await PostingManager.post( text: appState.inputText, images: appState.attachedImages, accounts: selectedAccounts, accountStore: accountStore ) var successes: [String] = [] var failures: [String] = [] for (account, result) in results { switch result { case .success: successes.append(account.displayName) case .failure(let error): failures.append("\(account.displayName): \(error.localizedDescription)") } } appState.isSubmitting = false if failures.isEmpty { appState.statusMessage = StatusMessage( text: "Posted to \(successes.joined(separator: ", "))", isError: false ) // Clear after short delay try? await Task.sleep(for: .seconds(1.5)) appState.reset() hidePanel() } else { let msg = failures.joined(separator: "\n") appState.statusMessage = StatusMessage(text: msg, isError: true) } } } // MARK: - Global Hotkey private func registerGlobalHotKey() { // Ctrl + Option + Cmd + T let modifiers: UInt32 = UInt32(cmdKey | optionKey | controlKey) let keyCode: UInt32 = UInt32(kVK_ANSI_T) var hotKeyID = EventHotKeyID() hotKeyID.signature = OSType(0x7153_5448) // 'qSTH' hotKeyID.id = 1 var eventType = EventTypeSpec() eventType.eventClass = OSType(kEventClassKeyboard) eventType.eventKind = UInt32(kEventHotKeyPressed) let selfPtr = Unmanaged.passUnretained(self).toOpaque() InstallEventHandler( GetApplicationEventTarget(), { (_, event, userData) -> OSStatus in guard let userData else { return OSStatus(eventNotHandledErr) } let delegate = Unmanaged.fromOpaque(userData).takeUnretainedValue() DispatchQueue.main.async { delegate.togglePanel() } return noErr }, 1, &eventType, selfPtr, nil ) RegisterEventHotKey( keyCode, modifiers, hotKeyID, GetApplicationEventTarget(), 0, &hotKeyRef ) } // MARK: - URL Handling func handleURL(_ url: URL) { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return } switch components.host { case "mastodon-callback": handleMastodonCallback(components) case "wordpress-callback": handleWordPressCallback(components) default: break } } private func handleMastodonCallback(_ components: URLComponents) { guard let code = components.queryItems?.first(where: { $0.name == "code" })?.value else { return } Task { guard let accountData = UserDefaults.standard.data(forKey: OAuthKeys.pendingMastodonAccount), var account = try? JSONDecoder().decode(Account.self, from: accountData), let clientID = account.mastodonClientID, let clientSecret = account.mastodonClientSecret else { return } do { let token = try await MastodonClient.exchangeCode( instanceURL: account.instanceURL, clientID: clientID, clientSecret: clientSecret, code: code ) let username = try await MastodonClient.verifyCredentials( instanceURL: account.instanceURL, token: token ) account.username = username account.displayName = "@\(username)@\(account.instanceURL.replacingOccurrences(of: "https://", with: ""))" try accountStore.addAccount(account, token: token) UserDefaults.standard.removeObject(forKey: OAuthKeys.pendingMastodonAccount) UserDefaults.standard.removeObject(forKey: OAuthKeys.pendingMastodonError) } catch { UserDefaults.standard.set( error.localizedDescription, forKey: OAuthKeys.pendingMastodonError ) UserDefaults.standard.removeObject(forKey: OAuthKeys.pendingMastodonAccount) } } } private func handleWordPressCallback(_ components: URLComponents) { guard let userLogin = components.queryItems?.first(where: { $0.name == "user_login" })?.value, let password = components.queryItems?.first(where: { $0.name == "password" })?.value, let siteURL = components.queryItems?.first(where: { $0.name == "site_url" })?.value else { return } Task { do { let normalizedSiteURL = try URLNormalizer.normalizeBaseURL( siteURL, fieldName: "WordPress site URL" ) let isValid = try await WordPressClient.verifyCredentials( siteURL: normalizedSiteURL, username: userLogin, password: password ) guard isValid else { return } let credentials = "\(userLogin):\(password)" let account = Account( serviceType: .wordpress, displayName: "\(userLogin)@\(normalizedSiteURL.replacingOccurrences(of: "https://", with: ""))", instanceURL: normalizedSiteURL, username: userLogin ) try accountStore.addAccount(account, token: credentials) } catch { print("WordPress auth callback error: \(error.localizedDescription)") } } } }