|
@@ -45,13 +45,18 @@ class ViewController: NSViewController {
|
|
45
|
private weak var zoomSocialButton: NSButton?
|
45
|
private weak var zoomSocialButton: NSButton?
|
|
46
|
private weak var timeLabel: NSTextField?
|
46
|
private weak var timeLabel: NSTextField?
|
|
47
|
private weak var dateLabel: NSTextField?
|
47
|
private weak var dateLabel: NSTextField?
|
|
|
|
48
|
+ private weak var meetingsDayHeaderLabel: NSTextField?
|
|
48
|
private weak var emptyMeetingLabel: NSTextField?
|
49
|
private weak var emptyMeetingLabel: NSTextField?
|
|
49
|
private weak var meetingsListStack: NSStackView?
|
50
|
private weak var meetingsListStack: NSStackView?
|
|
50
|
private weak var meetingsStatusLabel: NSTextField?
|
51
|
private weak var meetingsStatusLabel: NSTextField?
|
|
51
|
private weak var meetingsScrollView: NSScrollView?
|
52
|
private weak var meetingsScrollView: NSScrollView?
|
|
|
|
53
|
+ private weak var meetingsPrevDayButton: NSButton?
|
|
|
|
54
|
+ private weak var meetingsNextDayButton: NSButton?
|
|
|
|
55
|
+ private weak var meetingsTodayButton: NSButton?
|
|
52
|
private weak var homeSearchField: NSTextField?
|
56
|
private weak var homeSearchField: NSTextField?
|
|
53
|
private weak var homeSearchPill: NSView?
|
57
|
private weak var homeSearchPill: NSView?
|
|
54
|
private var allScheduledMeetings: [ScheduledMeeting] = []
|
58
|
private var allScheduledMeetings: [ScheduledMeeting] = []
|
|
|
|
59
|
+ private var selectedMeetingsDayStart: Date = Calendar.current.startOfDay(for: Date())
|
|
55
|
private var searchTextObserver: NSObjectProtocol?
|
60
|
private var searchTextObserver: NSObjectProtocol?
|
|
56
|
private var searchShortcutMonitor: Any?
|
61
|
private var searchShortcutMonitor: Any?
|
|
57
|
private var searchOutsideClickMonitor: Any?
|
62
|
private var searchOutsideClickMonitor: Any?
|
|
@@ -146,6 +151,7 @@ class ViewController: NSViewController {
|
|
146
|
homeSearchField = nil
|
151
|
homeSearchField = nil
|
|
147
|
homeSearchPill = nil
|
152
|
homeSearchPill = nil
|
|
148
|
homeView?.removeFromSuperview()
|
153
|
homeView?.removeFromSuperview()
|
|
|
|
154
|
+ selectedMeetingsDayStart = Calendar.current.startOfDay(for: Date())
|
|
149
|
homeView = makeHomeView(profile: profile)
|
155
|
homeView = makeHomeView(profile: profile)
|
|
150
|
if let homeView { attachToRoot(homeView) }
|
156
|
if let homeView { attachToRoot(homeView) }
|
|
151
|
installSearchShortcutMonitor()
|
157
|
installSearchShortcutMonitor()
|
|
@@ -441,7 +447,12 @@ class ViewController: NSViewController {
|
|
441
|
}
|
447
|
}
|
|
442
|
|
448
|
|
|
443
|
let query = (homeSearchField?.stringValue ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
449
|
let query = (homeSearchField?.stringValue ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
444
|
- let source = allScheduledMeetings
|
|
|
|
|
|
450
|
+ let calendar = Calendar.current
|
|
|
|
451
|
+ let dayStart = selectedMeetingsDayStart
|
|
|
|
452
|
+ let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) ?? dayStart.addingTimeInterval(60 * 60 * 24)
|
|
|
|
453
|
+ let source = allScheduledMeetings.filter { meeting in
|
|
|
|
454
|
+ meeting.start >= dayStart && meeting.start < dayEnd
|
|
|
|
455
|
+ }
|
|
445
|
let filtered: [ScheduledMeeting]
|
456
|
let filtered: [ScheduledMeeting]
|
|
446
|
if query.isEmpty {
|
457
|
if query.isEmpty {
|
|
447
|
filtered = source
|
458
|
filtered = source
|
|
@@ -457,22 +468,75 @@ class ViewController: NSViewController {
|
|
457
|
if ordered.isEmpty {
|
468
|
if ordered.isEmpty {
|
|
458
|
emptyMeetingLabel?.isHidden = false
|
469
|
emptyMeetingLabel?.isHidden = false
|
|
459
|
if source.isEmpty {
|
470
|
if source.isEmpty {
|
|
460
|
- meetingsStatusLabel?.stringValue = "No upcoming Zoom meetings found."
|
|
|
|
461
|
- emptyMeetingLabel?.stringValue = "No meetings scheduled for today."
|
|
|
|
|
|
471
|
+ meetingsStatusLabel?.stringValue = "Upcoming meetings"
|
|
|
|
472
|
+ emptyMeetingLabel?.stringValue = "No meetings scheduled for \(meetingsDayDisplayName(for: dayStart))."
|
|
462
|
} else {
|
473
|
} else {
|
|
463
|
- meetingsStatusLabel?.stringValue = "Zoom meetings"
|
|
|
|
|
|
474
|
+ meetingsStatusLabel?.stringValue = "Upcoming meetings"
|
|
464
|
emptyMeetingLabel?.stringValue = "No meetings match your search."
|
475
|
emptyMeetingLabel?.stringValue = "No meetings match your search."
|
|
465
|
}
|
476
|
}
|
|
466
|
return
|
477
|
return
|
|
467
|
}
|
478
|
}
|
|
468
|
|
479
|
|
|
469
|
emptyMeetingLabel?.isHidden = true
|
480
|
emptyMeetingLabel?.isHidden = true
|
|
470
|
- meetingsStatusLabel?.stringValue = "Zoom meetings"
|
|
|
|
|
|
481
|
+ meetingsStatusLabel?.stringValue = "Upcoming meetings"
|
|
471
|
for meeting in ordered {
|
482
|
for meeting in ordered {
|
|
472
|
stack.addArrangedSubview(makeMeetingRowCard(meeting))
|
483
|
stack.addArrangedSubview(makeMeetingRowCard(meeting))
|
|
473
|
}
|
484
|
}
|
|
474
|
}
|
485
|
}
|
|
475
|
|
486
|
|
|
|
|
487
|
+ @MainActor
|
|
|
|
488
|
+ private func setSelectedMeetingsDayStart(_ newDayStart: Date) {
|
|
|
|
489
|
+ selectedMeetingsDayStart = Calendar.current.startOfDay(for: newDayStart)
|
|
|
|
490
|
+ updateMeetingsDayUI()
|
|
|
|
491
|
+ applyFilteredMeetings()
|
|
|
|
492
|
+ }
|
|
|
|
493
|
+
|
|
|
|
494
|
+ @MainActor
|
|
|
|
495
|
+ private func updateMeetingsDayUI() {
|
|
|
|
496
|
+ let dayStart = selectedMeetingsDayStart
|
|
|
|
497
|
+ let formatter = DateFormatter()
|
|
|
|
498
|
+ formatter.dateFormat = "EEEE, MMM d"
|
|
|
|
499
|
+ meetingsDayHeaderLabel?.stringValue = formatter.string(from: dayStart)
|
|
|
|
500
|
+
|
|
|
|
501
|
+ let isToday = Calendar.current.isDate(dayStart, inSameDayAs: Date())
|
|
|
|
502
|
+ meetingsTodayButton?.isEnabled = isToday == false
|
|
|
|
503
|
+ meetingsTodayButton?.alphaValue = isToday ? 0.55 : 1.0
|
|
|
|
504
|
+ }
|
|
|
|
505
|
+
|
|
|
|
506
|
+ private func meetingsDayDisplayName(for dayStart: Date) -> String {
|
|
|
|
507
|
+ let calendar = Calendar.current
|
|
|
|
508
|
+ if calendar.isDate(dayStart, inSameDayAs: Date()) { return "today" }
|
|
|
|
509
|
+ if let tomorrow = calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: Date())),
|
|
|
|
510
|
+ calendar.isDate(dayStart, inSameDayAs: tomorrow) { return "tomorrow" }
|
|
|
|
511
|
+ if let yesterday = calendar.date(byAdding: .day, value: -1, to: calendar.startOfDay(for: Date())),
|
|
|
|
512
|
+ calendar.isDate(dayStart, inSameDayAs: yesterday) { return "yesterday" }
|
|
|
|
513
|
+ let formatter = DateFormatter()
|
|
|
|
514
|
+ formatter.dateFormat = "EEEE, MMM d"
|
|
|
|
515
|
+ return formatter.string(from: dayStart)
|
|
|
|
516
|
+ }
|
|
|
|
517
|
+
|
|
|
|
518
|
+ @objc private func meetingsPrevDayTapped() {
|
|
|
|
519
|
+ let calendar = Calendar.current
|
|
|
|
520
|
+ let prev = calendar.date(byAdding: .day, value: -1, to: selectedMeetingsDayStart) ?? selectedMeetingsDayStart.addingTimeInterval(-60 * 60 * 24)
|
|
|
|
521
|
+ Task { @MainActor in
|
|
|
|
522
|
+ self.setSelectedMeetingsDayStart(prev)
|
|
|
|
523
|
+ }
|
|
|
|
524
|
+ }
|
|
|
|
525
|
+
|
|
|
|
526
|
+ @objc private func meetingsNextDayTapped() {
|
|
|
|
527
|
+ let calendar = Calendar.current
|
|
|
|
528
|
+ let next = calendar.date(byAdding: .day, value: 1, to: selectedMeetingsDayStart) ?? selectedMeetingsDayStart.addingTimeInterval(60 * 60 * 24)
|
|
|
|
529
|
+ Task { @MainActor in
|
|
|
|
530
|
+ self.setSelectedMeetingsDayStart(next)
|
|
|
|
531
|
+ }
|
|
|
|
532
|
+ }
|
|
|
|
533
|
+
|
|
|
|
534
|
+ @objc private func meetingsTodayTapped() {
|
|
|
|
535
|
+ Task { @MainActor in
|
|
|
|
536
|
+ self.setSelectedMeetingsDayStart(Date())
|
|
|
|
537
|
+ }
|
|
|
|
538
|
+ }
|
|
|
|
539
|
+
|
|
476
|
private func loadScheduledMeetings() async {
|
540
|
private func loadScheduledMeetings() async {
|
|
477
|
if isLoadingMeetings { return }
|
541
|
if isLoadingMeetings { return }
|
|
478
|
isLoadingMeetings = true
|
542
|
isLoadingMeetings = true
|
|
@@ -949,6 +1013,16 @@ class ViewController: NSViewController {
|
|
949
|
todaysDateFormatter.dateFormat = "EEEE, MMM d"
|
1013
|
todaysDateFormatter.dateFormat = "EEEE, MMM d"
|
|
950
|
let panelHeader = makeLabel(todaysDateFormatter.string(from: Date()), size: 21, color: primaryText, weight: .semibold, centered: false)
|
1014
|
let panelHeader = makeLabel(todaysDateFormatter.string(from: Date()), size: 21, color: primaryText, weight: .semibold, centered: false)
|
|
951
|
let meetingsStatus = makeLabel("Upcoming meetings", size: 12, color: secondaryText, weight: .medium, centered: false)
|
1015
|
let meetingsStatus = makeLabel("Upcoming meetings", size: 12, color: secondaryText, weight: .medium, centered: false)
|
|
|
|
1016
|
+
|
|
|
|
1017
|
+ let meetingsDayNav = NSStackView()
|
|
|
|
1018
|
+ meetingsDayNav.orientation = .horizontal
|
|
|
|
1019
|
+ meetingsDayNav.spacing = 4
|
|
|
|
1020
|
+ meetingsDayNav.alignment = .centerY
|
|
|
|
1021
|
+ let prevDayButton = makeNavGlyphButton(symbol: "chevron.left", action: #selector(meetingsPrevDayTapped), dimension: 14, pointSize: 7, toolTip: "Previous day")
|
|
|
|
1022
|
+ let todayButton = makeMeetingsDayChipButton(title: "Today", symbol: "calendar", action: #selector(meetingsTodayTapped))
|
|
|
|
1023
|
+ let nextDayButton = makeNavGlyphButton(symbol: "chevron.right", action: #selector(meetingsNextDayTapped), dimension: 14, pointSize: 7, toolTip: "Next day")
|
|
|
|
1024
|
+ [prevDayButton, todayButton, nextDayButton].forEach { meetingsDayNav.addArrangedSubview($0) }
|
|
|
|
1025
|
+
|
|
952
|
let noMeeting = makeLabel("No meetings scheduled for today.", size: 18, color: secondaryText, weight: .regular, centered: true)
|
1026
|
let noMeeting = makeLabel("No meetings scheduled for today.", size: 18, color: secondaryText, weight: .regular, centered: true)
|
|
953
|
let meetingsScrollView = NSScrollView()
|
1027
|
let meetingsScrollView = NSScrollView()
|
|
954
|
meetingsScrollView.drawsBackground = false
|
1028
|
meetingsScrollView.drawsBackground = false
|
|
@@ -989,7 +1063,7 @@ class ViewController: NSViewController {
|
|
989
|
[searchIcon, searchField, searchHintLabel].forEach {
|
1063
|
[searchIcon, searchField, searchHintLabel].forEach {
|
|
990
|
searchPill.addSubview($0)
|
1064
|
searchPill.addSubview($0)
|
|
991
|
}
|
1065
|
}
|
|
992
|
- [welcome, timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, noMeeting, meetingsScrollView, openRecordings].forEach {
|
|
|
|
|
|
1066
|
+ [welcome, timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, meetingsDayNav, noMeeting, meetingsScrollView, openRecordings].forEach {
|
|
993
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
1067
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
994
|
contentColumn.addSubview($0)
|
1068
|
contentColumn.addSubview($0)
|
|
995
|
}
|
1069
|
}
|
|
@@ -1071,7 +1145,9 @@ class ViewController: NSViewController {
|
|
1071
|
panelHeader.topAnchor.constraint(equalTo: panel.topAnchor, constant: 20),
|
1145
|
panelHeader.topAnchor.constraint(equalTo: panel.topAnchor, constant: 20),
|
|
1072
|
panelHeader.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 16),
|
1146
|
panelHeader.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 16),
|
|
1073
|
meetingsStatus.centerYAnchor.constraint(equalTo: panelHeader.centerYAnchor),
|
1147
|
meetingsStatus.centerYAnchor.constraint(equalTo: panelHeader.centerYAnchor),
|
|
1074
|
- meetingsStatus.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -16),
|
|
|
|
|
|
1148
|
+ meetingsDayNav.centerYAnchor.constraint(equalTo: panelHeader.centerYAnchor),
|
|
|
|
1149
|
+ meetingsDayNav.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -16),
|
|
|
|
1150
|
+ meetingsStatus.trailingAnchor.constraint(equalTo: meetingsDayNav.leadingAnchor, constant: -10),
|
|
1075
|
noMeeting.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
|
1151
|
noMeeting.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
|
|
1076
|
noMeeting.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
|
1152
|
noMeeting.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
|
|
1077
|
noMeeting.centerYAnchor.constraint(equalTo: panel.centerYAnchor),
|
1153
|
noMeeting.centerYAnchor.constraint(equalTo: panel.centerYAnchor),
|
|
@@ -1095,11 +1171,17 @@ class ViewController: NSViewController {
|
|
1095
|
|
1171
|
|
|
1096
|
timeLabel = timeTitle
|
1172
|
timeLabel = timeTitle
|
|
1097
|
dateLabel = dateTitle
|
1173
|
dateLabel = dateTitle
|
|
|
|
1174
|
+ meetingsDayHeaderLabel = panelHeader
|
|
1098
|
meetingsListStack = meetingsStack
|
1175
|
meetingsListStack = meetingsStack
|
|
1099
|
meetingsStatusLabel = meetingsStatus
|
1176
|
meetingsStatusLabel = meetingsStatus
|
|
1100
|
emptyMeetingLabel = noMeeting
|
1177
|
emptyMeetingLabel = noMeeting
|
|
|
|
1178
|
+ meetingsPrevDayButton = prevDayButton
|
|
|
|
1179
|
+ meetingsTodayButton = todayButton
|
|
|
|
1180
|
+ meetingsNextDayButton = nextDayButton
|
|
1101
|
observeMeetingsScrollEdges(in: meetingsScrollView)
|
1181
|
observeMeetingsScrollEdges(in: meetingsScrollView)
|
|
1102
|
updateClock()
|
1182
|
updateClock()
|
|
|
|
1183
|
+ updateMeetingsDayUI()
|
|
|
|
1184
|
+ applyFilteredMeetings()
|
|
1103
|
|
1185
|
|
|
1104
|
homeSearchField = searchField
|
1186
|
homeSearchField = searchField
|
|
1105
|
homeSearchPill = searchPill
|
1187
|
homeSearchPill = searchPill
|
|
@@ -1414,6 +1496,41 @@ class ViewController: NSViewController {
|
|
1414
|
button.heightAnchor.constraint(equalToConstant: dimension).isActive = true
|
1496
|
button.heightAnchor.constraint(equalToConstant: dimension).isActive = true
|
|
1415
|
return button
|
1497
|
return button
|
|
1416
|
}
|
1498
|
}
|
|
|
|
1499
|
+
|
|
|
|
1500
|
+ private func makeMeetingsDayChipButton(title: String, symbol: String? = nil, action: Selector?) -> NSButton {
|
|
|
|
1501
|
+ let button = NSButton(title: title, target: action == nil ? nil : self, action: action)
|
|
|
|
1502
|
+ button.isBordered = false
|
|
|
|
1503
|
+ button.bezelStyle = .shadowlessSquare
|
|
|
|
1504
|
+ button.focusRingType = .none
|
|
|
|
1505
|
+ button.wantsLayer = true
|
|
|
|
1506
|
+ button.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.06).cgColor
|
|
|
|
1507
|
+ button.layer?.cornerRadius = 10
|
|
|
|
1508
|
+ button.layer?.borderWidth = 1
|
|
|
|
1509
|
+ button.layer?.borderColor = NSColor.white.withAlphaComponent(0.08).cgColor
|
|
|
|
1510
|
+ let tint = NSColor(calibratedWhite: 0.9, alpha: 1)
|
|
|
|
1511
|
+ let font = NSFont.systemFont(ofSize: 11, weight: .semibold)
|
|
|
|
1512
|
+ button.contentTintColor = tint
|
|
|
|
1513
|
+ button.font = font
|
|
|
|
1514
|
+ button.attributedTitle = NSAttributedString(string: title, attributes: [
|
|
|
|
1515
|
+ .foregroundColor: tint,
|
|
|
|
1516
|
+ .font: font
|
|
|
|
1517
|
+ ])
|
|
|
|
1518
|
+ if let symbol {
|
|
|
|
1519
|
+ let symbolConfig = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
|
|
|
|
1520
|
+ if let base = NSImage(systemSymbolName: symbol, accessibilityDescription: title),
|
|
|
|
1521
|
+ let image = base.withSymbolConfiguration(symbolConfig) {
|
|
|
|
1522
|
+ image.isTemplate = true
|
|
|
|
1523
|
+ button.image = image
|
|
|
|
1524
|
+ button.imagePosition = .imageLeading
|
|
|
|
1525
|
+ }
|
|
|
|
1526
|
+ button.imageHugsTitle = true
|
|
|
|
1527
|
+ button.imageScaling = .scaleNone
|
|
|
|
1528
|
+ }
|
|
|
|
1529
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
1530
|
+ button.heightAnchor.constraint(equalToConstant: 24).isActive = true
|
|
|
|
1531
|
+ button.widthAnchor.constraint(greaterThanOrEqualToConstant: 92).isActive = true
|
|
|
|
1532
|
+ return button
|
|
|
|
1533
|
+ }
|
|
1417
|
|
1534
|
|
|
1418
|
private func makeActionTile(title: String, symbol: String, color: NSColor, action: Selector? = nil) -> NSView {
|
1535
|
private func makeActionTile(title: String, symbol: String, color: NSColor, action: Selector? = nil) -> NSView {
|
|
1419
|
let root = NSView()
|
1536
|
let root = NSView()
|