From 683c75a8e28123b5d1bd98e618daeddbba0026c6 Mon Sep 17 00:00:00 2001
From: liWanr
Date: Mon, 2 Mar 2026 22:01:52 +0800
Subject: [PATCH] feat: add calendar subtitle modes and lunar support
---
boringNotch/Localizable.xcstrings | 37 +++++++++++++
.../components/Calendar/BoringCalendar.swift | 54 ++++++++++++++++++-
.../Settings/Views/CalendarSettingsView.swift | 19 +++++++
.../extensions/DataTypes+Extensions.swift | 25 +++++++++
boringNotch/models/Constants.swift | 36 +++++++++++++
5 files changed, 170 insertions(+), 1 deletion(-)
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)