// // ViewController.swift // meetings_app // // Created by Dev Mac 1 on 06/04/2026. // import Cocoa import WebKit private enum SidebarPage: Int { case joinMeetings = 0 case photo = 1 case video = 2 case tutorials = 3 case settings = 4 case browse = 5 } private enum ZoomJoinMode: Int { case id = 0 case url = 1 } private enum SettingsAction: Int { case restore = 0 case rateUs = 1 case support = 2 case moreApps = 3 case shareApp = 4 } private enum PremiumPlan: Int { case weekly = 0 case monthly = 1 case yearly = 2 case lifetime = 3 } final class ViewController: NSViewController { private var palette = Palette(isDarkMode: true) private let typography = Typography() private let launchContentSize = NSSize(width: 920, height: 690) private let launchMinContentSize = NSSize(width: 760, height: 600) private var mainContentHost: NSView? private var sidebarRowViews: [SidebarPage: NSView] = [:] private var selectedSidebarPage: SidebarPage = .joinMeetings private var selectedZoomJoinMode: ZoomJoinMode = .id private var pageCache: [SidebarPage: NSView] = [:] private var sidebarPageByView = [ObjectIdentifier: SidebarPage]() private var zoomJoinModeByView = [ObjectIdentifier: ZoomJoinMode]() private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:] private var settingsActionByView = [ObjectIdentifier: SettingsAction]() private weak var centeredTitleLabel: NSTextField? private weak var paywallWindow: NSWindow? private let paywallContentWidth: CGFloat = 520 private var selectedPremiumPlan: PremiumPlan = .monthly private var paywallPlanViews: [PremiumPlan: NSView] = [:] private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]() private weak var paywallOfferLabel: NSTextField? private weak var meetLinkField: NSTextField? private weak var browseAddressField: NSTextField? private var inAppBrowserWindowController: InAppBrowserWindowController? /// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`). private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll private let darkModeDefaultsKey = "settings.darkModeEnabled" private var darkModeEnabled: Bool { get { let hasValue = UserDefaults.standard.object(forKey: darkModeDefaultsKey) != nil return hasValue ? UserDefaults.standard.bool(forKey: darkModeDefaultsKey) : true } set { UserDefaults.standard.set(newValue, forKey: darkModeDefaultsKey) } } private func makeSettingsPopover() -> NSPopover { let popover = NSPopover() popover.behavior = .transient popover.animates = true popover.contentViewController = SettingsMenuViewController( palette: palette, typography: typography, darkModeEnabled: darkModeEnabled, onToggleDarkMode: { [weak self] enabled in self?.setDarkMode(enabled) }, onAction: { [weak self] action in self?.handleSettingsAction(action) } ) return popover } private var settingsPopover: NSPopover? override func viewDidLoad() { super.viewDidLoad() palette = Palette(isDarkMode: darkModeEnabled) setupRootView() buildMainLayout() } override func viewDidAppear() { super.viewDidAppear() applyWindowTitle(for: selectedSidebarPage) guard let window = view.window else { return } // Ensure launch size is applied even when macOS tries to restore prior window state. window.isRestorable = false window.setFrameAutosaveName("") DispatchQueue.main.async { [weak self, weak window] in guard let self, let window else { return } let frameSize = window.frameRect(forContentRect: NSRect(origin: .zero, size: self.launchContentSize)).size var newFrame = window.frame newFrame.size = frameSize window.setFrame(newFrame, display: true) window.center() window.minSize = window.frameRect(forContentRect: NSRect(origin: .zero, size: self.launchMinContentSize)).size self.installCenteredTitleIfNeeded(on: window) } } override var representedObject: Any? { didSet {} } } private extension ViewController { func setupRootView() { view.appearance = NSAppearance(named: darkModeEnabled ? .darkAqua : .aqua) view.wantsLayer = true view.layer?.backgroundColor = palette.pageBackground.cgColor } func buildMainLayout() { let splitContainer = NSStackView() splitContainer.translatesAutoresizingMaskIntoConstraints = false splitContainer.orientation = .horizontal splitContainer.spacing = 0 splitContainer.alignment = .top view.addSubview(splitContainer) NSLayoutConstraint.activate([ splitContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), splitContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor), splitContainer.topAnchor.constraint(equalTo: view.topAnchor), splitContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) let sidebar = makeSidebar() let mainPanel = makeMainPanel() splitContainer.addArrangedSubview(sidebar) splitContainer.addArrangedSubview(mainPanel) } @objc private func sidebarItemClicked(_ sender: NSClickGestureRecognizer) { guard let view = sender.view, let page = sidebarPageByView[ObjectIdentifier(view)], page != selectedSidebarPage || page == .settings else { return } if page == .settings { showSettingsPopover() return } showSidebarPage(page) } @objc private func zoomJoinModeClicked(_ sender: NSClickGestureRecognizer) { guard let view = sender.view, let mode = zoomJoinModeByView[ObjectIdentifier(view)], mode != selectedZoomJoinMode else { return } selectedZoomJoinMode = mode updateZoomJoinModeAppearance() if selectedSidebarPage == .joinMeetings { pageCache[.joinMeetings] = nil showSidebarPage(.joinMeetings) } } @objc private func premiumButtonClicked(_ sender: NSClickGestureRecognizer) { showPaywall() } @objc private func joinMeetClicked(_ sender: Any?) { let rawInput = meetLinkField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard let url = normalizedMeetJoinURL(from: rawInput) else { showSimpleAlert( title: "Invalid Meet link", message: "Enter a valid Google Meet link or meeting code (for example nkd-grps-duv, meet.google.com/nkd-grps-duv, or https://meet.google.com/nkd-grps-duv)." ) return } openInDefaultBrowser(url: url) } @objc private func cancelMeetJoinClicked(_ sender: Any?) { meetLinkField?.stringValue = "" } @objc private func browseOpenAddressClicked(_ sender: Any?) { let raw = browseAddressField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard raw.isEmpty == false else { showSimpleAlert(title: "Browse", message: "Enter a web address (for example meet.google.com).") return } let normalized = normalizedURLString(from: raw) guard let url = URL(string: normalized), url.scheme == "http" || url.scheme == "https" else { showSimpleAlert(title: "Invalid address", message: "Enter a valid http or https URL.") return } openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy) } @objc private func browseQuickLinkMeetClicked(_ sender: Any?) { guard let url = URL(string: "https://meet.google.com/") else { return } openInDefaultBrowser(url: url) } @objc private func browseQuickLinkMeetHelpClicked(_ sender: Any?) { guard let url = URL(string: "https://support.google.com/meet") else { return } openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy) } @objc private func browseQuickLinkZoomHelpClicked(_ sender: Any?) { guard let url = URL(string: "https://support.zoom.us") else { return } openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy) } private func normalizedURLString(from value: String) -> String { if value.lowercased().hasPrefix("http://") || value.lowercased().hasPrefix("https://") { return value } return "https://\(value)" } /// Typical Meet meeting code shape: three hyphen-separated groups (e.g. `nkd-grps-duv`). private func isValidMeetMeetingCode(_ code: String) -> Bool { let trimmed = code.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.isEmpty == false else { return false } let pattern = "^[a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{3}$" return trimmed.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil } /// Accepts `https://meet.google.com/...`, `meet.google.com/...`, or a bare code; returns canonical Meet URL or `nil`. private func normalizedMeetJoinURL(from rawInput: String) -> URL? { let trimmed = rawInput.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.isEmpty == false else { return nil } let lower = trimmed.lowercased() if lower.hasPrefix("http://") || lower.hasPrefix("https://") { guard let url = URL(string: trimmed), let host = url.host?.lowercased(), host == "meet.google.com" || host.hasSuffix(".meet.google.com") else { return nil } let path = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) guard path.isEmpty == false else { return nil } let firstSegment = path.split(separator: "/").first.map(String.init) ?? path guard isValidMeetMeetingCode(firstSegment) else { return nil } return URL(string: "https://meet.google.com/\(firstSegment.lowercased())") } if lower.hasPrefix("meet.google.com/") { let afterHost = trimmed.dropFirst("meet.google.com/".count) let beforeQuery = String(afterHost).split(separator: "?").first.map(String.init) ?? String(afterHost) let firstSegment = beforeQuery.split(separator: "/").first.map(String.init) ?? beforeQuery guard isValidMeetMeetingCode(firstSegment) else { return nil } return URL(string: "https://meet.google.com/\(firstSegment.lowercased())") } if isValidMeetMeetingCode(trimmed) { return URL(string: "https://meet.google.com/\(trimmed.lowercased())") } return nil } private func openInAppBrowser(with url: URL, policy: InAppBrowserURLPolicy = .allowAll) { let browserController: InAppBrowserWindowController if let existing = inAppBrowserWindowController { browserController = existing } else { browserController = InAppBrowserWindowController() inAppBrowserWindowController = browserController } browserController.load(url: url, policy: policy) browserController.applyDefaultFrameCenteredOnVisibleScreen() browserController.showWindow(nil) browserController.window?.makeKeyAndOrderFront(nil) browserController.window?.orderFrontRegardless() NSApp.activate(ignoringOtherApps: true) } private func openInDefaultBrowser(url: URL) { NSWorkspace.shared.open(url, configuration: NSWorkspace.OpenConfiguration()) { [weak self] _, error in if let error { DispatchQueue.main.async { self?.showSimpleAlert(title: "Unable to open browser", message: error.localizedDescription) } } } } private func openInSafari(url: URL) { guard let safariAppURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Safari") else { NSWorkspace.shared.open(url) return } let configuration = NSWorkspace.OpenConfiguration() NSWorkspace.shared.open([url], withApplicationAt: safariAppURL, configuration: configuration) { _, error in if let error { self.showSimpleAlert(title: "Unable to Open Safari", message: error.localizedDescription) } } } private func showSidebarPage(_ page: SidebarPage) { selectedSidebarPage = page updateSidebarAppearance() applyWindowTitle(for: page) guard let host = mainContentHost else { return } host.subviews.forEach { $0.removeFromSuperview() } let child = viewForPage(page) child.translatesAutoresizingMaskIntoConstraints = false host.addSubview(child) NSLayoutConstraint.activate([ child.leadingAnchor.constraint(equalTo: host.leadingAnchor), child.trailingAnchor.constraint(equalTo: host.trailingAnchor), child.topAnchor.constraint(equalTo: host.topAnchor), child.bottomAnchor.constraint(equalTo: host.bottomAnchor) ]) } private func showSettingsPopover() { guard let anchor = sidebarRowViews[.settings] else { return } if settingsPopover?.isShown == true { settingsPopover?.performClose(nil) return } settingsPopover = makeSettingsPopover() if let menu = settingsPopover?.contentViewController as? SettingsMenuViewController { menu.setDarkModeEnabled(darkModeEnabled) } settingsPopover?.show(relativeTo: anchor.bounds, of: anchor, preferredEdge: .maxX) } private func setDarkMode(_ enabled: Bool) { darkModeEnabled = enabled NSApp.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua) view.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua) palette = Palette(isDarkMode: enabled) settingsPopover?.performClose(nil) settingsPopover = nil reloadTheme() } private func reloadTheme() { pageCache.removeAll() sidebarRowViews.removeAll() sidebarPageByView.removeAll() zoomJoinModeByView.removeAll() zoomJoinModeViews.removeAll() settingsActionByView.removeAll() paywallPlanViews.removeAll() premiumPlanByView.removeAll() mainContentHost = nil view.subviews.forEach { $0.removeFromSuperview() } setupRootView() buildMainLayout() showSidebarPage(selectedSidebarPage) } private func handleSettingsAction(_ action: SettingsAction) { switch action { case .restore: showSimpleAlert(title: "Restore", message: "Restore action tapped.") case .rateUs: settingsPopover?.performClose(nil) settingsPopover = nil // Replace with your App Store product URL when the app is listed. if let url = URL(string: "https://apps.apple.com/app/id0000000000") { openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy) } case .support: settingsPopover?.performClose(nil) settingsPopover = nil if let url = URL(string: "https://support.google.com/meet") { openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy) } case .moreApps: settingsPopover?.performClose(nil) settingsPopover = nil // Replace with your App Store developer page URL. if let url = URL(string: "https://apps.apple.com/developer/id0000000000") { openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy) } case .shareApp: let urlString = "https://example.com" NSPasteboard.general.clearContents() NSPasteboard.general.setString(urlString, forType: .string) showSimpleAlert(title: "Share App", message: "Link copied to clipboard:\n\(urlString)") } } private func showSimpleAlert(title: String, message: String) { let alert = NSAlert() alert.messageText = title alert.informativeText = message alert.addButton(withTitle: "OK") alert.runModal() } private func showPaywall() { if let existing = paywallWindow { existing.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) return } let content = makePaywallContent() let controller = NSViewController() controller.view = content let panel = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 640, height: 820), styleMask: [.titled, .closable, .fullSizeContentView], backing: .buffered, defer: false ) panel.title = "Get Premium" panel.titleVisibility = .hidden panel.titlebarAppearsTransparent = true panel.isFloatingPanel = true panel.hidesOnDeactivate = false panel.standardWindowButton(.closeButton)?.isHidden = true panel.standardWindowButton(.miniaturizeButton)?.isHidden = true panel.standardWindowButton(.zoomButton)?.isHidden = true panel.center() panel.contentViewController = controller panel.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) paywallWindow = panel } @objc private func closePaywallClicked(_ sender: NSClickGestureRecognizer) { paywallWindow?.close() paywallWindow = nil } @objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) { guard let view = sender.view else { return } let text = (view.subviews.first { $0 is NSTextField } as? NSTextField)?.stringValue ?? "Link" let map: [String: String] = [ "Privacy Policy": "https://policies.google.com/privacy", "Support": "https://support.google.com/meet", "Terms of Services": "https://policies.google.com/terms" ] if let urlString = map[text], let url = URL(string: urlString) { openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy) return } showSimpleAlert(title: text, message: "\(text) tapped.") } @objc private func paywallPlanClicked(_ sender: NSClickGestureRecognizer) { guard let view = sender.view, let plan = premiumPlanByView[ObjectIdentifier(view)] else { return } selectedPremiumPlan = plan updatePaywallPlanSelection() } private func updatePaywallPlanSelection() { for (plan, view) in paywallPlanViews { applyPaywallPlanStyle(view, isSelected: plan == selectedPremiumPlan) } paywallOfferLabel?.stringValue = paywallOfferText(for: selectedPremiumPlan) } private func paywallOfferText(for plan: PremiumPlan) -> String { switch plan { case .weekly: return "Rs 1,100.00/week" case .monthly: return "Free for 3 Days then Rs 2,500.00/month" case .yearly: return "Rs 9,900.00/year (about 190.38/week)" case .lifetime: return "Rs 14,900.00 one-time purchase" } } private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool) { let selectedBorder = NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1) let idleBorder = palette.inputBorder let selectedBackground = darkModeEnabled ? NSColor(calibratedRed: 30.0 / 255.0, green: 34.0 / 255.0, blue: 42.0 / 255.0, alpha: 1) : NSColor(calibratedRed: 255.0 / 255.0, green: 246.0 / 255.0, blue: 236.0 / 255.0, alpha: 1) card.layer?.backgroundColor = (isSelected ? selectedBackground : palette.sectionCard).cgColor card.layer?.borderColor = (isSelected ? selectedBorder : idleBorder).cgColor card.layer?.borderWidth = isSelected ? 2 : 1 card.layer?.shadowColor = NSColor.black.cgColor card.layer?.shadowOpacity = isSelected ? (darkModeEnabled ? 0.26 : 0.10) : 0.12 card.layer?.shadowOffset = CGSize(width: 0, height: -1) card.layer?.shadowRadius = isSelected ? (darkModeEnabled ? 10 : 6) : 5 } private func viewForPage(_ page: SidebarPage) -> NSView { if let cached = pageCache[page] { return cached } let built: NSView switch page { case .joinMeetings: built = makeJoinMeetingsContent() case .photo: built = makePlaceholderPage(title: "Photo", subtitle: "Backgrounds — choose a photo background for your meetings.") case .video: built = makePlaceholderPage(title: "Video", subtitle: "Backgrounds — video background options.") case .tutorials: built = makePlaceholderPage(title: "Tutorials", subtitle: "Learn how to use the app.") case .settings: built = makePlaceholderPage(title: "Settings", subtitle: "Preferences and account options.") case .browse: built = makeBrowseWebContent() } pageCache[page] = built return built } private func makePlaceholderPage(title: String, subtitle: String) -> NSView { let panel = NSView() panel.translatesAutoresizingMaskIntoConstraints = false let titleLabel = textLabel(title, font: typography.pageTitle, color: palette.textPrimary) titleLabel.translatesAutoresizingMaskIntoConstraints = false let sub = textLabel(subtitle, font: typography.fieldLabel, color: palette.textSecondary) sub.translatesAutoresizingMaskIntoConstraints = false panel.addSubview(titleLabel) panel.addSubview(sub) NSLayoutConstraint.activate([ titleLabel.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28), titleLabel.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26), sub.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), sub.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8) ]) return panel } func makeBrowseWebContent() -> NSView { let panel = NSView() panel.translatesAutoresizingMaskIntoConstraints = false let titleLabel = textLabel("Browse the web", font: typography.pageTitle, color: palette.textPrimary) titleLabel.translatesAutoresizingMaskIntoConstraints = false let sub = textLabel( "Open sites in the in-app browser (back, forward, reload, address bar). OAuth and “Continue in browser” flows stay inside the app.", font: typography.fieldLabel, color: palette.textSecondary ) sub.translatesAutoresizingMaskIntoConstraints = false sub.maximumNumberOfLines = 0 sub.lineBreakMode = .byWordWrapping let fieldShell = roundedContainer(cornerRadius: 8, color: palette.inputBackground) fieldShell.translatesAutoresizingMaskIntoConstraints = false fieldShell.heightAnchor.constraint(equalToConstant: 44).isActive = true styleSurface(fieldShell, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let field = NSTextField(string: "") field.translatesAutoresizingMaskIntoConstraints = false field.isEditable = true field.isBordered = false field.drawsBackground = false field.focusRingType = .none field.font = NSFont.systemFont(ofSize: 14, weight: .regular) field.textColor = palette.textPrimary field.placeholderString = "https://example.com or example.com" field.delegate = self browseAddressField = field fieldShell.addSubview(field) let openBtn = meetActionButton( title: "Open in app browser", color: palette.primaryBlue, textColor: .white, width: 220, action: #selector(browseOpenAddressClicked(_:)) ) let quickTitle = textLabel("Quick links", font: typography.joinWithURLTitle, color: palette.textPrimary) quickTitle.translatesAutoresizingMaskIntoConstraints = false let quickRow = NSStackView() quickRow.translatesAutoresizingMaskIntoConstraints = false quickRow.orientation = .horizontal quickRow.spacing = 10 quickRow.addArrangedSubview(browseQuickLinkButton(title: "Google Meet", action: #selector(browseQuickLinkMeetClicked(_:)))) quickRow.addArrangedSubview(browseQuickLinkButton(title: "Meet help", action: #selector(browseQuickLinkMeetHelpClicked(_:)))) quickRow.addArrangedSubview(browseQuickLinkButton(title: "Zoom help", action: #selector(browseQuickLinkZoomHelpClicked(_:)))) panel.addSubview(titleLabel) panel.addSubview(sub) panel.addSubview(fieldShell) panel.addSubview(openBtn) panel.addSubview(quickTitle) panel.addSubview(quickRow) NSLayoutConstraint.activate([ titleLabel.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28), titleLabel.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26), titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: panel.trailingAnchor, constant: -28), sub.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), sub.trailingAnchor.constraint(lessThanOrEqualTo: panel.trailingAnchor, constant: -28), sub.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), fieldShell.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), fieldShell.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28), fieldShell.topAnchor.constraint(equalTo: sub.bottomAnchor, constant: 18), field.leadingAnchor.constraint(equalTo: fieldShell.leadingAnchor, constant: 12), field.trailingAnchor.constraint(equalTo: fieldShell.trailingAnchor, constant: -12), field.centerYAnchor.constraint(equalTo: fieldShell.centerYAnchor), openBtn.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), openBtn.topAnchor.constraint(equalTo: fieldShell.bottomAnchor, constant: 12), openBtn.heightAnchor.constraint(equalToConstant: 36), quickTitle.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), quickTitle.topAnchor.constraint(equalTo: openBtn.bottomAnchor, constant: 28), quickRow.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), quickRow.topAnchor.constraint(equalTo: quickTitle.bottomAnchor, constant: 10) ]) return panel } private func browseQuickLinkButton(title: String, action: Selector) -> NSButton { let b = NSButton(title: title, target: self, action: action) b.translatesAutoresizingMaskIntoConstraints = false b.bezelStyle = .rounded b.font = NSFont.systemFont(ofSize: 13, weight: .medium) return b } private func applyWindowTitle(for page: SidebarPage) { let title: String switch page { case .joinMeetings: title = "App for Google Meet" case .photo: title = "Backgrounds — Photo" case .video: title = "Backgrounds — Video" case .tutorials: title = "Tutorials" case .settings: title = "Settings" case .browse: title = "Browse" } view.window?.title = title centeredTitleLabel?.stringValue = title } private func installCenteredTitleIfNeeded(on window: NSWindow) { guard centeredTitleLabel == nil else { return } guard let titlebarView = window.standardWindowButton(.closeButton)?.superview else { return } let label = NSTextField(labelWithString: window.title) label.translatesAutoresizingMaskIntoConstraints = false label.alignment = .center label.font = NSFont.titleBarFont(ofSize: 0) label.textColor = .labelColor label.lineBreakMode = .byTruncatingTail label.maximumNumberOfLines = 1 titlebarView.addSubview(label) NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: titlebarView.centerXAnchor), label.centerYAnchor.constraint(equalTo: titlebarView.centerYAnchor), label.leadingAnchor.constraint(greaterThanOrEqualTo: titlebarView.leadingAnchor, constant: 90), label.trailingAnchor.constraint(lessThanOrEqualTo: titlebarView.trailingAnchor, constant: -90) ]) window.titleVisibility = .hidden centeredTitleLabel = label } private func updateSidebarAppearance() { for (page, row) in sidebarRowViews { applySidebarRowStyle(row, page: page, logoTemplate: logoTemplateForSidebarPage(page)) } } private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool { switch page { case .photo, .tutorials: return false case .joinMeetings, .video, .settings, .browse: return true } } func makeSidebar() -> NSView { let sidebar = NSView() sidebar.translatesAutoresizingMaskIntoConstraints = false sidebar.wantsLayer = true sidebar.layer?.backgroundColor = palette.sidebarBackground.cgColor sidebar.layer?.borderColor = palette.separator.cgColor sidebar.layer?.borderWidth = 1 sidebar.layer?.shadowColor = NSColor.black.cgColor sidebar.layer?.shadowOpacity = 0.18 sidebar.layer?.shadowOffset = CGSize(width: 2, height: 0) sidebar.layer?.shadowRadius = 10 sidebar.widthAnchor.constraint(equalToConstant: 210).isActive = true let titleRow = NSStackView(views: [ iconLabel("📅", size: 24), textLabel("Meetings", font: typography.sidebarBrand, color: palette.textPrimary) ]) titleRow.translatesAutoresizingMaskIntoConstraints = false titleRow.orientation = .horizontal titleRow.alignment = .centerY titleRow.spacing = 8 let menuStack = NSStackView() menuStack.translatesAutoresizingMaskIntoConstraints = false menuStack.orientation = .vertical menuStack.alignment = .leading menuStack.spacing = 10 menuStack.addArrangedSubview(sidebarSectionTitle("Meetings")) let joinRow = sidebarItem("Join Meetings", icon: "􀉣", page: .joinMeetings, logoImageName: "JoinMeetingsLogo", logoIconWidth: 24, logoHeightMultiplier: 56.0 / 52.0) menuStack.addArrangedSubview(joinRow) sidebarRowViews[.joinMeetings] = joinRow menuStack.addArrangedSubview(sidebarSectionTitle("Backgrounds")) let photoRow = sidebarItem("Photo", icon: "􀏂", page: .photo, logoImageName: "SidebarPhotoLogo", logoIconWidth: 24, logoHeightMultiplier: 82.0 / 62.0) menuStack.addArrangedSubview(photoRow) sidebarRowViews[.photo] = photoRow let videoRow = sidebarItem("Video", icon: "􀎚", page: .video, logoImageName: "SidebarVideoLogo", logoIconWidth: 28, logoHeightMultiplier: 52.0 / 60.0) menuStack.addArrangedSubview(videoRow) sidebarRowViews[.video] = videoRow menuStack.addArrangedSubview(sidebarSectionTitle("Additional")) let tutorialsRow = sidebarItem("Tutorials", icon: "􀛩", page: .tutorials, logoImageName: "SidebarTutorialsLogo", logoIconWidth: 24, logoHeightMultiplier: 50.0 / 60.0) menuStack.addArrangedSubview(tutorialsRow) sidebarRowViews[.tutorials] = tutorialsRow let browseRow = sidebarItem("Browse", icon: "􀎆", page: .browse) menuStack.addArrangedSubview(browseRow) sidebarRowViews[.browse] = browseRow let settingsRow = sidebarItem("Settings", icon: "􀍟", page: .settings, logoImageName: "SidebarSettingsLogo", logoIconWidth: 28, logoHeightMultiplier: 68.0 / 62.0, showsDisclosure: true) menuStack.addArrangedSubview(settingsRow) sidebarRowViews[.settings] = settingsRow let premiumButton = sidebarPremiumButton() sidebar.addSubview(titleRow) sidebar.addSubview(menuStack) sidebar.addSubview(premiumButton) NSLayoutConstraint.activate([ titleRow.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 16), titleRow.topAnchor.constraint(equalTo: sidebar.topAnchor, constant: 24), titleRow.trailingAnchor.constraint(lessThanOrEqualTo: sidebar.trailingAnchor, constant: -16), menuStack.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 12), menuStack.trailingAnchor.constraint(equalTo: sidebar.trailingAnchor, constant: -12), menuStack.topAnchor.constraint(equalTo: titleRow.bottomAnchor, constant: 20), menuStack.bottomAnchor.constraint(lessThanOrEqualTo: premiumButton.topAnchor, constant: -16), premiumButton.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 12), premiumButton.trailingAnchor.constraint(equalTo: sidebar.trailingAnchor, constant: -12), premiumButton.bottomAnchor.constraint(equalTo: sidebar.bottomAnchor, constant: -14) ]) for subview in menuStack.arrangedSubviews { subview.widthAnchor.constraint(equalTo: menuStack.widthAnchor).isActive = true } return sidebar } func sidebarPremiumButton() -> NSView { let button = HoverTrackingView() button.translatesAutoresizingMaskIntoConstraints = false button.wantsLayer = true button.layer?.cornerRadius = 17 button.layer?.backgroundColor = palette.primaryBlue.cgColor button.heightAnchor.constraint(equalToConstant: 34).isActive = true styleSurface(button, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: false) let icon = textLabel("★", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .white) let title = textLabel("Get Premium", font: NSFont.systemFont(ofSize: 14, weight: .semibold), color: .white) button.addSubview(icon) button.addSubview(title) NSLayoutConstraint.activate([ icon.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 12), icon.centerYAnchor.constraint(equalTo: button.centerYAnchor), title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 8), title.centerYAnchor.constraint(equalTo: button.centerYAnchor), title.trailingAnchor.constraint(lessThanOrEqualTo: button.trailingAnchor, constant: -12) ]) let baseColor = palette.primaryBlue let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black let hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor button.onHoverChanged = { hovering in button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor } button.onHoverChanged?(false) let click = NSClickGestureRecognizer(target: self, action: #selector(premiumButtonClicked(_:))) button.addGestureRecognizer(click) return button } func makeMainPanel() -> NSView { let panel = NSView() panel.translatesAutoresizingMaskIntoConstraints = false panel.wantsLayer = true panel.layer?.backgroundColor = palette.pageBackground.cgColor let host = NSView() host.translatesAutoresizingMaskIntoConstraints = false panel.addSubview(host) NSLayoutConstraint.activate([ host.leadingAnchor.constraint(equalTo: panel.leadingAnchor), host.trailingAnchor.constraint(equalTo: panel.trailingAnchor), host.topAnchor.constraint(equalTo: panel.topAnchor), host.bottomAnchor.constraint(equalTo: panel.bottomAnchor) ]) mainContentHost = host showSidebarPage(.joinMeetings) return panel } func makeJoinMeetingsContent() -> NSView { let panel = NSView() panel.translatesAutoresizingMaskIntoConstraints = false let contentStack = NSStackView() contentStack.translatesAutoresizingMaskIntoConstraints = false contentStack.orientation = .vertical contentStack.spacing = 14 contentStack.alignment = .leading let joinActions = meetJoinActionsRow() contentStack.addArrangedSubview(textLabel("Join Meetings", font: typography.pageTitle, color: palette.textPrimary)) contentStack.addArrangedSubview(meetJoinSectionRow()) contentStack.addArrangedSubview(joinActions) contentStack.setCustomSpacing(26, after: joinActions) contentStack.addArrangedSubview(scheduleHeader()) contentStack.addArrangedSubview(textLabel("Tuesday, 14 Apr", font: typography.dateHeading, color: palette.textSecondary)) contentStack.addArrangedSubview(scheduleCardsRow()) panel.addSubview(contentStack) NSLayoutConstraint.activate([ contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28), contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28), contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26) ]) return panel } func meetJoinSectionRow() -> NSView { let row = NSStackView() row.translatesAutoresizingMaskIntoConstraints = false row.orientation = .horizontal row.spacing = 12 row.alignment = .top row.distribution = .fillEqually row.widthAnchor.constraint(greaterThanOrEqualToConstant: 880).isActive = true row.heightAnchor.constraint(equalToConstant: 140).isActive = true let instant = HoverSurfaceView() instant.translatesAutoresizingMaskIntoConstraints = false instant.wantsLayer = true instant.layer?.cornerRadius = 14 instant.layer?.backgroundColor = palette.sectionCard.cgColor styleSurface(instant, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let iconWrap = roundedContainer(cornerRadius: 12, color: NSColor.clear) iconWrap.translatesAutoresizingMaskIntoConstraints = false iconWrap.widthAnchor.constraint(equalToConstant: 58).isActive = true iconWrap.heightAnchor.constraint(equalToConstant: 58).isActive = true iconWrap.layer?.borderWidth = 0 let meetLogoImage = NSImage(named: "MeetLogo") ?? NSImage() meetLogoImage.isTemplate = false let meetLogo = NSImageView(image: meetLogoImage) meetLogo.translatesAutoresizingMaskIntoConstraints = false meetLogo.imageScaling = .scaleProportionallyDown meetLogo.contentTintColor = nil iconWrap.addSubview(meetLogo) let instantTitle = textLabel("New Instant Meet", font: NSFont.systemFont(ofSize: 40 / 2, weight: .semibold), color: palette.textPrimary) let instantSub = textLabel("Start instant Meet in more section with\nGoogle Meet meet.", font: NSFont.systemFont(ofSize: 16 / 2, weight: .medium), color: palette.textSecondary) instantSub.maximumNumberOfLines = 2 instant.addSubview(iconWrap) instant.addSubview(instantTitle) instant.addSubview(instantSub) let codeCard = HoverSurfaceView() codeCard.translatesAutoresizingMaskIntoConstraints = false codeCard.wantsLayer = true codeCard.layer?.cornerRadius = 14 codeCard.layer?.backgroundColor = palette.sectionCard.cgColor styleSurface(codeCard, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let codeTitle = textLabel("Join with Link", font: NSFont.systemFont(ofSize: 40 / 2, weight: .semibold), color: palette.textPrimary) let codeInputShell = roundedContainer(cornerRadius: 8, color: palette.inputBackground) codeInputShell.translatesAutoresizingMaskIntoConstraints = false codeInputShell.heightAnchor.constraint(equalToConstant: 52).isActive = true styleSurface(codeInputShell, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let codeField = NSTextField(string: "") codeField.translatesAutoresizingMaskIntoConstraints = false codeField.isEditable = true codeField.isBordered = false codeField.drawsBackground = false codeField.focusRingType = .none codeField.font = NSFont.systemFont(ofSize: 36 / 2, weight: .regular) codeField.textColor = palette.textPrimary codeField.placeholderString = "Code or meet.google.com/…" codeInputShell.addSubview(codeField) meetLinkField = codeField codeCard.addSubview(codeTitle) codeCard.addSubview(codeInputShell) NSLayoutConstraint.activate([ meetLogo.centerXAnchor.constraint(equalTo: iconWrap.centerXAnchor), meetLogo.centerYAnchor.constraint(equalTo: iconWrap.centerYAnchor), meetLogo.widthAnchor.constraint(equalToConstant: 46), meetLogo.heightAnchor.constraint(equalToConstant: 46), iconWrap.leadingAnchor.constraint(equalTo: instant.leadingAnchor, constant: 18), iconWrap.topAnchor.constraint(equalTo: instant.topAnchor, constant: 22), instantTitle.leadingAnchor.constraint(equalTo: iconWrap.trailingAnchor, constant: 14), instantTitle.topAnchor.constraint(equalTo: instant.topAnchor, constant: 24), instantSub.leadingAnchor.constraint(equalTo: instantTitle.leadingAnchor), instantSub.topAnchor.constraint(equalTo: instantTitle.bottomAnchor, constant: 6), instantSub.trailingAnchor.constraint(lessThanOrEqualTo: instant.trailingAnchor, constant: -16), codeTitle.leadingAnchor.constraint(equalTo: codeCard.leadingAnchor, constant: 18), codeTitle.topAnchor.constraint(equalTo: codeCard.topAnchor, constant: 22), codeInputShell.leadingAnchor.constraint(equalTo: codeCard.leadingAnchor, constant: 18), codeInputShell.trailingAnchor.constraint(equalTo: codeCard.trailingAnchor, constant: -18), codeInputShell.topAnchor.constraint(equalTo: codeTitle.bottomAnchor, constant: 12), codeField.leadingAnchor.constraint(equalTo: codeInputShell.leadingAnchor, constant: 14), codeField.trailingAnchor.constraint(equalTo: codeInputShell.trailingAnchor, constant: -14), codeField.centerYAnchor.constraint(equalTo: codeInputShell.centerYAnchor) ]) let baseColor = palette.sectionCard let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black let hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor instant.onHoverChanged = { hovering in instant.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor } codeCard.onHoverChanged = { hovering in codeCard.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor } instant.onHoverChanged?(false) codeCard.onHoverChanged?(false) row.addArrangedSubview(instant) row.addArrangedSubview(codeCard) return row } func meetJoinActionsRow() -> NSView { let row = NSStackView() row.translatesAutoresizingMaskIntoConstraints = false row.orientation = .horizontal row.spacing = 12 row.alignment = .centerY row.widthAnchor.constraint(greaterThanOrEqualToConstant: 880).isActive = true let spacer = NSView() spacer.translatesAutoresizingMaskIntoConstraints = false row.addArrangedSubview(spacer) row.addArrangedSubview(meetActionButton( title: "Cancel", color: palette.cancelButton, textColor: palette.textSecondary, width: 110, action: #selector(cancelMeetJoinClicked(_:)) )) row.addArrangedSubview(meetActionButton( title: "Join", color: palette.primaryBlue, textColor: .white, width: 116, action: #selector(joinMeetClicked(_:)) )) return row } func meetActionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat, action: Selector) -> NSButton { let button = NSButton(title: title, target: self, action: action) button.translatesAutoresizingMaskIntoConstraints = false button.isBordered = false button.bezelStyle = .regularSquare button.wantsLayer = true button.layer?.cornerRadius = 9 button.layer?.backgroundColor = color.cgColor button.layer?.borderColor = (title == "Cancel" ? palette.inputBorder : palette.primaryBlueBorder).cgColor button.layer?.borderWidth = 1 button.font = typography.buttonText button.contentTintColor = textColor button.widthAnchor.constraint(equalToConstant: width).isActive = true button.heightAnchor.constraint(equalToConstant: 36).isActive = true return button } func makePaywallContent() -> NSView { paywallPlanViews.removeAll() premiumPlanByView.removeAll() let panel = NSView() panel.translatesAutoresizingMaskIntoConstraints = false panel.wantsLayer = true panel.layer?.backgroundColor = palette.pageBackground.cgColor let contentStack = NSStackView() contentStack.translatesAutoresizingMaskIntoConstraints = false contentStack.orientation = .vertical contentStack.spacing = 12 contentStack.alignment = .leading panel.addSubview(contentStack) let topRow = NSStackView() topRow.translatesAutoresizingMaskIntoConstraints = false topRow.orientation = .horizontal topRow.alignment = .centerY topRow.distribution = .fill topRow.spacing = 10 topRow.addArrangedSubview(textLabel("Get Premium", font: NSFont.systemFont(ofSize: 24, weight: .bold), color: palette.textPrimary)) let topSpacer = NSView() topSpacer.translatesAutoresizingMaskIntoConstraints = false topRow.addArrangedSubview(topSpacer) let closeButton = iconRoundButton("✕", size: 28) topRow.addArrangedSubview(closeButton) let closeClick = NSClickGestureRecognizer(target: self, action: #selector(closePaywallClicked(_:))) closeButton.addGestureRecognizer(closeClick) topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true contentStack.addArrangedSubview(topRow) contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.textSecondary)) let benefits = paywallBenefitsSection() contentStack.addArrangedSubview(benefits) contentStack.setCustomSpacing(18, after: benefits) let weeklyCard = paywallPlanCard( title: "Weekly", price: "Rs 1,100.00", badge: "Basic Deal", badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1), subtitle: nil, plan: .weekly, strikePrice: nil ) contentStack.addArrangedSubview(weeklyCard) let monthlyCard = paywallPlanCard( title: "Monthly", price: "Rs 2,500.00", badge: "Free Trial", badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1), subtitle: "625.00/week", plan: .monthly, strikePrice: nil ) contentStack.addArrangedSubview(monthlyCard) let yearlyCard = paywallPlanCard( title: "Yearly", price: "Rs 9,900.00", badge: "Best Deal", badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1), subtitle: "190.38/week", plan: .yearly, strikePrice: nil ) contentStack.addArrangedSubview(yearlyCard) let lifetimeCard = paywallPlanCard( title: "Lifetime", price: "Rs 14,900.00", badge: "Save 50%", badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1), subtitle: nil, plan: .lifetime, strikePrice: "Rs 29,800.00" ) contentStack.addArrangedSubview(lifetimeCard) updatePaywallPlanSelection() contentStack.setCustomSpacing(20, after: lifetimeCard) let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary) offer.alignment = .center paywallOfferLabel = offer let offerWrap = NSView() offerWrap.translatesAutoresizingMaskIntoConstraints = false offerWrap.addSubview(offer) NSLayoutConstraint.activate([ offerWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth), offer.centerXAnchor.constraint(equalTo: offerWrap.centerXAnchor), offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 6), offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2) ]) contentStack.addArrangedSubview(offerWrap) contentStack.setCustomSpacing(18, after: offerWrap) let continueButton = roundedContainer(cornerRadius: 14, color: palette.primaryBlue) continueButton.translatesAutoresizingMaskIntoConstraints = false continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true) let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white) continueButton.addSubview(continueLabel) NSLayoutConstraint.activate([ continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor), continueLabel.centerYAnchor.constraint(equalTo: continueButton.centerYAnchor) ]) contentStack.addArrangedSubview(continueButton) contentStack.setCustomSpacing(16, after: continueButton) let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary) secure.alignment = .center let secureWrap = NSView() secureWrap.translatesAutoresizingMaskIntoConstraints = false secureWrap.addSubview(secure) NSLayoutConstraint.activate([ secureWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth), secure.centerXAnchor.constraint(equalTo: secureWrap.centerXAnchor), secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 4), secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -8) ]) contentStack.addArrangedSubview(secureWrap) contentStack.setCustomSpacing(16, after: secureWrap) let footer = paywallFooterLinks() contentStack.addArrangedSubview(footer) NSLayoutConstraint.activate([ contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18), contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18), contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 16), contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -12) ]) return panel } func paywallPlanCard( title: String, price: String, badge: String, badgeColor: NSColor, subtitle: String?, plan: PremiumPlan, strikePrice: String? ) -> NSView { let wrapper = NSView() wrapper.translatesAutoresizingMaskIntoConstraints = false wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true let card = roundedContainer(cornerRadius: 16, color: palette.sectionCard) card.translatesAutoresizingMaskIntoConstraints = false card.heightAnchor.constraint(equalToConstant: 82).isActive = true wrapper.addSubview(card) NSLayoutConstraint.activate([ card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor), card.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor), card.topAnchor.constraint(equalTo: wrapper.topAnchor, constant: 12), card.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor) ]) styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let badgeLabel = textLabel(badge, font: NSFont.systemFont(ofSize: 10, weight: .bold), color: .white) let badgeWrap = roundedContainer(cornerRadius: 10, color: badgeColor) badgeWrap.translatesAutoresizingMaskIntoConstraints = false badgeWrap.wantsLayer = true badgeWrap.layer?.borderColor = NSColor(calibratedWhite: 1, alpha: 0.22).cgColor badgeWrap.layer?.borderWidth = 1 badgeWrap.layer?.shadowColor = NSColor.black.cgColor badgeWrap.layer?.shadowOpacity = 0.20 badgeWrap.layer?.shadowOffset = CGSize(width: 0, height: -1) badgeWrap.layer?.shadowRadius = 3 badgeWrap.addSubview(badgeLabel) NSLayoutConstraint.activate([ badgeLabel.leadingAnchor.constraint(equalTo: badgeWrap.leadingAnchor, constant: 8), badgeLabel.trailingAnchor.constraint(equalTo: badgeWrap.trailingAnchor, constant: -8), badgeLabel.topAnchor.constraint(equalTo: badgeWrap.topAnchor, constant: 2), badgeLabel.bottomAnchor.constraint(equalTo: badgeWrap.bottomAnchor, constant: -2) ]) wrapper.addSubview(badgeWrap) let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 15, weight: .bold), color: palette.primaryBlue) card.addSubview(titleLabel) let priceLabel = textLabel(price, font: NSFont.systemFont(ofSize: 12, weight: .bold), color: palette.textPrimary) card.addSubview(priceLabel) NSLayoutConstraint.activate([ badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor), badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor), titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16), titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34), priceLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16), priceLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 32) ]) if let subtitle { let sub = textLabel(subtitle, font: NSFont.systemFont(ofSize: 10, weight: .semibold), color: palette.textSecondary) card.addSubview(sub) NSLayoutConstraint.activate([ sub.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor), sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 0) ]) } if let strikePrice { let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed) card.addSubview(strike) NSLayoutConstraint.activate([ strike.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor), strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4) ]) } let click = NSClickGestureRecognizer(target: self, action: #selector(paywallPlanClicked(_:))) wrapper.addGestureRecognizer(click) premiumPlanByView[ObjectIdentifier(wrapper)] = plan paywallPlanViews[plan] = card return wrapper } func paywallFooterLinks() -> NSView { let wrap = NSView() wrap.translatesAutoresizingMaskIntoConstraints = false wrap.heightAnchor.constraint(equalToConstant: 34).isActive = true wrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true let row = NSStackView() row.translatesAutoresizingMaskIntoConstraints = false row.orientation = .horizontal row.distribution = .fillEqually row.alignment = .centerY row.spacing = 0 wrap.addSubview(row) row.addArrangedSubview(footerLink("Privacy Policy")) row.addArrangedSubview(footerLink("Support")) row.addArrangedSubview(footerLink("Terms of Services")) NSLayoutConstraint.activate([ row.leadingAnchor.constraint(equalTo: wrap.leadingAnchor), row.trailingAnchor.constraint(equalTo: wrap.trailingAnchor), row.topAnchor.constraint(equalTo: wrap.topAnchor), row.bottomAnchor.constraint(equalTo: wrap.bottomAnchor) ]) return wrap } func footerLink(_ title: String) -> NSView { let container = HoverTrackingView() container.translatesAutoresizingMaskIntoConstraints = false let label = textLabel(title, font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary) label.alignment = .center container.addSubview(label) NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: container.centerXAnchor), label.centerYAnchor.constraint(equalTo: container.centerYAnchor) ]) let click = NSClickGestureRecognizer(target: self, action: #selector(paywallFooterLinkClicked(_:))) container.addGestureRecognizer(click) container.onHoverChanged = { hovering in label.textColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary } container.onHoverChanged?(false) return container } func paywallBenefitsSection() -> NSView { let stack = NSStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.orientation = .vertical stack.spacing = 8 stack.alignment = .leading stack.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true let rowOne = NSStackView() rowOne.translatesAutoresizingMaskIntoConstraints = false rowOne.orientation = .horizontal rowOne.spacing = 10 rowOne.distribution = .fillEqually rowOne.alignment = .centerY rowOne.addArrangedSubview(paywallBenefitItem(icon: "📅", text: "Manage meetings")) rowOne.addArrangedSubview(paywallBenefitItem(icon: "🖼️", text: "Virtual backgrounds")) let rowTwo = NSStackView() rowTwo.translatesAutoresizingMaskIntoConstraints = false rowTwo.orientation = .horizontal rowTwo.spacing = 10 rowTwo.distribution = .fillEqually rowTwo.alignment = .centerY rowTwo.addArrangedSubview(paywallBenefitItem(icon: "⚡", text: "Tools for productivity")) rowTwo.addArrangedSubview(paywallBenefitItem(icon: "🛟", text: "24/7 support")) stack.addArrangedSubview(rowOne) stack.addArrangedSubview(rowTwo) return stack } func paywallBenefitItem(icon: String, text: String) -> NSView { let card = roundedContainer(cornerRadius: 10, color: palette.inputBackground) card.translatesAutoresizingMaskIntoConstraints = false card.heightAnchor.constraint(equalToConstant: 36).isActive = true styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let iconWrap = roundedContainer(cornerRadius: 8, color: palette.inputBackground) iconWrap.translatesAutoresizingMaskIntoConstraints = false iconWrap.widthAnchor.constraint(equalToConstant: 24).isActive = true iconWrap.heightAnchor.constraint(equalToConstant: 24).isActive = true styleSurface(iconWrap, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let iconLabel = textLabel(icon, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.primaryBlue) iconWrap.addSubview(iconLabel) NSLayoutConstraint.activate([ iconLabel.centerXAnchor.constraint(equalTo: iconWrap.centerXAnchor), iconLabel.centerYAnchor.constraint(equalTo: iconWrap.centerYAnchor) ]) let title = textLabel(text, font: NSFont.systemFont(ofSize: 11, weight: .medium), color: palette.textPrimary) card.addSubview(iconWrap) card.addSubview(title) NSLayoutConstraint.activate([ iconWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 8), iconWrap.centerYAnchor.constraint(equalTo: card.centerYAnchor), title.leadingAnchor.constraint(equalTo: iconWrap.trailingAnchor, constant: 10), title.centerYAnchor.constraint(equalTo: card.centerYAnchor), title.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -8) ]) return card } func zoomJoinModeTabs() -> NSView { let row = NSStackView() row.translatesAutoresizingMaskIntoConstraints = false row.orientation = .horizontal row.alignment = .centerY row.spacing = 28 row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true let idTab = joinModeTab("Join with ID", mode: .id) let urlTab = joinModeTab("Join with URL", mode: .url) row.addArrangedSubview(idTab) row.addArrangedSubview(urlTab) let spacer = NSView() spacer.translatesAutoresizingMaskIntoConstraints = false row.addArrangedSubview(spacer) zoomJoinModeViews[.id] = idTab zoomJoinModeViews[.url] = urlTab updateZoomJoinModeAppearance() return row } func joinModeTab(_ title: String, mode: ZoomJoinMode) -> NSView { let tab = HoverTrackingView() tab.translatesAutoresizingMaskIntoConstraints = false tab.wantsLayer = true tab.layer?.cornerRadius = 6 tab.layer?.backgroundColor = NSColor.clear.cgColor tab.heightAnchor.constraint(equalToConstant: 30).isActive = true zoomJoinModeByView[ObjectIdentifier(tab)] = mode let label = textLabel(title, font: NSFont.systemFont(ofSize: 33 / 2, weight: .medium), color: palette.textPrimary) tab.addSubview(label) NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: tab.leadingAnchor, constant: 4), label.trailingAnchor.constraint(equalTo: tab.trailingAnchor, constant: -4), label.topAnchor.constraint(equalTo: tab.topAnchor, constant: 4), label.bottomAnchor.constraint(equalTo: tab.bottomAnchor, constant: -6) ]) let click = NSClickGestureRecognizer(target: self, action: #selector(zoomJoinModeClicked(_:))) tab.addGestureRecognizer(click) return tab } func updateZoomJoinModeAppearance() { for (mode, tab) in zoomJoinModeViews { let selected = (mode == selectedZoomJoinMode) let textColor = selected ? palette.textPrimary : palette.textSecondary let label = tab.subviews.first { $0 is NSTextField } as? NSTextField label?.textColor = textColor // Keep the active tab visually underlined like the reference. if selected { if tab.subviews.contains(where: { $0.identifier?.rawValue == "modeUnderline" }) == false { let underline = NSView() underline.identifier = NSUserInterfaceItemIdentifier("modeUnderline") underline.translatesAutoresizingMaskIntoConstraints = false underline.wantsLayer = true underline.layer?.backgroundColor = palette.primaryBlue.cgColor tab.addSubview(underline) NSLayoutConstraint.activate([ underline.leadingAnchor.constraint(equalTo: tab.leadingAnchor), underline.trailingAnchor.constraint(equalTo: tab.trailingAnchor), underline.bottomAnchor.constraint(equalTo: tab.bottomAnchor), underline.heightAnchor.constraint(equalToConstant: 2) ]) } } else { tab.subviews .filter { $0.identifier?.rawValue == "modeUnderline" } .forEach { $0.removeFromSuperview() } } } } func joinWithIDHeading() -> NSView { let container = NSView() container.translatesAutoresizingMaskIntoConstraints = false let title = textLabel("Join with ID", font: typography.joinWithURLTitle, color: palette.textPrimary) title.alignment = .left title.setContentHuggingPriority(.defaultHigh, for: .horizontal) title.setContentCompressionResistancePriority(.required, for: .horizontal) let bar = NSView() bar.translatesAutoresizingMaskIntoConstraints = false bar.wantsLayer = true bar.layer?.backgroundColor = palette.primaryBlue.cgColor bar.heightAnchor.constraint(equalToConstant: 3).isActive = true container.addSubview(title) container.addSubview(bar) NSLayoutConstraint.activate([ title.leadingAnchor.constraint(equalTo: container.leadingAnchor), title.topAnchor.constraint(equalTo: container.topAnchor), bar.leadingAnchor.constraint(equalTo: title.leadingAnchor), bar.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), bar.widthAnchor.constraint(equalTo: title.widthAnchor), bar.bottomAnchor.constraint(equalTo: container.bottomAnchor), container.trailingAnchor.constraint(equalTo: title.trailingAnchor) ]) return container } func zoomMeetingIDSection() -> NSView { let wrapper = NSView() wrapper.translatesAutoresizingMaskIntoConstraints = false let fieldsRow = NSStackView() fieldsRow.translatesAutoresizingMaskIntoConstraints = false fieldsRow.orientation = .horizontal fieldsRow.alignment = .top fieldsRow.distribution = .fillEqually fieldsRow.spacing = 12 fieldsRow.addArrangedSubview(zoomInputField(title: "Meeting ID", placeholder: "Enter meeting ID...")) fieldsRow.addArrangedSubview(zoomInputField(title: "Meeting Passcode", placeholder: "Enter meeting passcode...")) let actions = NSStackView() actions.orientation = .horizontal actions.spacing = 10 actions.translatesAutoresizingMaskIntoConstraints = false actions.alignment = .centerY actions.addArrangedSubview(actionButton(title: "Cancel", color: palette.cancelButton, textColor: palette.textSecondary, width: 110)) actions.addArrangedSubview(actionButton(title: "Join", color: palette.primaryBlue, textColor: .white, width: 116)) wrapper.addSubview(fieldsRow) wrapper.addSubview(actions) NSLayoutConstraint.activate([ wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 780), fieldsRow.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor), fieldsRow.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor), fieldsRow.topAnchor.constraint(equalTo: wrapper.topAnchor), actions.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor), actions.topAnchor.constraint(equalTo: fieldsRow.bottomAnchor, constant: 14), actions.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor) ]) return wrapper } func zoomInputField(title: String, placeholder: String) -> NSView { let wrapper = NSView() wrapper.translatesAutoresizingMaskIntoConstraints = false let heading = textLabel(title, font: typography.fieldLabel, color: palette.textPrimary) let textFieldContainer = roundedContainer(cornerRadius: 10, color: palette.inputBackground) textFieldContainer.translatesAutoresizingMaskIntoConstraints = false textFieldContainer.heightAnchor.constraint(equalToConstant: 40).isActive = true styleSurface(textFieldContainer, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let field = NSTextField(string: "") field.translatesAutoresizingMaskIntoConstraints = false field.isEditable = true field.isSelectable = true field.isBordered = false field.drawsBackground = false field.placeholderString = placeholder field.font = typography.inputPlaceholder field.textColor = palette.textPrimary field.focusRingType = .none textFieldContainer.addSubview(field) wrapper.addSubview(heading) wrapper.addSubview(textFieldContainer) NSLayoutConstraint.activate([ heading.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor), heading.topAnchor.constraint(equalTo: wrapper.topAnchor), textFieldContainer.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor), textFieldContainer.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor), textFieldContainer.topAnchor.constraint(equalTo: heading.bottomAnchor, constant: 10), textFieldContainer.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor), field.leadingAnchor.constraint(equalTo: textFieldContainer.leadingAnchor, constant: 12), field.trailingAnchor.constraint(equalTo: textFieldContainer.trailingAnchor, constant: -12), field.centerYAnchor.constraint(equalTo: textFieldContainer.centerYAnchor) ]) return wrapper } func joinWithURLHeading() -> NSView { let container = NSView() container.translatesAutoresizingMaskIntoConstraints = false let title = textLabel("Join with URL", font: typography.joinWithURLTitle, color: palette.textPrimary) title.alignment = .left title.setContentHuggingPriority(.defaultHigh, for: .horizontal) title.setContentCompressionResistancePriority(.required, for: .horizontal) let bar = NSView() bar.translatesAutoresizingMaskIntoConstraints = false bar.wantsLayer = true bar.layer?.backgroundColor = palette.primaryBlue.cgColor bar.heightAnchor.constraint(equalToConstant: 3).isActive = true container.addSubview(title) container.addSubview(bar) NSLayoutConstraint.activate([ title.leadingAnchor.constraint(equalTo: container.leadingAnchor), title.topAnchor.constraint(equalTo: container.topAnchor), bar.leadingAnchor.constraint(equalTo: title.leadingAnchor), bar.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), bar.widthAnchor.constraint(equalTo: title.widthAnchor), bar.bottomAnchor.constraint(equalTo: container.bottomAnchor), container.trailingAnchor.constraint(equalTo: title.trailingAnchor) ]) return container } func meetingUrlSection() -> NSView { let wrapper = NSView() wrapper.translatesAutoresizingMaskIntoConstraints = false let title = textLabel("Meeting URL", font: typography.fieldLabel, color: palette.textSecondary) let textFieldContainer = roundedContainer(cornerRadius: 10, color: palette.inputBackground) textFieldContainer.translatesAutoresizingMaskIntoConstraints = false textFieldContainer.heightAnchor.constraint(equalToConstant: 40).isActive = true styleSurface(textFieldContainer, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let urlField = NSTextField(string: "") urlField.translatesAutoresizingMaskIntoConstraints = false urlField.isEditable = true urlField.isSelectable = true urlField.isBordered = false urlField.drawsBackground = false urlField.placeholderString = "Enter meeting URL..." urlField.font = typography.inputPlaceholder urlField.textColor = palette.textPrimary urlField.focusRingType = .none textFieldContainer.addSubview(urlField) let actions = NSStackView() actions.orientation = .horizontal actions.spacing = 10 actions.translatesAutoresizingMaskIntoConstraints = false actions.alignment = .centerY actions.addArrangedSubview(actionButton(title: "Cancel", color: palette.cancelButton, textColor: palette.textSecondary, width: 110)) actions.addArrangedSubview(actionButton(title: "Join", color: palette.primaryBlue, textColor: .white, width: 116)) wrapper.addSubview(title) wrapper.addSubview(textFieldContainer) wrapper.addSubview(actions) NSLayoutConstraint.activate([ wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 780), title.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor), title.topAnchor.constraint(equalTo: wrapper.topAnchor), textFieldContainer.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor), textFieldContainer.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor), textFieldContainer.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 10), urlField.leadingAnchor.constraint(equalTo: textFieldContainer.leadingAnchor, constant: 12), urlField.trailingAnchor.constraint(equalTo: textFieldContainer.trailingAnchor, constant: -12), urlField.centerYAnchor.constraint(equalTo: textFieldContainer.centerYAnchor), actions.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor), actions.topAnchor.constraint(equalTo: textFieldContainer.bottomAnchor, constant: 14), actions.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor) ]) return wrapper } func scheduleHeader() -> NSView { let row = NSStackView() row.translatesAutoresizingMaskIntoConstraints = false row.orientation = .horizontal row.alignment = .centerY row.distribution = .fill row.spacing = 12 row.addArrangedSubview(textLabel("Schedule", font: typography.sectionTitleBold, color: palette.textPrimary)) let spacer = NSView() spacer.translatesAutoresizingMaskIntoConstraints = false row.addArrangedSubview(spacer) spacer.setContentHuggingPriority(.defaultLow, for: .horizontal) row.addArrangedSubview(iconRoundButton("?", size: 34)) row.addArrangedSubview(iconRoundButton("⟳", size: 34)) let filter = roundedContainer(cornerRadius: 8, color: palette.inputBackground) filter.translatesAutoresizingMaskIntoConstraints = false filter.widthAnchor.constraint(equalToConstant: 156).isActive = true filter.heightAnchor.constraint(equalToConstant: 34).isActive = true styleSurface(filter, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let filterText = textLabel("All", font: typography.filterText, color: palette.textSecondary) let arrow = textLabel("▾", font: typography.filterArrow, color: palette.textMuted) filterText.translatesAutoresizingMaskIntoConstraints = false arrow.translatesAutoresizingMaskIntoConstraints = false filter.addSubview(filterText) filter.addSubview(arrow) NSLayoutConstraint.activate([ filterText.leadingAnchor.constraint(equalTo: filter.leadingAnchor, constant: 12), filterText.centerYAnchor.constraint(equalTo: filter.centerYAnchor), arrow.trailingAnchor.constraint(equalTo: filter.trailingAnchor, constant: -10), arrow.centerYAnchor.constraint(equalTo: filter.centerYAnchor) ]) row.addArrangedSubview(filter) row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true return row } func scheduleCardsRow() -> NSView { let row = NSStackView() row.translatesAutoresizingMaskIntoConstraints = false row.orientation = .horizontal row.spacing = 10 row.alignment = .top row.distribution = .fill row.setContentHuggingPriority(.defaultHigh, for: .horizontal) row.heightAnchor.constraint(equalToConstant: 136).isActive = true row.addArrangedSubview(scheduleCard()) row.addArrangedSubview(scheduleCard()) return row } func scheduleCard() -> NSView { let cardWidth: CGFloat = 264 let card = roundedContainer(cornerRadius: 10, color: palette.sectionCard) styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: true) card.translatesAutoresizingMaskIntoConstraints = false card.widthAnchor.constraint(equalToConstant: cardWidth).isActive = true card.heightAnchor.constraint(equalToConstant: 136).isActive = true let icon = roundedContainer(cornerRadius: 5, color: palette.meetingBadge) icon.translatesAutoresizingMaskIntoConstraints = false icon.widthAnchor.constraint(equalToConstant: 22).isActive = true icon.heightAnchor.constraint(equalToConstant: 22).isActive = true let iconText = textLabel("••", font: typography.cardIcon, color: .white) iconText.translatesAutoresizingMaskIntoConstraints = false icon.addSubview(iconText) NSLayoutConstraint.activate([ iconText.centerXAnchor.constraint(equalTo: icon.centerXAnchor), iconText.centerYAnchor.constraint(equalTo: icon.centerYAnchor) ]) let title = textLabel("General Meeting", font: typography.cardTitle, color: palette.textPrimary) let subtitle = textLabel("Baisakhi", font: typography.cardSubtitle, color: palette.textPrimary) let time = textLabel("12:00 AM - 11:59 PM", font: typography.cardTime, color: palette.textSecondary) card.addSubview(icon) card.addSubview(title) card.addSubview(subtitle) card.addSubview(time) NSLayoutConstraint.activate([ icon.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10), icon.topAnchor.constraint(equalTo: card.topAnchor, constant: 10), title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 6), title.centerYAnchor.constraint(equalTo: icon.centerYAnchor), title.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -10), subtitle.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10), subtitle.topAnchor.constraint(equalTo: icon.bottomAnchor, constant: 7), time.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10), time.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 4) ]) return card } } extension ViewController: NSTextFieldDelegate { func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { if control === browseAddressField, commandSelector == #selector(NSResponder.insertNewline(_:)) { browseOpenAddressClicked(nil) return true } return false } } /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them. private class RowHitTestView: NSView { override func hitTest(_ point: NSPoint) -> NSView? { return bounds.contains(point) ? self : nil } } private final class HoverTrackingView: RowHitTestView { var onHoverChanged: ((Bool) -> Void)? var showsHandCursor = true private var trackingAreaRef: NSTrackingArea? private var isHovering = false { didSet { guard isHovering != oldValue else { return } onHoverChanged?(isHovering) } } override func updateTrackingAreas() { super.updateTrackingAreas() if let trackingAreaRef { removeTrackingArea(trackingAreaRef) } let options: NSTrackingArea.Options = [ .activeInKeyWindow, .inVisibleRect, .mouseEnteredAndExited ] let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil) addTrackingArea(area) trackingAreaRef = area } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) isHovering = true } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) isHovering = false } override func resetCursorRects() { super.resetCursorRects() guard showsHandCursor else { return } addCursorRect(bounds, cursor: .pointingHand) } } /// Hover tracking without overriding hit-testing; keeps controls like text fields interactive. private final class HoverSurfaceView: NSView { var onHoverChanged: ((Bool) -> Void)? private var trackingAreaRef: NSTrackingArea? private var isHovering = false { didSet { guard isHovering != oldValue else { return } onHoverChanged?(isHovering) } } override func updateTrackingAreas() { super.updateTrackingAreas() if let trackingAreaRef { removeTrackingArea(trackingAreaRef) } let options: NSTrackingArea.Options = [ .activeInKeyWindow, .inVisibleRect, .mouseEnteredAndExited ] let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil) addTrackingArea(area) trackingAreaRef = area } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) isHovering = true } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) isHovering = false } } private final class SettingsMenuViewController: NSViewController { private let palette: Palette private let typography: Typography private let onToggleDarkMode: (Bool) -> Void private let onAction: (SettingsAction) -> Void private var darkToggle: NSSwitch? init( palette: Palette, typography: Typography, darkModeEnabled: Bool, onToggleDarkMode: @escaping (Bool) -> Void, onAction: @escaping (SettingsAction) -> Void ) { self.palette = palette self.typography = typography self.onToggleDarkMode = onToggleDarkMode self.onAction = onAction super.init(nibName: nil, bundle: nil) self.view = makeView(darkModeEnabled: darkModeEnabled) } @available(*, unavailable) required init?(coder: NSCoder) { nil } func setDarkModeEnabled(_ enabled: Bool) { darkToggle?.state = enabled ? .on : .off } private func makeView(darkModeEnabled: Bool) -> NSView { let root = NSView() root.translatesAutoresizingMaskIntoConstraints = false let card = roundedCard() root.addSubview(card) NSLayoutConstraint.activate([ card.leadingAnchor.constraint(equalTo: root.leadingAnchor), card.trailingAnchor.constraint(equalTo: root.trailingAnchor), card.topAnchor.constraint(equalTo: root.topAnchor), card.bottomAnchor.constraint(equalTo: root.bottomAnchor), root.widthAnchor.constraint(equalToConstant: 260) ]) let stack = NSStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.orientation = .vertical stack.spacing = 6 stack.alignment = .leading card.addSubview(stack) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14), stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14), stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 14), stack.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -14) ]) stack.addArrangedSubview(settingsDarkModeRow(enabled: darkModeEnabled)) stack.addArrangedSubview(settingsActionRow(icon: "⟳", title: "Restore", action: .restore)) stack.addArrangedSubview(settingsActionRow(icon: "★", title: "Rate Us", action: .rateUs)) stack.addArrangedSubview(settingsActionRow(icon: "💬", title: "Support", action: .support)) stack.addArrangedSubview(settingsActionRow(icon: "⋯", title: "More Apps", action: .moreApps)) stack.addArrangedSubview(settingsActionRow(icon: "⤴︎", title: "Share App", action: .shareApp)) for v in stack.arrangedSubviews { v.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true } return root } private func roundedCard() -> NSView { let view = NSView() view.translatesAutoresizingMaskIntoConstraints = false view.wantsLayer = true view.layer?.cornerRadius = 12 view.layer?.backgroundColor = palette.sectionCard.cgColor view.layer?.borderColor = palette.inputBorder.cgColor view.layer?.borderWidth = 1 view.layer?.shadowColor = NSColor.black.cgColor view.layer?.shadowOpacity = 0.28 view.layer?.shadowOffset = CGSize(width: 0, height: -1) view.layer?.shadowRadius = 10 return view } private func settingsDarkModeRow(enabled: Bool) -> NSView { let row = NSView() row.translatesAutoresizingMaskIntoConstraints = false row.heightAnchor.constraint(equalToConstant: 44).isActive = true row.wantsLayer = true row.layer?.cornerRadius = 10 let icon = NSTextField(labelWithString: "◐") icon.translatesAutoresizingMaskIntoConstraints = false icon.font = NSFont.systemFont(ofSize: 18, weight: .medium) icon.textColor = palette.textPrimary let title = NSTextField(labelWithString: "Dark Mode") title.translatesAutoresizingMaskIntoConstraints = false title.font = NSFont.systemFont(ofSize: 16, weight: .semibold) title.textColor = palette.textPrimary let toggle = NSSwitch() toggle.translatesAutoresizingMaskIntoConstraints = false toggle.state = enabled ? .on : .off toggle.target = self toggle.action = #selector(darkModeToggled(_:)) darkToggle = toggle row.addSubview(icon) row.addSubview(title) row.addSubview(toggle) row.layer?.backgroundColor = NSColor.clear.cgColor NSLayoutConstraint.activate([ icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4), icon.centerYAnchor.constraint(equalTo: row.centerYAnchor), title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 10), title.centerYAnchor.constraint(equalTo: row.centerYAnchor), toggle.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -2), toggle.centerYAnchor.constraint(equalTo: row.centerYAnchor) ]) return row } private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView { let row = HoverTrackingView() row.translatesAutoresizingMaskIntoConstraints = false row.heightAnchor.constraint(equalToConstant: 42).isActive = true let iconLabel = NSTextField(labelWithString: icon) iconLabel.translatesAutoresizingMaskIntoConstraints = false iconLabel.font = NSFont.systemFont(ofSize: 18, weight: .medium) iconLabel.textColor = palette.textPrimary let titleLabel = NSTextField(labelWithString: title) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.font = NSFont.systemFont(ofSize: 16, weight: .semibold) titleLabel.textColor = palette.textPrimary row.addSubview(iconLabel) row.addSubview(titleLabel) NSLayoutConstraint.activate([ iconLabel.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4), iconLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor), titleLabel.leadingAnchor.constraint(equalTo: iconLabel.trailingAnchor, constant: 10), titleLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor) ]) let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:))) row.addGestureRecognizer(click) row.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(action.rawValue)") row.onHoverChanged = { hovering in row.wantsLayer = true row.layer?.cornerRadius = 10 row.layer?.backgroundColor = (hovering ? self.palette.inputBackground : NSColor.clear).cgColor } row.onHoverChanged?(false) return row } @objc private func darkModeToggled(_ sender: NSSwitch) { onToggleDarkMode(sender.state == .on) } @objc private func settingsActionClicked(_ sender: NSClickGestureRecognizer) { guard let view = sender.view, let raw = Int(view.identifier?.rawValue ?? ""), let action = SettingsAction(rawValue: raw) else { return } onAction(action) } } private extension ViewController { func roundedContainer(cornerRadius: CGFloat, color: NSColor) -> NSView { let view = NSView() view.wantsLayer = true view.layer?.backgroundColor = color.cgColor view.layer?.cornerRadius = cornerRadius return view } func styleSurface(_ view: NSView, borderColor: NSColor, borderWidth: CGFloat, shadow: Bool) { view.layer?.borderColor = borderColor.cgColor view.layer?.borderWidth = borderWidth if shadow { view.layer?.shadowColor = NSColor.black.cgColor view.layer?.shadowOpacity = 0.18 view.layer?.shadowOffset = CGSize(width: 0, height: -1) view.layer?.shadowRadius = 5 } } func textLabel(_ text: String, font: NSFont, color: NSColor) -> NSTextField { let label = NSTextField(labelWithString: text) label.translatesAutoresizingMaskIntoConstraints = false label.textColor = color label.font = font return label } func iconLabel(_ text: String, size: CGFloat) -> NSTextField { let label = NSTextField(labelWithString: text) label.translatesAutoresizingMaskIntoConstraints = false label.font = NSFont.systemFont(ofSize: size) return label } func sidebarSectionTitle(_ text: String) -> NSTextField { let field = textLabel(text, font: typography.sidebarSection, color: palette.textMuted) field.alignment = .left return field } func sidebarItem(_ text: String, icon: String, page: SidebarPage, logoImageName: String? = nil, logoIconWidth: CGFloat = 18, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true, showsDisclosure: Bool = false) -> NSView { let item = HoverTrackingView() item.wantsLayer = true item.layer?.cornerRadius = 10 item.layer?.backgroundColor = NSColor.clear.cgColor item.translatesAutoresizingMaskIntoConstraints = false item.heightAnchor.constraint(equalToConstant: 36).isActive = true item.layer?.borderWidth = 0 sidebarPageByView[ObjectIdentifier(item)] = page let leadingView: NSView if let name = logoImageName, let logo = NSImage(named: name) { logo.isTemplate = true let imageView = NSImageView(image: logo) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.imageScaling = .scaleProportionallyDown imageView.imageAlignment = .alignCenter imageView.isEditable = false leadingView = imageView } else { leadingView = textLabel(icon, font: typography.sidebarIcon, color: palette.textSecondary) } let titleLabel = textLabel(text, font: typography.sidebarItem, color: palette.textSecondary) titleLabel.alignment = .left item.addSubview(leadingView) item.addSubview(titleLabel) var constraints: [NSLayoutConstraint] = [ leadingView.leadingAnchor.constraint(equalTo: item.leadingAnchor, constant: 12), leadingView.centerYAnchor.constraint(equalTo: item.centerYAnchor), titleLabel.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 8), titleLabel.centerYAnchor.constraint(equalTo: item.centerYAnchor) ] if showsDisclosure { let chevron = textLabel("›", font: NSFont.systemFont(ofSize: 22, weight: .semibold), color: palette.textSecondary) chevron.translatesAutoresizingMaskIntoConstraints = false chevron.alignment = .right item.addSubview(chevron) constraints.append(contentsOf: [ chevron.trailingAnchor.constraint(equalTo: item.trailingAnchor, constant: -10), chevron.centerYAnchor.constraint(equalTo: item.centerYAnchor), titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: chevron.leadingAnchor, constant: -8) ]) } else { constraints.append(titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: item.trailingAnchor, constant: -10)) } if logoImageName != nil { let h = logoIconWidth * logoHeightMultiplier constraints.append(contentsOf: [ leadingView.widthAnchor.constraint(equalToConstant: logoIconWidth), leadingView.heightAnchor.constraint(equalToConstant: h) ]) } NSLayoutConstraint.activate(constraints) applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate) item.onHoverChanged = { [weak self, weak item] hovering in guard let self, let item else { return } self.applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate, hovering: hovering) } let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:))) item.addGestureRecognizer(click) return item } func applySidebarRowStyle(_ item: NSView, page: SidebarPage, logoTemplate: Bool, hovering: Bool = false) { let selected = (page == selectedSidebarPage) let hoverColor = darkModeEnabled ? NSColor(calibratedWhite: 1, alpha: 0.07) : NSColor(calibratedWhite: 0, alpha: 0.08) item.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor let tint = selected ? NSColor.white : palette.textSecondary let sidebarIconTint = darkModeEnabled ? tint : NSColor.black guard item.subviews.count >= 2 else { return } let leading = item.subviews[0] let title = item.subviews.first { $0 is NSTextField } as? NSTextField title?.textColor = tint // Optional disclosure chevron (if present) is the last text field. if let chevron = item.subviews.last as? NSTextField, chevron !== title { chevron.textColor = sidebarIconTint } if let imageView = leading as? NSImageView { if logoTemplate { imageView.contentTintColor = sidebarIconTint } } else if let iconField = leading as? NSTextField { iconField.textColor = sidebarIconTint } } func actionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat) -> NSView { let button = HoverTrackingView() button.wantsLayer = true button.layer?.cornerRadius = 9 button.layer?.backgroundColor = color.cgColor button.translatesAutoresizingMaskIntoConstraints = false button.widthAnchor.constraint(equalToConstant: width).isActive = true button.heightAnchor.constraint(equalToConstant: 36).isActive = true styleSurface(button, borderColor: title == "Cancel" ? palette.inputBorder : palette.primaryBlueBorder, borderWidth: 1, shadow: false) if title == "Cancel" { button.layer?.backgroundColor = palette.cancelButton.cgColor } let label = textLabel(title, font: typography.buttonText, color: textColor) button.addSubview(label) NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: button.centerXAnchor), label.centerYAnchor.constraint(equalTo: button.centerYAnchor) ]) let baseColor = (title == "Cancel") ? palette.cancelButton : color let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black let hoverColor = baseColor.blended(withFraction: 0.12, of: hoverBlend) ?? baseColor button.onHoverChanged = { hovering in button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor } button.onHoverChanged?(false) return button } func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView { let button = HoverTrackingView() button.wantsLayer = true button.layer?.cornerRadius = size / 2 button.layer?.backgroundColor = palette.inputBackground.cgColor button.translatesAutoresizingMaskIntoConstraints = false button.widthAnchor.constraint(equalToConstant: size).isActive = true button.heightAnchor.constraint(equalToConstant: size).isActive = true styleSurface(button, borderColor: palette.inputBorder, borderWidth: 1, shadow: false) let label = textLabel(symbol, font: typography.iconButton, color: palette.textSecondary) button.addSubview(label) NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: button.centerXAnchor), label.centerYAnchor.constraint(equalTo: button.centerYAnchor) ]) let baseColor = palette.inputBackground let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black let hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor button.onHoverChanged = { hovering in button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor } button.onHoverChanged?(false) return button } } private struct Palette { let pageBackground: NSColor let sidebarBackground: NSColor let sectionCard: NSColor let tabBarBackground: NSColor let tabIdleBackground: NSColor let inputBackground: NSColor let inputBorder: NSColor let primaryBlue: NSColor let primaryBlueBorder: NSColor let cancelButton: NSColor let meetingBadge: NSColor let separator: NSColor let textPrimary: NSColor let textSecondary: NSColor let textTertiary: NSColor let textMuted: NSColor init(isDarkMode: Bool) { if isDarkMode { pageBackground = NSColor(calibratedRed: 10.0 / 255.0, green: 11.0 / 255.0, blue: 12.0 / 255.0, alpha: 1) sidebarBackground = NSColor(calibratedRed: 16.0 / 255.0, green: 17.0 / 255.0, blue: 19.0 / 255.0, alpha: 1) sectionCard = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1) tabBarBackground = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1) tabIdleBackground = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1) inputBackground = NSColor(calibratedRed: 20.0 / 255.0, green: 21.0 / 255.0, blue: 24.0 / 255.0, alpha: 1) inputBorder = NSColor(calibratedRed: 38.0 / 255.0, green: 40.0 / 255.0, blue: 44.0 / 255.0, alpha: 1) primaryBlue = NSColor(calibratedRed: 27.0 / 255.0, green: 115.0 / 255.0, blue: 232.0 / 255.0, alpha: 1) primaryBlueBorder = NSColor(calibratedRed: 42.0 / 255.0, green: 118.0 / 255.0, blue: 220.0 / 255.0, alpha: 1) cancelButton = NSColor(calibratedRed: 20.0 / 255.0, green: 21.0 / 255.0, blue: 24.0 / 255.0, alpha: 1) meetingBadge = NSColor(calibratedRed: 0.88, green: 0.66, blue: 0.14, alpha: 1) separator = NSColor(calibratedRed: 26.0 / 255.0, green: 27.0 / 255.0, blue: 30.0 / 255.0, alpha: 1) textPrimary = NSColor(calibratedWhite: 0.98, alpha: 1) textSecondary = NSColor(calibratedWhite: 0.78, alpha: 1) textTertiary = NSColor(calibratedWhite: 0.66, alpha: 1) textMuted = NSColor(calibratedWhite: 0.44, alpha: 1) } else { pageBackground = NSColor(calibratedRed: 244.0 / 255.0, green: 246.0 / 255.0, blue: 249.0 / 255.0, alpha: 1) sidebarBackground = NSColor(calibratedRed: 232.0 / 255.0, green: 236.0 / 255.0, blue: 242.0 / 255.0, alpha: 1) sectionCard = NSColor.white tabBarBackground = NSColor.white tabIdleBackground = NSColor.white inputBackground = NSColor(calibratedRed: 247.0 / 255.0, green: 249.0 / 255.0, blue: 252.0 / 255.0, alpha: 1) inputBorder = NSColor(calibratedRed: 211.0 / 255.0, green: 218.0 / 255.0, blue: 228.0 / 255.0, alpha: 1) primaryBlue = NSColor(calibratedRed: 27.0 / 255.0, green: 115.0 / 255.0, blue: 232.0 / 255.0, alpha: 1) primaryBlueBorder = NSColor(calibratedRed: 42.0 / 255.0, green: 118.0 / 255.0, blue: 220.0 / 255.0, alpha: 1) cancelButton = NSColor(calibratedRed: 240.0 / 255.0, green: 243.0 / 255.0, blue: 248.0 / 255.0, alpha: 1) meetingBadge = NSColor(calibratedRed: 0.88, green: 0.66, blue: 0.14, alpha: 1) separator = NSColor(calibratedRed: 212.0 / 255.0, green: 219.0 / 255.0, blue: 229.0 / 255.0, alpha: 1) textPrimary = NSColor(calibratedRed: 32.0 / 255.0, green: 38.0 / 255.0, blue: 47.0 / 255.0, alpha: 1) textSecondary = NSColor(calibratedRed: 82.0 / 255.0, green: 92.0 / 255.0, blue: 107.0 / 255.0, alpha: 1) textTertiary = NSColor(calibratedRed: 110.0 / 255.0, green: 120.0 / 255.0, blue: 136.0 / 255.0, alpha: 1) textMuted = NSColor(calibratedRed: 134.0 / 255.0, green: 145.0 / 255.0, blue: 162.0 / 255.0, alpha: 1) } } } private struct Typography { let sidebarBrand = NSFont.systemFont(ofSize: 26, weight: .bold) let sidebarSection = NSFont.systemFont(ofSize: 11, weight: .medium) let sidebarIcon = NSFont.systemFont(ofSize: 12, weight: .medium) let sidebarItem = NSFont.systemFont(ofSize: 16, weight: .medium) let pageTitle = NSFont.systemFont(ofSize: 27, weight: .semibold) let joinWithURLTitle = NSFont.systemFont(ofSize: 17, weight: .semibold) let sectionTitleBold = NSFont.systemFont(ofSize: 25, weight: .bold) let dateHeading = NSFont.systemFont(ofSize: 18, weight: .medium) let tabIcon = NSFont.systemFont(ofSize: 13, weight: .regular) let tabTitle = NSFont.systemFont(ofSize: 31 / 2, weight: .semibold) let fieldLabel = NSFont.systemFont(ofSize: 15, weight: .medium) let inputPlaceholder = NSFont.systemFont(ofSize: 14, weight: .regular) let buttonText = NSFont.systemFont(ofSize: 16, weight: .medium) let filterText = NSFont.systemFont(ofSize: 15, weight: .regular) let filterArrow = NSFont.systemFont(ofSize: 12, weight: .regular) let iconButton = NSFont.systemFont(ofSize: 14, weight: .medium) let cardIcon = NSFont.systemFont(ofSize: 8, weight: .bold) let cardTitle = NSFont.systemFont(ofSize: 15, weight: .semibold) let cardSubtitle = NSFont.systemFont(ofSize: 13, weight: .bold) let cardTime = NSFont.systemFont(ofSize: 12, weight: .regular) } // MARK: - In-app browser (macOS WKWebView + chrome) // Note: This target is AppKit/macOS. iOS would use WKWebView or SFSafariViewController; Android would use WebView or Custom Tabs. private enum InAppBrowserURLPolicy: Equatable { case allowAll case whitelist(hostSuffixes: [String]) } private func inAppBrowserURLAllowed(_ url: URL, policy: InAppBrowserURLPolicy) -> Bool { let scheme = (url.scheme ?? "").lowercased() if scheme == "about" { return true } guard scheme == "http" || scheme == "https" else { return false } guard let host = url.host?.lowercased() else { return false } switch policy { case .allowAll: return true case .whitelist(let suffixes): for suffix in suffixes { let s = suffix.lowercased() if host == s || host.hasSuffix("." + s) { return true } } return false } } private enum InAppBrowserWebKitSupport { static let sharedProcessPool = WKProcessPool() static func makeWebViewConfiguration() -> WKWebViewConfiguration { let config = WKWebViewConfiguration() config.processPool = sharedProcessPool config.websiteDataStore = .default() config.preferences.javaScriptCanOpenWindowsAutomatically = true if #available(macOS 12.3, *) { config.preferences.isElementFullscreenEnabled = true } config.mediaTypesRequiringUserActionForPlayback = [] if #available(macOS 11.0, *) { config.defaultWebpagePreferences.allowsContentJavaScript = true } config.applicationNameForUserAgent = "MeetingsApp/1.0" return config } } private final class InAppBrowserWindowController: NSWindowController { private static let defaultContentSize = NSSize(width: 1100, height: 760) private static let minimumContentSize = NSSize(width: 800, height: 520) private let browserViewController = InAppBrowserContainerViewController() init() { let browserWindow = NSWindow( contentRect: NSRect(origin: .zero, size: Self.defaultContentSize), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false ) browserWindow.title = "Browser" browserWindow.isRestorable = false browserWindow.setFrameAutosaveName("") browserWindow.minSize = browserWindow.frameRect(forContentRect: NSRect(origin: .zero, size: Self.minimumContentSize)).size browserWindow.center() browserWindow.contentViewController = browserViewController super.init(window: browserWindow) } @available(*, unavailable) required init?(coder: NSCoder) { nil } /// Resets size and position each time the browser is shown so a previously tiny window is never reused. func applyDefaultFrameCenteredOnVisibleScreen() { guard let w = window, let screen = w.screen ?? NSScreen.main else { return } let windowFrame = w.frameRect(forContentRect: NSRect(origin: .zero, size: Self.defaultContentSize)) let vf = screen.visibleFrame var frame = windowFrame frame.origin.x = vf.midX - frame.width / 2 frame.origin.y = vf.midY - frame.height / 2 if frame.maxX > vf.maxX { frame.origin.x = vf.maxX - frame.width } if frame.minX < vf.minX { frame.origin.x = vf.minX } if frame.maxY > vf.maxY { frame.origin.y = vf.maxY - frame.height } if frame.minY < vf.minY { frame.origin.y = vf.minY } w.setFrame(frame, display: true) } func load(url: URL, policy: InAppBrowserURLPolicy) { browserViewController.setNavigationPolicy(policy) browserViewController.load(url: url) } } private final class InAppBrowserContainerViewController: NSViewController, WKNavigationDelegate, WKUIDelegate, NSTextFieldDelegate { private var webView: WKWebView! private var webContainerView: NSView! private weak var urlField: NSTextField? private var backButton: NSButton! private var forwardButton: NSButton! private var reloadStopButton: NSButton! private var goButton: NSButton! private var progressBar: NSProgressIndicator! private var lastLoadedURL: URL? private var navigationPolicy: InAppBrowserURLPolicy = .allowAll private var processTerminateRetryCount = 0 /// Includes fresh WKWebView instances so each retry gets a new WebContent process after a crash. private let maxProcessTerminateRetries = 3 private var kvoTokens: [NSKeyValueObservation] = [] deinit { kvoTokens.removeAll() } func setNavigationPolicy(_ policy: InAppBrowserURLPolicy) { navigationPolicy = policy } override func loadView() { let root = NSView() root.translatesAutoresizingMaskIntoConstraints = false let wv = makeWebView() webView = wv let webHost = NSView() webHost.translatesAutoresizingMaskIntoConstraints = false webHost.wantsLayer = true webHost.addSubview(wv) NSLayoutConstraint.activate([ wv.leadingAnchor.constraint(equalTo: webHost.leadingAnchor), wv.trailingAnchor.constraint(equalTo: webHost.trailingAnchor), wv.topAnchor.constraint(equalTo: webHost.topAnchor), wv.bottomAnchor.constraint(equalTo: webHost.bottomAnchor) ]) webContainerView = webHost let toolbar = NSStackView() toolbar.translatesAutoresizingMaskIntoConstraints = false toolbar.orientation = .horizontal toolbar.spacing = 8 toolbar.alignment = .centerY toolbar.edgeInsets = NSEdgeInsets(top: 6, left: 10, bottom: 6, right: 10) backButton = makeToolbarButton(title: "◀", symbolName: "chevron.backward", accessibilityDescription: "Back") backButton.target = self backButton.action = #selector(goBack) forwardButton = makeToolbarButton(title: "▶", symbolName: "chevron.forward", accessibilityDescription: "Forward") forwardButton.target = self forwardButton.action = #selector(goForward) reloadStopButton = makeToolbarButton(title: "Reload", symbolName: "arrow.clockwise", accessibilityDescription: "Reload") reloadStopButton.target = self reloadStopButton.action = #selector(reloadOrStop) let field = NSTextField(string: "") field.translatesAutoresizingMaskIntoConstraints = false field.font = NSFont.systemFont(ofSize: 13, weight: .regular) field.placeholderString = "Address" field.cell?.sendsActionOnEndEditing = false field.delegate = self urlField = field goButton = NSButton(title: "Go", target: self, action: #selector(addressFieldSubmitted)) goButton.translatesAutoresizingMaskIntoConstraints = false goButton.bezelStyle = .rounded toolbar.addArrangedSubview(backButton) toolbar.addArrangedSubview(forwardButton) toolbar.addArrangedSubview(reloadStopButton) toolbar.addArrangedSubview(field) toolbar.addArrangedSubview(goButton) field.widthAnchor.constraint(greaterThanOrEqualToConstant: 240).isActive = true let bar = NSProgressIndicator() bar.translatesAutoresizingMaskIntoConstraints = false bar.style = .bar bar.isIndeterminate = false bar.minValue = 0 bar.maxValue = 1 bar.doubleValue = 0 bar.isHidden = true progressBar = bar let separator = NSBox() separator.translatesAutoresizingMaskIntoConstraints = false separator.boxType = .separator webView.navigationDelegate = self webView.uiDelegate = self root.addSubview(toolbar) root.addSubview(bar) root.addSubview(separator) root.addSubview(webHost) NSLayoutConstraint.activate([ toolbar.leadingAnchor.constraint(equalTo: root.leadingAnchor), toolbar.trailingAnchor.constraint(equalTo: root.trailingAnchor), toolbar.topAnchor.constraint(equalTo: root.topAnchor), bar.leadingAnchor.constraint(equalTo: root.leadingAnchor), bar.trailingAnchor.constraint(equalTo: root.trailingAnchor), bar.topAnchor.constraint(equalTo: toolbar.bottomAnchor), bar.heightAnchor.constraint(equalToConstant: 3), separator.leadingAnchor.constraint(equalTo: root.leadingAnchor), separator.trailingAnchor.constraint(equalTo: root.trailingAnchor), separator.topAnchor.constraint(equalTo: bar.bottomAnchor), webHost.leadingAnchor.constraint(equalTo: root.leadingAnchor), webHost.trailingAnchor.constraint(equalTo: root.trailingAnchor), webHost.topAnchor.constraint(equalTo: separator.bottomAnchor), webHost.bottomAnchor.constraint(equalTo: root.bottomAnchor) ]) view = root installWebViewObservers() syncToolbarFromWebView() } private func makeWebView() -> WKWebView { let wv = WKWebView(frame: .zero, configuration: InAppBrowserWebKitSupport.makeWebViewConfiguration()) wv.translatesAutoresizingMaskIntoConstraints = false return wv } private func teardownWebViewObservers() { kvoTokens.removeAll() } /// New `WKWebView` = new WebContent process (helps after GPU/JS crashes on heavy sites like Meet). private func replaceWebViewAndLoad(url: URL) { teardownWebViewObservers() webView.navigationDelegate = nil webView.uiDelegate = nil webView.removeFromSuperview() let wv = makeWebView() webView = wv webContainerView.addSubview(wv) NSLayoutConstraint.activate([ wv.leadingAnchor.constraint(equalTo: webContainerView.leadingAnchor), wv.trailingAnchor.constraint(equalTo: webContainerView.trailingAnchor), wv.topAnchor.constraint(equalTo: webContainerView.topAnchor), wv.bottomAnchor.constraint(equalTo: webContainerView.bottomAnchor) ]) webView.navigationDelegate = self webView.uiDelegate = self installWebViewObservers() syncToolbarFromWebView() webView.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData)) } private func makeToolbarButton(title: String, symbolName: String, accessibilityDescription: String) -> NSButton { let b = NSButton() b.translatesAutoresizingMaskIntoConstraints = false b.bezelStyle = .texturedRounded b.setAccessibilityLabel(accessibilityDescription) if #available(macOS 11.0, *), let img = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibilityDescription) { b.image = img b.imagePosition = .imageOnly } else { b.title = title } b.widthAnchor.constraint(greaterThanOrEqualToConstant: 32).isActive = true return b } private func installWebViewObservers() { kvoTokens.append(webView.observe(\.canGoBack, options: [.new]) { [weak self] _, _ in self?.syncToolbarFromWebView() }) kvoTokens.append(webView.observe(\.canGoForward, options: [.new]) { [weak self] _, _ in self?.syncToolbarFromWebView() }) kvoTokens.append(webView.observe(\.isLoading, options: [.new]) { [weak self] _, _ in self?.syncToolbarFromWebView() }) kvoTokens.append(webView.observe(\.estimatedProgress, options: [.new]) { [weak self] _, _ in self?.syncProgressFromWebView() }) kvoTokens.append(webView.observe(\.title, options: [.new]) { [weak self] _, _ in self?.syncWindowTitle() }) kvoTokens.append(webView.observe(\.url, options: [.new]) { [weak self] _, _ in self?.syncAddressFieldFromWebView() }) } private func syncToolbarFromWebView() { backButton?.isEnabled = webView.canGoBack forwardButton?.isEnabled = webView.canGoForward if webView.isLoading { if #available(macOS 11.0, *), let img = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Stop") { reloadStopButton.image = img reloadStopButton.imagePosition = .imageOnly reloadStopButton.title = "" } else { reloadStopButton.title = "Stop" } } else { if #available(macOS 11.0, *), let img = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: "Reload") { reloadStopButton.image = img reloadStopButton.imagePosition = .imageOnly reloadStopButton.title = "" } else { reloadStopButton.title = "Reload" } } syncProgressFromWebView() } private func syncProgressFromWebView() { guard let progressBar else { return } if webView.isLoading { progressBar.isHidden = false progressBar.doubleValue = webView.estimatedProgress } else { progressBar.isHidden = true progressBar.doubleValue = 0 } } private func syncAddressFieldFromWebView() { guard let urlField, urlField.currentEditor() == nil, let url = webView.url else { return } urlField.stringValue = url.absoluteString } private func syncWindowTitle() { let t = webView.title?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let host = webView.url?.host ?? "" view.window?.title = t.isEmpty ? (host.isEmpty ? "Browser" : host) : t } func load(url: URL) { lastLoadedURL = url processTerminateRetryCount = 0 urlField?.stringValue = url.absoluteString webView.load(URLRequest(url: url)) syncWindowTitle() } @objc private func goBack() { webView.goBack() } @objc private func goForward() { webView.goForward() } @objc private func reloadOrStop() { if webView.isLoading { webView.stopLoading() } else { webView.reload() } } @objc private func addressFieldSubmitted() { let raw = urlField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard raw.isEmpty == false else { return } var normalized = raw if normalized.lowercased().hasPrefix("http://") == false && normalized.lowercased().hasPrefix("https://") == false { normalized = "https://\(normalized)" } guard let url = URL(string: normalized), let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https", url.host != nil else { let alert = NSAlert() alert.messageText = "Invalid address" alert.informativeText = "Enter a valid web address, for example https://example.com" alert.addButton(withTitle: "OK") alert.runModal() return } guard inAppBrowserURLAllowed(url, policy: navigationPolicy) else { presentBlockedHostAlert() return } load(url: url) } private func presentBlockedHostAlert() { let alert = NSAlert() alert.messageText = "Address not allowed" alert.informativeText = "This URL is not permitted with the current in-app browser policy (whitelist)." alert.addButton(withTitle: "OK") alert.runModal() } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { processTerminateRetryCount = 0 syncAddressFieldFromWebView() syncWindowTitle() } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { let nsError = error as NSError if nsError.code == NSURLErrorCancelled { return } let alert = NSAlert() alert.messageText = "Unable to load page" alert.informativeText = "Could not load this page in the in-app browser.\n\n\(error.localizedDescription)" alert.addButton(withTitle: "Try Again") alert.addButton(withTitle: "OK") if alert.runModal() == .alertFirstButtonReturn, let url = lastLoadedURL { processTerminateRetryCount = 0 webView.load(URLRequest(url: url)) } } func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { guard let url = lastLoadedURL else { return } if processTerminateRetryCount < maxProcessTerminateRetries { processTerminateRetryCount += 1 replaceWebViewAndLoad(url: url) return } let alert = NSAlert() alert.messageText = "Page stopped loading" alert.informativeText = "The in-app browser closed this page unexpectedly. You can try loading it again in this same window." alert.addButton(withTitle: "Try Again") alert.addButton(withTitle: "OK") if alert.runModal() == .alertFirstButtonReturn { processTerminateRetryCount = 0 replaceWebViewAndLoad(url: url) } } func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void ) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } let scheme = (url.scheme ?? "").lowercased() if scheme == "mailto" || scheme == "tel" { decisionHandler(.cancel) return } if inAppBrowserURLAllowed(url, policy: navigationPolicy) == false { if navigationAction.targetFrame?.isMainFrame != false { DispatchQueue.main.async { [weak self] in self?.presentBlockedHostAlert() } } decisionHandler(.cancel) return } decisionHandler(.allow) } func webView( _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures ) -> WKWebView? { if navigationAction.targetFrame == nil, let requestURL = navigationAction.request.url { if inAppBrowserURLAllowed(requestURL, policy: navigationPolicy) { webView.load(URLRequest(url: requestURL)) } else { presentBlockedHostAlert() } } return nil } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { if control === urlField, commandSelector == #selector(NSResponder.insertNewline(_:)) { addressFieldSubmitted() return true } return false } }