|
|
@@ -45,13 +45,18 @@ class ViewController: NSViewController {
|
|
|
private weak var zoomSocialButton: NSButton?
|
|
|
private weak var timeLabel: NSTextField?
|
|
|
private weak var dateLabel: NSTextField?
|
|
|
+ private weak var meetingsDayHeaderLabel: NSTextField?
|
|
|
private weak var emptyMeetingLabel: NSTextField?
|
|
|
private weak var meetingsListStack: NSStackView?
|
|
|
private weak var meetingsStatusLabel: NSTextField?
|
|
|
private weak var meetingsScrollView: NSScrollView?
|
|
|
+ private weak var meetingsPrevDayButton: NSButton?
|
|
|
+ private weak var meetingsNextDayButton: NSButton?
|
|
|
+ private weak var meetingsTodayButton: NSButton?
|
|
|
private weak var homeSearchField: NSTextField?
|
|
|
private weak var homeSearchPill: NSView?
|
|
|
private var allScheduledMeetings: [ScheduledMeeting] = []
|
|
|
+ private var selectedMeetingsDayStart: Date = Calendar.current.startOfDay(for: Date())
|
|
|
private var searchTextObserver: NSObjectProtocol?
|
|
|
private var searchShortcutMonitor: Any?
|
|
|
private var searchOutsideClickMonitor: Any?
|
|
|
@@ -146,6 +151,7 @@ class ViewController: NSViewController {
|
|
|
homeSearchField = nil
|
|
|
homeSearchPill = nil
|
|
|
homeView?.removeFromSuperview()
|
|
|
+ selectedMeetingsDayStart = Calendar.current.startOfDay(for: Date())
|
|
|
homeView = makeHomeView(profile: profile)
|
|
|
if let homeView { attachToRoot(homeView) }
|
|
|
installSearchShortcutMonitor()
|
|
|
@@ -441,7 +447,12 @@ class ViewController: NSViewController {
|
|
|
}
|
|
|
|
|
|
let query = (homeSearchField?.stringValue ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
|
- let source = allScheduledMeetings
|
|
|
+ let calendar = Calendar.current
|
|
|
+ let dayStart = selectedMeetingsDayStart
|
|
|
+ let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) ?? dayStart.addingTimeInterval(60 * 60 * 24)
|
|
|
+ let source = allScheduledMeetings.filter { meeting in
|
|
|
+ meeting.start >= dayStart && meeting.start < dayEnd
|
|
|
+ }
|
|
|
let filtered: [ScheduledMeeting]
|
|
|
if query.isEmpty {
|
|
|
filtered = source
|
|
|
@@ -457,22 +468,75 @@ class ViewController: NSViewController {
|
|
|
if ordered.isEmpty {
|
|
|
emptyMeetingLabel?.isHidden = false
|
|
|
if source.isEmpty {
|
|
|
- meetingsStatusLabel?.stringValue = "No upcoming Zoom meetings found."
|
|
|
- emptyMeetingLabel?.stringValue = "No meetings scheduled for today."
|
|
|
+ meetingsStatusLabel?.stringValue = "Upcoming meetings"
|
|
|
+ emptyMeetingLabel?.stringValue = "No meetings scheduled for \(meetingsDayDisplayName(for: dayStart))."
|
|
|
} else {
|
|
|
- meetingsStatusLabel?.stringValue = "Zoom meetings"
|
|
|
+ meetingsStatusLabel?.stringValue = "Upcoming meetings"
|
|
|
emptyMeetingLabel?.stringValue = "No meetings match your search."
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
emptyMeetingLabel?.isHidden = true
|
|
|
- meetingsStatusLabel?.stringValue = "Zoom meetings"
|
|
|
+ meetingsStatusLabel?.stringValue = "Upcoming meetings"
|
|
|
for meeting in ordered {
|
|
|
stack.addArrangedSubview(makeMeetingRowCard(meeting))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ @MainActor
|
|
|
+ private func setSelectedMeetingsDayStart(_ newDayStart: Date) {
|
|
|
+ selectedMeetingsDayStart = Calendar.current.startOfDay(for: newDayStart)
|
|
|
+ updateMeetingsDayUI()
|
|
|
+ applyFilteredMeetings()
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor
|
|
|
+ private func updateMeetingsDayUI() {
|
|
|
+ let dayStart = selectedMeetingsDayStart
|
|
|
+ let formatter = DateFormatter()
|
|
|
+ formatter.dateFormat = "EEEE, MMM d"
|
|
|
+ meetingsDayHeaderLabel?.stringValue = formatter.string(from: dayStart)
|
|
|
+
|
|
|
+ let isToday = Calendar.current.isDate(dayStart, inSameDayAs: Date())
|
|
|
+ meetingsTodayButton?.isEnabled = isToday == false
|
|
|
+ meetingsTodayButton?.alphaValue = isToday ? 0.55 : 1.0
|
|
|
+ }
|
|
|
+
|
|
|
+ private func meetingsDayDisplayName(for dayStart: Date) -> String {
|
|
|
+ let calendar = Calendar.current
|
|
|
+ if calendar.isDate(dayStart, inSameDayAs: Date()) { return "today" }
|
|
|
+ if let tomorrow = calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: Date())),
|
|
|
+ calendar.isDate(dayStart, inSameDayAs: tomorrow) { return "tomorrow" }
|
|
|
+ if let yesterday = calendar.date(byAdding: .day, value: -1, to: calendar.startOfDay(for: Date())),
|
|
|
+ calendar.isDate(dayStart, inSameDayAs: yesterday) { return "yesterday" }
|
|
|
+ let formatter = DateFormatter()
|
|
|
+ formatter.dateFormat = "EEEE, MMM d"
|
|
|
+ return formatter.string(from: dayStart)
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func meetingsPrevDayTapped() {
|
|
|
+ let calendar = Calendar.current
|
|
|
+ let prev = calendar.date(byAdding: .day, value: -1, to: selectedMeetingsDayStart) ?? selectedMeetingsDayStart.addingTimeInterval(-60 * 60 * 24)
|
|
|
+ Task { @MainActor in
|
|
|
+ self.setSelectedMeetingsDayStart(prev)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func meetingsNextDayTapped() {
|
|
|
+ let calendar = Calendar.current
|
|
|
+ let next = calendar.date(byAdding: .day, value: 1, to: selectedMeetingsDayStart) ?? selectedMeetingsDayStart.addingTimeInterval(60 * 60 * 24)
|
|
|
+ Task { @MainActor in
|
|
|
+ self.setSelectedMeetingsDayStart(next)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func meetingsTodayTapped() {
|
|
|
+ Task { @MainActor in
|
|
|
+ self.setSelectedMeetingsDayStart(Date())
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private func loadScheduledMeetings() async {
|
|
|
if isLoadingMeetings { return }
|
|
|
isLoadingMeetings = true
|
|
|
@@ -949,6 +1013,16 @@ class ViewController: NSViewController {
|
|
|
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 meetingsDayNav = NSStackView()
|
|
|
+ meetingsDayNav.orientation = .horizontal
|
|
|
+ meetingsDayNav.spacing = 4
|
|
|
+ meetingsDayNav.alignment = .centerY
|
|
|
+ let prevDayButton = makeNavGlyphButton(symbol: "chevron.left", action: #selector(meetingsPrevDayTapped), dimension: 14, pointSize: 7, toolTip: "Previous day")
|
|
|
+ let todayButton = makeMeetingsDayChipButton(title: "Today", symbol: "calendar", action: #selector(meetingsTodayTapped))
|
|
|
+ let nextDayButton = makeNavGlyphButton(symbol: "chevron.right", action: #selector(meetingsNextDayTapped), dimension: 14, pointSize: 7, toolTip: "Next day")
|
|
|
+ [prevDayButton, todayButton, nextDayButton].forEach { meetingsDayNav.addArrangedSubview($0) }
|
|
|
+
|
|
|
let noMeeting = makeLabel("No meetings scheduled for today.", size: 18, color: secondaryText, weight: .regular, centered: true)
|
|
|
let meetingsScrollView = NSScrollView()
|
|
|
meetingsScrollView.drawsBackground = false
|
|
|
@@ -989,7 +1063,7 @@ class ViewController: NSViewController {
|
|
|
[searchIcon, searchField, searchHintLabel].forEach {
|
|
|
searchPill.addSubview($0)
|
|
|
}
|
|
|
- [welcome, timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, noMeeting, meetingsScrollView, openRecordings].forEach {
|
|
|
+ [welcome, timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, meetingsDayNav, noMeeting, meetingsScrollView, openRecordings].forEach {
|
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
|
contentColumn.addSubview($0)
|
|
|
}
|
|
|
@@ -1071,7 +1145,9 @@ class ViewController: NSViewController {
|
|
|
panelHeader.topAnchor.constraint(equalTo: panel.topAnchor, constant: 20),
|
|
|
panelHeader.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 16),
|
|
|
meetingsStatus.centerYAnchor.constraint(equalTo: panelHeader.centerYAnchor),
|
|
|
- meetingsStatus.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -16),
|
|
|
+ meetingsDayNav.centerYAnchor.constraint(equalTo: panelHeader.centerYAnchor),
|
|
|
+ meetingsDayNav.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -16),
|
|
|
+ meetingsStatus.trailingAnchor.constraint(equalTo: meetingsDayNav.leadingAnchor, constant: -10),
|
|
|
noMeeting.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
|
|
|
noMeeting.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
|
|
|
noMeeting.centerYAnchor.constraint(equalTo: panel.centerYAnchor),
|
|
|
@@ -1095,11 +1171,17 @@ class ViewController: NSViewController {
|
|
|
|
|
|
timeLabel = timeTitle
|
|
|
dateLabel = dateTitle
|
|
|
+ meetingsDayHeaderLabel = panelHeader
|
|
|
meetingsListStack = meetingsStack
|
|
|
meetingsStatusLabel = meetingsStatus
|
|
|
emptyMeetingLabel = noMeeting
|
|
|
+ meetingsPrevDayButton = prevDayButton
|
|
|
+ meetingsTodayButton = todayButton
|
|
|
+ meetingsNextDayButton = nextDayButton
|
|
|
observeMeetingsScrollEdges(in: meetingsScrollView)
|
|
|
updateClock()
|
|
|
+ updateMeetingsDayUI()
|
|
|
+ applyFilteredMeetings()
|
|
|
|
|
|
homeSearchField = searchField
|
|
|
homeSearchPill = searchPill
|
|
|
@@ -1414,6 +1496,41 @@ class ViewController: NSViewController {
|
|
|
button.heightAnchor.constraint(equalToConstant: dimension).isActive = true
|
|
|
return button
|
|
|
}
|
|
|
+
|
|
|
+ private func makeMeetingsDayChipButton(title: String, symbol: String? = nil, action: Selector?) -> NSButton {
|
|
|
+ let button = NSButton(title: title, target: action == nil ? nil : self, action: action)
|
|
|
+ button.isBordered = false
|
|
|
+ button.bezelStyle = .shadowlessSquare
|
|
|
+ button.focusRingType = .none
|
|
|
+ button.wantsLayer = true
|
|
|
+ button.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.06).cgColor
|
|
|
+ button.layer?.cornerRadius = 10
|
|
|
+ button.layer?.borderWidth = 1
|
|
|
+ button.layer?.borderColor = NSColor.white.withAlphaComponent(0.08).cgColor
|
|
|
+ let tint = NSColor(calibratedWhite: 0.9, alpha: 1)
|
|
|
+ let font = NSFont.systemFont(ofSize: 11, weight: .semibold)
|
|
|
+ button.contentTintColor = tint
|
|
|
+ button.font = font
|
|
|
+ button.attributedTitle = NSAttributedString(string: title, attributes: [
|
|
|
+ .foregroundColor: tint,
|
|
|
+ .font: font
|
|
|
+ ])
|
|
|
+ if let symbol {
|
|
|
+ let symbolConfig = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
|
|
|
+ if let base = NSImage(systemSymbolName: symbol, accessibilityDescription: title),
|
|
|
+ let image = base.withSymbolConfiguration(symbolConfig) {
|
|
|
+ image.isTemplate = true
|
|
|
+ button.image = image
|
|
|
+ button.imagePosition = .imageLeading
|
|
|
+ }
|
|
|
+ button.imageHugsTitle = true
|
|
|
+ button.imageScaling = .scaleNone
|
|
|
+ }
|
|
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ button.heightAnchor.constraint(equalToConstant: 24).isActive = true
|
|
|
+ button.widthAnchor.constraint(greaterThanOrEqualToConstant: 92).isActive = true
|
|
|
+ return button
|
|
|
+ }
|
|
|
|
|
|
private func makeActionTile(title: String, symbol: String, color: NSColor, action: Selector? = nil) -> NSView {
|
|
|
let root = NSView()
|