// // ViewController.swift // meetings_app // // Created by Dev Mac 1 on 06/04/2026. // import Cocoa private enum SidebarPage: Int { case joinMeetings = 0 case photo = 1 case video = 2 case tutorials = 3 case settings = 4 } private enum MeetingProvider: Int { case meet = 0 case zoom = 1 case teams = 2 case zoho = 3 } private enum SettingsAction: Int { case restore = 0 case rateUs = 1 case support = 2 case moreApps = 3 case shareApp = 4 } final class ViewController: NSViewController { private let palette = Palette() private let typography = Typography() private var mainContentHost: NSView? private var sidebarRowViews: [SidebarPage: NSView] = [:] private var tabViews: [MeetingProvider: NSView] = [:] private var selectedSidebarPage: SidebarPage = .joinMeetings private var selectedMeetingProvider: MeetingProvider = .meet private var pageCache: [SidebarPage: NSView] = [:] private var sidebarPageByView = [ObjectIdentifier: SidebarPage]() private var meetingProviderByView = [ObjectIdentifier: MeetingProvider]() private var settingsActionByView = [ObjectIdentifier: SettingsAction]() 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 lazy var settingsPopover: 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 }() override func viewDidLoad() { super.viewDidLoad() setupRootView() buildMainLayout() } override func viewDidAppear() { super.viewDidAppear() view.window?.setContentSize(NSSize(width: 1120, height: 690)) view.window?.minSize = NSSize(width: 940, height: 600) applyWindowTitle(for: selectedSidebarPage) } 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 meetingTabClicked(_ sender: NSClickGestureRecognizer) { guard let view = sender.view, let provider = meetingProviderByView[ObjectIdentifier(view)], provider != selectedMeetingProvider else { return } selectedMeetingProvider = provider updateTabAppearance() } 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 { settingsPopover.performClose(nil) return } 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) } private func handleSettingsAction(_ action: SettingsAction) { switch action { case .restore: showSimpleAlert(title: "Restore", message: "Restore action tapped.") case .rateUs: // Replace with your App Store URL when ready. showSimpleAlert(title: "Rate Us", message: "Rate Us tapped (add App Store URL).") case .support: showSimpleAlert(title: "Support", message: "Support tapped (add support email / page).") case .moreApps: showSimpleAlert(title: "More Apps", message: "More Apps tapped (add developer page URL).") 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 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.") } 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 } private func applyWindowTitle(for page: SidebarPage) { switch page { case .joinMeetings: view.window?.title = "App for Google Meet" case .photo: view.window?.title = "Backgrounds โ€” Photo" case .video: view.window?.title = "Backgrounds โ€” Video" case .tutorials: view.window?.title = "Tutorials" case .settings: view.window?.title = "Settings" } } private func updateSidebarAppearance() { for (page, row) in sidebarRowViews { applySidebarRowStyle(row, page: page, logoTemplate: logoTemplateForSidebarPage(page)) } } private func updateTabAppearance() { for (provider, tab) in tabViews { applyTabStyle(tab, provider: provider, logoTemplate: logoTemplateForMeetingProvider(provider)) } } private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool { switch page { case .photo, .tutorials: return false case .joinMeetings, .video, .settings: return true } } private func logoTemplateForMeetingProvider(_ provider: MeetingProvider) -> Bool { switch provider { case .teams: return false case .meet, .zoom, .zoho: 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, logoTemplate: false) 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, logoTemplate: false) menuStack.addArrangedSubview(tutorialsRow) sidebarRowViews[.tutorials] = tutorialsRow let settingsRow = sidebarItem("Settings", icon: "๔€Ÿ", page: .settings, logoImageName: "SidebarSettingsLogo", logoIconWidth: 28, logoHeightMultiplier: 68.0 / 62.0, showsDisclosure: true) menuStack.addArrangedSubview(settingsRow) sidebarRowViews[.settings] = settingsRow sidebar.addSubview(titleRow) sidebar.addSubview(menuStack) 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) ]) for subview in menuStack.arrangedSubviews { subview.widthAnchor.constraint(equalTo: menuStack.widthAnchor).isActive = true } return sidebar } 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 contentStack.addArrangedSubview(textLabel("Join Meetings", font: typography.pageTitle, color: palette.textPrimary)) contentStack.addArrangedSubview(meetingTypeTabs()) contentStack.addArrangedSubview(joinWithURLHeading()) contentStack.addArrangedSubview(meetingUrlSection()) 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 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 meetingTypeTabs() -> NSView { let wrapper = NSView() wrapper.translatesAutoresizingMaskIntoConstraints = false let shell = roundedContainer(cornerRadius: 24, color: palette.tabBarBackground) shell.translatesAutoresizingMaskIntoConstraints = false shell.heightAnchor.constraint(equalToConstant: 48).isActive = true let stack = NSStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.orientation = .horizontal stack.distribution = .fillEqually stack.spacing = 4 let meetTab = topTab("Meet", icon: "๔€ค†", provider: .meet, logoImageName: "MeetLogo") stack.addArrangedSubview(meetTab) tabViews[.meet] = meetTab let zoomTab = topTab("Zoom", icon: "๔€ค‰", provider: .zoom, logoImageName: "ZoomLogo", logoPointSize: 34) stack.addArrangedSubview(zoomTab) tabViews[.zoom] = zoomTab let teamsTab = topTab("Teams", icon: "๔€‰จ", provider: .teams, logoImageName: "TeamsLogo", logoPointSize: 26, logoHeightMultiplier: 62.0 / 50.0, logoTemplate: false) stack.addArrangedSubview(teamsTab) tabViews[.teams] = teamsTab let zohoTab = topTab("Zoho", icon: "๔€ฏถ", provider: .zoho, logoImageName: "ZohoLogo", logoPointSize: 28) stack.addArrangedSubview(zohoTab) tabViews[.zoho] = zohoTab shell.addSubview(stack) wrapper.addSubview(shell) NSLayoutConstraint.activate([ wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 780), shell.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor), shell.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor), shell.topAnchor.constraint(equalTo: wrapper.topAnchor), shell.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor), stack.leadingAnchor.constraint(equalTo: shell.leadingAnchor, constant: 72), stack.trailingAnchor.constraint(equalTo: shell.trailingAnchor, constant: -28), stack.topAnchor.constraint(equalTo: shell.topAnchor, constant: 6), stack.bottomAnchor.constraint(equalTo: shell.bottomAnchor, constant: -6) ]) return wrapper } 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 } } /// 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? { guard let superview else { return nil } let local = convert(point, from: superview) return bounds.contains(local) ? 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) } } 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 = NSColor(calibratedWhite: 0.12, alpha: 1).cgColor view.layer?.borderColor = NSColor(calibratedWhite: 0.22, alpha: 1).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 = HoverTrackingView() row.translatesAutoresizingMaskIntoConstraints = false row.heightAnchor.constraint(equalToConstant: 44).isActive = true let icon = NSTextField(labelWithString: "โ—") icon.translatesAutoresizingMaskIntoConstraints = false icon.font = NSFont.systemFont(ofSize: 18, weight: .medium) icon.textColor = .white let title = NSTextField(labelWithString: "Dark Mode") title.translatesAutoresizingMaskIntoConstraints = false title.font = NSFont.systemFont(ofSize: 16, weight: .semibold) title.textColor = .white 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.onHoverChanged = { hovering in row.wantsLayer = true row.layer?.cornerRadius = 10 row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor } row.onHoverChanged?(false) 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 = .white let titleLabel = NSTextField(labelWithString: title) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.font = NSFont.systemFont(ofSize: 16, weight: .semibold) titleLabel.textColor = .white 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 ? NSColor(calibratedWhite: 1, alpha: 0.06) : 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) { 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 = NSColor(calibratedWhite: 1, alpha: 0.07) item.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor let tint = selected ? NSColor.white : palette.textSecondary 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 = tint } if let imageView = leading as? NSImageView { if logoTemplate { imageView.contentTintColor = tint } } else if let iconField = leading as? NSTextField { iconField.textColor = tint } } func topTab(_ title: String, icon: String, provider: MeetingProvider, logoImageName: String? = nil, logoPointSize: CGFloat = 26, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true) -> NSView { let tab = HoverTrackingView() tab.wantsLayer = true tab.layer?.cornerRadius = 19 tab.layer?.backgroundColor = NSColor.clear.cgColor tab.translatesAutoresizingMaskIntoConstraints = false meetingProviderByView[ObjectIdentifier(tab)] = provider let leadingView: NSView if let name = logoImageName, let logo = NSImage(named: name) { let imageView = NSImageView(image: logo) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.imageScaling = .scaleProportionallyDown imageView.imageAlignment = .alignCenter imageView.isEditable = false if logoTemplate { imageView.contentTintColor = palette.textPrimary } leadingView = imageView } else { leadingView = textLabel(icon, font: typography.tabIcon, color: palette.textPrimary) } let titleLabel = textLabel(title, font: typography.tabTitle, color: palette.textPrimary) tab.addSubview(leadingView) tab.addSubview(titleLabel) var constraints: [NSLayoutConstraint] = [ leadingView.leadingAnchor.constraint(equalTo: tab.leadingAnchor, constant: 16), leadingView.centerYAnchor.constraint(equalTo: tab.centerYAnchor), titleLabel.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 6), titleLabel.centerYAnchor.constraint(equalTo: tab.centerYAnchor) ] if logoImageName != nil { constraints.append(contentsOf: [ leadingView.widthAnchor.constraint(equalToConstant: logoPointSize), leadingView.heightAnchor.constraint(equalToConstant: logoPointSize * logoHeightMultiplier) ]) } NSLayoutConstraint.activate(constraints) applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate) tab.onHoverChanged = { [weak self, weak tab] hovering in guard let self, let tab else { return } self.applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate, hovering: hovering) } let click = NSClickGestureRecognizer(target: self, action: #selector(meetingTabClicked(_:))) tab.addGestureRecognizer(click) return tab } func applyTabStyle(_ tab: NSView, provider: MeetingProvider, logoTemplate: Bool, hovering: Bool = false) { let selected = (provider == selectedMeetingProvider) let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07) tab.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor guard tab.subviews.count >= 2 else { return } let leading = tab.subviews[0] let title = tab.subviews[1] as? NSTextField let textColor = palette.textPrimary title?.textColor = textColor if let imageView = leading as? NSImageView { if logoTemplate { imageView.contentTintColor = textColor } } else if let iconField = leading as? NSTextField { iconField.textColor = textColor } } 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 hoverColor = baseColor.blended(withFraction: 0.12, of: NSColor.white) ?? 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 hoverColor = baseColor.blended(withFraction: 0.10, of: NSColor.white) ?? baseColor button.onHoverChanged = { hovering in button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor } button.onHoverChanged?(false) return button } } private struct Palette { let pageBackground = NSColor(calibratedRed: 10.0 / 255.0, green: 11.0 / 255.0, blue: 12.0 / 255.0, alpha: 1) let sidebarBackground = NSColor(calibratedRed: 16.0 / 255.0, green: 17.0 / 255.0, blue: 19.0 / 255.0, alpha: 1) let sectionCard = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1) let tabBarBackground = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1) let tabIdleBackground = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1) let inputBackground = NSColor(calibratedRed: 20.0 / 255.0, green: 21.0 / 255.0, blue: 24.0 / 255.0, alpha: 1) let inputBorder = NSColor(calibratedRed: 38.0 / 255.0, green: 40.0 / 255.0, blue: 44.0 / 255.0, alpha: 1) let primaryBlue = NSColor(calibratedRed: 27.0 / 255.0, green: 115.0 / 255.0, blue: 232.0 / 255.0, alpha: 1) let primaryBlueBorder = NSColor(calibratedRed: 42.0 / 255.0, green: 118.0 / 255.0, blue: 220.0 / 255.0, alpha: 1) let cancelButton = NSColor(calibratedRed: 20.0 / 255.0, green: 21.0 / 255.0, blue: 24.0 / 255.0, alpha: 1) let meetingBadge = NSColor(calibratedRed: 0.88, green: 0.66, blue: 0.14, alpha: 1) let separator = NSColor(calibratedRed: 26.0 / 255.0, green: 27.0 / 255.0, blue: 30.0 / 255.0, alpha: 1) let textPrimary = NSColor(calibratedWhite: 0.98, alpha: 1) let textSecondary = NSColor(calibratedWhite: 0.78, alpha: 1) let textTertiary = NSColor(calibratedWhite: 0.66, alpha: 1) let textMuted = NSColor(calibratedWhite: 0.44, 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) }