|
|
@@ -211,6 +211,16 @@ class ViewController: NSViewController {
|
|
211
|
211
|
private weak var homeSearchField: NSTextField?
|
|
212
|
212
|
private weak var homeSearchPill: NSView?
|
|
213
|
213
|
private weak var homeSettingsView: NSView?
|
|
|
214
|
+ private weak var schedulerRootView: NSView?
|
|
|
215
|
+ private weak var schedulerMonthLabel: NSTextField?
|
|
|
216
|
+ private weak var schedulerCalendarGridStack: NSStackView?
|
|
|
217
|
+ private weak var schedulerSelectedDateLabel: NSTextField?
|
|
|
218
|
+ private weak var schedulerMeetingsListStack: NSStackView?
|
|
|
219
|
+ private weak var schedulerMeetingsEmptyLabel: NSTextField?
|
|
|
220
|
+ private weak var schedulerTodayButton: NSButton?
|
|
|
221
|
+ private var schedulerDayButtons: [Date: NSButton] = [:]
|
|
|
222
|
+ private var schedulerDateByButtonTag: [Int: Date] = [:]
|
|
|
223
|
+ private var nextSchedulerButtonTag: Int = 1
|
|
214
|
224
|
private weak var settingsDarkModeSwitch: NSSwitch?
|
|
215
|
225
|
private weak var settingsUpgradeButton: NSButton?
|
|
216
|
226
|
private weak var settingsRestoreButton: NSButton?
|
|
|
@@ -244,6 +254,12 @@ class ViewController: NSViewController {
|
|
244
|
254
|
private var lastKnownPremiumAccess = false
|
|
245
|
255
|
private var allScheduledMeetings: [ScheduledMeeting] = []
|
|
246
|
256
|
private var selectedMeetingsDayStart: Date = Calendar.current.startOfDay(for: Date())
|
|
|
257
|
+ private var schedulerVisibleMonthStart: Date = {
|
|
|
258
|
+ let calendar = Calendar.current
|
|
|
259
|
+ let today = Date()
|
|
|
260
|
+ let components = calendar.dateComponents([.year, .month], from: today)
|
|
|
261
|
+ return calendar.date(from: components) ?? calendar.startOfDay(for: today)
|
|
|
262
|
+ }()
|
|
247
|
263
|
private var selectedHomeSidebarItem: String = "Home"
|
|
248
|
264
|
private var homeSidebarRowViews: [String: NSView] = [:]
|
|
249
|
265
|
private var homeSidebarIconViews: [String: NSImageView] = [:]
|
|
|
@@ -1586,6 +1602,8 @@ class ViewController: NSViewController {
|
|
1586
|
1602
|
meetingsStatusLabel?.stringValue = "Upcoming meetings"
|
|
1587
|
1603
|
emptyMeetingLabel?.stringValue = "No meetings match your search."
|
|
1588
|
1604
|
}
|
|
|
1605
|
+ applySchedulerMeetingsForSelectedDate()
|
|
|
1606
|
+ renderSchedulerCalendarGrid()
|
|
1589
|
1607
|
return
|
|
1590
|
1608
|
}
|
|
1591
|
1609
|
|
|
|
@@ -1599,11 +1617,27 @@ class ViewController: NSViewController {
|
|
1599
|
1617
|
card.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
1600
|
1618
|
}
|
|
1601
|
1619
|
}
|
|
|
1620
|
+ applySchedulerMeetingsForSelectedDate()
|
|
|
1621
|
+ renderSchedulerCalendarGrid()
|
|
|
1622
|
+ }
|
|
|
1623
|
+
|
|
|
1624
|
+ private func meetingsForDay(_ dayStart: Date) -> [ScheduledMeeting] {
|
|
|
1625
|
+ let calendar = Calendar.current
|
|
|
1626
|
+ let safeDayStart = calendar.startOfDay(for: dayStart)
|
|
|
1627
|
+ let dayEnd = calendar.date(byAdding: .day, value: 1, to: safeDayStart) ?? safeDayStart.addingTimeInterval(60 * 60 * 24)
|
|
|
1628
|
+ return allScheduledMeetings
|
|
|
1629
|
+ .filter { meeting in
|
|
|
1630
|
+ meeting.start >= safeDayStart && meeting.start < dayEnd
|
|
|
1631
|
+ }
|
|
|
1632
|
+ .sorted(by: { $0.start < $1.start })
|
|
1602
|
1633
|
}
|
|
1603
|
1634
|
|
|
1604
|
1635
|
@MainActor
|
|
1605
|
1636
|
private func setSelectedMeetingsDayStart(_ newDayStart: Date) {
|
|
1606
|
|
- selectedMeetingsDayStart = Calendar.current.startOfDay(for: newDayStart)
|
|
|
1637
|
+ let calendar = Calendar.current
|
|
|
1638
|
+ selectedMeetingsDayStart = calendar.startOfDay(for: newDayStart)
|
|
|
1639
|
+ let monthComponents = calendar.dateComponents([.year, .month], from: selectedMeetingsDayStart)
|
|
|
1640
|
+ schedulerVisibleMonthStart = calendar.date(from: monthComponents) ?? selectedMeetingsDayStart
|
|
1607
|
1641
|
updateMeetingsDayUI()
|
|
1608
|
1642
|
applyFilteredMeetings()
|
|
1609
|
1643
|
}
|
|
|
@@ -1618,6 +1652,11 @@ class ViewController: NSViewController {
|
|
1618
|
1652
|
let isToday = Calendar.current.isDate(dayStart, inSameDayAs: Date())
|
|
1619
|
1653
|
meetingsTodayButton?.isEnabled = isToday == false
|
|
1620
|
1654
|
meetingsTodayButton?.alphaValue = isToday ? 0.55 : 1.0
|
|
|
1655
|
+ schedulerTodayButton?.isEnabled = isToday == false
|
|
|
1656
|
+ schedulerTodayButton?.alphaValue = isToday ? 0.55 : 1.0
|
|
|
1657
|
+ updateSchedulerDateHeader()
|
|
|
1658
|
+ applySchedulerMeetingsForSelectedDate()
|
|
|
1659
|
+ renderSchedulerCalendarGrid()
|
|
1621
|
1660
|
}
|
|
1622
|
1661
|
|
|
1623
|
1662
|
private func meetingsDayDisplayName(for dayStart: Date) -> String {
|
|
|
@@ -1654,6 +1693,166 @@ class ViewController: NSViewController {
|
|
1654
|
1693
|
}
|
|
1655
|
1694
|
}
|
|
1656
|
1695
|
|
|
|
1696
|
+ private func updateSchedulerDateHeader() {
|
|
|
1697
|
+ let formatter = DateFormatter()
|
|
|
1698
|
+ formatter.dateFormat = "MMMM yyyy"
|
|
|
1699
|
+ schedulerMonthLabel?.stringValue = formatter.string(from: schedulerVisibleMonthStart)
|
|
|
1700
|
+
|
|
|
1701
|
+ let selectedFormatter = DateFormatter()
|
|
|
1702
|
+ selectedFormatter.dateFormat = "EEEE, MMM d"
|
|
|
1703
|
+ schedulerSelectedDateLabel?.stringValue = selectedFormatter.string(from: selectedMeetingsDayStart)
|
|
|
1704
|
+ }
|
|
|
1705
|
+
|
|
|
1706
|
+ @MainActor
|
|
|
1707
|
+ private func applySchedulerMeetingsForSelectedDate() {
|
|
|
1708
|
+ guard let stack = schedulerMeetingsListStack else { return }
|
|
|
1709
|
+ stack.arrangedSubviews.forEach { view in
|
|
|
1710
|
+ stack.removeArrangedSubview(view)
|
|
|
1711
|
+ view.removeFromSuperview()
|
|
|
1712
|
+ }
|
|
|
1713
|
+
|
|
|
1714
|
+ let meetings = meetingsForDay(selectedMeetingsDayStart)
|
|
|
1715
|
+ if meetings.isEmpty {
|
|
|
1716
|
+ schedulerMeetingsEmptyLabel?.isHidden = false
|
|
|
1717
|
+ schedulerMeetingsEmptyLabel?.stringValue = "No meetings scheduled for \(meetingsDayDisplayName(for: selectedMeetingsDayStart))."
|
|
|
1718
|
+ return
|
|
|
1719
|
+ }
|
|
|
1720
|
+
|
|
|
1721
|
+ schedulerMeetingsEmptyLabel?.isHidden = true
|
|
|
1722
|
+ for meeting in meetings {
|
|
|
1723
|
+ let card = makeMeetingRowCard(meeting)
|
|
|
1724
|
+ stack.addArrangedSubview(card)
|
|
|
1725
|
+ if card.constraints.contains(where: { $0.firstAttribute == .width }) == false {
|
|
|
1726
|
+ card.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
1727
|
+ }
|
|
|
1728
|
+ }
|
|
|
1729
|
+ }
|
|
|
1730
|
+
|
|
|
1731
|
+ @MainActor
|
|
|
1732
|
+ private func renderSchedulerCalendarGrid() {
|
|
|
1733
|
+ guard let gridStack = schedulerCalendarGridStack else { return }
|
|
|
1734
|
+ schedulerDayButtons = [:]
|
|
|
1735
|
+ schedulerDateByButtonTag = [:]
|
|
|
1736
|
+ nextSchedulerButtonTag = 1
|
|
|
1737
|
+ gridStack.arrangedSubviews.forEach { row in
|
|
|
1738
|
+ gridStack.removeArrangedSubview(row)
|
|
|
1739
|
+ row.removeFromSuperview()
|
|
|
1740
|
+ }
|
|
|
1741
|
+
|
|
|
1742
|
+ let calendar = Calendar.current
|
|
|
1743
|
+ let monthStart = schedulerVisibleMonthStart
|
|
|
1744
|
+ let monthRange = calendar.range(of: .day, in: .month, for: monthStart) ?? 1..<32
|
|
|
1745
|
+ let monthWeekday = calendar.component(.weekday, from: monthStart)
|
|
|
1746
|
+ let leadingSlots = monthWeekday - 1
|
|
|
1747
|
+ let totalSlots = 42
|
|
|
1748
|
+
|
|
|
1749
|
+ for week in 0..<6 {
|
|
|
1750
|
+ let row = NSStackView()
|
|
|
1751
|
+ row.orientation = .horizontal
|
|
|
1752
|
+ row.distribution = .fillEqually
|
|
|
1753
|
+ row.spacing = 6
|
|
|
1754
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1755
|
+ gridStack.addArrangedSubview(row)
|
|
|
1756
|
+
|
|
|
1757
|
+ for weekday in 0..<7 {
|
|
|
1758
|
+ let cellIndex = (week * 7) + weekday
|
|
|
1759
|
+ let dayOffset = cellIndex - leadingSlots
|
|
|
1760
|
+ let dayDate = calendar.date(byAdding: .day, value: dayOffset, to: monthStart) ?? monthStart
|
|
|
1761
|
+ let dayNumber = calendar.component(.day, from: dayDate)
|
|
|
1762
|
+ let inCurrentMonth = monthRange.contains(dayNumber) && calendar.isDate(dayDate, equalTo: monthStart, toGranularity: .month)
|
|
|
1763
|
+ let hasMeetings = meetingsForDay(dayDate).isEmpty == false
|
|
|
1764
|
+ let button = makeSchedulerDayCell(
|
|
|
1765
|
+ day: dayNumber,
|
|
|
1766
|
+ date: dayDate,
|
|
|
1767
|
+ inCurrentMonth: inCurrentMonth,
|
|
|
1768
|
+ selected: calendar.isDate(dayDate, inSameDayAs: selectedMeetingsDayStart),
|
|
|
1769
|
+ isToday: calendar.isDateInToday(dayDate),
|
|
|
1770
|
+ hasMeetings: hasMeetings
|
|
|
1771
|
+ )
|
|
|
1772
|
+ row.addArrangedSubview(button)
|
|
|
1773
|
+ schedulerDayButtons[calendar.startOfDay(for: dayDate)] = button
|
|
|
1774
|
+ }
|
|
|
1775
|
+ }
|
|
|
1776
|
+
|
|
|
1777
|
+ // Keep a consistent 6x7 grid footprint.
|
|
|
1778
|
+ if totalSlots == 42 {
|
|
|
1779
|
+ gridStack.needsLayout = true
|
|
|
1780
|
+ }
|
|
|
1781
|
+ }
|
|
|
1782
|
+
|
|
|
1783
|
+ private func makeSchedulerDayCell(
|
|
|
1784
|
+ day: Int,
|
|
|
1785
|
+ date: Date,
|
|
|
1786
|
+ inCurrentMonth: Bool,
|
|
|
1787
|
+ selected: Bool,
|
|
|
1788
|
+ isToday: Bool,
|
|
|
1789
|
+ hasMeetings: Bool
|
|
|
1790
|
+ ) -> NSButton {
|
|
|
1791
|
+ let button = NSButton(title: "\(day)", target: self, action: #selector(schedulerDayTapped(_:)))
|
|
|
1792
|
+ button.isBordered = false
|
|
|
1793
|
+ button.bezelStyle = .shadowlessSquare
|
|
|
1794
|
+ button.focusRingType = .none
|
|
|
1795
|
+ button.font = .systemFont(ofSize: 16, weight: selected ? .semibold : .regular)
|
|
|
1796
|
+ button.alignment = .center
|
|
|
1797
|
+ button.wantsLayer = true
|
|
|
1798
|
+ button.layer?.cornerRadius = 16
|
|
|
1799
|
+ button.layer?.borderWidth = selected ? 1.5 : (hasMeetings ? 1 : 0)
|
|
|
1800
|
+ button.layer?.backgroundColor = selected
|
|
|
1801
|
+ ? accentBlue.withAlphaComponent(palette.isDarkMode ? 0.92 : 0.16).cgColor
|
|
|
1802
|
+ : NSColor.clear.cgColor
|
|
|
1803
|
+ if selected {
|
|
|
1804
|
+ button.layer?.borderColor = accentBlue.cgColor
|
|
|
1805
|
+ } else if isToday {
|
|
|
1806
|
+ button.layer?.borderColor = accentOrange.withAlphaComponent(0.8).cgColor
|
|
|
1807
|
+ button.layer?.borderWidth = 1
|
|
|
1808
|
+ } else if hasMeetings {
|
|
|
1809
|
+ button.layer?.borderColor = accentBlue.withAlphaComponent(0.45).cgColor
|
|
|
1810
|
+ } else {
|
|
|
1811
|
+ button.layer?.borderColor = NSColor.clear.cgColor
|
|
|
1812
|
+ }
|
|
|
1813
|
+ button.contentTintColor = selected
|
|
|
1814
|
+ ? primaryText
|
|
|
1815
|
+ : (inCurrentMonth ? primaryText : mutedText.withAlphaComponent(0.75))
|
|
|
1816
|
+ let tag = nextSchedulerButtonTag
|
|
|
1817
|
+ nextSchedulerButtonTag += 1
|
|
|
1818
|
+ button.tag = tag
|
|
|
1819
|
+ schedulerDateByButtonTag[tag] = Calendar.current.startOfDay(for: date)
|
|
|
1820
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1821
|
+ button.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
|
1822
|
+ return button
|
|
|
1823
|
+ }
|
|
|
1824
|
+
|
|
|
1825
|
+ @objc private func schedulerDayTapped(_ sender: NSButton) {
|
|
|
1826
|
+ guard let selectedDate = schedulerDateByButtonTag[sender.tag] else { return }
|
|
|
1827
|
+ Task { @MainActor in
|
|
|
1828
|
+ self.setSelectedMeetingsDayStart(selectedDate)
|
|
|
1829
|
+ }
|
|
|
1830
|
+ }
|
|
|
1831
|
+
|
|
|
1832
|
+ @objc private func schedulerPrevMonthTapped() {
|
|
|
1833
|
+ let calendar = Calendar.current
|
|
|
1834
|
+ let prev = calendar.date(byAdding: .month, value: -1, to: schedulerVisibleMonthStart) ?? schedulerVisibleMonthStart
|
|
|
1835
|
+ let monthComponents = calendar.dateComponents([.year, .month], from: prev)
|
|
|
1836
|
+ schedulerVisibleMonthStart = calendar.date(from: monthComponents) ?? prev
|
|
|
1837
|
+ updateSchedulerDateHeader()
|
|
|
1838
|
+ renderSchedulerCalendarGrid()
|
|
|
1839
|
+ }
|
|
|
1840
|
+
|
|
|
1841
|
+ @objc private func schedulerNextMonthTapped() {
|
|
|
1842
|
+ let calendar = Calendar.current
|
|
|
1843
|
+ let next = calendar.date(byAdding: .month, value: 1, to: schedulerVisibleMonthStart) ?? schedulerVisibleMonthStart
|
|
|
1844
|
+ let monthComponents = calendar.dateComponents([.year, .month], from: next)
|
|
|
1845
|
+ schedulerVisibleMonthStart = calendar.date(from: monthComponents) ?? next
|
|
|
1846
|
+ updateSchedulerDateHeader()
|
|
|
1847
|
+ renderSchedulerCalendarGrid()
|
|
|
1848
|
+ }
|
|
|
1849
|
+
|
|
|
1850
|
+ @objc private func schedulerTodayTapped() {
|
|
|
1851
|
+ Task { @MainActor in
|
|
|
1852
|
+ self.setSelectedMeetingsDayStart(Date())
|
|
|
1853
|
+ }
|
|
|
1854
|
+ }
|
|
|
1855
|
+
|
|
1657
|
1856
|
private func loadScheduledMeetings() async {
|
|
1658
|
1857
|
if isLoadingMeetings { return }
|
|
1659
|
1858
|
isLoadingMeetings = true
|
|
|
@@ -1682,7 +1881,7 @@ class ViewController: NSViewController {
|
|
1682
|
1881
|
} else if case ZoomOAuthError.missingClientSecret = error {
|
|
1683
|
1882
|
self.meetingsStatusLabel?.stringValue = "Zoom OAuth app not configured."
|
|
1684
|
1883
|
self.promptForZoomOAuthCredentialsIfNeeded()
|
|
1685
|
|
- } else if case ZoomOAuthError.missingRequiredScope(let scopeMessage) = error {
|
|
|
1884
|
+ } else if case ZoomOAuthError.missingRequiredScope = error {
|
|
1686
|
1885
|
self.zoomOAuth.clearSavedTokens()
|
|
1687
|
1886
|
self.meetingsStatusLabel?.stringValue = "Zoom permissions are missing. Update your Zoom app scopes, then sign in again."
|
|
1688
|
1887
|
} else if case ZoomOAuthError.rateLimited(let retryAfterSeconds) = error {
|
|
|
@@ -3163,6 +3362,108 @@ class ViewController: NSViewController {
|
|
3163
|
3362
|
let settingsView = makeSettingsView()
|
|
3164
|
3363
|
settingsView.isHidden = selectedHomeSidebarItem != "Settings"
|
|
3165
|
3364
|
|
|
|
3365
|
+ let schedulerRoot = NSView()
|
|
|
3366
|
+ schedulerRoot.isHidden = selectedHomeSidebarItem != "Scheduler"
|
|
|
3367
|
+
|
|
|
3368
|
+ let schedulerSplit = NSStackView()
|
|
|
3369
|
+ schedulerSplit.orientation = .horizontal
|
|
|
3370
|
+ schedulerSplit.spacing = 14
|
|
|
3371
|
+ schedulerSplit.distribution = .fillEqually
|
|
|
3372
|
+ schedulerSplit.alignment = .top
|
|
|
3373
|
+
|
|
|
3374
|
+ let schedulerCalendarPanel = NSView()
|
|
|
3375
|
+ schedulerCalendarPanel.wantsLayer = true
|
|
|
3376
|
+ schedulerCalendarPanel.layer?.backgroundColor = secondaryCardBackground.withAlphaComponent(0.94).cgColor
|
|
|
3377
|
+ schedulerCalendarPanel.layer?.cornerRadius = 16
|
|
|
3378
|
+ schedulerCalendarPanel.layer?.borderWidth = 1
|
|
|
3379
|
+ schedulerCalendarPanel.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
3380
|
+
|
|
|
3381
|
+ let schedulerMeetingsPanel = NSView()
|
|
|
3382
|
+ schedulerMeetingsPanel.wantsLayer = true
|
|
|
3383
|
+ schedulerMeetingsPanel.layer?.backgroundColor = secondaryCardBackground.withAlphaComponent(0.94).cgColor
|
|
|
3384
|
+ schedulerMeetingsPanel.layer?.cornerRadius = 16
|
|
|
3385
|
+ schedulerMeetingsPanel.layer?.borderWidth = 1
|
|
|
3386
|
+ schedulerMeetingsPanel.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
3387
|
+
|
|
|
3388
|
+ let schedulerCalendarTopRow = NSStackView()
|
|
|
3389
|
+ schedulerCalendarTopRow.orientation = .horizontal
|
|
|
3390
|
+ schedulerCalendarTopRow.alignment = .centerY
|
|
|
3391
|
+ schedulerCalendarTopRow.distribution = .fill
|
|
|
3392
|
+ schedulerCalendarTopRow.spacing = 10
|
|
|
3393
|
+
|
|
|
3394
|
+ let schedulerConnectLabel = makeLabel("Connect calendar", size: 18, color: accentBlue, weight: .semibold, centered: false)
|
|
|
3395
|
+ let schedulerPlusButton = NSButton(title: "", target: nil, action: nil)
|
|
|
3396
|
+ schedulerPlusButton.isBordered = false
|
|
|
3397
|
+ schedulerPlusButton.bezelStyle = .shadowlessSquare
|
|
|
3398
|
+ schedulerPlusButton.focusRingType = .none
|
|
|
3399
|
+ schedulerPlusButton.wantsLayer = true
|
|
|
3400
|
+ schedulerPlusButton.layer?.backgroundColor = accentBlue.cgColor
|
|
|
3401
|
+ schedulerPlusButton.layer?.cornerRadius = 14
|
|
|
3402
|
+ schedulerPlusButton.contentTintColor = .white
|
|
|
3403
|
+ schedulerPlusButton.toolTip = "Connect calendar"
|
|
|
3404
|
+ if let image = NSImage(systemSymbolName: "plus", accessibilityDescription: "Connect calendar") {
|
|
|
3405
|
+ schedulerPlusButton.image = image
|
|
|
3406
|
+ schedulerPlusButton.imagePosition = .imageOnly
|
|
|
3407
|
+ }
|
|
|
3408
|
+ schedulerPlusButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
3409
|
+ schedulerPlusButton.widthAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
3410
|
+ schedulerPlusButton.heightAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
3411
|
+
|
|
|
3412
|
+ let schedulerMonthRow = NSStackView()
|
|
|
3413
|
+ schedulerMonthRow.orientation = .horizontal
|
|
|
3414
|
+ schedulerMonthRow.alignment = .centerY
|
|
|
3415
|
+ schedulerMonthRow.spacing = 8
|
|
|
3416
|
+
|
|
|
3417
|
+ let schedulerPrevMonthButton = makeNavGlyphButton(symbol: "chevron.left", action: #selector(schedulerPrevMonthTapped), dimension: 18, pointSize: 10, toolTip: "Previous month")
|
|
|
3418
|
+ let schedulerMonthLabel = makeLabel("-", size: 30, color: primaryText, weight: .semibold, centered: false)
|
|
|
3419
|
+ let schedulerNextMonthButton = makeNavGlyphButton(symbol: "chevron.right", action: #selector(schedulerNextMonthTapped), dimension: 18, pointSize: 10, toolTip: "Next month")
|
|
|
3420
|
+ [schedulerPrevMonthButton, schedulerMonthLabel, schedulerNextMonthButton].forEach { schedulerMonthRow.addArrangedSubview($0) }
|
|
|
3421
|
+
|
|
|
3422
|
+ let schedulerWeekdaysRow = NSStackView()
|
|
|
3423
|
+ schedulerWeekdaysRow.orientation = .horizontal
|
|
|
3424
|
+ schedulerWeekdaysRow.distribution = .fillEqually
|
|
|
3425
|
+ schedulerWeekdaysRow.spacing = 4
|
|
|
3426
|
+ let weekdaySymbols = ["S", "M", "T", "W", "T", "F", "S"]
|
|
|
3427
|
+ for symbol in weekdaySymbols {
|
|
|
3428
|
+ let weekdayLabel = makeLabel(symbol, size: 12, color: mutedText, weight: .medium, centered: true)
|
|
|
3429
|
+ schedulerWeekdaysRow.addArrangedSubview(weekdayLabel)
|
|
|
3430
|
+ }
|
|
|
3431
|
+
|
|
|
3432
|
+ let schedulerCalendarGrid = NSStackView()
|
|
|
3433
|
+ schedulerCalendarGrid.orientation = .vertical
|
|
|
3434
|
+ schedulerCalendarGrid.distribution = .fillEqually
|
|
|
3435
|
+ schedulerCalendarGrid.spacing = 6
|
|
|
3436
|
+
|
|
|
3437
|
+ let schedulerRightTopRow = NSStackView()
|
|
|
3438
|
+ schedulerRightTopRow.orientation = .horizontal
|
|
|
3439
|
+ schedulerRightTopRow.alignment = .centerY
|
|
|
3440
|
+ schedulerRightTopRow.spacing = 10
|
|
|
3441
|
+
|
|
|
3442
|
+ let schedulerSelectedDateLabel = makeLabel("-", size: 30, color: primaryText, weight: .semibold, centered: false)
|
|
|
3443
|
+ let schedulerTodayButton = makeMeetingsDayChipButton(title: "Today", symbol: "calendar", action: #selector(schedulerTodayTapped))
|
|
|
3444
|
+ schedulerTodayButton.toolTip = "Jump to today"
|
|
|
3445
|
+ [schedulerTodayButton].forEach { schedulerRightTopRow.addArrangedSubview($0) }
|
|
|
3446
|
+
|
|
|
3447
|
+ let schedulerMeetingsHeader = makeLabel("Scheduled meetings", size: 13, color: secondaryText, weight: .medium, centered: false)
|
|
|
3448
|
+
|
|
|
3449
|
+ let schedulerMeetingsScroll = NSScrollView()
|
|
|
3450
|
+ schedulerMeetingsScroll.drawsBackground = false
|
|
|
3451
|
+ schedulerMeetingsScroll.hasVerticalScroller = true
|
|
|
3452
|
+ schedulerMeetingsScroll.hasHorizontalScroller = false
|
|
|
3453
|
+ schedulerMeetingsScroll.autohidesScrollers = true
|
|
|
3454
|
+ let schedulerMeetingsDocument = FlippedView()
|
|
|
3455
|
+ let schedulerMeetingsStack = NSStackView()
|
|
|
3456
|
+ schedulerMeetingsStack.orientation = .vertical
|
|
|
3457
|
+ schedulerMeetingsStack.spacing = 12
|
|
|
3458
|
+ schedulerMeetingsStack.alignment = .leading
|
|
|
3459
|
+ schedulerMeetingsStack.setContentHuggingPriority(.required, for: .vertical)
|
|
|
3460
|
+ schedulerMeetingsStack.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
|
3461
|
+ schedulerMeetingsScroll.documentView = schedulerMeetingsDocument
|
|
|
3462
|
+ schedulerMeetingsDocument.addSubview(schedulerMeetingsStack)
|
|
|
3463
|
+
|
|
|
3464
|
+ let schedulerEmptyLabel = makeLabel("No meetings scheduled for this date.", size: 15, color: secondaryText, weight: .regular, centered: true)
|
|
|
3465
|
+ schedulerEmptyLabel.isHidden = true
|
|
|
3466
|
+
|
|
3166
|
3467
|
let contentColumn = NSView()
|
|
3167
|
3468
|
contentColumn.translatesAutoresizingMaskIntoConstraints = false
|
|
3168
|
3469
|
content.addSubview(topBar)
|
|
|
@@ -3181,10 +3482,20 @@ class ViewController: NSViewController {
|
|
3181
|
3482
|
[searchIcon, searchField, searchHintLabel].forEach {
|
|
3182
|
3483
|
searchPill.addSubview($0)
|
|
3183
|
3484
|
}
|
|
3184
|
|
- [welcome, timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, meetingsDayNav, noMeeting, meetingsScrollView, refreshMeetingsButton, placeholder, settingsView].forEach {
|
|
|
3485
|
+ [welcome, timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, meetingsDayNav, noMeeting, meetingsScrollView, refreshMeetingsButton, placeholder, settingsView, schedulerRoot].forEach {
|
|
3185
|
3486
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
3186
|
3487
|
contentColumn.addSubview($0)
|
|
3187
|
3488
|
}
|
|
|
3489
|
+ [schedulerSplit, schedulerCalendarPanel, schedulerMeetingsPanel, schedulerCalendarTopRow, schedulerConnectLabel, schedulerPlusButton, schedulerMonthRow, schedulerWeekdaysRow, schedulerCalendarGrid, schedulerRightTopRow, schedulerSelectedDateLabel, schedulerTodayButton, schedulerMeetingsHeader, schedulerMeetingsScroll, schedulerEmptyLabel, schedulerMeetingsDocument, schedulerMeetingsStack].forEach {
|
|
|
3490
|
+ $0.translatesAutoresizingMaskIntoConstraints = false
|
|
|
3491
|
+ }
|
|
|
3492
|
+ schedulerRoot.addSubview(schedulerSplit)
|
|
|
3493
|
+ [schedulerCalendarPanel, schedulerMeetingsPanel].forEach { schedulerSplit.addArrangedSubview($0) }
|
|
|
3494
|
+ [schedulerCalendarTopRow, schedulerMonthRow, schedulerWeekdaysRow, schedulerCalendarGrid].forEach { schedulerCalendarPanel.addSubview($0) }
|
|
|
3495
|
+ schedulerCalendarTopRow.addArrangedSubview(schedulerConnectLabel)
|
|
|
3496
|
+ schedulerCalendarTopRow.addArrangedSubview(NSView())
|
|
|
3497
|
+ schedulerCalendarTopRow.addArrangedSubview(schedulerPlusButton)
|
|
|
3498
|
+ [schedulerSelectedDateLabel, schedulerRightTopRow, schedulerMeetingsHeader, schedulerMeetingsScroll, schedulerEmptyLabel].forEach { schedulerMeetingsPanel.addSubview($0) }
|
|
3188
|
3499
|
topBar.translatesAutoresizingMaskIntoConstraints = false
|
|
3189
|
3500
|
topBarDivider.translatesAutoresizingMaskIntoConstraints = false
|
|
3190
|
3501
|
meetingsDocument.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -3292,7 +3603,60 @@ class ViewController: NSViewController {
|
|
3292
|
3603
|
settingsView.topAnchor.constraint(equalTo: contentColumn.topAnchor),
|
|
3293
|
3604
|
settingsView.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor),
|
|
3294
|
3605
|
settingsView.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor),
|
|
3295
|
|
- settingsView.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor)
|
|
|
3606
|
+ settingsView.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor),
|
|
|
3607
|
+
|
|
|
3608
|
+ schedulerRoot.topAnchor.constraint(equalTo: contentColumn.topAnchor, constant: 8),
|
|
|
3609
|
+ schedulerRoot.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor, constant: 8),
|
|
|
3610
|
+ schedulerRoot.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor, constant: -8),
|
|
|
3611
|
+ schedulerRoot.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor, constant: -8),
|
|
|
3612
|
+
|
|
|
3613
|
+ schedulerSplit.topAnchor.constraint(equalTo: schedulerRoot.topAnchor),
|
|
|
3614
|
+ schedulerSplit.leadingAnchor.constraint(equalTo: schedulerRoot.leadingAnchor),
|
|
|
3615
|
+ schedulerSplit.trailingAnchor.constraint(equalTo: schedulerRoot.trailingAnchor),
|
|
|
3616
|
+ schedulerSplit.bottomAnchor.constraint(equalTo: schedulerRoot.bottomAnchor),
|
|
|
3617
|
+
|
|
|
3618
|
+ schedulerCalendarTopRow.topAnchor.constraint(equalTo: schedulerCalendarPanel.topAnchor, constant: 18),
|
|
|
3619
|
+ schedulerCalendarTopRow.leadingAnchor.constraint(equalTo: schedulerCalendarPanel.leadingAnchor, constant: 16),
|
|
|
3620
|
+ schedulerCalendarTopRow.trailingAnchor.constraint(equalTo: schedulerCalendarPanel.trailingAnchor, constant: -16),
|
|
|
3621
|
+
|
|
|
3622
|
+ schedulerMonthRow.topAnchor.constraint(equalTo: schedulerCalendarTopRow.bottomAnchor, constant: 18),
|
|
|
3623
|
+ schedulerMonthRow.leadingAnchor.constraint(equalTo: schedulerCalendarPanel.leadingAnchor, constant: 16),
|
|
|
3624
|
+ schedulerMonthRow.trailingAnchor.constraint(lessThanOrEqualTo: schedulerCalendarPanel.trailingAnchor, constant: -16),
|
|
|
3625
|
+
|
|
|
3626
|
+ schedulerWeekdaysRow.topAnchor.constraint(equalTo: schedulerMonthRow.bottomAnchor, constant: 14),
|
|
|
3627
|
+ schedulerWeekdaysRow.leadingAnchor.constraint(equalTo: schedulerCalendarPanel.leadingAnchor, constant: 16),
|
|
|
3628
|
+ schedulerWeekdaysRow.trailingAnchor.constraint(equalTo: schedulerCalendarPanel.trailingAnchor, constant: -16),
|
|
|
3629
|
+
|
|
|
3630
|
+ schedulerCalendarGrid.topAnchor.constraint(equalTo: schedulerWeekdaysRow.bottomAnchor, constant: 10),
|
|
|
3631
|
+ schedulerCalendarGrid.leadingAnchor.constraint(equalTo: schedulerCalendarPanel.leadingAnchor, constant: 16),
|
|
|
3632
|
+ schedulerCalendarGrid.trailingAnchor.constraint(equalTo: schedulerCalendarPanel.trailingAnchor, constant: -16),
|
|
|
3633
|
+ schedulerCalendarGrid.bottomAnchor.constraint(equalTo: schedulerCalendarPanel.bottomAnchor, constant: -16),
|
|
|
3634
|
+ schedulerCalendarGrid.heightAnchor.constraint(greaterThanOrEqualToConstant: 280),
|
|
|
3635
|
+
|
|
|
3636
|
+ schedulerSelectedDateLabel.topAnchor.constraint(equalTo: schedulerMeetingsPanel.topAnchor, constant: 18),
|
|
|
3637
|
+ schedulerSelectedDateLabel.leadingAnchor.constraint(equalTo: schedulerMeetingsPanel.leadingAnchor, constant: 16),
|
|
|
3638
|
+ schedulerSelectedDateLabel.trailingAnchor.constraint(lessThanOrEqualTo: schedulerRightTopRow.leadingAnchor, constant: -10),
|
|
|
3639
|
+
|
|
|
3640
|
+ schedulerRightTopRow.centerYAnchor.constraint(equalTo: schedulerSelectedDateLabel.centerYAnchor),
|
|
|
3641
|
+ schedulerRightTopRow.trailingAnchor.constraint(equalTo: schedulerMeetingsPanel.trailingAnchor, constant: -16),
|
|
|
3642
|
+
|
|
|
3643
|
+ schedulerMeetingsHeader.topAnchor.constraint(equalTo: schedulerSelectedDateLabel.bottomAnchor, constant: 12),
|
|
|
3644
|
+ schedulerMeetingsHeader.leadingAnchor.constraint(equalTo: schedulerMeetingsPanel.leadingAnchor, constant: 16),
|
|
|
3645
|
+ schedulerMeetingsHeader.trailingAnchor.constraint(equalTo: schedulerMeetingsPanel.trailingAnchor, constant: -16),
|
|
|
3646
|
+
|
|
|
3647
|
+ schedulerMeetingsScroll.topAnchor.constraint(equalTo: schedulerMeetingsHeader.bottomAnchor, constant: 10),
|
|
|
3648
|
+ schedulerMeetingsScroll.leadingAnchor.constraint(equalTo: schedulerMeetingsPanel.leadingAnchor, constant: 14),
|
|
|
3649
|
+ schedulerMeetingsScroll.trailingAnchor.constraint(equalTo: schedulerMeetingsPanel.trailingAnchor, constant: -14),
|
|
|
3650
|
+ schedulerMeetingsScroll.bottomAnchor.constraint(equalTo: schedulerMeetingsPanel.bottomAnchor, constant: -14),
|
|
|
3651
|
+
|
|
|
3652
|
+ schedulerMeetingsDocument.widthAnchor.constraint(equalTo: schedulerMeetingsScroll.contentView.widthAnchor),
|
|
|
3653
|
+ schedulerMeetingsStack.topAnchor.constraint(equalTo: schedulerMeetingsDocument.topAnchor),
|
|
|
3654
|
+ schedulerMeetingsStack.leadingAnchor.constraint(equalTo: schedulerMeetingsDocument.leadingAnchor),
|
|
|
3655
|
+ schedulerMeetingsStack.trailingAnchor.constraint(equalTo: schedulerMeetingsDocument.trailingAnchor),
|
|
|
3656
|
+ schedulerMeetingsStack.bottomAnchor.constraint(lessThanOrEqualTo: schedulerMeetingsDocument.bottomAnchor),
|
|
|
3657
|
+
|
|
|
3658
|
+ schedulerEmptyLabel.centerXAnchor.constraint(equalTo: schedulerMeetingsPanel.centerXAnchor),
|
|
|
3659
|
+ schedulerEmptyLabel.centerYAnchor.constraint(equalTo: schedulerMeetingsPanel.centerYAnchor)
|
|
3296
|
3660
|
])
|
|
3297
|
3661
|
|
|
3298
|
3662
|
timeLabel = timeTitle
|
|
|
@@ -3304,6 +3668,13 @@ class ViewController: NSViewController {
|
|
3304
|
3668
|
homeMeetingsPanel = panel
|
|
3305
|
3669
|
homePlaceholderLabel = placeholder
|
|
3306
|
3670
|
homeSettingsView = settingsView
|
|
|
3671
|
+ schedulerRootView = schedulerRoot
|
|
|
3672
|
+ self.schedulerMonthLabel = schedulerMonthLabel
|
|
|
3673
|
+ schedulerCalendarGridStack = schedulerCalendarGrid
|
|
|
3674
|
+ self.schedulerSelectedDateLabel = schedulerSelectedDateLabel
|
|
|
3675
|
+ schedulerMeetingsListStack = schedulerMeetingsStack
|
|
|
3676
|
+ schedulerMeetingsEmptyLabel = schedulerEmptyLabel
|
|
|
3677
|
+ self.schedulerTodayButton = schedulerTodayButton
|
|
3307
|
3678
|
meetingsDayHeaderLabel = panelHeader
|
|
3308
|
3679
|
meetingsListStack = meetingsStack
|
|
3309
|
3680
|
meetingsStatusLabel = meetingsStatus
|
|
|
@@ -3315,6 +3686,9 @@ class ViewController: NSViewController {
|
|
3315
|
3686
|
updateClock()
|
|
3316
|
3687
|
updateMeetingsDayUI()
|
|
3317
|
3688
|
applyFilteredMeetings()
|
|
|
3689
|
+ renderSchedulerCalendarGrid()
|
|
|
3690
|
+ updateSchedulerDateHeader()
|
|
|
3691
|
+ applySchedulerMeetingsForSelectedDate()
|
|
3318
|
3692
|
|
|
3319
|
3693
|
homeSearchField = searchField
|
|
3320
|
3694
|
homeSearchPill = searchPill
|
|
|
@@ -3323,7 +3697,9 @@ class ViewController: NSViewController {
|
|
3323
|
3697
|
object: searchField,
|
|
3324
|
3698
|
queue: .main
|
|
3325
|
3699
|
) { [weak self] _ in
|
|
3326
|
|
- self?.applyFilteredMeetings()
|
|
|
3700
|
+ Task { @MainActor [weak self] in
|
|
|
3701
|
+ self?.applyFilteredMeetings()
|
|
|
3702
|
+ }
|
|
3327
|
3703
|
updateSearchHintVisibility()
|
|
3328
|
3704
|
}
|
|
3329
|
3705
|
|
|
|
@@ -3591,10 +3967,9 @@ class ViewController: NSViewController {
|
|
3591
|
3967
|
private func updateSelectedHomeSectionUI() {
|
|
3592
|
3968
|
let isHome = selectedHomeSidebarItem == "Home"
|
|
3593
|
3969
|
let isSettings = selectedHomeSidebarItem == "Settings"
|
|
3594
|
|
- let title = selectedHomeSidebarItem
|
|
3595
|
|
-
|
|
3596
|
|
- homeWelcomeLabel?.stringValue = title
|
|
3597
|
|
- homeWelcomeLabel?.isHidden = isSettings
|
|
|
3970
|
+ let isScheduler = selectedHomeSidebarItem == "Scheduler"
|
|
|
3971
|
+ homeWelcomeLabel?.stringValue = "Home"
|
|
|
3972
|
+ homeWelcomeLabel?.isHidden = (isHome == false) || isSettings
|
|
3598
|
3973
|
|
|
3599
|
3974
|
let dashboardViews: [NSView?] = [
|
|
3600
|
3975
|
homeTimeLabelView,
|
|
|
@@ -3620,6 +3995,7 @@ class ViewController: NSViewController {
|
|
3620
|
3995
|
emptyMeetingLabel?.isHidden = hasMeetingCards
|
|
3621
|
3996
|
}
|
|
3622
|
3997
|
homeSettingsView?.isHidden = isSettings == false
|
|
|
3998
|
+ schedulerRootView?.isHidden = isScheduler == false
|
|
3623
|
3999
|
|
|
3624
|
4000
|
if isHome {
|
|
3625
|
4001
|
homePlaceholderLabel?.isHidden = true
|
|
|
@@ -3627,6 +4003,12 @@ class ViewController: NSViewController {
|
|
3627
|
4003
|
// Keep non-Home pages empty for now.
|
|
3628
|
4004
|
homePlaceholderLabel?.isHidden = true
|
|
3629
|
4005
|
}
|
|
|
4006
|
+
|
|
|
4007
|
+ if isScheduler {
|
|
|
4008
|
+ updateSchedulerDateHeader()
|
|
|
4009
|
+ applySchedulerMeetingsForSelectedDate()
|
|
|
4010
|
+ renderSchedulerCalendarGrid()
|
|
|
4011
|
+ }
|
|
3630
|
4012
|
}
|
|
3631
|
4013
|
|
|
3632
|
4014
|
private func sidebarSymbolName(for item: String, filled: Bool = false) -> String {
|