Explorar o código

Improve Upcoming meetings cards

Make meeting cards full-width with more spacing, reduce panel/card text sizes, and add hover + click-to-open meeting in browser.

Made-with: Cursor
huzaifahayat12 hai 5 días
pai
achega
af60d123d5
Modificáronse 1 ficheiros con 97 adicións e 11 borrados
  1. 97 11
      zoom_app/ViewController.swift

+ 97 - 11
zoom_app/ViewController.swift

@@ -430,6 +430,7 @@ class ViewController: NSViewController {
430
         let end: Date?
430
         let end: Date?
431
         let host: String
431
         let host: String
432
         let source: String
432
         let source: String
433
+        let webURL: URL?
433
     }
434
     }
434
 
435
 
435
     @MainActor
436
     @MainActor
@@ -480,7 +481,12 @@ class ViewController: NSViewController {
480
         emptyMeetingLabel?.isHidden = true
481
         emptyMeetingLabel?.isHidden = true
481
         meetingsStatusLabel?.stringValue = "Upcoming meetings"
482
         meetingsStatusLabel?.stringValue = "Upcoming meetings"
482
         for meeting in ordered {
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
     private func fetchZoomScheduledMeetings(accessToken: String) async throws -> [ScheduledMeeting] {
662
     private func fetchZoomScheduledMeetings(accessToken: String) async throws -> [ScheduledMeeting] {
657
         struct ZoomMeeting: Decodable {
663
         struct ZoomMeeting: Decodable {
664
+            let id: Int?
658
             let topic: String?
665
             let topic: String?
659
             let start_time: String?
666
             let start_time: String?
660
             let duration: Int?
667
             let duration: Int?
661
             let host_id: String?
668
             let host_id: String?
669
+            let join_url: String?
662
         }
670
         }
663
 
671
 
664
         struct ZoomMeetingsPage: Decodable {
672
         struct ZoomMeetingsPage: Decodable {
@@ -677,12 +685,22 @@ class ViewController: NSViewController {
677
                 let start = iso.date(from: startRaw) ?? fallbackISO.date(from: startRaw)
685
                 let start = iso.date(from: startRaw) ?? fallbackISO.date(from: startRaw)
678
                 guard let start else { return nil }
686
                 guard let start else { return nil }
679
                 let end = meeting.duration.map { start.addingTimeInterval(TimeInterval($0 * 60)) }
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
                 return ScheduledMeeting(
697
                 return ScheduledMeeting(
681
                     title: meeting.topic?.isEmpty == false ? meeting.topic! : "Zoom meeting",
698
                     title: meeting.topic?.isEmpty == false ? meeting.topic! : "Zoom meeting",
682
                     start: start,
699
                     start: start,
683
                     end: end,
700
                     end: end,
684
                     host: meeting.host_id ?? "Zoom Host",
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
         let todaysDateFormatter = DateFormatter()
1030
         let todaysDateFormatter = DateFormatter()
1013
         todaysDateFormatter.dateFormat = "EEEE, MMM d"
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
         let meetingsDayNav = NSStackView()
1035
         let meetingsDayNav = NSStackView()
1018
         meetingsDayNav.orientation = .horizontal
1036
         meetingsDayNav.orientation = .horizontal
@@ -1023,7 +1041,7 @@ class ViewController: NSViewController {
1023
         let nextDayButton = makeNavGlyphButton(symbol: "chevron.right", action: #selector(meetingsNextDayTapped), dimension: 14, pointSize: 7, toolTip: "Next day")
1041
         let nextDayButton = makeNavGlyphButton(symbol: "chevron.right", action: #selector(meetingsNextDayTapped), dimension: 14, pointSize: 7, toolTip: "Next day")
1024
         [todayButton, prevDayButton, nextDayButton].forEach { meetingsDayNav.addArrangedSubview($0) }
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
         let meetingsScrollView = NSScrollView()
1045
         let meetingsScrollView = NSScrollView()
1028
         meetingsScrollView.drawsBackground = false
1046
         meetingsScrollView.drawsBackground = false
1029
         meetingsScrollView.hasVerticalScroller = true
1047
         meetingsScrollView.hasVerticalScroller = true
@@ -1033,7 +1051,7 @@ class ViewController: NSViewController {
1033
         let meetingsDocument = NSView()
1051
         let meetingsDocument = NSView()
1034
         let meetingsStack = NSStackView()
1052
         let meetingsStack = NSStackView()
1035
         meetingsStack.orientation = .vertical
1053
         meetingsStack.orientation = .vertical
1036
-        meetingsStack.spacing = 10
1054
+        meetingsStack.spacing = 14
1037
         meetingsStack.alignment = .leading
1055
         meetingsStack.alignment = .leading
1038
         let openRecordings = NSButton(title: "Open recordings", target: nil, action: nil)
1056
         let openRecordings = NSButton(title: "Open recordings", target: nil, action: nil)
1039
         openRecordings.isBordered = false
1057
         openRecordings.isBordered = false
@@ -1572,7 +1590,7 @@ class ViewController: NSViewController {
1572
     }
1590
     }
1573
 
1591
 
1574
     private func makeMeetingRowCard(_ meeting: ScheduledMeeting) -> NSView {
1592
     private func makeMeetingRowCard(_ meeting: ScheduledMeeting) -> NSView {
1575
-        let card = NSView()
1593
+        let card = MeetingCardView(url: meeting.webURL)
1576
         card.wantsLayer = true
1594
         card.wantsLayer = true
1577
         card.layer?.backgroundColor = meetingCardBackground.cgColor
1595
         card.layer?.backgroundColor = meetingCardBackground.cgColor
1578
         card.layer?.cornerRadius = 13
1596
         card.layer?.cornerRadius = 13
@@ -1589,10 +1607,10 @@ class ViewController: NSViewController {
1589
         let endText = meeting.end.map { timeFormatter.string(from: $0) } ?? ""
1607
         let endText = meeting.end.map { timeFormatter.string(from: $0) } ?? ""
1590
         let range = endText.isEmpty ? startText : "\(startText) - \(endText)"
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
         detail.maximumNumberOfLines = 2
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
         [title, detail, host].forEach {
1615
         [title, detail, host].forEach {
1598
             $0.translatesAutoresizingMaskIntoConstraints = false
1616
             $0.translatesAutoresizingMaskIntoConstraints = false
@@ -1610,7 +1628,8 @@ class ViewController: NSViewController {
1610
 
1628
 
1611
             host.topAnchor.constraint(equalTo: detail.bottomAnchor, constant: 7),
1629
             host.topAnchor.constraint(equalTo: detail.bottomAnchor, constant: 7),
1612
             host.leadingAnchor.constraint(equalTo: title.leadingAnchor),
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
         return card
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
 struct GoogleOAuthTokens: Codable, Equatable {
1770
 struct GoogleOAuthTokens: Codable, Equatable {
1685
     var accessToken: String
1771
     var accessToken: String
1686
     var refreshToken: String?
1772
     var refreshToken: String?