|
|
@@ -7,6 +7,7 @@
|
|
7
|
7
|
|
|
8
|
8
|
import Cocoa
|
|
9
|
9
|
import WebKit
|
|
|
10
|
+import AuthenticationServices
|
|
10
|
11
|
|
|
11
|
12
|
private enum SidebarPage: Int {
|
|
12
|
13
|
case joinMeetings = 0
|
|
|
@@ -61,6 +62,19 @@ final class ViewController: NSViewController {
|
|
61
|
62
|
private weak var meetLinkField: NSTextField?
|
|
62
|
63
|
private weak var browseAddressField: NSTextField?
|
|
63
|
64
|
private var inAppBrowserWindowController: InAppBrowserWindowController?
|
|
|
65
|
+ private let googleOAuth = GoogleOAuthService.shared
|
|
|
66
|
+ private let calendarClient = GoogleCalendarClient()
|
|
|
67
|
+
|
|
|
68
|
+ private enum ScheduleFilter: Int {
|
|
|
69
|
+ case all = 0
|
|
|
70
|
+ case today = 1
|
|
|
71
|
+ case week = 2
|
|
|
72
|
+ }
|
|
|
73
|
+
|
|
|
74
|
+ private var scheduleFilter: ScheduleFilter = .all
|
|
|
75
|
+ private weak var scheduleDateHeadingLabel: NSTextField?
|
|
|
76
|
+ private weak var scheduleCardsStack: NSStackView?
|
|
|
77
|
+ private weak var scheduleFilterLabel: NSTextField?
|
|
64
|
78
|
|
|
65
|
79
|
/// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
|
|
66
|
80
|
private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll
|
|
|
@@ -909,8 +923,13 @@ private extension ViewController {
|
|
909
|
923
|
contentStack.addArrangedSubview(joinActions)
|
|
910
|
924
|
contentStack.setCustomSpacing(26, after: joinActions)
|
|
911
|
925
|
contentStack.addArrangedSubview(scheduleHeader())
|
|
912
|
|
- contentStack.addArrangedSubview(textLabel("Tuesday, 14 Apr", font: typography.dateHeading, color: palette.textSecondary))
|
|
913
|
|
- contentStack.addArrangedSubview(scheduleCardsRow())
|
|
|
926
|
+
|
|
|
927
|
+ let dateHeading = textLabel(scheduleInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
|
|
|
928
|
+ scheduleDateHeadingLabel = dateHeading
|
|
|
929
|
+ contentStack.addArrangedSubview(dateHeading)
|
|
|
930
|
+
|
|
|
931
|
+ let cardsRow = scheduleCardsRow(meetings: [])
|
|
|
932
|
+ contentStack.addArrangedSubview(cardsRow)
|
|
914
|
933
|
|
|
915
|
934
|
panel.addSubview(contentStack)
|
|
916
|
935
|
|
|
|
@@ -920,6 +939,10 @@ private extension ViewController {
|
|
920
|
939
|
contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26)
|
|
921
|
940
|
])
|
|
922
|
941
|
|
|
|
942
|
+ Task { [weak self] in
|
|
|
943
|
+ await self?.loadSchedule()
|
|
|
944
|
+ }
|
|
|
945
|
+
|
|
923
|
946
|
return panel
|
|
924
|
947
|
}
|
|
925
|
948
|
|
|
|
@@ -1777,15 +1800,25 @@ private extension ViewController {
|
|
1777
|
1800
|
row.addArrangedSubview(spacer)
|
|
1778
|
1801
|
spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1779
|
1802
|
|
|
1780
|
|
- row.addArrangedSubview(iconRoundButton("?", size: 34))
|
|
1781
|
|
- row.addArrangedSubview(iconRoundButton("⟳", size: 34))
|
|
|
1803
|
+ row.addArrangedSubview(iconRoundButton("?", size: 34, onClick: { [weak self] in
|
|
|
1804
|
+ self?.showScheduleHelp()
|
|
|
1805
|
+ }))
|
|
|
1806
|
+ row.addArrangedSubview(iconRoundButton("⟳", size: 34, onClick: { [weak self] in
|
|
|
1807
|
+ self?.scheduleReloadClicked()
|
|
|
1808
|
+ }))
|
|
|
1809
|
+
|
|
|
1810
|
+ let connectButton = makeSchedulePillButton(title: googleOAuth.loadTokens() == nil ? "Connect" : "Connected")
|
|
|
1811
|
+ connectButton.target = self
|
|
|
1812
|
+ connectButton.action = #selector(scheduleConnectButtonPressed(_:))
|
|
|
1813
|
+ row.addArrangedSubview(connectButton)
|
|
1782
|
1814
|
|
|
1783
|
1815
|
let filter = roundedContainer(cornerRadius: 8, color: palette.inputBackground)
|
|
1784
|
1816
|
filter.translatesAutoresizingMaskIntoConstraints = false
|
|
1785
|
1817
|
filter.widthAnchor.constraint(equalToConstant: 156).isActive = true
|
|
1786
|
1818
|
filter.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
1787
|
1819
|
styleSurface(filter, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
1788
|
|
- let filterText = textLabel("All", font: typography.filterText, color: palette.textSecondary)
|
|
|
1820
|
+ let filterText = textLabel(scheduleFilterTitle(scheduleFilter), font: typography.filterText, color: palette.textSecondary)
|
|
|
1821
|
+ scheduleFilterLabel = filterText
|
|
1789
|
1822
|
let arrow = textLabel("▾", font: typography.filterArrow, color: palette.textMuted)
|
|
1790
|
1823
|
filterText.translatesAutoresizingMaskIntoConstraints = false
|
|
1791
|
1824
|
arrow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -1799,12 +1832,56 @@ private extension ViewController {
|
|
1799
|
1832
|
arrow.centerYAnchor.constraint(equalTo: filter.centerYAnchor)
|
|
1800
|
1833
|
])
|
|
1801
|
1834
|
|
|
1802
|
|
- row.addArrangedSubview(filter)
|
|
|
1835
|
+ let filterHit = HoverTrackingView()
|
|
|
1836
|
+ filterHit.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1837
|
+ filterHit.addSubview(filter)
|
|
|
1838
|
+ NSLayoutConstraint.activate([
|
|
|
1839
|
+ filterHit.widthAnchor.constraint(equalToConstant: 156),
|
|
|
1840
|
+ filterHit.heightAnchor.constraint(equalToConstant: 34),
|
|
|
1841
|
+ filter.leadingAnchor.constraint(equalTo: filterHit.leadingAnchor),
|
|
|
1842
|
+ filter.trailingAnchor.constraint(equalTo: filterHit.trailingAnchor),
|
|
|
1843
|
+ filter.topAnchor.constraint(equalTo: filterHit.topAnchor),
|
|
|
1844
|
+ filter.bottomAnchor.constraint(equalTo: filterHit.bottomAnchor)
|
|
|
1845
|
+ ])
|
|
|
1846
|
+ filterHit.onClick = { [weak self, weak filterHit] in
|
|
|
1847
|
+ guard let self, let anchor = filterHit else { return }
|
|
|
1848
|
+ self.showScheduleFilterMenu(anchor: anchor)
|
|
|
1849
|
+ }
|
|
|
1850
|
+
|
|
|
1851
|
+ row.addArrangedSubview(filterHit)
|
|
1803
|
1852
|
row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
|
|
1804
|
1853
|
return row
|
|
1805
|
1854
|
}
|
|
1806
|
1855
|
|
|
1807
|
|
- func scheduleCardsRow() -> NSView {
|
|
|
1856
|
+ private func makeSchedulePillButton(title: String) -> NSButton {
|
|
|
1857
|
+ let button = NSButton(title: title, target: nil, action: nil)
|
|
|
1858
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1859
|
+ button.isBordered = false
|
|
|
1860
|
+ button.bezelStyle = .regularSquare
|
|
|
1861
|
+ button.wantsLayer = true
|
|
|
1862
|
+ button.layer?.cornerRadius = 8
|
|
|
1863
|
+ button.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
1864
|
+ button.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
1865
|
+ button.layer?.borderWidth = 1
|
|
|
1866
|
+ button.font = typography.filterText
|
|
|
1867
|
+ button.contentTintColor = palette.textSecondary
|
|
|
1868
|
+ button.setButtonType(.momentaryChange)
|
|
|
1869
|
+ button.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
|
1870
|
+ button.widthAnchor.constraint(greaterThanOrEqualToConstant: 104).isActive = true
|
|
|
1871
|
+ return button
|
|
|
1872
|
+ }
|
|
|
1873
|
+
|
|
|
1874
|
+ func scheduleCardsRow(meetings: [ScheduledMeeting]) -> NSView {
|
|
|
1875
|
+ let scroll = NSScrollView()
|
|
|
1876
|
+ scroll.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1877
|
+ scroll.drawsBackground = false
|
|
|
1878
|
+ scroll.hasHorizontalScroller = true
|
|
|
1879
|
+ scroll.hasVerticalScroller = false
|
|
|
1880
|
+ scroll.horizontalScrollElasticity = .allowed
|
|
|
1881
|
+ scroll.verticalScrollElasticity = .none
|
|
|
1882
|
+ scroll.autohidesScrollers = true
|
|
|
1883
|
+ scroll.borderType = .noBorder
|
|
|
1884
|
+
|
|
1808
|
1885
|
let row = NSStackView()
|
|
1809
|
1886
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
1810
|
1887
|
row.orientation = .horizontal
|
|
|
@@ -1813,13 +1890,25 @@ private extension ViewController {
|
|
1813
|
1890
|
row.distribution = .fill
|
|
1814
|
1891
|
row.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
1815
|
1892
|
row.heightAnchor.constraint(equalToConstant: 136).isActive = true
|
|
|
1893
|
+ scheduleCardsStack = row
|
|
1816
|
1894
|
|
|
1817
|
|
- row.addArrangedSubview(scheduleCard())
|
|
1818
|
|
- row.addArrangedSubview(scheduleCard())
|
|
1819
|
|
- return row
|
|
|
1895
|
+ scroll.documentView = row
|
|
|
1896
|
+ scroll.contentView.postsBoundsChangedNotifications = true
|
|
|
1897
|
+
|
|
|
1898
|
+ // Ensure the stack view determines content size for horizontal scrolling.
|
|
|
1899
|
+ NSLayoutConstraint.activate([
|
|
|
1900
|
+ row.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
|
|
|
1901
|
+ row.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
|
|
|
1902
|
+ row.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
|
|
|
1903
|
+ row.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
|
|
|
1904
|
+ row.heightAnchor.constraint(equalToConstant: 136)
|
|
|
1905
|
+ ])
|
|
|
1906
|
+
|
|
|
1907
|
+ renderScheduleCards(into: row, meetings: meetings)
|
|
|
1908
|
+ return scroll
|
|
1820
|
1909
|
}
|
|
1821
|
1910
|
|
|
1822
|
|
- func scheduleCard() -> NSView {
|
|
|
1911
|
+ func scheduleCard(meeting: ScheduledMeeting) -> NSView {
|
|
1823
|
1912
|
let cardWidth: CGFloat = 264
|
|
1824
|
1913
|
|
|
1825
|
1914
|
let card = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
|
|
|
@@ -1840,9 +1929,9 @@ private extension ViewController {
|
|
1840
|
1929
|
iconText.centerYAnchor.constraint(equalTo: icon.centerYAnchor)
|
|
1841
|
1930
|
])
|
|
1842
|
1931
|
|
|
1843
|
|
- let title = textLabel("General Meeting", font: typography.cardTitle, color: palette.textPrimary)
|
|
1844
|
|
- let subtitle = textLabel("Baisakhi", font: typography.cardSubtitle, color: palette.textPrimary)
|
|
1845
|
|
- let time = textLabel("12:00 AM - 11:59 PM", font: typography.cardTime, color: palette.textSecondary)
|
|
|
1932
|
+ let title = textLabel(meeting.title, font: typography.cardTitle, color: palette.textPrimary)
|
|
|
1933
|
+ let subtitle = textLabel(meeting.subtitle ?? "Google Calendar", font: typography.cardSubtitle, color: palette.textPrimary)
|
|
|
1934
|
+ let time = textLabel(scheduleTimeText(for: meeting), font: typography.cardTime, color: palette.textSecondary)
|
|
1846
|
1935
|
|
|
1847
|
1936
|
card.addSubview(icon)
|
|
1848
|
1937
|
card.addSubview(title)
|
|
|
@@ -1864,7 +1953,28 @@ private extension ViewController {
|
|
1864
|
1953
|
time.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 4)
|
|
1865
|
1954
|
])
|
|
1866
|
1955
|
|
|
1867
|
|
- return card
|
|
|
1956
|
+ let hit = HoverTrackingView()
|
|
|
1957
|
+ hit.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1958
|
+ hit.addSubview(card)
|
|
|
1959
|
+ NSLayoutConstraint.activate([
|
|
|
1960
|
+ card.leadingAnchor.constraint(equalTo: hit.leadingAnchor),
|
|
|
1961
|
+ card.trailingAnchor.constraint(equalTo: hit.trailingAnchor),
|
|
|
1962
|
+ card.topAnchor.constraint(equalTo: hit.topAnchor),
|
|
|
1963
|
+ card.bottomAnchor.constraint(equalTo: hit.bottomAnchor)
|
|
|
1964
|
+ ])
|
|
|
1965
|
+ hit.onClick = { [weak self] in
|
|
|
1966
|
+ self?.openMeetingURL(meeting.meetURL)
|
|
|
1967
|
+ }
|
|
|
1968
|
+ hit.onHoverChanged = { [weak self] hovering in
|
|
|
1969
|
+ guard let self else { return }
|
|
|
1970
|
+ let base = self.palette.sectionCard
|
|
|
1971
|
+ let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
1972
|
+ let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
|
|
|
1973
|
+ card.layer?.backgroundColor = (hovering ? hover : base).cgColor
|
|
|
1974
|
+ }
|
|
|
1975
|
+ hit.onHoverChanged?(false)
|
|
|
1976
|
+
|
|
|
1977
|
+ return hit
|
|
1868
|
1978
|
}
|
|
1869
|
1979
|
}
|
|
1870
|
1980
|
|
|
|
@@ -2375,7 +2485,7 @@ private extension ViewController {
|
|
2375
|
2485
|
return button
|
|
2376
|
2486
|
}
|
|
2377
|
2487
|
|
|
2378
|
|
- func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView {
|
|
|
2488
|
+ func iconRoundButton(_ symbol: String, size: CGFloat, onClick: (() -> Void)? = nil) -> NSView {
|
|
2379
|
2489
|
let button = HoverTrackingView()
|
|
2380
|
2490
|
button.wantsLayer = true
|
|
2381
|
2491
|
button.layer?.cornerRadius = size / 2
|
|
|
@@ -2399,11 +2509,246 @@ private extension ViewController {
|
|
2399
|
2509
|
button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
|
|
2400
|
2510
|
}
|
|
2401
|
2511
|
button.onHoverChanged?(false)
|
|
|
2512
|
+ button.onClick = onClick
|
|
2402
|
2513
|
|
|
2403
|
2514
|
return button
|
|
2404
|
2515
|
}
|
|
2405
|
2516
|
}
|
|
2406
|
2517
|
|
|
|
2518
|
+// MARK: - Schedule actions (OAuth entry)
|
|
|
2519
|
+
|
|
|
2520
|
+private extension ViewController {
|
|
|
2521
|
+ @objc func scheduleConnectButtonPressed(_ sender: NSButton) {
|
|
|
2522
|
+ scheduleConnectClicked()
|
|
|
2523
|
+ }
|
|
|
2524
|
+
|
|
|
2525
|
+ private func scheduleInitialHeadingText() -> String {
|
|
|
2526
|
+ googleOAuth.loadTokens() == nil ? "Connect Google to see meetings" : "Loading…"
|
|
|
2527
|
+ }
|
|
|
2528
|
+
|
|
|
2529
|
+ private func scheduleFilterTitle(_ filter: ScheduleFilter) -> String {
|
|
|
2530
|
+ switch filter {
|
|
|
2531
|
+ case .all: return "All"
|
|
|
2532
|
+ case .today: return "Today"
|
|
|
2533
|
+ case .week: return "This week"
|
|
|
2534
|
+ }
|
|
|
2535
|
+ }
|
|
|
2536
|
+
|
|
|
2537
|
+ private func showScheduleFilterMenu(anchor: NSView) {
|
|
|
2538
|
+ let menu = NSMenu()
|
|
|
2539
|
+ let items: [(ScheduleFilter, String)] = [
|
|
|
2540
|
+ (.all, "All"),
|
|
|
2541
|
+ (.today, "Today"),
|
|
|
2542
|
+ (.week, "This week")
|
|
|
2543
|
+ ]
|
|
|
2544
|
+ for (filter, title) in items {
|
|
|
2545
|
+ let item = NSMenuItem(title: title, action: #selector(scheduleFilterSelected(_:)), keyEquivalent: "")
|
|
|
2546
|
+ item.target = self
|
|
|
2547
|
+ item.tag = filter.rawValue
|
|
|
2548
|
+ item.state = (filter == scheduleFilter) ? .on : .off
|
|
|
2549
|
+ menu.addItem(item)
|
|
|
2550
|
+ }
|
|
|
2551
|
+ let loc = NSPoint(x: 8, y: anchor.bounds.height + 2)
|
|
|
2552
|
+ menu.popUp(positioning: nil, at: loc, in: anchor)
|
|
|
2553
|
+ }
|
|
|
2554
|
+
|
|
|
2555
|
+ @objc func scheduleFilterSelected(_ sender: NSMenuItem) {
|
|
|
2556
|
+ guard let filter = ScheduleFilter(rawValue: sender.tag) else { return }
|
|
|
2557
|
+ scheduleFilter = filter
|
|
|
2558
|
+ scheduleFilterLabel?.stringValue = scheduleFilterTitle(filter)
|
|
|
2559
|
+ Task { [weak self] in
|
|
|
2560
|
+ await self?.loadSchedule()
|
|
|
2561
|
+ }
|
|
|
2562
|
+ }
|
|
|
2563
|
+
|
|
|
2564
|
+ private func scheduleTimeText(for meeting: ScheduledMeeting) -> String {
|
|
|
2565
|
+ if meeting.isAllDay { return "All day" }
|
|
|
2566
|
+ let f = DateFormatter()
|
|
|
2567
|
+ f.locale = Locale.current
|
|
|
2568
|
+ f.timeZone = TimeZone.current
|
|
|
2569
|
+ f.dateStyle = .none
|
|
|
2570
|
+ f.timeStyle = .short
|
|
|
2571
|
+ return "\(f.string(from: meeting.startDate)) - \(f.string(from: meeting.endDate))"
|
|
|
2572
|
+ }
|
|
|
2573
|
+
|
|
|
2574
|
+ private func scheduleHeadingText(for meetings: [ScheduledMeeting]) -> String {
|
|
|
2575
|
+ guard let first = meetings.first else {
|
|
|
2576
|
+ return googleOAuth.loadTokens() == nil ? "Connect Google to see meetings" : "No upcoming meetings"
|
|
|
2577
|
+ }
|
|
|
2578
|
+
|
|
|
2579
|
+ let day = Calendar.current.startOfDay(for: first.startDate)
|
|
|
2580
|
+ let f = DateFormatter()
|
|
|
2581
|
+ f.locale = Locale.current
|
|
|
2582
|
+ f.timeZone = TimeZone.current
|
|
|
2583
|
+ f.dateFormat = "EEEE, d MMM"
|
|
|
2584
|
+ return f.string(from: day)
|
|
|
2585
|
+ }
|
|
|
2586
|
+
|
|
|
2587
|
+ private func openMeetingURL(_ url: URL) {
|
|
|
2588
|
+ NSWorkspace.shared.open(url)
|
|
|
2589
|
+ }
|
|
|
2590
|
+
|
|
|
2591
|
+ private func renderScheduleCards(into stack: NSStackView, meetings: [ScheduledMeeting]) {
|
|
|
2592
|
+ stack.arrangedSubviews.forEach { v in
|
|
|
2593
|
+ stack.removeArrangedSubview(v)
|
|
|
2594
|
+ v.removeFromSuperview()
|
|
|
2595
|
+ }
|
|
|
2596
|
+
|
|
|
2597
|
+ if meetings.isEmpty {
|
|
|
2598
|
+ let empty = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
|
|
|
2599
|
+ empty.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2600
|
+ empty.widthAnchor.constraint(equalToConstant: 264).isActive = true
|
|
|
2601
|
+ empty.heightAnchor.constraint(equalToConstant: 136).isActive = true
|
|
|
2602
|
+ styleSurface(empty, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
2603
|
+
|
|
|
2604
|
+ let label = textLabel(googleOAuth.loadTokens() == nil ? "Connect to load schedule" : "No meetings", font: typography.cardSubtitle, color: palette.textSecondary)
|
|
|
2605
|
+ label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2606
|
+ empty.addSubview(label)
|
|
|
2607
|
+ NSLayoutConstraint.activate([
|
|
|
2608
|
+ label.centerXAnchor.constraint(equalTo: empty.centerXAnchor),
|
|
|
2609
|
+ label.centerYAnchor.constraint(equalTo: empty.centerYAnchor)
|
|
|
2610
|
+ ])
|
|
|
2611
|
+ stack.addArrangedSubview(empty)
|
|
|
2612
|
+ return
|
|
|
2613
|
+ }
|
|
|
2614
|
+
|
|
|
2615
|
+ for meeting in meetings {
|
|
|
2616
|
+ stack.addArrangedSubview(scheduleCard(meeting: meeting))
|
|
|
2617
|
+ }
|
|
|
2618
|
+ }
|
|
|
2619
|
+
|
|
|
2620
|
+ private func filteredMeetings(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
|
|
|
2621
|
+ switch scheduleFilter {
|
|
|
2622
|
+ case .all:
|
|
|
2623
|
+ return meetings
|
|
|
2624
|
+ case .today:
|
|
|
2625
|
+ let start = Calendar.current.startOfDay(for: Date())
|
|
|
2626
|
+ let end = Calendar.current.date(byAdding: .day, value: 1, to: start) ?? start.addingTimeInterval(86400)
|
|
|
2627
|
+ return meetings.filter { $0.startDate >= start && $0.startDate < end }
|
|
|
2628
|
+ case .week:
|
|
|
2629
|
+ let now = Date()
|
|
|
2630
|
+ let end = Calendar.current.date(byAdding: .day, value: 7, to: now) ?? now.addingTimeInterval(7 * 86400)
|
|
|
2631
|
+ return meetings.filter { $0.startDate >= now && $0.startDate <= end }
|
|
|
2632
|
+ }
|
|
|
2633
|
+ }
|
|
|
2634
|
+
|
|
|
2635
|
+ private func loadSchedule() async {
|
|
|
2636
|
+ do {
|
|
|
2637
|
+ if googleOAuth.loadTokens() == nil {
|
|
|
2638
|
+ await MainActor.run {
|
|
|
2639
|
+ scheduleDateHeadingLabel?.stringValue = "Connect Google to see meetings"
|
|
|
2640
|
+ if let stack = scheduleCardsStack {
|
|
|
2641
|
+ renderScheduleCards(into: stack, meetings: [])
|
|
|
2642
|
+ }
|
|
|
2643
|
+ }
|
|
|
2644
|
+ return
|
|
|
2645
|
+ }
|
|
|
2646
|
+
|
|
|
2647
|
+ let token = try await googleOAuth.validAccessToken(presentingWindow: view.window)
|
|
|
2648
|
+ let meetings = try await calendarClient.fetchUpcomingMeetings(accessToken: token)
|
|
|
2649
|
+ let filtered = filteredMeetings(meetings)
|
|
|
2650
|
+
|
|
|
2651
|
+ await MainActor.run {
|
|
|
2652
|
+ scheduleDateHeadingLabel?.stringValue = scheduleHeadingText(for: filtered)
|
|
|
2653
|
+ if let stack = scheduleCardsStack {
|
|
|
2654
|
+ renderScheduleCards(into: stack, meetings: filtered)
|
|
|
2655
|
+ }
|
|
|
2656
|
+ }
|
|
|
2657
|
+ } catch {
|
|
|
2658
|
+ await MainActor.run {
|
|
|
2659
|
+ scheduleDateHeadingLabel?.stringValue = "Couldn’t load schedule"
|
|
|
2660
|
+ if let stack = scheduleCardsStack {
|
|
|
2661
|
+ renderScheduleCards(into: stack, meetings: [])
|
|
|
2662
|
+ }
|
|
|
2663
|
+ showSimpleError("Couldn’t load schedule.", error: error)
|
|
|
2664
|
+ }
|
|
|
2665
|
+ }
|
|
|
2666
|
+ }
|
|
|
2667
|
+
|
|
|
2668
|
+ func showScheduleHelp() {
|
|
|
2669
|
+ let alert = NSAlert()
|
|
|
2670
|
+ alert.messageText = "Google schedule"
|
|
|
2671
|
+ alert.informativeText = "To show scheduled meetings, connect your Google account. You’ll need a Google OAuth client ID (Desktop) and a redirect URI matching the app’s callback scheme."
|
|
|
2672
|
+ alert.addButton(withTitle: "OK")
|
|
|
2673
|
+ alert.runModal()
|
|
|
2674
|
+ }
|
|
|
2675
|
+
|
|
|
2676
|
+ func scheduleReloadClicked() {
|
|
|
2677
|
+ // Data loading is wired in the Calendar step.
|
|
|
2678
|
+ // For now, this triggers a sign-in if needed so the next step can fetch events.
|
|
|
2679
|
+ Task { [weak self] in
|
|
|
2680
|
+ guard let self else { return }
|
|
|
2681
|
+ _ = try? await googleOAuth.validAccessToken(presentingWindow: view.window)
|
|
|
2682
|
+ }
|
|
|
2683
|
+ }
|
|
|
2684
|
+
|
|
|
2685
|
+ func scheduleConnectClicked() {
|
|
|
2686
|
+ Task { [weak self] in
|
|
|
2687
|
+ guard let self else { return }
|
|
|
2688
|
+ do {
|
|
|
2689
|
+ try await ensureGoogleClientIdConfigured(presentingWindow: view.window)
|
|
|
2690
|
+ _ = try await googleOAuth.validAccessToken(presentingWindow: view.window)
|
|
|
2691
|
+ pageCache[.joinMeetings] = nil
|
|
|
2692
|
+ showSidebarPage(.joinMeetings)
|
|
|
2693
|
+ } catch {
|
|
|
2694
|
+ showSimpleError("Couldn’t connect Google account.", error: error)
|
|
|
2695
|
+ }
|
|
|
2696
|
+ }
|
|
|
2697
|
+ }
|
|
|
2698
|
+
|
|
|
2699
|
+ @MainActor
|
|
|
2700
|
+ func ensureGoogleClientIdConfigured(presentingWindow: NSWindow?) async throws {
|
|
|
2701
|
+ if googleOAuth.configuredClientId() != nil, googleOAuth.configuredClientSecret() != nil { return }
|
|
|
2702
|
+
|
|
|
2703
|
+ let alert = NSAlert()
|
|
|
2704
|
+ alert.messageText = "Enter Google OAuth credentials"
|
|
|
2705
|
+ alert.informativeText = "Paste the OAuth Client ID and Client Secret from your downloaded Desktop OAuth JSON."
|
|
|
2706
|
+
|
|
|
2707
|
+ let accessory = NSStackView()
|
|
|
2708
|
+ accessory.orientation = .vertical
|
|
|
2709
|
+ accessory.spacing = 8
|
|
|
2710
|
+ accessory.alignment = .leading
|
|
|
2711
|
+
|
|
|
2712
|
+ let idField = NSTextField(string: googleOAuth.configuredClientId() ?? "")
|
|
|
2713
|
+ idField.placeholderString = "Client ID (....apps.googleusercontent.com)"
|
|
|
2714
|
+ idField.frame = NSRect(x: 0, y: 0, width: 460, height: 24)
|
|
|
2715
|
+
|
|
|
2716
|
+ let secretField = NSSecureTextField(string: googleOAuth.configuredClientSecret() ?? "")
|
|
|
2717
|
+ secretField.placeholderString = "Client Secret (GOCSPX-...)"
|
|
|
2718
|
+ secretField.frame = NSRect(x: 0, y: 0, width: 460, height: 24)
|
|
|
2719
|
+
|
|
|
2720
|
+ accessory.addArrangedSubview(idField)
|
|
|
2721
|
+ accessory.addArrangedSubview(secretField)
|
|
|
2722
|
+ alert.accessoryView = accessory
|
|
|
2723
|
+
|
|
|
2724
|
+ alert.addButton(withTitle: "Save")
|
|
|
2725
|
+ alert.addButton(withTitle: "Cancel")
|
|
|
2726
|
+
|
|
|
2727
|
+ // Keep this synchronous to avoid additional sheet state handling.
|
|
|
2728
|
+ let response = alert.runModal()
|
|
|
2729
|
+ if response != .alertFirstButtonReturn { throw GoogleOAuthError.missingClientId }
|
|
|
2730
|
+
|
|
|
2731
|
+ let idValue = idField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
2732
|
+ let secretValue = secretField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
2733
|
+ if idValue.isEmpty { throw GoogleOAuthError.missingClientId }
|
|
|
2734
|
+ if secretValue.isEmpty { throw GoogleOAuthError.missingClientSecret }
|
|
|
2735
|
+
|
|
|
2736
|
+ googleOAuth.setClientIdForTesting(idValue)
|
|
|
2737
|
+ googleOAuth.setClientSecretForTesting(secretValue)
|
|
|
2738
|
+ }
|
|
|
2739
|
+
|
|
|
2740
|
+ func showSimpleError(_ title: String, error: Error) {
|
|
|
2741
|
+ DispatchQueue.main.async {
|
|
|
2742
|
+ let alert = NSAlert()
|
|
|
2743
|
+ alert.alertStyle = .warning
|
|
|
2744
|
+ alert.messageText = title
|
|
|
2745
|
+ alert.informativeText = error.localizedDescription
|
|
|
2746
|
+ alert.addButton(withTitle: "OK")
|
|
|
2747
|
+ alert.runModal()
|
|
|
2748
|
+ }
|
|
|
2749
|
+ }
|
|
|
2750
|
+}
|
|
|
2751
|
+
|
|
2407
|
2752
|
private struct Palette {
|
|
2408
|
2753
|
let pageBackground: NSColor
|
|
2409
|
2754
|
let sidebarBackground: NSColor
|
|
|
@@ -2514,11 +2859,8 @@ private func inAppBrowserURLAllowed(_ url: URL, policy: InAppBrowserURLPolicy) -
|
|
2514
|
2859
|
}
|
|
2515
|
2860
|
|
|
2516
|
2861
|
private enum InAppBrowserWebKitSupport {
|
|
2517
|
|
- static let sharedProcessPool = WKProcessPool()
|
|
2518
|
|
-
|
|
2519
|
2862
|
static func makeWebViewConfiguration() -> WKWebViewConfiguration {
|
|
2520
|
2863
|
let config = WKWebViewConfiguration()
|
|
2521
|
|
- config.processPool = sharedProcessPool
|
|
2522
|
2864
|
config.websiteDataStore = .default()
|
|
2523
|
2865
|
config.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
2524
|
2866
|
if #available(macOS 12.3, *) {
|