|
|
@@ -292,6 +292,14 @@ final class ViewController: NSViewController {
|
|
292
|
292
|
case week = 2
|
|
293
|
293
|
}
|
|
294
|
294
|
|
|
|
295
|
+ private enum SchedulePageFilter: Int {
|
|
|
296
|
+ case all = 0
|
|
|
297
|
+ case today = 1
|
|
|
298
|
+ case week = 2
|
|
|
299
|
+ case month = 3
|
|
|
300
|
+ case customRange = 4
|
|
|
301
|
+ }
|
|
|
302
|
+
|
|
295
|
303
|
private var scheduleFilter: ScheduleFilter = .all
|
|
296
|
304
|
private weak var scheduleDateHeadingLabel: NSTextField?
|
|
297
|
305
|
private weak var scheduleCardsStack: NSStackView?
|
|
|
@@ -313,6 +321,25 @@ final class ViewController: NSViewController {
|
|
313
|
321
|
private var scheduleProfileMenuAvatar: NSImage?
|
|
314
|
322
|
private var scheduleProfileImageTask: Task<Void, Never>?
|
|
315
|
323
|
private var googleAccountPopover: NSPopover?
|
|
|
324
|
+ private var scheduleCachedMeetings: [ScheduledMeeting] = []
|
|
|
325
|
+
|
|
|
326
|
+ private var schedulePageFilter: SchedulePageFilter = .all
|
|
|
327
|
+ private var schedulePageFromDate: Date = Calendar.current.startOfDay(for: Date())
|
|
|
328
|
+ private var schedulePageToDate: Date = Calendar.current.startOfDay(for: Date())
|
|
|
329
|
+ private var schedulePageFilteredMeetings: [ScheduledMeeting] = []
|
|
|
330
|
+ private var schedulePageVisibleCount: Int = 0
|
|
|
331
|
+ private let schedulePageBatchSize: Int = 20
|
|
|
332
|
+ private let schedulePageCardsPerRow: Int = 3
|
|
|
333
|
+ private let schedulePageCardWidth: CGFloat = 240
|
|
|
334
|
+ private let schedulePageCardSpacing: CGFloat = 12
|
|
|
335
|
+ private var schedulePageScrollObservation: NSObjectProtocol?
|
|
|
336
|
+ private weak var schedulePageDateHeadingLabel: NSTextField?
|
|
|
337
|
+ private weak var schedulePageFilterDropdown: NSPopUpButton?
|
|
|
338
|
+ private weak var schedulePageFromDatePicker: NSDatePicker?
|
|
|
339
|
+ private weak var schedulePageToDatePicker: NSDatePicker?
|
|
|
340
|
+ private weak var schedulePageRangeErrorLabel: NSTextField?
|
|
|
341
|
+ private weak var schedulePageCardsStack: NSStackView?
|
|
|
342
|
+ private weak var schedulePageCardsScrollView: NSScrollView?
|
|
316
|
343
|
|
|
317
|
344
|
/// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
|
|
318
|
345
|
private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll
|
|
|
@@ -413,6 +440,9 @@ final class ViewController: NSViewController {
|
|
413
|
440
|
if hasObservedAppLifecycleForUsage {
|
|
414
|
441
|
NotificationCenter.default.removeObserver(self)
|
|
415
|
442
|
}
|
|
|
443
|
+ if let observer = schedulePageScrollObservation {
|
|
|
444
|
+ NotificationCenter.default.removeObserver(observer)
|
|
|
445
|
+ }
|
|
416
|
446
|
storeKitStartupTask?.cancel()
|
|
417
|
447
|
paywallPurchaseTask?.cancel()
|
|
418
|
448
|
launchPaywallWorkItem?.cancel()
|
|
|
@@ -735,6 +765,10 @@ private extension ViewController {
|
|
735
|
765
|
|
|
736
|
766
|
private func reloadTheme() {
|
|
737
|
767
|
pageCache.removeAll()
|
|
|
768
|
+ if let observer = schedulePageScrollObservation {
|
|
|
769
|
+ NotificationCenter.default.removeObserver(observer)
|
|
|
770
|
+ }
|
|
|
771
|
+ schedulePageScrollObservation = nil
|
|
738
|
772
|
sidebarRowViews.removeAll()
|
|
739
|
773
|
sidebarPageByView.removeAll()
|
|
740
|
774
|
zoomJoinModeByView.removeAll()
|
|
|
@@ -1293,7 +1327,7 @@ private extension ViewController {
|
|
1293
|
1327
|
case .joinMeetings:
|
|
1294
|
1328
|
built = makeJoinMeetingsContent()
|
|
1295
|
1329
|
case .photo:
|
|
1296
|
|
- built = makePlaceholderPage(title: "Schedule", subtitle: "Create and manage your upcoming meetings.")
|
|
|
1330
|
+ built = makeSchedulePageContent()
|
|
1297
|
1331
|
case .video:
|
|
1298
|
1332
|
built = makePlaceholderPage(title: "Calendar", subtitle: "View meetings by date and track your plan.")
|
|
1299
|
1333
|
case .settings:
|
|
|
@@ -1699,6 +1733,47 @@ private extension ViewController {
|
|
1699
|
1733
|
return panel
|
|
1700
|
1734
|
}
|
|
1701
|
1735
|
|
|
|
1736
|
+ func makeSchedulePageContent() -> NSView {
|
|
|
1737
|
+ let panel = NSView()
|
|
|
1738
|
+ panel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1739
|
+
|
|
|
1740
|
+ let contentStack = NSStackView()
|
|
|
1741
|
+ contentStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1742
|
+ contentStack.orientation = .vertical
|
|
|
1743
|
+ contentStack.spacing = 12
|
|
|
1744
|
+ contentStack.alignment = .leading
|
|
|
1745
|
+
|
|
|
1746
|
+ contentStack.addArrangedSubview(schedulePageHeader())
|
|
|
1747
|
+
|
|
|
1748
|
+ let heading = textLabel(schedulePageInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
|
|
|
1749
|
+ schedulePageDateHeadingLabel = heading
|
|
|
1750
|
+ contentStack.addArrangedSubview(heading)
|
|
|
1751
|
+
|
|
|
1752
|
+ let rangeError = textLabel("", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .systemRed)
|
|
|
1753
|
+ rangeError.isHidden = true
|
|
|
1754
|
+ schedulePageRangeErrorLabel = rangeError
|
|
|
1755
|
+ contentStack.addArrangedSubview(rangeError)
|
|
|
1756
|
+
|
|
|
1757
|
+ let cardsContainer = makeSchedulePageCardsContainer()
|
|
|
1758
|
+ contentStack.addArrangedSubview(cardsContainer)
|
|
|
1759
|
+
|
|
|
1760
|
+ panel.addSubview(contentStack)
|
|
|
1761
|
+
|
|
|
1762
|
+ NSLayoutConstraint.activate([
|
|
|
1763
|
+ contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
|
|
|
1764
|
+ contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
|
|
|
1765
|
+ contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 6),
|
|
|
1766
|
+ cardsContainer.leadingAnchor.constraint(equalTo: contentStack.leadingAnchor),
|
|
|
1767
|
+ cardsContainer.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -16)
|
|
|
1768
|
+ ])
|
|
|
1769
|
+
|
|
|
1770
|
+ Task { [weak self] in
|
|
|
1771
|
+ await self?.loadSchedule()
|
|
|
1772
|
+ }
|
|
|
1773
|
+
|
|
|
1774
|
+ return panel
|
|
|
1775
|
+ }
|
|
|
1776
|
+
|
|
1702
|
1777
|
func meetJoinSectionRow() -> NSView {
|
|
1703
|
1778
|
let row = NSStackView()
|
|
1704
|
1779
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -2580,6 +2655,163 @@ private extension ViewController {
|
|
2580
|
2655
|
return row
|
|
2581
|
2656
|
}
|
|
2582
|
2657
|
|
|
|
2658
|
+ private func schedulePageHeader() -> NSView {
|
|
|
2659
|
+ let container = NSStackView()
|
|
|
2660
|
+ container.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2661
|
+ container.orientation = .vertical
|
|
|
2662
|
+ container.spacing = 10
|
|
|
2663
|
+ container.alignment = .leading
|
|
|
2664
|
+
|
|
|
2665
|
+ let titleLabel = textLabel("Schedule", font: typography.pageTitle, color: palette.textPrimary)
|
|
|
2666
|
+ container.addArrangedSubview(titleLabel)
|
|
|
2667
|
+
|
|
|
2668
|
+ let filterRow = NSStackView()
|
|
|
2669
|
+ filterRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2670
|
+ filterRow.orientation = .horizontal
|
|
|
2671
|
+ filterRow.alignment = .centerY
|
|
|
2672
|
+ filterRow.spacing = 10
|
|
|
2673
|
+
|
|
|
2674
|
+ let filterDropdown = makeSchedulePageFilterDropdown()
|
|
|
2675
|
+ schedulePageFilterDropdown = filterDropdown
|
|
|
2676
|
+ filterRow.addArrangedSubview(filterDropdown)
|
|
|
2677
|
+
|
|
|
2678
|
+ let fromPicker = makeScheduleDatePicker(date: schedulePageFromDate)
|
|
|
2679
|
+ schedulePageFromDatePicker = fromPicker
|
|
|
2680
|
+ filterRow.addArrangedSubview(fromPicker)
|
|
|
2681
|
+
|
|
|
2682
|
+ let toPicker = makeScheduleDatePicker(date: schedulePageToDate)
|
|
|
2683
|
+ schedulePageToDatePicker = toPicker
|
|
|
2684
|
+ filterRow.addArrangedSubview(toPicker)
|
|
|
2685
|
+
|
|
|
2686
|
+ filterRow.addArrangedSubview(makeSchedulePagePillButton(title: "Apply", action: #selector(schedulePageApplyDateRangePressed(_:))))
|
|
|
2687
|
+ filterRow.addArrangedSubview(makeSchedulePagePillButton(title: "Reset", action: #selector(schedulePageResetFiltersPressed(_:))))
|
|
|
2688
|
+ filterRow.addArrangedSubview(makeScheduleRefreshButton())
|
|
|
2689
|
+
|
|
|
2690
|
+ container.addArrangedSubview(filterRow)
|
|
|
2691
|
+ container.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
|
|
|
2692
|
+ refreshSchedulePageDateFilterUI()
|
|
|
2693
|
+ return container
|
|
|
2694
|
+ }
|
|
|
2695
|
+
|
|
|
2696
|
+ private func makeSchedulePageFilterDropdown() -> NSPopUpButton {
|
|
|
2697
|
+ let button = HoverPopUpButton(frame: .zero, pullsDown: false)
|
|
|
2698
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2699
|
+ button.autoenablesItems = false
|
|
|
2700
|
+ button.isBordered = false
|
|
|
2701
|
+ button.bezelStyle = .regularSquare
|
|
|
2702
|
+ button.wantsLayer = true
|
|
|
2703
|
+ button.layer?.cornerRadius = 8
|
|
|
2704
|
+ button.layer?.masksToBounds = true
|
|
|
2705
|
+ button.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
2706
|
+ button.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
2707
|
+ button.layer?.borderWidth = 1
|
|
|
2708
|
+ button.font = typography.filterText
|
|
|
2709
|
+ button.contentTintColor = palette.textSecondary
|
|
|
2710
|
+ button.target = self
|
|
|
2711
|
+ button.action = #selector(schedulePageFilterDropdownChanged(_:))
|
|
|
2712
|
+ button.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
|
2713
|
+ button.widthAnchor.constraint(equalToConstant: 190).isActive = true
|
|
|
2714
|
+
|
|
|
2715
|
+ button.removeAllItems()
|
|
|
2716
|
+ button.addItems(withTitles: ["All", "Today", "This week", "This month", "Custom range"])
|
|
|
2717
|
+ button.selectItem(at: schedulePageFilter.rawValue)
|
|
|
2718
|
+ if let menu = button.menu {
|
|
|
2719
|
+ for (index, item) in menu.items.enumerated() {
|
|
|
2720
|
+ item.tag = index
|
|
|
2721
|
+ }
|
|
|
2722
|
+ }
|
|
|
2723
|
+
|
|
|
2724
|
+ let baseColor = palette.inputBackground
|
|
|
2725
|
+ let baseBorder = palette.inputBorder
|
|
|
2726
|
+ let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
2727
|
+ let hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor
|
|
|
2728
|
+ let hoverBorder = baseBorder.blended(withFraction: 0.16, of: hoverBlend) ?? baseBorder
|
|
|
2729
|
+ button.onHoverChanged = { [weak button] hovering in
|
|
|
2730
|
+ button?.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
|
|
|
2731
|
+ button?.layer?.borderColor = (hovering ? hoverBorder : baseBorder).cgColor
|
|
|
2732
|
+ }
|
|
|
2733
|
+ button.onHoverChanged?(false)
|
|
|
2734
|
+ return button
|
|
|
2735
|
+ }
|
|
|
2736
|
+
|
|
|
2737
|
+ private func makeScheduleDatePicker(date: Date) -> NSDatePicker {
|
|
|
2738
|
+ let picker = NSDatePicker()
|
|
|
2739
|
+ picker.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2740
|
+ picker.datePickerStyle = .textFieldAndStepper
|
|
|
2741
|
+ picker.datePickerElements = [.yearMonthDay]
|
|
|
2742
|
+ picker.dateValue = date
|
|
|
2743
|
+ picker.font = typography.filterText
|
|
|
2744
|
+ picker.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
|
2745
|
+ picker.widthAnchor.constraint(equalToConstant: 156).isActive = true
|
|
|
2746
|
+ picker.target = self
|
|
|
2747
|
+ picker.action = #selector(schedulePageDatePickerChanged(_:))
|
|
|
2748
|
+ return picker
|
|
|
2749
|
+ }
|
|
|
2750
|
+
|
|
|
2751
|
+ private func makeSchedulePagePillButton(title: String, action: Selector) -> NSButton {
|
|
|
2752
|
+ let button = makeSchedulePillButton(title: title)
|
|
|
2753
|
+ button.target = self
|
|
|
2754
|
+ button.action = action
|
|
|
2755
|
+ button.widthAnchor.constraint(equalToConstant: 92).isActive = true
|
|
|
2756
|
+ return button
|
|
|
2757
|
+ }
|
|
|
2758
|
+
|
|
|
2759
|
+ private func makeSchedulePageCardsContainer() -> NSView {
|
|
|
2760
|
+ if let observer = schedulePageScrollObservation {
|
|
|
2761
|
+ NotificationCenter.default.removeObserver(observer)
|
|
|
2762
|
+ }
|
|
|
2763
|
+ schedulePageScrollObservation = nil
|
|
|
2764
|
+
|
|
|
2765
|
+ let wrapper = NSView()
|
|
|
2766
|
+ wrapper.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2767
|
+
|
|
|
2768
|
+ let scroll = NSScrollView()
|
|
|
2769
|
+ scroll.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2770
|
+ scroll.drawsBackground = false
|
|
|
2771
|
+ scroll.hasHorizontalScroller = false
|
|
|
2772
|
+ scroll.hasVerticalScroller = true
|
|
|
2773
|
+ scroll.autohidesScrollers = true
|
|
|
2774
|
+ scroll.borderType = .noBorder
|
|
|
2775
|
+ let viewportWidth = (schedulePageCardWidth * CGFloat(schedulePageCardsPerRow)) + (schedulePageCardSpacing * CGFloat(schedulePageCardsPerRow - 1))
|
|
|
2776
|
+ scroll.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
|
|
|
2777
|
+ schedulePageCardsScrollView = scroll
|
|
|
2778
|
+ wrapper.addSubview(scroll)
|
|
|
2779
|
+
|
|
|
2780
|
+ let stack = NSStackView()
|
|
|
2781
|
+ stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2782
|
+ stack.orientation = .vertical
|
|
|
2783
|
+ stack.spacing = 12
|
|
|
2784
|
+ stack.alignment = .leading
|
|
|
2785
|
+ schedulePageCardsStack = stack
|
|
|
2786
|
+ scroll.documentView = stack
|
|
|
2787
|
+
|
|
|
2788
|
+ NSLayoutConstraint.activate([
|
|
|
2789
|
+ scroll.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
|
|
|
2790
|
+ scroll.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
|
|
|
2791
|
+ scroll.topAnchor.constraint(equalTo: wrapper.topAnchor),
|
|
|
2792
|
+ scroll.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor),
|
|
|
2793
|
+ scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 420),
|
|
|
2794
|
+
|
|
|
2795
|
+ stack.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
|
|
|
2796
|
+ stack.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
|
|
|
2797
|
+ stack.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
|
|
|
2798
|
+ stack.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
|
|
|
2799
|
+ stack.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor)
|
|
|
2800
|
+ ])
|
|
|
2801
|
+
|
|
|
2802
|
+ scroll.contentView.postsBoundsChangedNotifications = true
|
|
|
2803
|
+ schedulePageScrollObservation = NotificationCenter.default.addObserver(
|
|
|
2804
|
+ forName: NSView.boundsDidChangeNotification,
|
|
|
2805
|
+ object: scroll.contentView,
|
|
|
2806
|
+ queue: .main
|
|
|
2807
|
+ ) { [weak self] _ in
|
|
|
2808
|
+ self?.schedulePageScrolled()
|
|
|
2809
|
+ }
|
|
|
2810
|
+
|
|
|
2811
|
+ renderSchedulePageCards()
|
|
|
2812
|
+ return wrapper
|
|
|
2813
|
+ }
|
|
|
2814
|
+
|
|
2583
|
2815
|
private func scheduleTopAuthRow() -> NSView {
|
|
2584
|
2816
|
let row = NSStackView()
|
|
2585
|
2817
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -3960,6 +4192,10 @@ private extension ViewController {
|
|
3960
|
4192
|
googleOAuth.loadTokens() == nil ? "Connect Google to see meetings" : "Loading…"
|
|
3961
|
4193
|
}
|
|
3962
|
4194
|
|
|
|
4195
|
+ private func schedulePageInitialHeadingText() -> String {
|
|
|
4196
|
+ googleOAuth.loadTokens() == nil ? "Connect Google to see meetings" : "Loading schedule…"
|
|
|
4197
|
+ }
|
|
|
4198
|
+
|
|
3963
|
4199
|
@objc func scheduleFilterDropdownChanged(_ sender: NSPopUpButton) {
|
|
3964
|
4200
|
guard let selectedItem = sender.selectedItem,
|
|
3965
|
4201
|
let filter = ScheduleFilter(rawValue: selectedItem.tag) else { return }
|
|
|
@@ -3974,6 +4210,38 @@ private extension ViewController {
|
|
3974
|
4210
|
}
|
|
3975
|
4211
|
}
|
|
3976
|
4212
|
|
|
|
4213
|
+ @objc func schedulePageFilterDropdownChanged(_ sender: NSPopUpButton) {
|
|
|
4214
|
+ guard let selectedItem = sender.selectedItem,
|
|
|
4215
|
+ let filter = SchedulePageFilter(rawValue: selectedItem.tag) else { return }
|
|
|
4216
|
+ schedulePageFilter = filter
|
|
|
4217
|
+ refreshSchedulePageDateFilterUI()
|
|
|
4218
|
+ applySchedulePageFiltersAndRender()
|
|
|
4219
|
+ }
|
|
|
4220
|
+
|
|
|
4221
|
+ @objc func schedulePageDatePickerChanged(_ sender: NSDatePicker) {
|
|
|
4222
|
+ schedulePageFromDate = schedulePageFromDatePicker?.dateValue ?? schedulePageFromDate
|
|
|
4223
|
+ schedulePageToDate = schedulePageToDatePicker?.dateValue ?? schedulePageToDate
|
|
|
4224
|
+ }
|
|
|
4225
|
+
|
|
|
4226
|
+ @objc func schedulePageApplyDateRangePressed(_ sender: NSButton) {
|
|
|
4227
|
+ schedulePageFilter = .customRange
|
|
|
4228
|
+ schedulePageFilterDropdown?.selectItem(at: SchedulePageFilter.customRange.rawValue)
|
|
|
4229
|
+ refreshSchedulePageDateFilterUI()
|
|
|
4230
|
+ applySchedulePageFiltersAndRender()
|
|
|
4231
|
+ }
|
|
|
4232
|
+
|
|
|
4233
|
+ @objc func schedulePageResetFiltersPressed(_ sender: NSButton) {
|
|
|
4234
|
+ schedulePageFilter = .all
|
|
|
4235
|
+ schedulePageFilterDropdown?.selectItem(at: SchedulePageFilter.all.rawValue)
|
|
|
4236
|
+ let today = Calendar.current.startOfDay(for: Date())
|
|
|
4237
|
+ schedulePageFromDate = today
|
|
|
4238
|
+ schedulePageToDate = today
|
|
|
4239
|
+ schedulePageFromDatePicker?.dateValue = today
|
|
|
4240
|
+ schedulePageToDatePicker?.dateValue = today
|
|
|
4241
|
+ refreshSchedulePageDateFilterUI()
|
|
|
4242
|
+ applySchedulePageFiltersAndRender()
|
|
|
4243
|
+ }
|
|
|
4244
|
+
|
|
3977
|
4245
|
private func scheduleTimeText(for meeting: ScheduledMeeting) -> String {
|
|
3978
|
4246
|
if meeting.isAllDay { return "All day" }
|
|
3979
|
4247
|
let f = DateFormatter()
|
|
|
@@ -4073,6 +4341,148 @@ private extension ViewController {
|
|
4073
|
4341
|
}
|
|
4074
|
4342
|
}
|
|
4075
|
4343
|
|
|
|
4344
|
+ private func filteredMeetingsForSchedulePage(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
|
|
|
4345
|
+ let calendar = Calendar.current
|
|
|
4346
|
+ switch schedulePageFilter {
|
|
|
4347
|
+ case .all:
|
|
|
4348
|
+ return meetings
|
|
|
4349
|
+ case .today:
|
|
|
4350
|
+ let start = calendar.startOfDay(for: Date())
|
|
|
4351
|
+ let end = calendar.date(byAdding: .day, value: 1, to: start) ?? start.addingTimeInterval(86400)
|
|
|
4352
|
+ return meetings.filter { $0.startDate >= start && $0.startDate < end }
|
|
|
4353
|
+ case .week:
|
|
|
4354
|
+ let now = Date()
|
|
|
4355
|
+ let end = calendar.date(byAdding: .day, value: 7, to: now) ?? now.addingTimeInterval(7 * 86400)
|
|
|
4356
|
+ return meetings.filter { $0.startDate >= now && $0.startDate <= end }
|
|
|
4357
|
+ case .month:
|
|
|
4358
|
+ let now = Date()
|
|
|
4359
|
+ let end = calendar.date(byAdding: .month, value: 1, to: now) ?? now.addingTimeInterval(30 * 86400)
|
|
|
4360
|
+ return meetings.filter { $0.startDate >= now && $0.startDate <= end }
|
|
|
4361
|
+ case .customRange:
|
|
|
4362
|
+ let start = calendar.startOfDay(for: schedulePageFromDate)
|
|
|
4363
|
+ let inclusiveEndDay = calendar.startOfDay(for: schedulePageToDate)
|
|
|
4364
|
+ guard let end = calendar.date(byAdding: .day, value: 1, to: inclusiveEndDay) else {
|
|
|
4365
|
+ return meetings
|
|
|
4366
|
+ }
|
|
|
4367
|
+ return meetings.filter { $0.startDate >= start && $0.startDate < end }
|
|
|
4368
|
+ }
|
|
|
4369
|
+ }
|
|
|
4370
|
+
|
|
|
4371
|
+ private func refreshSchedulePageDateFilterUI() {
|
|
|
4372
|
+ let isCustom = schedulePageFilter == .customRange
|
|
|
4373
|
+ schedulePageFromDatePicker?.isEnabled = isCustom
|
|
|
4374
|
+ schedulePageToDatePicker?.isEnabled = isCustom
|
|
|
4375
|
+ schedulePageFromDatePicker?.alphaValue = isCustom ? 1.0 : 0.65
|
|
|
4376
|
+ schedulePageToDatePicker?.alphaValue = isCustom ? 1.0 : 0.65
|
|
|
4377
|
+ }
|
|
|
4378
|
+
|
|
|
4379
|
+ private func schedulePageHasValidCustomRange() -> Bool {
|
|
|
4380
|
+ let start = Calendar.current.startOfDay(for: schedulePageFromDate)
|
|
|
4381
|
+ let end = Calendar.current.startOfDay(for: schedulePageToDate)
|
|
|
4382
|
+ return start <= end
|
|
|
4383
|
+ }
|
|
|
4384
|
+
|
|
|
4385
|
+ private func setSchedulePageRangeError(_ message: String?) {
|
|
|
4386
|
+ guard let label = schedulePageRangeErrorLabel else { return }
|
|
|
4387
|
+ label.stringValue = message ?? ""
|
|
|
4388
|
+ label.isHidden = message == nil
|
|
|
4389
|
+ }
|
|
|
4390
|
+
|
|
|
4391
|
+ private func applySchedulePageFiltersAndRender() {
|
|
|
4392
|
+ schedulePageFromDate = schedulePageFromDatePicker?.dateValue ?? schedulePageFromDate
|
|
|
4393
|
+ schedulePageToDate = schedulePageToDatePicker?.dateValue ?? schedulePageToDate
|
|
|
4394
|
+ if schedulePageFilter == .customRange && !schedulePageHasValidCustomRange() {
|
|
|
4395
|
+ setSchedulePageRangeError("Start date must be on or before end date.")
|
|
|
4396
|
+ schedulePageFilteredMeetings = []
|
|
|
4397
|
+ schedulePageVisibleCount = 0
|
|
|
4398
|
+ renderSchedulePageCards()
|
|
|
4399
|
+ schedulePageDateHeadingLabel?.stringValue = "Invalid custom date range"
|
|
|
4400
|
+ return
|
|
|
4401
|
+ }
|
|
|
4402
|
+
|
|
|
4403
|
+ setSchedulePageRangeError(nil)
|
|
|
4404
|
+ schedulePageFilteredMeetings = filteredMeetingsForSchedulePage(scheduleCachedMeetings)
|
|
|
4405
|
+ schedulePageVisibleCount = min(schedulePageBatchSize, schedulePageFilteredMeetings.count)
|
|
|
4406
|
+ renderSchedulePageCards()
|
|
|
4407
|
+ schedulePageDateHeadingLabel?.stringValue = scheduleHeadingText(for: schedulePageFilteredMeetings)
|
|
|
4408
|
+ }
|
|
|
4409
|
+
|
|
|
4410
|
+ private func appendSchedulePageBatchIfNeeded() {
|
|
|
4411
|
+ guard schedulePageVisibleCount < schedulePageFilteredMeetings.count else { return }
|
|
|
4412
|
+ let nextCount = min(schedulePageVisibleCount + schedulePageBatchSize, schedulePageFilteredMeetings.count)
|
|
|
4413
|
+ guard nextCount > schedulePageVisibleCount else { return }
|
|
|
4414
|
+ schedulePageVisibleCount = nextCount
|
|
|
4415
|
+ renderSchedulePageCards()
|
|
|
4416
|
+ }
|
|
|
4417
|
+
|
|
|
4418
|
+ private func schedulePageScrolled() {
|
|
|
4419
|
+ guard let scroll = schedulePageCardsScrollView else { return }
|
|
|
4420
|
+ let contentBounds = scroll.contentView.bounds
|
|
|
4421
|
+ let contentHeight = scroll.documentView?.bounds.height ?? 0
|
|
|
4422
|
+ guard contentHeight > 0 else { return }
|
|
|
4423
|
+ let remaining = contentHeight - (contentBounds.origin.y + contentBounds.height)
|
|
|
4424
|
+ if remaining < 180 {
|
|
|
4425
|
+ appendSchedulePageBatchIfNeeded()
|
|
|
4426
|
+ }
|
|
|
4427
|
+ }
|
|
|
4428
|
+
|
|
|
4429
|
+ private func renderSchedulePageCards() {
|
|
|
4430
|
+ guard let stack = schedulePageCardsStack else { return }
|
|
|
4431
|
+ stack.arrangedSubviews.forEach { v in
|
|
|
4432
|
+ stack.removeArrangedSubview(v)
|
|
|
4433
|
+ v.removeFromSuperview()
|
|
|
4434
|
+ }
|
|
|
4435
|
+
|
|
|
4436
|
+ let visibleMeetings = Array(schedulePageFilteredMeetings.prefix(schedulePageVisibleCount))
|
|
|
4437
|
+ if visibleMeetings.isEmpty {
|
|
|
4438
|
+ let empty = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
|
|
|
4439
|
+ empty.translatesAutoresizingMaskIntoConstraints = false
|
|
|
4440
|
+ empty.heightAnchor.constraint(equalToConstant: 120).isActive = true
|
|
|
4441
|
+ let viewportWidth = (schedulePageCardWidth * CGFloat(schedulePageCardsPerRow)) + (schedulePageCardSpacing * CGFloat(schedulePageCardsPerRow - 1))
|
|
|
4442
|
+ empty.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
|
|
|
4443
|
+ styleSurface(empty, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
4444
|
+ let label = textLabel(googleOAuth.loadTokens() == nil ? "Connect to load schedule" : "No meetings for selected filters", font: typography.cardSubtitle, color: palette.textSecondary)
|
|
|
4445
|
+ label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
4446
|
+ empty.addSubview(label)
|
|
|
4447
|
+ NSLayoutConstraint.activate([
|
|
|
4448
|
+ label.centerXAnchor.constraint(equalTo: empty.centerXAnchor),
|
|
|
4449
|
+ label.centerYAnchor.constraint(equalTo: empty.centerYAnchor)
|
|
|
4450
|
+ ])
|
|
|
4451
|
+ stack.addArrangedSubview(empty)
|
|
|
4452
|
+ return
|
|
|
4453
|
+ }
|
|
|
4454
|
+
|
|
|
4455
|
+ var index = 0
|
|
|
4456
|
+ while index < visibleMeetings.count {
|
|
|
4457
|
+ let row = NSStackView()
|
|
|
4458
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
4459
|
+ row.orientation = .horizontal
|
|
|
4460
|
+ row.alignment = .top
|
|
|
4461
|
+ row.spacing = schedulePageCardSpacing
|
|
|
4462
|
+ row.distribution = .fill
|
|
|
4463
|
+ let rowEnd = min(index + schedulePageCardsPerRow, visibleMeetings.count)
|
|
|
4464
|
+ for meeting in visibleMeetings[index..<rowEnd] {
|
|
|
4465
|
+ row.addArrangedSubview(scheduleCard(meeting: meeting))
|
|
|
4466
|
+ }
|
|
|
4467
|
+ if rowEnd - index < schedulePageCardsPerRow {
|
|
|
4468
|
+ for _ in 0..<(schedulePageCardsPerRow - (rowEnd - index)) {
|
|
|
4469
|
+ let spacer = NSView()
|
|
|
4470
|
+ spacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
4471
|
+ spacer.widthAnchor.constraint(equalToConstant: schedulePageCardWidth).isActive = true
|
|
|
4472
|
+ spacer.heightAnchor.constraint(equalToConstant: 150).isActive = true
|
|
|
4473
|
+ row.addArrangedSubview(spacer)
|
|
|
4474
|
+ }
|
|
|
4475
|
+ }
|
|
|
4476
|
+ stack.addArrangedSubview(row)
|
|
|
4477
|
+ index = rowEnd
|
|
|
4478
|
+ }
|
|
|
4479
|
+
|
|
|
4480
|
+ if schedulePageVisibleCount < schedulePageFilteredMeetings.count {
|
|
|
4481
|
+ let moreLabel = textLabel("Scroll to load more (\(schedulePageVisibleCount)/\(schedulePageFilteredMeetings.count))", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.textMuted)
|
|
|
4482
|
+ stack.addArrangedSubview(moreLabel)
|
|
|
4483
|
+ }
|
|
|
4484
|
+ }
|
|
|
4485
|
+
|
|
4076
|
4486
|
private func scrollScheduleCards(direction: Int) {
|
|
4077
|
4487
|
guard let scroll = scheduleCardsScrollView else { return }
|
|
4078
|
4488
|
let contentBounds = scroll.contentView.bounds
|
|
|
@@ -4091,9 +4501,12 @@ private extension ViewController {
|
|
4091
|
4501
|
updateGoogleAuthButtonTitle()
|
|
4092
|
4502
|
applyGoogleProfile(nil)
|
|
4093
|
4503
|
scheduleDateHeadingLabel?.stringValue = "Connect Google to see meetings"
|
|
|
4504
|
+ schedulePageDateHeadingLabel?.stringValue = "Connect Google to see meetings"
|
|
4094
|
4505
|
if let stack = scheduleCardsStack {
|
|
4095
|
4506
|
renderScheduleCards(into: stack, meetings: [])
|
|
4096
|
4507
|
}
|
|
|
4508
|
+ scheduleCachedMeetings = []
|
|
|
4509
|
+ applySchedulePageFiltersAndRender()
|
|
4097
|
4510
|
}
|
|
4098
|
4511
|
return
|
|
4099
|
4512
|
}
|
|
|
@@ -4110,6 +4523,8 @@ private extension ViewController {
|
|
4110
|
4523
|
if let stack = scheduleCardsStack {
|
|
4111
|
4524
|
renderScheduleCards(into: stack, meetings: filtered)
|
|
4112
|
4525
|
}
|
|
|
4526
|
+ scheduleCachedMeetings = meetings
|
|
|
4527
|
+ applySchedulePageFiltersAndRender()
|
|
4113
|
4528
|
}
|
|
4114
|
4529
|
} catch {
|
|
4115
|
4530
|
await MainActor.run {
|
|
|
@@ -4118,9 +4533,12 @@ private extension ViewController {
|
|
4118
|
4533
|
applyGoogleProfile(nil)
|
|
4119
|
4534
|
}
|
|
4120
|
4535
|
scheduleDateHeadingLabel?.stringValue = "Couldn’t load schedule"
|
|
|
4536
|
+ schedulePageDateHeadingLabel?.stringValue = "Couldn’t load schedule"
|
|
4121
|
4537
|
if let stack = scheduleCardsStack {
|
|
4122
|
4538
|
renderScheduleCards(into: stack, meetings: [])
|
|
4123
|
4539
|
}
|
|
|
4540
|
+ scheduleCachedMeetings = []
|
|
|
4541
|
+ applySchedulePageFiltersAndRender()
|
|
4124
|
4542
|
showSimpleError("Couldn’t load schedule.", error: error)
|
|
4125
|
4543
|
}
|
|
4126
|
4544
|
}
|
|
|
@@ -4143,7 +4561,8 @@ private extension ViewController {
|
|
4143
|
4561
|
await MainActor.run {
|
|
4144
|
4562
|
self.scheduleDateHeadingLabel?.stringValue = "Refreshing…"
|
|
4145
|
4563
|
self.pageCache[.joinMeetings] = nil
|
|
4146
|
|
- self.showSidebarPage(.joinMeetings)
|
|
|
4564
|
+ self.pageCache[.photo] = nil
|
|
|
4565
|
+ self.showSidebarPage(self.selectedSidebarPage)
|
|
4147
|
4566
|
}
|
|
4148
|
4567
|
await self.loadSchedule()
|
|
4149
|
4568
|
} catch {
|
|
|
@@ -4170,7 +4589,8 @@ private extension ViewController {
|
|
4170
|
4589
|
self.updateGoogleAuthButtonTitle()
|
|
4171
|
4590
|
self.applyGoogleProfile(profile.map { self.makeGoogleProfileDisplay(from: $0) })
|
|
4172
|
4591
|
self.pageCache[.joinMeetings] = nil
|
|
4173
|
|
- self.showSidebarPage(.joinMeetings)
|
|
|
4592
|
+ self.pageCache[.photo] = nil
|
|
|
4593
|
+ self.showSidebarPage(self.selectedSidebarPage)
|
|
4174
|
4594
|
}
|
|
4175
|
4595
|
} catch {
|
|
4176
|
4596
|
self.showSimpleError("Couldn’t connect Google account.", error: error)
|