diff --git a/boringNotch/Localizable.xcstrings b/boringNotch/Localizable.xcstrings index 1efca2fa9..cd65df705 100644 --- a/boringNotch/Localizable.xcstrings +++ b/boringNotch/Localizable.xcstrings @@ -1805,6 +1805,23 @@ } } } + }, + "Alternative calendars" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternative calendars" + } + } + } + }, + "Always alternate" : { + + }, + "Always default" : { + }, "Always show full event titles" : { "localizations" : { @@ -3412,6 +3429,17 @@ } } }, + "Calendar subtitle display" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calendar subtitle display" + } + } + } + }, "Calendars" : { "localizations" : { "ar" : { @@ -10647,6 +10675,9 @@ } } } + }, + "Lunar" : { + }, "Made with 🫶🏻 by not so boring not.people" : { "localizations" : { @@ -13561,6 +13592,9 @@ } } } + }, + "Now Playing" : { + }, "Open Calendar Settings" : { "localizations" : { @@ -19000,6 +19034,9 @@ } } } + }, + "Tap to switch" : { + }, "Time to Full Charge: %lld min" : { "localizations" : { diff --git a/boringNotch/components/Calendar/BoringCalendar.swift b/boringNotch/components/Calendar/BoringCalendar.swift index 93c67fd11..f1d70d1db 100644 --- a/boringNotch/components/Calendar/BoringCalendar.swift +++ b/boringNotch/components/Calendar/BoringCalendar.swift @@ -233,6 +233,34 @@ struct CalendarView: View { @EnvironmentObject var vm: BoringViewModel @ObservedObject private var calendarManager = CalendarManager.shared @State private var selectedDate = Date() + @Default(.calendarSubtitleDisplayMode) var displayMode + @Default(.alternativeCalendar) var alternativeCalendar + @Default(.calendarIsShowingAlternate) var isShowingAlternate + + private var shouldShowAlternate: Bool { + switch displayMode { + case .alwaysDefault: + return false + case .alwaysAlternate: + return true + case .tapToSwitch: + return isShowingAlternate + } + } + + // A hidden view strictly for sizing + private func subtitleSizingView(date: Date) -> some View { + ZStack { + Text(date.formatted(.dateTime.year())) + switch alternativeCalendar { + case .lunar: + Text(date.formatted(.dateTime.lunar())) + } + } + .font(.title3) + .fontWeight(.light) + .fixedSize() // Ensure it takes up its natural calculated size + } var body: some View { VStack(spacing: 0) { @@ -242,10 +270,34 @@ struct CalendarView: View { .font(.title3) .fontWeight(.semibold) .foregroundColor(.white) - Text(selectedDate.formatted(.dateTime.year())) + + ZStack(alignment: .leading) { + // Invisible layer to reserve space for the widest possible content + subtitleSizingView(date: selectedDate) + .hidden() + + // Visible content + Group { + if shouldShowAlternate { + switch alternativeCalendar { + case .lunar: + Text(selectedDate.formatted(.dateTime.lunar())) + } + } else { + Text(selectedDate.formatted(.dateTime.year())) + } + } .font(.title3) .fontWeight(.light) .foregroundColor(Color(white: 0.65)) + .onTapGesture { + if displayMode == .tapToSwitch { + withAnimation(.smooth(duration: 0.2)) { + isShowingAlternate.toggle() + } + } + } + } } ZStack(alignment: .top) { diff --git a/boringNotch/components/Settings/Views/CalendarSettingsView.swift b/boringNotch/components/Settings/Views/CalendarSettingsView.swift index 62c1d9943..5021171bf 100644 --- a/boringNotch/components/Settings/Views/CalendarSettingsView.swift +++ b/boringNotch/components/Settings/Views/CalendarSettingsView.swift @@ -15,6 +15,8 @@ struct CalendarSettings: View { @Default(.hideCompletedReminders) var hideCompletedReminders @Default(.hideAllDayEvents) var hideAllDayEvents @Default(.autoScrollToNextEvent) var autoScrollToNextEvent + @Default(.calendarSubtitleDisplayMode) var calendarSubtitleDisplayMode + @Default(.alternativeCalendar) var alternativeCalendar var body: some View { Form { @@ -33,6 +35,23 @@ struct CalendarSettings: View { Defaults.Toggle(key: .showFullEventTitles) { Text("Always show full event titles") } + + Picker("Calendar subtitle display", selection: $calendarSubtitleDisplayMode) { + ForEach(CalendarSubtitleDisplayMode.allCases) { item in + Text(item.localizedString).tag(item) + } + } + .pickerStyle(.menu) + + if calendarSubtitleDisplayMode != .alwaysDefault { + Picker("Alternative calendars", selection: $alternativeCalendar) { + ForEach(AlternativeCalendarType.allCases) { item in + Text(item.localizedString).tag(item) + } + } + .pickerStyle(.menu) + } + Section(header: Text("Calendars")) { if calendarManager.calendarAuthorizationStatus != .fullAccess { Text("Calendar access is denied. Please enable it in System Settings.") diff --git a/boringNotch/extensions/DataTypes+Extensions.swift b/boringNotch/extensions/DataTypes+Extensions.swift index 1b662e236..bbaa53eeb 100644 --- a/boringNotch/extensions/DataTypes+Extensions.swift +++ b/boringNotch/extensions/DataTypes+Extensions.swift @@ -42,6 +42,31 @@ extension Date { } } +struct LunarDateStyle: FormatStyle { + func format(_ value: Date) -> String { + let formatter = DateFormatter() + formatter.calendar = Foundation.Calendar(identifier: .chinese) + formatter.locale = Locale(identifier: "zh_CN") + formatter.dateStyle = .long + formatter.timeStyle = .none + + let fullDate = formatter.string(from: value) + + if let yearRange = fullDate.range(of: "年") { + let extracted = String(fullDate[yearRange.upperBound...]) + return extracted + } + + return fullDate + } +} + +extension Date.FormatStyle { + func lunar() -> LunarDateStyle { + LunarDateStyle() + } +} + extension NSSize { var s: String { "\(width.i)×\(height.i)" } diff --git a/boringNotch/models/Constants.swift b/boringNotch/models/Constants.swift index 8960fcfe2..7e20aba4e 100644 --- a/boringNotch/models/Constants.swift +++ b/boringNotch/models/Constants.swift @@ -134,6 +134,38 @@ enum OSDControlSource: String, CaseIterable, Identifiable, Defaults.Serializable } } +enum CalendarSubtitleDisplayMode: String, CaseIterable, Identifiable, Defaults.Serializable { + case alwaysDefault + case alwaysAlternate + case tapToSwitch + + var id: String { self.rawValue } + + var localizedString: String { + switch self { + case .alwaysDefault: + return NSLocalizedString("Always default", comment: "") + case .alwaysAlternate: + return NSLocalizedString("Always alternate", comment: "") + case .tapToSwitch: + return NSLocalizedString("Tap to switch", comment: "") + } + } +} + +enum AlternativeCalendarType: String, CaseIterable, Identifiable, Defaults.Serializable { + case lunar = "Lunar" + + var id: String { self.rawValue } + + var localizedString: String { + switch self { + case .lunar: + return NSLocalizedString("Lunar", comment: "") + } + } +} + extension Defaults.Keys { // MARK: General static let menubarIcon = Key("menubarIcon", default: true) @@ -242,6 +274,10 @@ extension Defaults.Keys { static let showFullEventTitles = Key("showFullEventTitles", default: false) static let autoScrollToNextEvent = Key("autoScrollToNextEvent", default: true) + static let calendarSubtitleDisplayMode = Key("calendarSubtitleDisplayMode", default: .alwaysDefault) + static let alternativeCalendar = Key("alternativeCalendar", default: .lunar) + static let calendarIsShowingAlternate = Key("calendarIsShowingAlternate", default: false) + // MARK: Fullscreen Media Detection static let hideNotchOption = Key("hideNotchOption", default: .nowPlayingOnly)