diff --git a/MeetingBar/Core/Managers/EventManager.swift b/MeetingBar/Core/Managers/EventManager.swift index 614c0ee0..d40adaa7 100644 --- a/MeetingBar/Core/Managers/EventManager.swift +++ b/MeetingBar/Core/Managers/EventManager.swift @@ -104,11 +104,32 @@ public class EventManager: ObservableObject { let dateFrom = Calendar.current.startOfDay(for: Date()) var dateTo: Date + let calendar = Calendar.current switch Defaults[.showEventsForPeriod] { case .today: - dateTo = Calendar.current.date(byAdding: .day, value: 1, to: dateFrom)! + dateTo = calendar.date(byAdding: .day, value: 1, to: dateFrom)! case .today_n_tomorrow: - dateTo = Calendar.current.date(byAdding: .day, value: 2, to: dateFrom)! + dateTo = calendar.date(byAdding: .day, value: 2, to: dateFrom)! + case .fiveDays: + dateTo = calendar.date(byAdding: .day, value: 5, to: dateFrom)! + case .sevenDays: + dateTo = calendar.date(byAdding: .day, value: 7, to: dateFrom)! + case .workWeek: + let weekday = calendar.component(.weekday, from: dateFrom) // 1 = Sunday, 7 = Saturday + if weekday == 1 { + // Sunday: next week Mon–Fri + let nextMonday = calendar.date(byAdding: .day, value: 1, to: dateFrom)! + dateTo = calendar.date(byAdding: .day, value: 5, to: nextMonday)! + } else if weekday == 7 { + // Saturday: next week Mon–Fri + let nextMonday = calendar.date(byAdding: .day, value: 2, to: dateFrom)! + dateTo = calendar.date(byAdding: .day, value: 5, to: nextMonday)! + } else { + // Monday (2) through Friday (6): this week through end of Friday + let daysUntilFriday = 6 - weekday + let friday = calendar.date(byAdding: .day, value: daysUntilFriday, to: dateFrom)! + dateTo = calendar.date(byAdding: .day, value: 1, to: friday)! + } } let selectedCalendars = fromCalendars.filter { Defaults[.selectedCalendarIDs].contains($0.id) } diff --git a/MeetingBar/Core/Models/MBEvent+Helpers.swift b/MeetingBar/Core/Models/MBEvent+Helpers.swift index 7c647ac7..bc63e1b4 100644 --- a/MeetingBar/Core/Models/MBEvent+Helpers.swift +++ b/MeetingBar/Core/Models/MBEvent+Helpers.swift @@ -92,12 +92,30 @@ public extension Array where Element == MBEvent { let startPeriod = Calendar.current.date(byAdding: .minute, value: 1, to: now)! var endPeriod: Date - let todayMidnight = Calendar.current.startOfDay(for: now) + let calendar = Calendar.current + let todayMidnight = calendar.startOfDay(for: now) switch Defaults[.showEventsForPeriod] { case .today: - endPeriod = Calendar.current.date(byAdding: .day, value: 1, to: todayMidnight)! + endPeriod = calendar.date(byAdding: .day, value: 1, to: todayMidnight)! case .today_n_tomorrow: - endPeriod = Calendar.current.date(byAdding: .day, value: 2, to: todayMidnight)! + endPeriod = calendar.date(byAdding: .day, value: 2, to: todayMidnight)! + case .fiveDays: + endPeriod = calendar.date(byAdding: .day, value: 5, to: todayMidnight)! + case .sevenDays: + endPeriod = calendar.date(byAdding: .day, value: 7, to: todayMidnight)! + case .workWeek: + let weekday = calendar.component(.weekday, from: todayMidnight) // 1 = Sunday, 7 = Saturday + if weekday == 1 { + let nextMonday = calendar.date(byAdding: .day, value: 1, to: todayMidnight)! + endPeriod = calendar.date(byAdding: .day, value: 5, to: nextMonday)! + } else if weekday == 7 { + let nextMonday = calendar.date(byAdding: .day, value: 2, to: todayMidnight)! + endPeriod = calendar.date(byAdding: .day, value: 5, to: nextMonday)! + } else { + let daysUntilFriday = 6 - weekday + let friday = calendar.date(byAdding: .day, value: daysUntilFriday, to: todayMidnight)! + endPeriod = calendar.date(byAdding: .day, value: 1, to: friday)! + } } // Filter out passed or not started events diff --git a/MeetingBar/Resources /Localization /en.lproj/Localizable.strings b/MeetingBar/Resources /Localization /en.lproj/Localizable.strings index 5c95734f..9b469301 100644 --- a/MeetingBar/Resources /Localization /en.lproj/Localizable.strings +++ b/MeetingBar/Resources /Localization /en.lproj/Localizable.strings @@ -67,6 +67,9 @@ "preferences_appearance_events_show_events_for_title" = "Show events for"; "preferences_appearance_events_show_events_for_today_value" = "today"; "preferences_appearance_events_show_events_for_today_tomorrow_value" = "today and tomorrow"; +"preferences_appearance_events_show_events_for_5_days_value" = "5 days"; +"preferences_appearance_events_show_events_for_7_days_value" = "7 days"; +"preferences_appearance_events_show_events_for_work_week_value" = "work week (Mon–Fri)"; "preferences_appearance_events_non_all_day_title" = "Other events:"; "preferences_appearance_events_value_inactive_without_meeting_link" = "show those without meeting links as inactive"; "preferences_appearance_events_value_hide_without_meeting_link" = "hide all without meeting links"; diff --git a/MeetingBar/UI/StatusBar/StatusBarItemController.swift b/MeetingBar/UI/StatusBar/StatusBarItemController.swift index f663446a..2ab6f8f5 100644 --- a/MeetingBar/UI/StatusBar/StatusBarItemController.swift +++ b/MeetingBar/UI/StatusBar/StatusBarItemController.swift @@ -321,6 +321,33 @@ final class StatusBarItemController { * ------------------------ */ + /// Returns the start-of-day dates for each day in the given period, from today (or next Monday for work week on weekend). + private static func daysInPeriod(_ period: ShowEventsForPeriod, from today: Date, calendar: Calendar) -> [Date] { + switch period { + case .fiveDays: + return (0 ..< 5).map { calendar.date(byAdding: .day, value: $0, to: today)! } + case .sevenDays: + return (0 ..< 7).map { calendar.date(byAdding: .day, value: $0, to: today)! } + case .workWeek: + let weekday = calendar.component(.weekday, from: today) // 1 = Sunday, 7 = Saturday + if weekday == 1 { + // Sunday: next week Mon–Fri + let nextMonday = calendar.date(byAdding: .day, value: 1, to: today)! + return (0 ..< 5).map { calendar.date(byAdding: .day, value: $0, to: nextMonday)! } + } else if weekday == 7 { + // Saturday: next week Mon–Fri + let nextMonday = calendar.date(byAdding: .day, value: 2, to: today)! + return (0 ..< 5).map { calendar.date(byAdding: .day, value: $0, to: nextMonday)! } + } else { + // Monday (2) through Friday (6): today through Friday + let daysUntilFriday = 6 - weekday + return (0 ... daysUntilFriday).map { calendar.date(byAdding: .day, value: $0, to: today)! } + } + case .today, .today_n_tomorrow: + return [today] + } + } + func updateMenu() { // Don't update the menu while it's open to avoid flickering if statusItem.menu != nil { @@ -368,6 +395,27 @@ final class StatusBarItemController { let tomorrowEvents = events.filter { Calendar.current.isDate($0.startDate, inSameDayAs: tomorrow) } statusItemMenu.items += builder.buildDateSection(date: tomorrow, title: "status_bar_section_tomorrow".loco(), events: tomorrowEvents) + case .fiveDays, .sevenDays, .workWeek: + let calendar = Calendar.current + let dayDates = Self.daysInPeriod(Defaults[.showEventsForPeriod], from: today, calendar: calendar) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "E, d MMM" + dateFormatter.locale = I18N.instance.locale + for (index, dayStart) in dayDates.enumerated() { + if index > 0 { + statusItemMenu.addItem(NSMenuItem.separator()) + } + let dayEvents = events.filter { calendar.isDate($0.startDate, inSameDayAs: dayStart) } + let sectionTitle: String + if calendar.isDate(dayStart, inSameDayAs: today) { + sectionTitle = "status_bar_section_today".loco() + } else if calendar.isDate(dayStart, inSameDayAs: tomorrow) { + sectionTitle = "status_bar_section_tomorrow".loco() + } else { + sectionTitle = dateFormatter.string(from: dayStart) + } + statusItemMenu.items += builder.buildDateSection(date: dayStart, title: sectionTitle, events: dayEvents) + } } } else { let text = "status_bar_empty_calendar_message".loco() diff --git a/MeetingBar/UI/Views/Preferences/AppearanceTab.swift b/MeetingBar/UI/Views/Preferences/AppearanceTab.swift index 38c25073..9c8aed6e 100644 --- a/MeetingBar/UI/Views/Preferences/AppearanceTab.swift +++ b/MeetingBar/UI/Views/Preferences/AppearanceTab.swift @@ -42,6 +42,12 @@ struct EventsSection: View { Text( "preferences_appearance_events_show_events_for_today_tomorrow_value".loco() ).tag(ShowEventsForPeriod.today_n_tomorrow) + Text("preferences_appearance_events_show_events_for_5_days_value".loco()).tag( + ShowEventsForPeriod.fiveDays) + Text("preferences_appearance_events_show_events_for_7_days_value".loco()).tag( + ShowEventsForPeriod.sevenDays) + Text("preferences_appearance_events_show_events_for_work_week_value".loco()).tag( + ShowEventsForPeriod.workWeek) } Picker( diff --git a/MeetingBar/Utilities/Constants.swift b/MeetingBar/Utilities/Constants.swift index e79d6775..2ee1107e 100644 --- a/MeetingBar/Utilities/Constants.swift +++ b/MeetingBar/Utilities/Constants.swift @@ -103,6 +103,9 @@ enum PastEventsAppereance: String, Defaults.Serializable, Codable, CaseIterable enum ShowEventsForPeriod: String, Defaults.Serializable, Codable, CaseIterable { case today case today_n_tomorrow + case fiveDays + case sevenDays + case workWeek } enum OngoingEventVisibility: String, Defaults.Serializable, Codable, CaseIterable { diff --git a/MeetingBarTests/NextEventTests.swift b/MeetingBarTests/NextEventTests.swift index fb9c1f50..0d9c991b 100644 --- a/MeetingBarTests/NextEventTests.swift +++ b/MeetingBarTests/NextEventTests.swift @@ -131,4 +131,43 @@ class NextEventTests: BaseTestCase { let array = [future, running] XCTAssertEqual(array.nextEvent(), running) } + + func test_fiveDaysPeriod_includesEventWithinFiveDays_excludesEventAfter() { + Defaults[.showEventsForPeriod] = .fiveDays + let calendar = Calendar.current + let todayStart = calendar.startOfDay(for: now) + let oneHour: TimeInterval = 3600 + let oneDay: TimeInterval = 24 * 3600 + + let withinFive = makeFakeEvent( + id: "within", + start: todayStart.addingTimeInterval(2 * oneDay + oneHour), + end: todayStart.addingTimeInterval(2 * oneDay + 2 * oneHour), + withLink: true + ) + let afterFive = makeFakeEvent( + id: "after", + start: todayStart.addingTimeInterval(6 * oneDay), + end: todayStart.addingTimeInterval(6 * oneDay + oneHour), + withLink: true + ) + let array = [afterFive, withinFive] + XCTAssertEqual(array.nextEvent(), withinFive) + } + + func test_sevenDaysPeriod_includesEventWithinSevenDays() { + Defaults[.showEventsForPeriod] = .sevenDays + let calendar = Calendar.current + let todayStart = calendar.startOfDay(for: now) + let oneHour: TimeInterval = 3600 + let oneDay: TimeInterval = 24 * 3600 + + let inSixDays = makeFakeEvent( + id: "six", + start: todayStart.addingTimeInterval(6 * oneDay + oneHour), + end: todayStart.addingTimeInterval(6 * oneDay + 2 * oneHour), + withLink: true + ) + XCTAssertEqual([inSixDays].nextEvent(), inSixDays) + } }