Nenhuma descrição

ViewController.swift 51KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187
  1. //
  2. // ViewController.swift
  3. // meetings_app
  4. //
  5. // Created by Dev Mac 1 on 06/04/2026.
  6. //
  7. import Cocoa
  8. private enum SidebarPage: Int {
  9. case joinMeetings = 0
  10. case photo = 1
  11. case video = 2
  12. case tutorials = 3
  13. case settings = 4
  14. }
  15. private enum MeetingProvider: Int {
  16. case meet = 0
  17. case zoom = 1
  18. case teams = 2
  19. case zoho = 3
  20. }
  21. private enum SettingsAction: Int {
  22. case restore = 0
  23. case rateUs = 1
  24. case support = 2
  25. case moreApps = 3
  26. case shareApp = 4
  27. }
  28. final class ViewController: NSViewController {
  29. private let palette = Palette()
  30. private let typography = Typography()
  31. private var mainContentHost: NSView?
  32. private var sidebarRowViews: [SidebarPage: NSView] = [:]
  33. private var tabViews: [MeetingProvider: NSView] = [:]
  34. private var selectedSidebarPage: SidebarPage = .joinMeetings
  35. private var selectedMeetingProvider: MeetingProvider = .meet
  36. private var pageCache: [SidebarPage: NSView] = [:]
  37. private var sidebarPageByView = [ObjectIdentifier: SidebarPage]()
  38. private var meetingProviderByView = [ObjectIdentifier: MeetingProvider]()
  39. private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
  40. private let darkModeDefaultsKey = "settings.darkModeEnabled"
  41. private var darkModeEnabled: Bool {
  42. get {
  43. let hasValue = UserDefaults.standard.object(forKey: darkModeDefaultsKey) != nil
  44. return hasValue ? UserDefaults.standard.bool(forKey: darkModeDefaultsKey) : true
  45. }
  46. set { UserDefaults.standard.set(newValue, forKey: darkModeDefaultsKey) }
  47. }
  48. private lazy var settingsPopover: NSPopover = {
  49. let popover = NSPopover()
  50. popover.behavior = .transient
  51. popover.animates = true
  52. popover.contentViewController = SettingsMenuViewController(
  53. palette: palette,
  54. typography: typography,
  55. darkModeEnabled: darkModeEnabled,
  56. onToggleDarkMode: { [weak self] enabled in
  57. self?.setDarkMode(enabled)
  58. },
  59. onAction: { [weak self] action in
  60. self?.handleSettingsAction(action)
  61. }
  62. )
  63. return popover
  64. }()
  65. override func viewDidLoad() {
  66. super.viewDidLoad()
  67. setupRootView()
  68. buildMainLayout()
  69. }
  70. override func viewDidAppear() {
  71. super.viewDidAppear()
  72. view.window?.setContentSize(NSSize(width: 1120, height: 690))
  73. view.window?.minSize = NSSize(width: 940, height: 600)
  74. applyWindowTitle(for: selectedSidebarPage)
  75. }
  76. override var representedObject: Any? {
  77. didSet {}
  78. }
  79. }
  80. private extension ViewController {
  81. func setupRootView() {
  82. view.appearance = NSAppearance(named: darkModeEnabled ? .darkAqua : .aqua)
  83. view.wantsLayer = true
  84. view.layer?.backgroundColor = palette.pageBackground.cgColor
  85. }
  86. func buildMainLayout() {
  87. let splitContainer = NSStackView()
  88. splitContainer.translatesAutoresizingMaskIntoConstraints = false
  89. splitContainer.orientation = .horizontal
  90. splitContainer.spacing = 0
  91. splitContainer.alignment = .top
  92. view.addSubview(splitContainer)
  93. NSLayoutConstraint.activate([
  94. splitContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  95. splitContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  96. splitContainer.topAnchor.constraint(equalTo: view.topAnchor),
  97. splitContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor)
  98. ])
  99. let sidebar = makeSidebar()
  100. let mainPanel = makeMainPanel()
  101. splitContainer.addArrangedSubview(sidebar)
  102. splitContainer.addArrangedSubview(mainPanel)
  103. }
  104. @objc private func sidebarItemClicked(_ sender: NSClickGestureRecognizer) {
  105. guard let view = sender.view,
  106. let page = sidebarPageByView[ObjectIdentifier(view)],
  107. page != selectedSidebarPage || page == .settings else { return }
  108. if page == .settings {
  109. showSettingsPopover()
  110. return
  111. }
  112. showSidebarPage(page)
  113. }
  114. @objc private func meetingTabClicked(_ sender: NSClickGestureRecognizer) {
  115. guard let view = sender.view,
  116. let provider = meetingProviderByView[ObjectIdentifier(view)],
  117. provider != selectedMeetingProvider else { return }
  118. selectedMeetingProvider = provider
  119. updateTabAppearance()
  120. }
  121. private func showSidebarPage(_ page: SidebarPage) {
  122. selectedSidebarPage = page
  123. updateSidebarAppearance()
  124. applyWindowTitle(for: page)
  125. guard let host = mainContentHost else { return }
  126. host.subviews.forEach { $0.removeFromSuperview() }
  127. let child = viewForPage(page)
  128. child.translatesAutoresizingMaskIntoConstraints = false
  129. host.addSubview(child)
  130. NSLayoutConstraint.activate([
  131. child.leadingAnchor.constraint(equalTo: host.leadingAnchor),
  132. child.trailingAnchor.constraint(equalTo: host.trailingAnchor),
  133. child.topAnchor.constraint(equalTo: host.topAnchor),
  134. child.bottomAnchor.constraint(equalTo: host.bottomAnchor)
  135. ])
  136. }
  137. private func showSettingsPopover() {
  138. guard let anchor = sidebarRowViews[.settings] else { return }
  139. if settingsPopover.isShown {
  140. settingsPopover.performClose(nil)
  141. return
  142. }
  143. if let menu = settingsPopover.contentViewController as? SettingsMenuViewController {
  144. menu.setDarkModeEnabled(darkModeEnabled)
  145. }
  146. settingsPopover.show(relativeTo: anchor.bounds, of: anchor, preferredEdge: .maxX)
  147. }
  148. private func setDarkMode(_ enabled: Bool) {
  149. darkModeEnabled = enabled
  150. NSApp.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua)
  151. view.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua)
  152. }
  153. private func handleSettingsAction(_ action: SettingsAction) {
  154. switch action {
  155. case .restore:
  156. showSimpleAlert(title: "Restore", message: "Restore action tapped.")
  157. case .rateUs:
  158. // Replace with your App Store URL when ready.
  159. showSimpleAlert(title: "Rate Us", message: "Rate Us tapped (add App Store URL).")
  160. case .support:
  161. showSimpleAlert(title: "Support", message: "Support tapped (add support email / page).")
  162. case .moreApps:
  163. showSimpleAlert(title: "More Apps", message: "More Apps tapped (add developer page URL).")
  164. case .shareApp:
  165. let urlString = "https://example.com"
  166. NSPasteboard.general.clearContents()
  167. NSPasteboard.general.setString(urlString, forType: .string)
  168. showSimpleAlert(title: "Share App", message: "Link copied to clipboard:\n\(urlString)")
  169. }
  170. }
  171. private func showSimpleAlert(title: String, message: String) {
  172. let alert = NSAlert()
  173. alert.messageText = title
  174. alert.informativeText = message
  175. alert.addButton(withTitle: "OK")
  176. alert.runModal()
  177. }
  178. private func viewForPage(_ page: SidebarPage) -> NSView {
  179. if let cached = pageCache[page] { return cached }
  180. let built: NSView
  181. switch page {
  182. case .joinMeetings:
  183. built = makeJoinMeetingsContent()
  184. case .photo:
  185. built = makePlaceholderPage(title: "Photo", subtitle: "Backgrounds — choose a photo background for your meetings.")
  186. case .video:
  187. built = makePlaceholderPage(title: "Video", subtitle: "Backgrounds — video background options.")
  188. case .tutorials:
  189. built = makePlaceholderPage(title: "Tutorials", subtitle: "Learn how to use the app.")
  190. case .settings:
  191. built = makePlaceholderPage(title: "Settings", subtitle: "Preferences and account options.")
  192. }
  193. pageCache[page] = built
  194. return built
  195. }
  196. private func makePlaceholderPage(title: String, subtitle: String) -> NSView {
  197. let panel = NSView()
  198. panel.translatesAutoresizingMaskIntoConstraints = false
  199. let titleLabel = textLabel(title, font: typography.pageTitle, color: palette.textPrimary)
  200. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  201. let sub = textLabel(subtitle, font: typography.fieldLabel, color: palette.textSecondary)
  202. sub.translatesAutoresizingMaskIntoConstraints = false
  203. panel.addSubview(titleLabel)
  204. panel.addSubview(sub)
  205. NSLayoutConstraint.activate([
  206. titleLabel.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
  207. titleLabel.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26),
  208. sub.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
  209. sub.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8)
  210. ])
  211. return panel
  212. }
  213. private func applyWindowTitle(for page: SidebarPage) {
  214. switch page {
  215. case .joinMeetings:
  216. view.window?.title = "App for Google Meet"
  217. case .photo:
  218. view.window?.title = "Backgrounds — Photo"
  219. case .video:
  220. view.window?.title = "Backgrounds — Video"
  221. case .tutorials:
  222. view.window?.title = "Tutorials"
  223. case .settings:
  224. view.window?.title = "Settings"
  225. }
  226. }
  227. private func updateSidebarAppearance() {
  228. for (page, row) in sidebarRowViews {
  229. applySidebarRowStyle(row, page: page, logoTemplate: logoTemplateForSidebarPage(page))
  230. }
  231. }
  232. private func updateTabAppearance() {
  233. for (provider, tab) in tabViews {
  234. applyTabStyle(tab, provider: provider, logoTemplate: logoTemplateForMeetingProvider(provider))
  235. }
  236. }
  237. private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
  238. switch page {
  239. case .photo, .tutorials: return false
  240. case .joinMeetings, .video, .settings: return true
  241. }
  242. }
  243. private func logoTemplateForMeetingProvider(_ provider: MeetingProvider) -> Bool {
  244. switch provider {
  245. case .teams: return false
  246. case .meet, .zoom, .zoho: return true
  247. }
  248. }
  249. func makeSidebar() -> NSView {
  250. let sidebar = NSView()
  251. sidebar.translatesAutoresizingMaskIntoConstraints = false
  252. sidebar.wantsLayer = true
  253. sidebar.layer?.backgroundColor = palette.sidebarBackground.cgColor
  254. sidebar.layer?.borderColor = palette.separator.cgColor
  255. sidebar.layer?.borderWidth = 1
  256. sidebar.layer?.shadowColor = NSColor.black.cgColor
  257. sidebar.layer?.shadowOpacity = 0.18
  258. sidebar.layer?.shadowOffset = CGSize(width: 2, height: 0)
  259. sidebar.layer?.shadowRadius = 10
  260. sidebar.widthAnchor.constraint(equalToConstant: 210).isActive = true
  261. let titleRow = NSStackView(views: [
  262. iconLabel("📅", size: 24),
  263. textLabel("Meetings", font: typography.sidebarBrand, color: palette.textPrimary)
  264. ])
  265. titleRow.translatesAutoresizingMaskIntoConstraints = false
  266. titleRow.orientation = .horizontal
  267. titleRow.alignment = .centerY
  268. titleRow.spacing = 8
  269. let menuStack = NSStackView()
  270. menuStack.translatesAutoresizingMaskIntoConstraints = false
  271. menuStack.orientation = .vertical
  272. menuStack.alignment = .leading
  273. menuStack.spacing = 10
  274. menuStack.addArrangedSubview(sidebarSectionTitle("Meetings"))
  275. let joinRow = sidebarItem("Join Meetings", icon: "􀉣", page: .joinMeetings, logoImageName: "JoinMeetingsLogo", logoIconWidth: 24, logoHeightMultiplier: 56.0 / 52.0)
  276. menuStack.addArrangedSubview(joinRow)
  277. sidebarRowViews[.joinMeetings] = joinRow
  278. menuStack.addArrangedSubview(sidebarSectionTitle("Backgrounds"))
  279. let photoRow = sidebarItem("Photo", icon: "􀏂", page: .photo, logoImageName: "SidebarPhotoLogo", logoIconWidth: 24, logoHeightMultiplier: 82.0 / 62.0, logoTemplate: false)
  280. menuStack.addArrangedSubview(photoRow)
  281. sidebarRowViews[.photo] = photoRow
  282. let videoRow = sidebarItem("Video", icon: "􀎚", page: .video, logoImageName: "SidebarVideoLogo", logoIconWidth: 28, logoHeightMultiplier: 52.0 / 60.0)
  283. menuStack.addArrangedSubview(videoRow)
  284. sidebarRowViews[.video] = videoRow
  285. menuStack.addArrangedSubview(sidebarSectionTitle("Additional"))
  286. let tutorialsRow = sidebarItem("Tutorials", icon: "􀛩", page: .tutorials, logoImageName: "SidebarTutorialsLogo", logoIconWidth: 24, logoHeightMultiplier: 50.0 / 60.0, logoTemplate: false)
  287. menuStack.addArrangedSubview(tutorialsRow)
  288. sidebarRowViews[.tutorials] = tutorialsRow
  289. let settingsRow = sidebarItem("Settings", icon: "􀍟", page: .settings, logoImageName: "SidebarSettingsLogo", logoIconWidth: 28, logoHeightMultiplier: 68.0 / 62.0, showsDisclosure: true)
  290. menuStack.addArrangedSubview(settingsRow)
  291. sidebarRowViews[.settings] = settingsRow
  292. sidebar.addSubview(titleRow)
  293. sidebar.addSubview(menuStack)
  294. NSLayoutConstraint.activate([
  295. titleRow.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 16),
  296. titleRow.topAnchor.constraint(equalTo: sidebar.topAnchor, constant: 24),
  297. titleRow.trailingAnchor.constraint(lessThanOrEqualTo: sidebar.trailingAnchor, constant: -16),
  298. menuStack.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 12),
  299. menuStack.trailingAnchor.constraint(equalTo: sidebar.trailingAnchor, constant: -12),
  300. menuStack.topAnchor.constraint(equalTo: titleRow.bottomAnchor, constant: 20)
  301. ])
  302. for subview in menuStack.arrangedSubviews {
  303. subview.widthAnchor.constraint(equalTo: menuStack.widthAnchor).isActive = true
  304. }
  305. return sidebar
  306. }
  307. func makeMainPanel() -> NSView {
  308. let panel = NSView()
  309. panel.translatesAutoresizingMaskIntoConstraints = false
  310. panel.wantsLayer = true
  311. panel.layer?.backgroundColor = palette.pageBackground.cgColor
  312. let host = NSView()
  313. host.translatesAutoresizingMaskIntoConstraints = false
  314. panel.addSubview(host)
  315. NSLayoutConstraint.activate([
  316. host.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
  317. host.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
  318. host.topAnchor.constraint(equalTo: panel.topAnchor),
  319. host.bottomAnchor.constraint(equalTo: panel.bottomAnchor)
  320. ])
  321. mainContentHost = host
  322. showSidebarPage(.joinMeetings)
  323. return panel
  324. }
  325. func makeJoinMeetingsContent() -> NSView {
  326. let panel = NSView()
  327. panel.translatesAutoresizingMaskIntoConstraints = false
  328. let contentStack = NSStackView()
  329. contentStack.translatesAutoresizingMaskIntoConstraints = false
  330. contentStack.orientation = .vertical
  331. contentStack.spacing = 14
  332. contentStack.alignment = .leading
  333. contentStack.addArrangedSubview(textLabel("Join Meetings", font: typography.pageTitle, color: palette.textPrimary))
  334. contentStack.addArrangedSubview(meetingTypeTabs())
  335. contentStack.addArrangedSubview(joinWithURLHeading())
  336. contentStack.addArrangedSubview(meetingUrlSection())
  337. contentStack.addArrangedSubview(scheduleHeader())
  338. contentStack.addArrangedSubview(textLabel("Tuesday, 14 Apr", font: typography.dateHeading, color: palette.textSecondary))
  339. contentStack.addArrangedSubview(scheduleCardsRow())
  340. panel.addSubview(contentStack)
  341. NSLayoutConstraint.activate([
  342. contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
  343. contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
  344. contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26)
  345. ])
  346. return panel
  347. }
  348. func joinWithURLHeading() -> NSView {
  349. let container = NSView()
  350. container.translatesAutoresizingMaskIntoConstraints = false
  351. let title = textLabel("Join with URL", font: typography.joinWithURLTitle, color: palette.textPrimary)
  352. title.alignment = .left
  353. title.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  354. title.setContentCompressionResistancePriority(.required, for: .horizontal)
  355. let bar = NSView()
  356. bar.translatesAutoresizingMaskIntoConstraints = false
  357. bar.wantsLayer = true
  358. bar.layer?.backgroundColor = palette.primaryBlue.cgColor
  359. bar.heightAnchor.constraint(equalToConstant: 3).isActive = true
  360. container.addSubview(title)
  361. container.addSubview(bar)
  362. NSLayoutConstraint.activate([
  363. title.leadingAnchor.constraint(equalTo: container.leadingAnchor),
  364. title.topAnchor.constraint(equalTo: container.topAnchor),
  365. bar.leadingAnchor.constraint(equalTo: title.leadingAnchor),
  366. bar.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6),
  367. bar.widthAnchor.constraint(equalTo: title.widthAnchor),
  368. bar.bottomAnchor.constraint(equalTo: container.bottomAnchor),
  369. container.trailingAnchor.constraint(equalTo: title.trailingAnchor)
  370. ])
  371. return container
  372. }
  373. func meetingTypeTabs() -> NSView {
  374. let wrapper = NSView()
  375. wrapper.translatesAutoresizingMaskIntoConstraints = false
  376. let shell = roundedContainer(cornerRadius: 24, color: palette.tabBarBackground)
  377. shell.translatesAutoresizingMaskIntoConstraints = false
  378. shell.heightAnchor.constraint(equalToConstant: 48).isActive = true
  379. let stack = NSStackView()
  380. stack.translatesAutoresizingMaskIntoConstraints = false
  381. stack.orientation = .horizontal
  382. stack.distribution = .fillEqually
  383. stack.spacing = 4
  384. let meetTab = topTab("Meet", icon: "􀤆", provider: .meet, logoImageName: "MeetLogo")
  385. stack.addArrangedSubview(meetTab)
  386. tabViews[.meet] = meetTab
  387. let zoomTab = topTab("Zoom", icon: "􀤉", provider: .zoom, logoImageName: "ZoomLogo", logoPointSize: 34)
  388. stack.addArrangedSubview(zoomTab)
  389. tabViews[.zoom] = zoomTab
  390. let teamsTab = topTab("Teams", icon: "􀉨", provider: .teams, logoImageName: "TeamsLogo", logoPointSize: 26, logoHeightMultiplier: 62.0 / 50.0, logoTemplate: false)
  391. stack.addArrangedSubview(teamsTab)
  392. tabViews[.teams] = teamsTab
  393. let zohoTab = topTab("Zoho", icon: "􀯶", provider: .zoho, logoImageName: "ZohoLogo", logoPointSize: 28)
  394. stack.addArrangedSubview(zohoTab)
  395. tabViews[.zoho] = zohoTab
  396. shell.addSubview(stack)
  397. wrapper.addSubview(shell)
  398. NSLayoutConstraint.activate([
  399. wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 780),
  400. shell.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
  401. shell.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
  402. shell.topAnchor.constraint(equalTo: wrapper.topAnchor),
  403. shell.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor),
  404. stack.leadingAnchor.constraint(equalTo: shell.leadingAnchor, constant: 72),
  405. stack.trailingAnchor.constraint(equalTo: shell.trailingAnchor, constant: -28),
  406. stack.topAnchor.constraint(equalTo: shell.topAnchor, constant: 6),
  407. stack.bottomAnchor.constraint(equalTo: shell.bottomAnchor, constant: -6)
  408. ])
  409. return wrapper
  410. }
  411. func meetingUrlSection() -> NSView {
  412. let wrapper = NSView()
  413. wrapper.translatesAutoresizingMaskIntoConstraints = false
  414. let title = textLabel("Meeting URL", font: typography.fieldLabel, color: palette.textSecondary)
  415. let textFieldContainer = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
  416. textFieldContainer.translatesAutoresizingMaskIntoConstraints = false
  417. textFieldContainer.heightAnchor.constraint(equalToConstant: 40).isActive = true
  418. styleSurface(textFieldContainer, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  419. let urlField = NSTextField(string: "")
  420. urlField.translatesAutoresizingMaskIntoConstraints = false
  421. urlField.isEditable = true
  422. urlField.isSelectable = true
  423. urlField.isBordered = false
  424. urlField.drawsBackground = false
  425. urlField.placeholderString = "Enter meeting URL..."
  426. urlField.font = typography.inputPlaceholder
  427. urlField.textColor = palette.textPrimary
  428. urlField.focusRingType = .none
  429. textFieldContainer.addSubview(urlField)
  430. let actions = NSStackView()
  431. actions.orientation = .horizontal
  432. actions.spacing = 10
  433. actions.translatesAutoresizingMaskIntoConstraints = false
  434. actions.alignment = .centerY
  435. actions.addArrangedSubview(actionButton(title: "Cancel", color: palette.cancelButton, textColor: palette.textSecondary, width: 110))
  436. actions.addArrangedSubview(actionButton(title: "Join", color: palette.primaryBlue, textColor: .white, width: 116))
  437. wrapper.addSubview(title)
  438. wrapper.addSubview(textFieldContainer)
  439. wrapper.addSubview(actions)
  440. NSLayoutConstraint.activate([
  441. wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 780),
  442. title.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
  443. title.topAnchor.constraint(equalTo: wrapper.topAnchor),
  444. textFieldContainer.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
  445. textFieldContainer.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
  446. textFieldContainer.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 10),
  447. urlField.leadingAnchor.constraint(equalTo: textFieldContainer.leadingAnchor, constant: 12),
  448. urlField.trailingAnchor.constraint(equalTo: textFieldContainer.trailingAnchor, constant: -12),
  449. urlField.centerYAnchor.constraint(equalTo: textFieldContainer.centerYAnchor),
  450. actions.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
  451. actions.topAnchor.constraint(equalTo: textFieldContainer.bottomAnchor, constant: 14),
  452. actions.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor)
  453. ])
  454. return wrapper
  455. }
  456. func scheduleHeader() -> NSView {
  457. let row = NSStackView()
  458. row.translatesAutoresizingMaskIntoConstraints = false
  459. row.orientation = .horizontal
  460. row.alignment = .centerY
  461. row.distribution = .fill
  462. row.spacing = 12
  463. row.addArrangedSubview(textLabel("Schedule", font: typography.sectionTitleBold, color: palette.textPrimary))
  464. let spacer = NSView()
  465. spacer.translatesAutoresizingMaskIntoConstraints = false
  466. row.addArrangedSubview(spacer)
  467. spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
  468. row.addArrangedSubview(iconRoundButton("?", size: 34))
  469. row.addArrangedSubview(iconRoundButton("⟳", size: 34))
  470. let filter = roundedContainer(cornerRadius: 8, color: palette.inputBackground)
  471. filter.translatesAutoresizingMaskIntoConstraints = false
  472. filter.widthAnchor.constraint(equalToConstant: 156).isActive = true
  473. filter.heightAnchor.constraint(equalToConstant: 34).isActive = true
  474. styleSurface(filter, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  475. let filterText = textLabel("All", font: typography.filterText, color: palette.textSecondary)
  476. let arrow = textLabel("▾", font: typography.filterArrow, color: palette.textMuted)
  477. filterText.translatesAutoresizingMaskIntoConstraints = false
  478. arrow.translatesAutoresizingMaskIntoConstraints = false
  479. filter.addSubview(filterText)
  480. filter.addSubview(arrow)
  481. NSLayoutConstraint.activate([
  482. filterText.leadingAnchor.constraint(equalTo: filter.leadingAnchor, constant: 12),
  483. filterText.centerYAnchor.constraint(equalTo: filter.centerYAnchor),
  484. arrow.trailingAnchor.constraint(equalTo: filter.trailingAnchor, constant: -10),
  485. arrow.centerYAnchor.constraint(equalTo: filter.centerYAnchor)
  486. ])
  487. row.addArrangedSubview(filter)
  488. row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
  489. return row
  490. }
  491. func scheduleCardsRow() -> NSView {
  492. let row = NSStackView()
  493. row.translatesAutoresizingMaskIntoConstraints = false
  494. row.orientation = .horizontal
  495. row.spacing = 10
  496. row.alignment = .top
  497. row.distribution = .fill
  498. row.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  499. row.heightAnchor.constraint(equalToConstant: 136).isActive = true
  500. row.addArrangedSubview(scheduleCard())
  501. row.addArrangedSubview(scheduleCard())
  502. return row
  503. }
  504. func scheduleCard() -> NSView {
  505. let cardWidth: CGFloat = 264
  506. let card = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
  507. styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: true)
  508. card.translatesAutoresizingMaskIntoConstraints = false
  509. card.widthAnchor.constraint(equalToConstant: cardWidth).isActive = true
  510. card.heightAnchor.constraint(equalToConstant: 136).isActive = true
  511. let icon = roundedContainer(cornerRadius: 5, color: palette.meetingBadge)
  512. icon.translatesAutoresizingMaskIntoConstraints = false
  513. icon.widthAnchor.constraint(equalToConstant: 22).isActive = true
  514. icon.heightAnchor.constraint(equalToConstant: 22).isActive = true
  515. let iconText = textLabel("••", font: typography.cardIcon, color: .white)
  516. iconText.translatesAutoresizingMaskIntoConstraints = false
  517. icon.addSubview(iconText)
  518. NSLayoutConstraint.activate([
  519. iconText.centerXAnchor.constraint(equalTo: icon.centerXAnchor),
  520. iconText.centerYAnchor.constraint(equalTo: icon.centerYAnchor)
  521. ])
  522. let title = textLabel("General Meeting", font: typography.cardTitle, color: palette.textPrimary)
  523. let subtitle = textLabel("Baisakhi", font: typography.cardSubtitle, color: palette.textPrimary)
  524. let time = textLabel("12:00 AM - 11:59 PM", font: typography.cardTime, color: palette.textSecondary)
  525. card.addSubview(icon)
  526. card.addSubview(title)
  527. card.addSubview(subtitle)
  528. card.addSubview(time)
  529. NSLayoutConstraint.activate([
  530. icon.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
  531. icon.topAnchor.constraint(equalTo: card.topAnchor, constant: 10),
  532. title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 6),
  533. title.centerYAnchor.constraint(equalTo: icon.centerYAnchor),
  534. title.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -10),
  535. subtitle.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
  536. subtitle.topAnchor.constraint(equalTo: icon.bottomAnchor, constant: 7),
  537. time.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
  538. time.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 4)
  539. ])
  540. return card
  541. }
  542. }
  543. /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
  544. private class RowHitTestView: NSView {
  545. override func hitTest(_ point: NSPoint) -> NSView? {
  546. guard let superview else { return nil }
  547. let local = convert(point, from: superview)
  548. return bounds.contains(local) ? self : nil
  549. }
  550. }
  551. private final class HoverTrackingView: RowHitTestView {
  552. var onHoverChanged: ((Bool) -> Void)?
  553. var showsHandCursor = true
  554. private var trackingAreaRef: NSTrackingArea?
  555. private var isHovering = false {
  556. didSet {
  557. guard isHovering != oldValue else { return }
  558. onHoverChanged?(isHovering)
  559. }
  560. }
  561. override func updateTrackingAreas() {
  562. super.updateTrackingAreas()
  563. if let trackingAreaRef {
  564. removeTrackingArea(trackingAreaRef)
  565. }
  566. let options: NSTrackingArea.Options = [
  567. .activeInKeyWindow,
  568. .inVisibleRect,
  569. .mouseEnteredAndExited
  570. ]
  571. let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
  572. addTrackingArea(area)
  573. trackingAreaRef = area
  574. }
  575. override func mouseEntered(with event: NSEvent) {
  576. super.mouseEntered(with: event)
  577. isHovering = true
  578. }
  579. override func mouseExited(with event: NSEvent) {
  580. super.mouseExited(with: event)
  581. isHovering = false
  582. }
  583. override func resetCursorRects() {
  584. super.resetCursorRects()
  585. guard showsHandCursor else { return }
  586. addCursorRect(bounds, cursor: .pointingHand)
  587. }
  588. }
  589. private final class SettingsMenuViewController: NSViewController {
  590. private let palette: Palette
  591. private let typography: Typography
  592. private let onToggleDarkMode: (Bool) -> Void
  593. private let onAction: (SettingsAction) -> Void
  594. private var darkToggle: NSSwitch?
  595. init(
  596. palette: Palette,
  597. typography: Typography,
  598. darkModeEnabled: Bool,
  599. onToggleDarkMode: @escaping (Bool) -> Void,
  600. onAction: @escaping (SettingsAction) -> Void
  601. ) {
  602. self.palette = palette
  603. self.typography = typography
  604. self.onToggleDarkMode = onToggleDarkMode
  605. self.onAction = onAction
  606. super.init(nibName: nil, bundle: nil)
  607. self.view = makeView(darkModeEnabled: darkModeEnabled)
  608. }
  609. @available(*, unavailable)
  610. required init?(coder: NSCoder) {
  611. nil
  612. }
  613. func setDarkModeEnabled(_ enabled: Bool) {
  614. darkToggle?.state = enabled ? .on : .off
  615. }
  616. private func makeView(darkModeEnabled: Bool) -> NSView {
  617. let root = NSView()
  618. root.translatesAutoresizingMaskIntoConstraints = false
  619. let card = roundedCard()
  620. root.addSubview(card)
  621. NSLayoutConstraint.activate([
  622. card.leadingAnchor.constraint(equalTo: root.leadingAnchor),
  623. card.trailingAnchor.constraint(equalTo: root.trailingAnchor),
  624. card.topAnchor.constraint(equalTo: root.topAnchor),
  625. card.bottomAnchor.constraint(equalTo: root.bottomAnchor),
  626. root.widthAnchor.constraint(equalToConstant: 260)
  627. ])
  628. let stack = NSStackView()
  629. stack.translatesAutoresizingMaskIntoConstraints = false
  630. stack.orientation = .vertical
  631. stack.spacing = 6
  632. stack.alignment = .leading
  633. card.addSubview(stack)
  634. NSLayoutConstraint.activate([
  635. stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14),
  636. stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14),
  637. stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
  638. stack.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -14)
  639. ])
  640. stack.addArrangedSubview(settingsDarkModeRow(enabled: darkModeEnabled))
  641. stack.addArrangedSubview(settingsActionRow(icon: "⟳", title: "Restore", action: .restore))
  642. stack.addArrangedSubview(settingsActionRow(icon: "★", title: "Rate Us", action: .rateUs))
  643. stack.addArrangedSubview(settingsActionRow(icon: "💬", title: "Support", action: .support))
  644. stack.addArrangedSubview(settingsActionRow(icon: "⋯", title: "More Apps", action: .moreApps))
  645. stack.addArrangedSubview(settingsActionRow(icon: "⤴︎", title: "Share App", action: .shareApp))
  646. for v in stack.arrangedSubviews {
  647. v.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
  648. }
  649. return root
  650. }
  651. private func roundedCard() -> NSView {
  652. let view = NSView()
  653. view.translatesAutoresizingMaskIntoConstraints = false
  654. view.wantsLayer = true
  655. view.layer?.cornerRadius = 12
  656. view.layer?.backgroundColor = NSColor(calibratedWhite: 0.12, alpha: 1).cgColor
  657. view.layer?.borderColor = NSColor(calibratedWhite: 0.22, alpha: 1).cgColor
  658. view.layer?.borderWidth = 1
  659. view.layer?.shadowColor = NSColor.black.cgColor
  660. view.layer?.shadowOpacity = 0.28
  661. view.layer?.shadowOffset = CGSize(width: 0, height: -1)
  662. view.layer?.shadowRadius = 10
  663. return view
  664. }
  665. private func settingsDarkModeRow(enabled: Bool) -> NSView {
  666. let row = HoverTrackingView()
  667. row.translatesAutoresizingMaskIntoConstraints = false
  668. row.heightAnchor.constraint(equalToConstant: 44).isActive = true
  669. let icon = NSTextField(labelWithString: "◐")
  670. icon.translatesAutoresizingMaskIntoConstraints = false
  671. icon.font = NSFont.systemFont(ofSize: 18, weight: .medium)
  672. icon.textColor = .white
  673. let title = NSTextField(labelWithString: "Dark Mode")
  674. title.translatesAutoresizingMaskIntoConstraints = false
  675. title.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
  676. title.textColor = .white
  677. let toggle = NSSwitch()
  678. toggle.translatesAutoresizingMaskIntoConstraints = false
  679. toggle.state = enabled ? .on : .off
  680. toggle.target = self
  681. toggle.action = #selector(darkModeToggled(_:))
  682. darkToggle = toggle
  683. row.addSubview(icon)
  684. row.addSubview(title)
  685. row.addSubview(toggle)
  686. row.onHoverChanged = { hovering in
  687. row.wantsLayer = true
  688. row.layer?.cornerRadius = 10
  689. row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
  690. }
  691. row.onHoverChanged?(false)
  692. NSLayoutConstraint.activate([
  693. icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
  694. icon.centerYAnchor.constraint(equalTo: row.centerYAnchor),
  695. title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 10),
  696. title.centerYAnchor.constraint(equalTo: row.centerYAnchor),
  697. toggle.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -2),
  698. toggle.centerYAnchor.constraint(equalTo: row.centerYAnchor)
  699. ])
  700. return row
  701. }
  702. private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView {
  703. let row = HoverTrackingView()
  704. row.translatesAutoresizingMaskIntoConstraints = false
  705. row.heightAnchor.constraint(equalToConstant: 42).isActive = true
  706. let iconLabel = NSTextField(labelWithString: icon)
  707. iconLabel.translatesAutoresizingMaskIntoConstraints = false
  708. iconLabel.font = NSFont.systemFont(ofSize: 18, weight: .medium)
  709. iconLabel.textColor = .white
  710. let titleLabel = NSTextField(labelWithString: title)
  711. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  712. titleLabel.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
  713. titleLabel.textColor = .white
  714. row.addSubview(iconLabel)
  715. row.addSubview(titleLabel)
  716. NSLayoutConstraint.activate([
  717. iconLabel.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
  718. iconLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor),
  719. titleLabel.leadingAnchor.constraint(equalTo: iconLabel.trailingAnchor, constant: 10),
  720. titleLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor)
  721. ])
  722. let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
  723. row.addGestureRecognizer(click)
  724. row.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(action.rawValue)")
  725. row.onHoverChanged = { hovering in
  726. row.wantsLayer = true
  727. row.layer?.cornerRadius = 10
  728. row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
  729. }
  730. row.onHoverChanged?(false)
  731. return row
  732. }
  733. @objc private func darkModeToggled(_ sender: NSSwitch) {
  734. onToggleDarkMode(sender.state == .on)
  735. }
  736. @objc private func settingsActionClicked(_ sender: NSClickGestureRecognizer) {
  737. guard let view = sender.view,
  738. let raw = Int(view.identifier?.rawValue ?? ""),
  739. let action = SettingsAction(rawValue: raw) else { return }
  740. onAction(action)
  741. }
  742. }
  743. private extension ViewController {
  744. func roundedContainer(cornerRadius: CGFloat, color: NSColor) -> NSView {
  745. let view = NSView()
  746. view.wantsLayer = true
  747. view.layer?.backgroundColor = color.cgColor
  748. view.layer?.cornerRadius = cornerRadius
  749. return view
  750. }
  751. func styleSurface(_ view: NSView, borderColor: NSColor, borderWidth: CGFloat, shadow: Bool) {
  752. view.layer?.borderColor = borderColor.cgColor
  753. view.layer?.borderWidth = borderWidth
  754. if shadow {
  755. view.layer?.shadowColor = NSColor.black.cgColor
  756. view.layer?.shadowOpacity = 0.18
  757. view.layer?.shadowOffset = CGSize(width: 0, height: -1)
  758. view.layer?.shadowRadius = 5
  759. }
  760. }
  761. func textLabel(_ text: String, font: NSFont, color: NSColor) -> NSTextField {
  762. let label = NSTextField(labelWithString: text)
  763. label.translatesAutoresizingMaskIntoConstraints = false
  764. label.textColor = color
  765. label.font = font
  766. return label
  767. }
  768. func iconLabel(_ text: String, size: CGFloat) -> NSTextField {
  769. let label = NSTextField(labelWithString: text)
  770. label.translatesAutoresizingMaskIntoConstraints = false
  771. label.font = NSFont.systemFont(ofSize: size)
  772. return label
  773. }
  774. func sidebarSectionTitle(_ text: String) -> NSTextField {
  775. let field = textLabel(text, font: typography.sidebarSection, color: palette.textMuted)
  776. field.alignment = .left
  777. return field
  778. }
  779. func sidebarItem(_ text: String, icon: String, page: SidebarPage, logoImageName: String? = nil, logoIconWidth: CGFloat = 18, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true, showsDisclosure: Bool = false) -> NSView {
  780. let item = HoverTrackingView()
  781. item.wantsLayer = true
  782. item.layer?.cornerRadius = 10
  783. item.layer?.backgroundColor = NSColor.clear.cgColor
  784. item.translatesAutoresizingMaskIntoConstraints = false
  785. item.heightAnchor.constraint(equalToConstant: 36).isActive = true
  786. item.layer?.borderWidth = 0
  787. sidebarPageByView[ObjectIdentifier(item)] = page
  788. let leadingView: NSView
  789. if let name = logoImageName, let logo = NSImage(named: name) {
  790. let imageView = NSImageView(image: logo)
  791. imageView.translatesAutoresizingMaskIntoConstraints = false
  792. imageView.imageScaling = .scaleProportionallyDown
  793. imageView.imageAlignment = .alignCenter
  794. imageView.isEditable = false
  795. leadingView = imageView
  796. } else {
  797. leadingView = textLabel(icon, font: typography.sidebarIcon, color: palette.textSecondary)
  798. }
  799. let titleLabel = textLabel(text, font: typography.sidebarItem, color: palette.textSecondary)
  800. titleLabel.alignment = .left
  801. item.addSubview(leadingView)
  802. item.addSubview(titleLabel)
  803. var constraints: [NSLayoutConstraint] = [
  804. leadingView.leadingAnchor.constraint(equalTo: item.leadingAnchor, constant: 12),
  805. leadingView.centerYAnchor.constraint(equalTo: item.centerYAnchor),
  806. titleLabel.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 8),
  807. titleLabel.centerYAnchor.constraint(equalTo: item.centerYAnchor)
  808. ]
  809. if showsDisclosure {
  810. let chevron = textLabel("›", font: NSFont.systemFont(ofSize: 22, weight: .semibold), color: palette.textSecondary)
  811. chevron.translatesAutoresizingMaskIntoConstraints = false
  812. chevron.alignment = .right
  813. item.addSubview(chevron)
  814. constraints.append(contentsOf: [
  815. chevron.trailingAnchor.constraint(equalTo: item.trailingAnchor, constant: -10),
  816. chevron.centerYAnchor.constraint(equalTo: item.centerYAnchor),
  817. titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: chevron.leadingAnchor, constant: -8)
  818. ])
  819. } else {
  820. constraints.append(titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: item.trailingAnchor, constant: -10))
  821. }
  822. if logoImageName != nil {
  823. let h = logoIconWidth * logoHeightMultiplier
  824. constraints.append(contentsOf: [
  825. leadingView.widthAnchor.constraint(equalToConstant: logoIconWidth),
  826. leadingView.heightAnchor.constraint(equalToConstant: h)
  827. ])
  828. }
  829. NSLayoutConstraint.activate(constraints)
  830. applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate)
  831. item.onHoverChanged = { [weak self, weak item] hovering in
  832. guard let self, let item else { return }
  833. self.applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate, hovering: hovering)
  834. }
  835. let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:)))
  836. item.addGestureRecognizer(click)
  837. return item
  838. }
  839. func applySidebarRowStyle(_ item: NSView, page: SidebarPage, logoTemplate: Bool, hovering: Bool = false) {
  840. let selected = (page == selectedSidebarPage)
  841. let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
  842. item.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
  843. let tint = selected ? NSColor.white : palette.textSecondary
  844. guard item.subviews.count >= 2 else { return }
  845. let leading = item.subviews[0]
  846. let title = item.subviews.first { $0 is NSTextField } as? NSTextField
  847. title?.textColor = tint
  848. // Optional disclosure chevron (if present) is the last text field.
  849. if let chevron = item.subviews.last as? NSTextField, chevron !== title {
  850. chevron.textColor = tint
  851. }
  852. if let imageView = leading as? NSImageView {
  853. if logoTemplate {
  854. imageView.contentTintColor = tint
  855. }
  856. } else if let iconField = leading as? NSTextField {
  857. iconField.textColor = tint
  858. }
  859. }
  860. func topTab(_ title: String, icon: String, provider: MeetingProvider, logoImageName: String? = nil, logoPointSize: CGFloat = 26, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true) -> NSView {
  861. let tab = HoverTrackingView()
  862. tab.wantsLayer = true
  863. tab.layer?.cornerRadius = 19
  864. tab.layer?.backgroundColor = NSColor.clear.cgColor
  865. tab.translatesAutoresizingMaskIntoConstraints = false
  866. meetingProviderByView[ObjectIdentifier(tab)] = provider
  867. let leadingView: NSView
  868. if let name = logoImageName, let logo = NSImage(named: name) {
  869. let imageView = NSImageView(image: logo)
  870. imageView.translatesAutoresizingMaskIntoConstraints = false
  871. imageView.imageScaling = .scaleProportionallyDown
  872. imageView.imageAlignment = .alignCenter
  873. imageView.isEditable = false
  874. if logoTemplate {
  875. imageView.contentTintColor = palette.textPrimary
  876. }
  877. leadingView = imageView
  878. } else {
  879. leadingView = textLabel(icon, font: typography.tabIcon, color: palette.textPrimary)
  880. }
  881. let titleLabel = textLabel(title, font: typography.tabTitle, color: palette.textPrimary)
  882. tab.addSubview(leadingView)
  883. tab.addSubview(titleLabel)
  884. var constraints: [NSLayoutConstraint] = [
  885. leadingView.leadingAnchor.constraint(equalTo: tab.leadingAnchor, constant: 16),
  886. leadingView.centerYAnchor.constraint(equalTo: tab.centerYAnchor),
  887. titleLabel.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 6),
  888. titleLabel.centerYAnchor.constraint(equalTo: tab.centerYAnchor)
  889. ]
  890. if logoImageName != nil {
  891. constraints.append(contentsOf: [
  892. leadingView.widthAnchor.constraint(equalToConstant: logoPointSize),
  893. leadingView.heightAnchor.constraint(equalToConstant: logoPointSize * logoHeightMultiplier)
  894. ])
  895. }
  896. NSLayoutConstraint.activate(constraints)
  897. applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate)
  898. tab.onHoverChanged = { [weak self, weak tab] hovering in
  899. guard let self, let tab else { return }
  900. self.applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate, hovering: hovering)
  901. }
  902. let click = NSClickGestureRecognizer(target: self, action: #selector(meetingTabClicked(_:)))
  903. tab.addGestureRecognizer(click)
  904. return tab
  905. }
  906. func applyTabStyle(_ tab: NSView, provider: MeetingProvider, logoTemplate: Bool, hovering: Bool = false) {
  907. let selected = (provider == selectedMeetingProvider)
  908. let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
  909. tab.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
  910. guard tab.subviews.count >= 2 else { return }
  911. let leading = tab.subviews[0]
  912. let title = tab.subviews[1] as? NSTextField
  913. let textColor = palette.textPrimary
  914. title?.textColor = textColor
  915. if let imageView = leading as? NSImageView {
  916. if logoTemplate {
  917. imageView.contentTintColor = textColor
  918. }
  919. } else if let iconField = leading as? NSTextField {
  920. iconField.textColor = textColor
  921. }
  922. }
  923. func actionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat) -> NSView {
  924. let button = HoverTrackingView()
  925. button.wantsLayer = true
  926. button.layer?.cornerRadius = 9
  927. button.layer?.backgroundColor = color.cgColor
  928. button.translatesAutoresizingMaskIntoConstraints = false
  929. button.widthAnchor.constraint(equalToConstant: width).isActive = true
  930. button.heightAnchor.constraint(equalToConstant: 36).isActive = true
  931. styleSurface(button, borderColor: title == "Cancel" ? palette.inputBorder : palette.primaryBlueBorder, borderWidth: 1, shadow: false)
  932. if title == "Cancel" {
  933. button.layer?.backgroundColor = palette.cancelButton.cgColor
  934. }
  935. let label = textLabel(title, font: typography.buttonText, color: textColor)
  936. button.addSubview(label)
  937. NSLayoutConstraint.activate([
  938. label.centerXAnchor.constraint(equalTo: button.centerXAnchor),
  939. label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
  940. ])
  941. let baseColor = (title == "Cancel") ? palette.cancelButton : color
  942. let hoverColor = baseColor.blended(withFraction: 0.12, of: NSColor.white) ?? baseColor
  943. button.onHoverChanged = { hovering in
  944. button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
  945. }
  946. button.onHoverChanged?(false)
  947. return button
  948. }
  949. func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView {
  950. let button = HoverTrackingView()
  951. button.wantsLayer = true
  952. button.layer?.cornerRadius = size / 2
  953. button.layer?.backgroundColor = palette.inputBackground.cgColor
  954. button.translatesAutoresizingMaskIntoConstraints = false
  955. button.widthAnchor.constraint(equalToConstant: size).isActive = true
  956. button.heightAnchor.constraint(equalToConstant: size).isActive = true
  957. styleSurface(button, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  958. let label = textLabel(symbol, font: typography.iconButton, color: palette.textSecondary)
  959. button.addSubview(label)
  960. NSLayoutConstraint.activate([
  961. label.centerXAnchor.constraint(equalTo: button.centerXAnchor),
  962. label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
  963. ])
  964. let baseColor = palette.inputBackground
  965. let hoverColor = baseColor.blended(withFraction: 0.10, of: NSColor.white) ?? baseColor
  966. button.onHoverChanged = { hovering in
  967. button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
  968. }
  969. button.onHoverChanged?(false)
  970. return button
  971. }
  972. }
  973. private struct Palette {
  974. let pageBackground = NSColor(calibratedRed: 10.0 / 255.0, green: 11.0 / 255.0, blue: 12.0 / 255.0, alpha: 1)
  975. let sidebarBackground = NSColor(calibratedRed: 16.0 / 255.0, green: 17.0 / 255.0, blue: 19.0 / 255.0, alpha: 1)
  976. let sectionCard = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1)
  977. let tabBarBackground = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1)
  978. let tabIdleBackground = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1)
  979. let inputBackground = NSColor(calibratedRed: 20.0 / 255.0, green: 21.0 / 255.0, blue: 24.0 / 255.0, alpha: 1)
  980. let inputBorder = NSColor(calibratedRed: 38.0 / 255.0, green: 40.0 / 255.0, blue: 44.0 / 255.0, alpha: 1)
  981. let primaryBlue = NSColor(calibratedRed: 27.0 / 255.0, green: 115.0 / 255.0, blue: 232.0 / 255.0, alpha: 1)
  982. let primaryBlueBorder = NSColor(calibratedRed: 42.0 / 255.0, green: 118.0 / 255.0, blue: 220.0 / 255.0, alpha: 1)
  983. let cancelButton = NSColor(calibratedRed: 20.0 / 255.0, green: 21.0 / 255.0, blue: 24.0 / 255.0, alpha: 1)
  984. let meetingBadge = NSColor(calibratedRed: 0.88, green: 0.66, blue: 0.14, alpha: 1)
  985. let separator = NSColor(calibratedRed: 26.0 / 255.0, green: 27.0 / 255.0, blue: 30.0 / 255.0, alpha: 1)
  986. let textPrimary = NSColor(calibratedWhite: 0.98, alpha: 1)
  987. let textSecondary = NSColor(calibratedWhite: 0.78, alpha: 1)
  988. let textTertiary = NSColor(calibratedWhite: 0.66, alpha: 1)
  989. let textMuted = NSColor(calibratedWhite: 0.44, alpha: 1)
  990. }
  991. private struct Typography {
  992. let sidebarBrand = NSFont.systemFont(ofSize: 26, weight: .bold)
  993. let sidebarSection = NSFont.systemFont(ofSize: 11, weight: .medium)
  994. let sidebarIcon = NSFont.systemFont(ofSize: 12, weight: .medium)
  995. let sidebarItem = NSFont.systemFont(ofSize: 16, weight: .medium)
  996. let pageTitle = NSFont.systemFont(ofSize: 27, weight: .semibold)
  997. let joinWithURLTitle = NSFont.systemFont(ofSize: 17, weight: .semibold)
  998. let sectionTitleBold = NSFont.systemFont(ofSize: 25, weight: .bold)
  999. let dateHeading = NSFont.systemFont(ofSize: 18, weight: .medium)
  1000. let tabIcon = NSFont.systemFont(ofSize: 13, weight: .regular)
  1001. let tabTitle = NSFont.systemFont(ofSize: 31 / 2, weight: .semibold)
  1002. let fieldLabel = NSFont.systemFont(ofSize: 15, weight: .medium)
  1003. let inputPlaceholder = NSFont.systemFont(ofSize: 14, weight: .regular)
  1004. let buttonText = NSFont.systemFont(ofSize: 16, weight: .medium)
  1005. let filterText = NSFont.systemFont(ofSize: 15, weight: .regular)
  1006. let filterArrow = NSFont.systemFont(ofSize: 12, weight: .regular)
  1007. let iconButton = NSFont.systemFont(ofSize: 14, weight: .medium)
  1008. let cardIcon = NSFont.systemFont(ofSize: 8, weight: .bold)
  1009. let cardTitle = NSFont.systemFont(ofSize: 15, weight: .semibold)
  1010. let cardSubtitle = NSFont.systemFont(ofSize: 13, weight: .bold)
  1011. let cardTime = NSFont.systemFont(ofSize: 12, weight: .regular)
  1012. }