fix(calendar): 联动农历设置并优化菜单栏日期字体
This commit is contained in:
1 parent
0b63a4ed04
commit
c0976ef649
15 files changed
+58
-36
No files matched your search
+3
-1
@@ -192,7 +192,9 @@ Calendar enablement, optional features and week start persist through `AppSettin
|
||||
optional fields in `SettingsDocument`. Week start initially uses `Calendar.current.firstWeekday`,
|
||||
then persists independently of later system changes; all seven weekdays are supported by the
|
||||
settings picker, backups, month grids and widgets. Menu-bar lunar modes remain available in
|
||||
every app language and use localized lunar dates. Older backups preserve those settings. Additional
|
||||
every app language and use localized lunar dates when lunar display is enabled. Disabling
|
||||
lunar display hides its menu-bar presets and text-style controls, renders the saved lunar
|
||||
preset as date and weekday, and stops applying lunar text styles without overwriting them. Older backups preserve those settings. Additional
|
||||
calendars default off and appear only in selected-day details. There is no event access or
|
||||
network request at runtime. Use `--snapshot <directory> --calendar-only` for deterministic
|
||||
light/dark calendar screenshots without starting unrelated samplers or scans.
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ GitHub Actions 只在分支 push 且 `server/**` 发生变化时构建并发布
|
||||
构建固定使用 `arm64`,仅支持 Apple Silicon Mac。
|
||||
有 Developer ID Application 证书时,Taskfile 会自动使用钥匙串中第一个证书的唯一指纹;否则由构建脚本逐层补齐 ad-hoc 签名与 entitlement。临时签名没有团队身份,不能加载启用团队验证的 Sparkle 动态库,因此仅此本地/CI 路径关闭 Hardened Runtime;正式 Developer ID 构建仍强制启用,且临时签名构建不能在线安装或操作特权辅助工具。
|
||||
桌面小组件与主应用通过 `group.work.12306.xstats` App Group 共享只读展示摘要。
|
||||
菜单栏日历在所有应用语言下均提供“日期与星期”“日期与农历”“农历”“自定义”显示方式;日期与农历按应用语言呈现,切换语言保留用户选择。自定义可自由组合 y/M/d 字段与分隔符,年份可省略,输入时显示预览并提示无效格式,不影响月历与单日详情的星期显示;支持农历字号与对比度、可选悬停展开,按分钟刷新,休眠或关闭功能后停止。日程和到期提醒默认关闭,通过明确的授权按钮请求 EventKit 权限,只在日历面板可见时查询当前 42 格日期范围;只读展示,不修改日程,也不向 Widget、设置备份或服务端传递内容。列表 ID 仅保存在本机,显示偏好参与设置备份。节假日覆盖年份来自内置 Tyme 数据并随应用更新;“假期倒计时与请假建议”仅在开启“中国大陆节假日与调休”后显示在该开关下方;关闭节假日时隐藏子选项并保留原选择。假期倒计时与最多 3 天的请假建议只基于已公布的中国调休与双休安排,跨到未知年份时停止推算。切换到未收录法定安排的年份时,不查询该年的法定节假日,不计算或沿用当前年份的假期概览与请假建议,仅提示缺少该年数据;返回有数据的年份后恢复展示。
|
||||
菜单栏日历在所有应用语言下均提供“日期与星期”“日期与农历”“农历”“自定义”显示方式;日期与农历按应用语言呈现,切换语言保留用户选择。关闭农历时,字号与对比度子选项以及菜单栏中的两种农历方式隐藏;已保存的农历方式暂以“日期与星期”呈现,字号和对比度样式不生效,重新开启后恢复原选择。子选项位于农历开关下方。自定义可自由组合 y/M/d 字段与分隔符,年份可省略,输入时显示预览并提示无效格式,不影响月历与单日详情的星期显示;支持农历字号与对比度、可选悬停展开,按分钟刷新,休眠或关闭功能后停止。日程和到期提醒默认关闭,通过明确的授权按钮请求 EventKit 权限,只在日历面板可见时查询当前 42 格日期范围;只读展示,不修改日程,也不向 Widget、设置备份或服务端传递内容。列表 ID 仅保存在本机,显示偏好参与设置备份。节假日覆盖年份来自内置 Tyme 数据并随应用更新;“假期倒计时与请假建议”仅在开启“中国大陆节假日与调休”后显示在该开关下方;关闭节假日时隐藏子选项并保留原选择。假期倒计时与最多 3 天的请假建议只基于已公布的中国调休与双休安排,跨到未知年份时停止推算。切换到未收录法定安排的年份时,不查询该年的法定节假日,不计算或沿用当前年份的假期概览与请假建议,仅提示缺少该年数据;返回有数据的年份后恢复展示。
|
||||
日历首次初始化按 macOS 地区设置决定默认值,不按设备销售地或应用语言判断:中国大陆(CN)默认启用农历、大陆节假日与调休、传统与公历节日、节气与时令及假期概览;其他地区(含台湾、香港、澳门)默认不启用这些地区相关的可选内容。各地区的月历和单日详情均固定显示星期,不提供独立开关;旧配置与备份中的星期开关被忽略,其他选择保留。藏历与回历各地区均默认关闭。“节气与时令”统一控制二十四节气、三伏、梅雨与数九;月历只显示节气当天、时令阶段起点及出梅,单日详情与单日小组件保留具体天数。旧节气或时令开关任一开启即恢复为开启,全部关闭才恢复为关闭;使用新的设置键使 Widget 旧摘要失效并重新生成。月历不再显示干支,也不提供对应开关;年月日干支只在日期详情中显示,旧偏好与备份中的干支选项被忽略,其他选项保留。用户可自行启用或关闭,首次默认值保存后不随语言、地区变化重置;已有本机偏好及旧备份保持原选择。每周起始日首次采用 macOS 系统日历设置并保存,支持星期日到星期六,手动选择及备份恢复优先;后续系统设置变化不覆盖已保存的值,主应用月历与小组件使用同一周起始日。大陆节假日数据不能用于推断其他地区的法定安排。单日及整月小组件遵循日历选项,未启用大陆节假日时“明天上班吗”提示“仅支持中国大陆节假日”;黄历在单日详情中固定显示,旧偏好与备份中的黄历开关被忽略;宜忌详情仍只为选中日期按需计算,小组件复用主应用预计算的八天摘要缓存。
|
||||
月历使用固定六行布局,日期 ID 在生成日期时计算一次。面板打开时测量初始高度,后续翻月、详情与日程内容变化在内部滚动,不从内容几何回调反向调整窗口,避免窗口高度与懒布局估算形成反馈循环。
|
||||
月历计算仅缓存最近一个月份(包含周起始日和时区),供实际面板、高度测量和重复打开共用;翻月替换缓存,避免浏览历史导致内存累积。日程日期标记在工作 actor 内随查询结果生成,界面不再逐格扫描日程列表;连续 EventKit 通知按 300 毫秒窗口合并,关闭面板时取消待执行刷新。
|
||||
|
||||
@@ -36,7 +36,6 @@ extension Translations {
|
||||
{}第{}天 {1}, day {2}
|
||||
藏历 Tibetan calendar
|
||||
回历 Hijri calendar
|
||||
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Gregorian dates stay visible. Tibetan and Hijri dates appear in day details. Plum-rain days follow traditional calendar rules, not weather forecasts.
|
||||
该日期暂无藏历数据 Tibetan calendar data is unavailable for this date
|
||||
该年份暂无中国法定假日与调休数据 Chinese holiday and makeup workday data is unavailable for this year
|
||||
按传统历法推算,并非天气预报 Traditional calendar calculation, not a weather forecast
|
||||
|
||||
@@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence
|
||||
{}第{}天 اليوم {2} من {1}
|
||||
藏历 التقويم التبتي
|
||||
回历 التقويم الهجري
|
||||
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 تظل التواريخ الميلادية ظاهرة. تظهر التواريخ التبتية والهجرية في تفاصيل اليوم. تُحسب أمطار البرقوق وفق القواعد التقليدية وليست توقعات جوية.
|
||||
该日期暂无藏历数据 لا تتوفر بيانات التقويم التبتي لهذا التاريخ
|
||||
该年份暂无中国法定假日与调休数据 لا تتوفر بيانات العطلات الصينية وأيام العمل التعويضية لهذه السنة
|
||||
按传统历法推算,并非天气预报 حساب وفق التقويم التقليدي، وليس توقعًا جويًا
|
||||
|
||||
|
@@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence
|
||||
{}第{}天 {1}, Tag {2}
|
||||
藏历 Tibetischer Kalender
|
||||
回历 Hidschri-Kalender
|
||||
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Gregorianische Daten bleiben sichtbar. Tibetische und Hidschri-Daten stehen in den Tagesdetails. Die Pflaumenregenzeit folgt traditionellen Kalenderregeln, nicht Wettervorhersagen.
|
||||
该日期暂无藏历数据 Für dieses Datum sind keine tibetischen Kalenderdaten verfügbar
|
||||
该年份暂无中国法定假日与调休数据 Für dieses Jahr fehlen chinesische Feiertags- und Ausgleichsarbeitstagsdaten
|
||||
按传统历法推算,并非天气预报 Traditionelle Kalenderberechnung, keine Wettervorhersage
|
||||
|
||||
|
@@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence
|
||||
{}第{}天 {1}, día {2}
|
||||
藏历 Calendario tibetano
|
||||
回历 Calendario hiyri
|
||||
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Las fechas gregorianas siempre se muestran. Los calendarios tibetano e hiyri aparecen en los detalles del día. Las lluvias de ciruela se calculan según reglas tradicionales, no son pronósticos.
|
||||
该日期暂无藏历数据 No hay datos del calendario tibetano para esta fecha
|
||||
该年份暂无中国法定假日与调休数据 No hay datos de festivos chinos ni días laborales compensatorios para este año
|
||||
按传统历法推算,并非天气预报 Cálculo del calendario tradicional, no un pronóstico
|
||||
|
||||
|
@@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence
|
||||
{}第{}天 {1}, jour {2}
|
||||
藏历 Calendrier tibétain
|
||||
回历 Calendrier hégirien
|
||||
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Les dates grégoriennes restent visibles. Les dates tibétaines et hégiriennes figurent dans les détails du jour. Les pluies des prunes suivent les règles traditionnelles, sans prévision météo.
|
||||
该日期暂无藏历数据 Aucune donnée du calendrier tibétain pour cette date
|
||||
该年份暂无中国法定假日与调休数据 Aucune donnée sur les jours fériés chinois et les jours de travail compensatoires pour cette année
|
||||
按传统历法推算,并非天气预报 Calcul du calendrier traditionnel, pas une prévision météo
|
||||
|
||||
|
@@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence
|
||||
{}第{}天 {1}の{2}日目
|
||||
藏历 チベット暦
|
||||
回历 ヒジュラ暦
|
||||
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 西暦は常に表示します。チベット暦とヒジュラ暦は日付の詳細に表示します。入梅・出梅は伝統的な暦の計算で、天気予報ではありません。
|
||||
该日期暂无藏历数据 この日付のチベット暦データはありません
|
||||
该年份暂无中国法定假日与调休数据 この年の中国の祝日・振替出勤日データはありません
|
||||
按传统历法推算,并非天气预报 伝統的な暦の計算であり、天気予報ではありません
|
||||
|
||||
|
@@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence
|
||||
{}第{}天 {1} {2}일째
|
||||
藏历 티베트력
|
||||
回历 이슬람력
|
||||
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 양력은 항상 표시됩니다. 티베트력과 이슬람력은 날짜 상세 정보에 표시됩니다. 입매·출매는 전통 달력 계산이며 일기 예보가 아닙니다.
|
||||
该日期暂无藏历数据 이 날짜의 티베트력 데이터가 없습니다
|
||||
该年份暂无中国法定假日与调休数据 이 연도의 중국 공휴일 및 대체 근무일 데이터가 없습니다
|
||||
按传统历法推算,并非天气预报 전통 달력 계산이며 일기 예보가 아닙니다
|
||||
|
||||
|
@@ -1336,7 +1336,6 @@ Apple 智能 Apple Intelligence
|
||||
{}第{}天 {1}第{2}天
|
||||
藏历 藏曆
|
||||
回历 回曆
|
||||
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 公曆始終顯示。藏曆與回曆顯示在日期詳情中;梅雨天按傳統曆法推算,並非天氣預報。
|
||||
该日期暂无藏历数据 該日期暫無藏曆資料
|
||||
该年份暂无中国法定假日与调休数据 該年份暫無中國法定假日與調休資料
|
||||
按传统历法推算,并非天气预报 按傳統曆法推算,並非天氣預報
|
||||
|
||||
|
@@ -100,7 +100,7 @@ final class CalendarMenuBarController: NSObject {
|
||||
item.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
|
||||
item.button?.image = NSImage(systemSymbolName: "calendar", accessibilityDescription: tr("日历"))
|
||||
item.button?.imagePosition = .imageLeading
|
||||
item.button?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
|
||||
item.button?.font = .menuBarFont(ofSize: 0)
|
||||
self.item = item
|
||||
}
|
||||
if timer == nil {
|
||||
@@ -130,7 +130,8 @@ final class CalendarMenuBarController: NSObject {
|
||||
private func refreshTitle() {
|
||||
guard let button = item?.button else { return }
|
||||
let preferences = model.settings.calendarPreferences
|
||||
let title = preferences.title(at: Date(), locale: L10n.locale)
|
||||
let title = preferences.title(at: Date(), locale: L10n.locale,
|
||||
showsLunar: model.settings.calendarFeatures.contains(.lunar))
|
||||
button.title = title.isEmpty ? "" : " " + title
|
||||
button.image = NSImage(systemSymbolName: "calendar", accessibilityDescription: tr("日历"))
|
||||
let formatter = DateFormatter()
|
||||
|
||||
@@ -339,19 +339,22 @@ private struct CalendarDayCell: View {
|
||||
private var foreground: Color { isSelected ? .white : isRest ? .red : DS.Palette.textPrimary }
|
||||
|
||||
var body: some View {
|
||||
let showsLunar = features.contains(.lunar)
|
||||
let largeLunarText = showsLunar && preferences.largeLunarText
|
||||
let strongerLunarText = showsLunar && preferences.strongerLunarText
|
||||
Button(action: action) {
|
||||
VStack(spacing: sizing.cellHeight < 50 ? 2 : 3) {
|
||||
Text(String(day.day)).font(.system(size: sizing.dayFontSize,
|
||||
weight: isToday ? .semibold : .regular, design: .rounded))
|
||||
Text(tr(day.subtitle(features: features)))
|
||||
.font(.system(size: preferences.largeLunarText ? sizing.subtitleFontSize + 2 : sizing.subtitleFontSize,
|
||||
weight: preferences.strongerLunarText ? .semibold : .regular))
|
||||
.font(.system(size: largeLunarText ? sizing.subtitleFontSize + 2 : sizing.subtitleFontSize,
|
||||
weight: strongerLunarText ? .semibold : .regular))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.75)
|
||||
}
|
||||
.foregroundStyle(foreground)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: sizing.cellHeight + (preferences.largeLunarText ? 8 : 0))
|
||||
.frame(height: sizing.cellHeight + (largeLunarText ? 8 : 0))
|
||||
.background(isSelected ? Color.accentColor : holiday != nil ? foreground.opacity(0.05) : .clear,
|
||||
in: RoundedRectangle(cornerRadius: 10))
|
||||
.overlay {
|
||||
|
||||
@@ -10,6 +10,11 @@ public struct CalendarPreferences: Codable, Equatable, Sendable {
|
||||
case standard, dateLunar, lunar, custom
|
||||
public var id: String { rawValue }
|
||||
|
||||
/// 总开关只改变实际呈现,保留原模式,重新开启农历时即可恢复。
|
||||
func effective(showsLunar: Bool) -> Self {
|
||||
!showsLunar && (self == .lunar || self == .dateLunar) ? .standard : self
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .standard: tr("日期与星期")
|
||||
@@ -70,14 +75,15 @@ public struct CalendarPreferences: Codable, Equatable, Sendable {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func title(at date: Date, locale: Locale, timeZone: TimeZone = .autoupdatingCurrent) -> String {
|
||||
func title(at date: Date, locale: Locale, timeZone: TimeZone = .autoupdatingCurrent,
|
||||
showsLunar: Bool = true) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = locale
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = timeZone
|
||||
formatter.calendar = calendar
|
||||
formatter.timeZone = timeZone
|
||||
switch display {
|
||||
switch display.effective(showsLunar: showsLunar) {
|
||||
case .standard: formatter.setLocalizedDateFormatFromTemplate("MdEEE")
|
||||
case .custom:
|
||||
// 用户明确选择顺序,不能再用本地化模板把日月重新排回月日。
|
||||
|
||||
@@ -63,6 +63,18 @@ struct CalendarOptionsPopover: View {
|
||||
}), label: feature.title)
|
||||
}
|
||||
}
|
||||
if feature == .lunar, settings.calendarFeatures.contains(.lunar) {
|
||||
GroupRow {
|
||||
SettingRow(title: tr("放大农历文字")) {
|
||||
DSToggle(isOn: $settings.calendarPreferences.largeLunarText, label: tr("放大农历文字"))
|
||||
}
|
||||
}
|
||||
GroupRow {
|
||||
SettingRow(title: tr("增强农历对比度")) {
|
||||
DSToggle(isOn: $settings.calendarPreferences.strongerLunarText, label: tr("增强农历对比度"))
|
||||
}
|
||||
}
|
||||
}
|
||||
if feature == .holidays, settings.calendarFeatures.contains(.holidays) {
|
||||
GroupRow {
|
||||
SettingRow(title: tr("假期倒计时与请假建议")) {
|
||||
@@ -71,22 +83,6 @@ struct CalendarOptionsPopover: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
GroupRow {
|
||||
Text(tr("公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。"))
|
||||
.dsFont(.xs)
|
||||
.foregroundStyle(DS.Palette.textSecondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
GroupRow {
|
||||
SettingRow(title: tr("放大农历文字")) {
|
||||
DSToggle(isOn: $settings.calendarPreferences.largeLunarText, label: tr("放大农历文字"))
|
||||
}
|
||||
}
|
||||
GroupRow {
|
||||
SettingRow(title: tr("增强农历对比度")) {
|
||||
DSToggle(isOn: $settings.calendarPreferences.strongerLunarText, label: tr("增强农历对比度"))
|
||||
}
|
||||
}
|
||||
}
|
||||
agendaOptions
|
||||
}
|
||||
@@ -111,11 +107,16 @@ struct CalendarOptionsPopover: View {
|
||||
|
||||
private var presentationOptions: some View {
|
||||
@Bindable var settings = model.settings
|
||||
let showsLunar = settings.calendarFeatures.contains(.lunar)
|
||||
let displays: [CalendarPreferences.Display] = showsLunar ? CalendarPreferences.Display.allCases : [.standard, .custom]
|
||||
return SettingsGroup(caption: tr("菜单栏")) {
|
||||
GroupRow(showsDivider: false) {
|
||||
SettingRow(title: tr("显示方式")) {
|
||||
Picker(tr("显示方式"), selection: $settings.calendarPreferences.display) {
|
||||
ForEach(CalendarPreferences.Display.allCases) { Text($0.title).tag($0) }
|
||||
Picker(tr("显示方式"), selection: Binding(
|
||||
get: { settings.calendarPreferences.display.effective(showsLunar: showsLunar) },
|
||||
set: { settings.calendarPreferences.display = $0 }
|
||||
)) {
|
||||
ForEach(displays) { Text($0.title).tag($0) }
|
||||
}.labelsHidden().fixedSize()
|
||||
}
|
||||
}
|
||||
@@ -138,7 +139,7 @@ struct CalendarOptionsPopover: View {
|
||||
}
|
||||
}
|
||||
GroupRow {
|
||||
let preview = settings.calendarPreferences.title(at: Date(), locale: L10n.locale)
|
||||
let preview = settings.calendarPreferences.title(at: Date(), locale: L10n.locale, showsLunar: showsLunar)
|
||||
HStack {
|
||||
Text(tr("预览")).foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
|
||||
@@ -80,6 +80,23 @@ struct CalendarEnhancementTests {
|
||||
#expect(restored.title(at: date, locale: locale, timeZone: zone) == preferences.title(at: date, locale: locale, timeZone: zone))
|
||||
}
|
||||
|
||||
@Test(arguments: [CalendarPreferences.Display.lunar, .dateLunar])
|
||||
func disablingLunarDisplayPreservesSavedPresentation(display: CalendarPreferences.Display) throws {
|
||||
let date = try #require(ISO8601DateFormatter().date(from: "2026-09-28T04:00:00Z"))
|
||||
let locale = Locale(identifier: "en_US")
|
||||
let standard = CalendarPreferences().title(at: date, locale: locale, timeZone: zone)
|
||||
var preferences = CalendarPreferences()
|
||||
preferences.display = display
|
||||
preferences.largeLunarText = true
|
||||
preferences.strongerLunarText = true
|
||||
let enabled = preferences.title(at: date, locale: locale, timeZone: zone)
|
||||
#expect(preferences.title(at: date, locale: locale, timeZone: zone, showsLunar: false) == standard)
|
||||
let restored = try JSONDecoder().decode(CalendarPreferences.self, from: JSONEncoder().encode(preferences))
|
||||
#expect(restored.display == display)
|
||||
#expect(restored.largeLunarText && restored.strongerLunarText)
|
||||
#expect(restored.title(at: date, locale: locale, timeZone: zone, showsLunar: true) == enabled)
|
||||
}
|
||||
|
||||
@Test(arguments: Array(1...7))
|
||||
func monthGridAndWidgetHonorEveryWeekStart(firstWeekday: Int) throws {
|
||||
let days = CalendarEngine.month(year: 2026, month: 9, firstWeekday: firstWeekday, timeZone: zone)
|
||||
|
||||
Reference in new issue
Block a user