|
|
@@ -430,6 +430,7 @@ class ViewController: NSViewController {
|
|
|
let end: Date?
|
|
|
let host: String
|
|
|
let source: String
|
|
|
+ let webURL: URL?
|
|
|
}
|
|
|
|
|
|
@MainActor
|
|
|
@@ -480,7 +481,12 @@ class ViewController: NSViewController {
|
|
|
emptyMeetingLabel?.isHidden = true
|
|
|
meetingsStatusLabel?.stringValue = "Upcoming meetings"
|
|
|
for meeting in ordered {
|
|
|
- stack.addArrangedSubview(makeMeetingRowCard(meeting))
|
|
|
+ let card = makeMeetingRowCard(meeting)
|
|
|
+ stack.addArrangedSubview(card)
|
|
|
+ // Make each meeting card span the full list width (like Zoom).
|
|
|
+ if card.constraints.contains(where: { $0.firstAttribute == .width }) == false {
|
|
|
+ card.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -655,10 +661,12 @@ class ViewController: NSViewController {
|
|
|
|
|
|
private func fetchZoomScheduledMeetings(accessToken: String) async throws -> [ScheduledMeeting] {
|
|
|
struct ZoomMeeting: Decodable {
|
|
|
+ let id: Int?
|
|
|
let topic: String?
|
|
|
let start_time: String?
|
|
|
let duration: Int?
|
|
|
let host_id: String?
|
|
|
+ let join_url: String?
|
|
|
}
|
|
|
|
|
|
struct ZoomMeetingsPage: Decodable {
|
|
|
@@ -677,12 +685,22 @@ class ViewController: NSViewController {
|
|
|
let start = iso.date(from: startRaw) ?? fallbackISO.date(from: startRaw)
|
|
|
guard let start else { return nil }
|
|
|
let end = meeting.duration.map { start.addingTimeInterval(TimeInterval($0 * 60)) }
|
|
|
+ let webURL: URL? = {
|
|
|
+ if let join = meeting.join_url, let url = URL(string: join), url.scheme != nil {
|
|
|
+ return url
|
|
|
+ }
|
|
|
+ if let id = meeting.id {
|
|
|
+ return URL(string: "https://zoom.us/j/\(id)")
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ }()
|
|
|
return ScheduledMeeting(
|
|
|
title: meeting.topic?.isEmpty == false ? meeting.topic! : "Zoom meeting",
|
|
|
start: start,
|
|
|
end: end,
|
|
|
host: meeting.host_id ?? "Zoom Host",
|
|
|
- source: "Zoom"
|
|
|
+ source: "Zoom",
|
|
|
+ webURL: webURL
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
@@ -1011,8 +1029,8 @@ class ViewController: NSViewController {
|
|
|
|
|
|
let todaysDateFormatter = DateFormatter()
|
|
|
todaysDateFormatter.dateFormat = "EEEE, MMM d"
|
|
|
- let panelHeader = makeLabel(todaysDateFormatter.string(from: Date()), size: 21, color: primaryText, weight: .semibold, centered: false)
|
|
|
- let meetingsStatus = makeLabel("Upcoming meetings", size: 12, color: secondaryText, weight: .medium, centered: false)
|
|
|
+ let panelHeader = makeLabel(todaysDateFormatter.string(from: Date()), size: 18, color: primaryText, weight: .semibold, centered: false)
|
|
|
+ let meetingsStatus = makeLabel("Upcoming meetings", size: 11, color: secondaryText, weight: .medium, centered: false)
|
|
|
|
|
|
let meetingsDayNav = NSStackView()
|
|
|
meetingsDayNav.orientation = .horizontal
|
|
|
@@ -1023,7 +1041,7 @@ class ViewController: NSViewController {
|
|
|
let nextDayButton = makeNavGlyphButton(symbol: "chevron.right", action: #selector(meetingsNextDayTapped), dimension: 14, pointSize: 7, toolTip: "Next day")
|
|
|
[todayButton, prevDayButton, nextDayButton].forEach { meetingsDayNav.addArrangedSubview($0) }
|
|
|
|
|
|
- let noMeeting = makeLabel("No meetings scheduled for today.", size: 18, color: secondaryText, weight: .regular, centered: true)
|
|
|
+ let noMeeting = makeLabel("No meetings scheduled for today.", size: 15, color: secondaryText, weight: .regular, centered: true)
|
|
|
let meetingsScrollView = NSScrollView()
|
|
|
meetingsScrollView.drawsBackground = false
|
|
|
meetingsScrollView.hasVerticalScroller = true
|
|
|
@@ -1033,7 +1051,7 @@ class ViewController: NSViewController {
|
|
|
let meetingsDocument = NSView()
|
|
|
let meetingsStack = NSStackView()
|
|
|
meetingsStack.orientation = .vertical
|
|
|
- meetingsStack.spacing = 10
|
|
|
+ meetingsStack.spacing = 14
|
|
|
meetingsStack.alignment = .leading
|
|
|
let openRecordings = NSButton(title: "Open recordings", target: nil, action: nil)
|
|
|
openRecordings.isBordered = false
|
|
|
@@ -1572,7 +1590,7 @@ class ViewController: NSViewController {
|
|
|
}
|
|
|
|
|
|
private func makeMeetingRowCard(_ meeting: ScheduledMeeting) -> NSView {
|
|
|
- let card = NSView()
|
|
|
+ let card = MeetingCardView(url: meeting.webURL)
|
|
|
card.wantsLayer = true
|
|
|
card.layer?.backgroundColor = meetingCardBackground.cgColor
|
|
|
card.layer?.cornerRadius = 13
|
|
|
@@ -1589,10 +1607,10 @@ class ViewController: NSViewController {
|
|
|
let endText = meeting.end.map { timeFormatter.string(from: $0) } ?? ""
|
|
|
let range = endText.isEmpty ? startText : "\(startText) - \(endText)"
|
|
|
|
|
|
- let title = makeLabel(meeting.title, size: 26, color: primaryText, weight: .regular, centered: false)
|
|
|
- let detail = makeLabel("\(dateFormatter.string(from: meeting.start))\n\(range)", size: 14, color: secondaryText, weight: .regular, centered: false)
|
|
|
+ let title = makeLabel(meeting.title, size: 18, color: primaryText, weight: .semibold, centered: false)
|
|
|
+ let detail = makeLabel("\(dateFormatter.string(from: meeting.start))\n\(range)", size: 12, color: secondaryText, weight: .regular, centered: false)
|
|
|
detail.maximumNumberOfLines = 2
|
|
|
- let host = makeLabel("Host: \(meeting.host) • \(meeting.source)", size: 13, color: secondaryText, weight: .regular, centered: false)
|
|
|
+ let host = makeLabel("Host: \(meeting.host) • \(meeting.source)", size: 11, color: secondaryText, weight: .regular, centered: false)
|
|
|
|
|
|
[title, detail, host].forEach {
|
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -1610,7 +1628,8 @@ class ViewController: NSViewController {
|
|
|
|
|
|
host.topAnchor.constraint(equalTo: detail.bottomAnchor, constant: 7),
|
|
|
host.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
|
- host.trailingAnchor.constraint(equalTo: title.trailingAnchor)
|
|
|
+ host.trailingAnchor.constraint(equalTo: title.trailingAnchor),
|
|
|
+ host.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -12)
|
|
|
])
|
|
|
|
|
|
return card
|
|
|
@@ -1681,6 +1700,73 @@ private final class SearchPillTextField: NSTextField {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+private final class MeetingCardView: NSView {
|
|
|
+ private let url: URL?
|
|
|
+ private var tracking: NSTrackingArea?
|
|
|
+ private var isHovering = false
|
|
|
+ private var normalBackgroundColor: CGColor?
|
|
|
+ private var normalBorderColor: CGColor?
|
|
|
+
|
|
|
+ init(url: URL?) {
|
|
|
+ self.url = url
|
|
|
+ super.init(frame: .zero)
|
|
|
+ }
|
|
|
+
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ self.url = nil
|
|
|
+ super.init(coder: coder)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func updateTrackingAreas() {
|
|
|
+ super.updateTrackingAreas()
|
|
|
+ if let tracking { removeTrackingArea(tracking) }
|
|
|
+ let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect]
|
|
|
+ let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil)
|
|
|
+ addTrackingArea(area)
|
|
|
+ tracking = area
|
|
|
+ }
|
|
|
+
|
|
|
+ override func resetCursorRects() {
|
|
|
+ super.resetCursorRects()
|
|
|
+ if url != nil {
|
|
|
+ addCursorRect(bounds, cursor: .pointingHand)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseEntered(with event: NSEvent) {
|
|
|
+ super.mouseEntered(with: event)
|
|
|
+ guard url != nil else { return }
|
|
|
+ isHovering = true
|
|
|
+ applyHoverState()
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseExited(with event: NSEvent) {
|
|
|
+ super.mouseExited(with: event)
|
|
|
+ guard url != nil else { return }
|
|
|
+ isHovering = false
|
|
|
+ applyHoverState()
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseUp(with event: NSEvent) {
|
|
|
+ super.mouseUp(with: event)
|
|
|
+ guard let url else { return }
|
|
|
+ NSWorkspace.shared.open(url)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func applyHoverState() {
|
|
|
+ guard let layer else { return }
|
|
|
+ if normalBackgroundColor == nil { normalBackgroundColor = layer.backgroundColor }
|
|
|
+ if normalBorderColor == nil { normalBorderColor = layer.borderColor }
|
|
|
+ if isHovering {
|
|
|
+ layer.backgroundColor = NSColor.white.withAlphaComponent(0.075).cgColor
|
|
|
+ layer.borderColor = NSColor.white.withAlphaComponent(0.12).cgColor
|
|
|
+ } else {
|
|
|
+ layer.backgroundColor = normalBackgroundColor
|
|
|
+ layer.borderColor = normalBorderColor
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
struct GoogleOAuthTokens: Codable, Equatable {
|
|
|
var accessToken: String
|
|
|
var refreshToken: String?
|