|
|
@@ -430,6 +430,7 @@ class ViewController: NSViewController {
|
|
430
|
430
|
let end: Date?
|
|
431
|
431
|
let host: String
|
|
432
|
432
|
let source: String
|
|
|
433
|
+ let webURL: URL?
|
|
433
|
434
|
}
|
|
434
|
435
|
|
|
435
|
436
|
@MainActor
|
|
|
@@ -480,7 +481,12 @@ class ViewController: NSViewController {
|
|
480
|
481
|
emptyMeetingLabel?.isHidden = true
|
|
481
|
482
|
meetingsStatusLabel?.stringValue = "Upcoming meetings"
|
|
482
|
483
|
for meeting in ordered {
|
|
483
|
|
- stack.addArrangedSubview(makeMeetingRowCard(meeting))
|
|
|
484
|
+ let card = makeMeetingRowCard(meeting)
|
|
|
485
|
+ stack.addArrangedSubview(card)
|
|
|
486
|
+ // Make each meeting card span the full list width (like Zoom).
|
|
|
487
|
+ if card.constraints.contains(where: { $0.firstAttribute == .width }) == false {
|
|
|
488
|
+ card.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
489
|
+ }
|
|
484
|
490
|
}
|
|
485
|
491
|
}
|
|
486
|
492
|
|
|
|
@@ -655,10 +661,12 @@ class ViewController: NSViewController {
|
|
655
|
661
|
|
|
656
|
662
|
private func fetchZoomScheduledMeetings(accessToken: String) async throws -> [ScheduledMeeting] {
|
|
657
|
663
|
struct ZoomMeeting: Decodable {
|
|
|
664
|
+ let id: Int?
|
|
658
|
665
|
let topic: String?
|
|
659
|
666
|
let start_time: String?
|
|
660
|
667
|
let duration: Int?
|
|
661
|
668
|
let host_id: String?
|
|
|
669
|
+ let join_url: String?
|
|
662
|
670
|
}
|
|
663
|
671
|
|
|
664
|
672
|
struct ZoomMeetingsPage: Decodable {
|
|
|
@@ -677,12 +685,22 @@ class ViewController: NSViewController {
|
|
677
|
685
|
let start = iso.date(from: startRaw) ?? fallbackISO.date(from: startRaw)
|
|
678
|
686
|
guard let start else { return nil }
|
|
679
|
687
|
let end = meeting.duration.map { start.addingTimeInterval(TimeInterval($0 * 60)) }
|
|
|
688
|
+ let webURL: URL? = {
|
|
|
689
|
+ if let join = meeting.join_url, let url = URL(string: join), url.scheme != nil {
|
|
|
690
|
+ return url
|
|
|
691
|
+ }
|
|
|
692
|
+ if let id = meeting.id {
|
|
|
693
|
+ return URL(string: "https://zoom.us/j/\(id)")
|
|
|
694
|
+ }
|
|
|
695
|
+ return nil
|
|
|
696
|
+ }()
|
|
680
|
697
|
return ScheduledMeeting(
|
|
681
|
698
|
title: meeting.topic?.isEmpty == false ? meeting.topic! : "Zoom meeting",
|
|
682
|
699
|
start: start,
|
|
683
|
700
|
end: end,
|
|
684
|
701
|
host: meeting.host_id ?? "Zoom Host",
|
|
685
|
|
- source: "Zoom"
|
|
|
702
|
+ source: "Zoom",
|
|
|
703
|
+ webURL: webURL
|
|
686
|
704
|
)
|
|
687
|
705
|
}
|
|
688
|
706
|
}
|
|
|
@@ -1011,8 +1029,8 @@ class ViewController: NSViewController {
|
|
1011
|
1029
|
|
|
1012
|
1030
|
let todaysDateFormatter = DateFormatter()
|
|
1013
|
1031
|
todaysDateFormatter.dateFormat = "EEEE, MMM d"
|
|
1014
|
|
- let panelHeader = makeLabel(todaysDateFormatter.string(from: Date()), size: 21, color: primaryText, weight: .semibold, centered: false)
|
|
1015
|
|
- let meetingsStatus = makeLabel("Upcoming meetings", size: 12, color: secondaryText, weight: .medium, centered: false)
|
|
|
1032
|
+ let panelHeader = makeLabel(todaysDateFormatter.string(from: Date()), size: 18, color: primaryText, weight: .semibold, centered: false)
|
|
|
1033
|
+ let meetingsStatus = makeLabel("Upcoming meetings", size: 11, color: secondaryText, weight: .medium, centered: false)
|
|
1016
|
1034
|
|
|
1017
|
1035
|
let meetingsDayNav = NSStackView()
|
|
1018
|
1036
|
meetingsDayNav.orientation = .horizontal
|
|
|
@@ -1023,7 +1041,7 @@ class ViewController: NSViewController {
|
|
1023
|
1041
|
let nextDayButton = makeNavGlyphButton(symbol: "chevron.right", action: #selector(meetingsNextDayTapped), dimension: 14, pointSize: 7, toolTip: "Next day")
|
|
1024
|
1042
|
[todayButton, prevDayButton, nextDayButton].forEach { meetingsDayNav.addArrangedSubview($0) }
|
|
1025
|
1043
|
|
|
1026
|
|
- let noMeeting = makeLabel("No meetings scheduled for today.", size: 18, color: secondaryText, weight: .regular, centered: true)
|
|
|
1044
|
+ let noMeeting = makeLabel("No meetings scheduled for today.", size: 15, color: secondaryText, weight: .regular, centered: true)
|
|
1027
|
1045
|
let meetingsScrollView = NSScrollView()
|
|
1028
|
1046
|
meetingsScrollView.drawsBackground = false
|
|
1029
|
1047
|
meetingsScrollView.hasVerticalScroller = true
|
|
|
@@ -1033,7 +1051,7 @@ class ViewController: NSViewController {
|
|
1033
|
1051
|
let meetingsDocument = NSView()
|
|
1034
|
1052
|
let meetingsStack = NSStackView()
|
|
1035
|
1053
|
meetingsStack.orientation = .vertical
|
|
1036
|
|
- meetingsStack.spacing = 10
|
|
|
1054
|
+ meetingsStack.spacing = 14
|
|
1037
|
1055
|
meetingsStack.alignment = .leading
|
|
1038
|
1056
|
let openRecordings = NSButton(title: "Open recordings", target: nil, action: nil)
|
|
1039
|
1057
|
openRecordings.isBordered = false
|
|
|
@@ -1572,7 +1590,7 @@ class ViewController: NSViewController {
|
|
1572
|
1590
|
}
|
|
1573
|
1591
|
|
|
1574
|
1592
|
private func makeMeetingRowCard(_ meeting: ScheduledMeeting) -> NSView {
|
|
1575
|
|
- let card = NSView()
|
|
|
1593
|
+ let card = MeetingCardView(url: meeting.webURL)
|
|
1576
|
1594
|
card.wantsLayer = true
|
|
1577
|
1595
|
card.layer?.backgroundColor = meetingCardBackground.cgColor
|
|
1578
|
1596
|
card.layer?.cornerRadius = 13
|
|
|
@@ -1589,10 +1607,10 @@ class ViewController: NSViewController {
|
|
1589
|
1607
|
let endText = meeting.end.map { timeFormatter.string(from: $0) } ?? ""
|
|
1590
|
1608
|
let range = endText.isEmpty ? startText : "\(startText) - \(endText)"
|
|
1591
|
1609
|
|
|
1592
|
|
- let title = makeLabel(meeting.title, size: 26, color: primaryText, weight: .regular, centered: false)
|
|
1593
|
|
- let detail = makeLabel("\(dateFormatter.string(from: meeting.start))\n\(range)", size: 14, color: secondaryText, weight: .regular, centered: false)
|
|
|
1610
|
+ let title = makeLabel(meeting.title, size: 18, color: primaryText, weight: .semibold, centered: false)
|
|
|
1611
|
+ let detail = makeLabel("\(dateFormatter.string(from: meeting.start))\n\(range)", size: 12, color: secondaryText, weight: .regular, centered: false)
|
|
1594
|
1612
|
detail.maximumNumberOfLines = 2
|
|
1595
|
|
- let host = makeLabel("Host: \(meeting.host) • \(meeting.source)", size: 13, color: secondaryText, weight: .regular, centered: false)
|
|
|
1613
|
+ let host = makeLabel("Host: \(meeting.host) • \(meeting.source)", size: 11, color: secondaryText, weight: .regular, centered: false)
|
|
1596
|
1614
|
|
|
1597
|
1615
|
[title, detail, host].forEach {
|
|
1598
|
1616
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -1610,7 +1628,8 @@ class ViewController: NSViewController {
|
|
1610
|
1628
|
|
|
1611
|
1629
|
host.topAnchor.constraint(equalTo: detail.bottomAnchor, constant: 7),
|
|
1612
|
1630
|
host.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
1613
|
|
- host.trailingAnchor.constraint(equalTo: title.trailingAnchor)
|
|
|
1631
|
+ host.trailingAnchor.constraint(equalTo: title.trailingAnchor),
|
|
|
1632
|
+ host.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -12)
|
|
1614
|
1633
|
])
|
|
1615
|
1634
|
|
|
1616
|
1635
|
return card
|
|
|
@@ -1681,6 +1700,73 @@ private final class SearchPillTextField: NSTextField {
|
|
1681
|
1700
|
}
|
|
1682
|
1701
|
}
|
|
1683
|
1702
|
|
|
|
1703
|
+private final class MeetingCardView: NSView {
|
|
|
1704
|
+ private let url: URL?
|
|
|
1705
|
+ private var tracking: NSTrackingArea?
|
|
|
1706
|
+ private var isHovering = false
|
|
|
1707
|
+ private var normalBackgroundColor: CGColor?
|
|
|
1708
|
+ private var normalBorderColor: CGColor?
|
|
|
1709
|
+
|
|
|
1710
|
+ init(url: URL?) {
|
|
|
1711
|
+ self.url = url
|
|
|
1712
|
+ super.init(frame: .zero)
|
|
|
1713
|
+ }
|
|
|
1714
|
+
|
|
|
1715
|
+ required init?(coder: NSCoder) {
|
|
|
1716
|
+ self.url = nil
|
|
|
1717
|
+ super.init(coder: coder)
|
|
|
1718
|
+ }
|
|
|
1719
|
+
|
|
|
1720
|
+ override func updateTrackingAreas() {
|
|
|
1721
|
+ super.updateTrackingAreas()
|
|
|
1722
|
+ if let tracking { removeTrackingArea(tracking) }
|
|
|
1723
|
+ let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect]
|
|
|
1724
|
+ let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil)
|
|
|
1725
|
+ addTrackingArea(area)
|
|
|
1726
|
+ tracking = area
|
|
|
1727
|
+ }
|
|
|
1728
|
+
|
|
|
1729
|
+ override func resetCursorRects() {
|
|
|
1730
|
+ super.resetCursorRects()
|
|
|
1731
|
+ if url != nil {
|
|
|
1732
|
+ addCursorRect(bounds, cursor: .pointingHand)
|
|
|
1733
|
+ }
|
|
|
1734
|
+ }
|
|
|
1735
|
+
|
|
|
1736
|
+ override func mouseEntered(with event: NSEvent) {
|
|
|
1737
|
+ super.mouseEntered(with: event)
|
|
|
1738
|
+ guard url != nil else { return }
|
|
|
1739
|
+ isHovering = true
|
|
|
1740
|
+ applyHoverState()
|
|
|
1741
|
+ }
|
|
|
1742
|
+
|
|
|
1743
|
+ override func mouseExited(with event: NSEvent) {
|
|
|
1744
|
+ super.mouseExited(with: event)
|
|
|
1745
|
+ guard url != nil else { return }
|
|
|
1746
|
+ isHovering = false
|
|
|
1747
|
+ applyHoverState()
|
|
|
1748
|
+ }
|
|
|
1749
|
+
|
|
|
1750
|
+ override func mouseUp(with event: NSEvent) {
|
|
|
1751
|
+ super.mouseUp(with: event)
|
|
|
1752
|
+ guard let url else { return }
|
|
|
1753
|
+ NSWorkspace.shared.open(url)
|
|
|
1754
|
+ }
|
|
|
1755
|
+
|
|
|
1756
|
+ private func applyHoverState() {
|
|
|
1757
|
+ guard let layer else { return }
|
|
|
1758
|
+ if normalBackgroundColor == nil { normalBackgroundColor = layer.backgroundColor }
|
|
|
1759
|
+ if normalBorderColor == nil { normalBorderColor = layer.borderColor }
|
|
|
1760
|
+ if isHovering {
|
|
|
1761
|
+ layer.backgroundColor = NSColor.white.withAlphaComponent(0.075).cgColor
|
|
|
1762
|
+ layer.borderColor = NSColor.white.withAlphaComponent(0.12).cgColor
|
|
|
1763
|
+ } else {
|
|
|
1764
|
+ layer.backgroundColor = normalBackgroundColor
|
|
|
1765
|
+ layer.borderColor = normalBorderColor
|
|
|
1766
|
+ }
|
|
|
1767
|
+ }
|
|
|
1768
|
+}
|
|
|
1769
|
+
|
|
1684
|
1770
|
struct GoogleOAuthTokens: Codable, Equatable {
|
|
1685
|
1771
|
var accessToken: String
|
|
1686
|
1772
|
var refreshToken: String?
|