refactor(calendar): 固定显示星期与黄历并简化设置
This commit is contained in:
1 parent
01bf5c77b2
commit
0b63a4ed04
22 files changed
+72
-67
No files matched your search
+6
-1
@@ -183,7 +183,12 @@ lookups. Changing to an unsupported display year clears the holiday overview bef
|
||||
reuse or daily planning, so the current year's countdown cannot appear in the next year.
|
||||
Plum-rain dates describe traditional calendar rules, not observed or forecast weather.
|
||||
|
||||
Calendar enablement, visible features and week start persist through `AppSettings` and the
|
||||
Weekday headers and weekday labels in selected-day dates are always visible, matching the
|
||||
month widget. Legacy `weekdays` settings are ignored without discarding other choices.
|
||||
Selected-day almanac is always enabled; legacy `showAlmanac` preferences are ignored.
|
||||
Details still calculate on demand, while widgets reuse the bounded eight-day summary.
|
||||
The snapshot keeps `showsAlmanac = true` to invalidate old disabled-almanac caches.
|
||||
Calendar enablement, optional features and week start persist through `AppSettings` and the
|
||||
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
|
||||
|
||||
+2
-2
@@ -54,8 +54,8 @@ 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 天的请假建议只基于已公布的中国调休与双休安排,跨到未知年份时停止推算。切换到未收录法定安排的年份时,不查询该年的法定节假日,不计算或沿用当前年份的假期概览与请假建议,仅提示缺少该年数据;返回有数据的年份后恢复展示。
|
||||
日历首次初始化按 macOS 地区设置决定默认值,不按设备销售地或应用语言判断:中国大陆(CN)默认启用农历、星期、大陆节假日与调休、传统与公历节日、节气与时令、黄历及假期概览;其他地区(含台湾、香港、澳门)默认只启用星期。藏历与回历各地区均默认关闭。“节气与时令”统一控制二十四节气、三伏、梅雨与数九;月历只显示节气当天、时令阶段起点及出梅,单日详情与单日小组件保留具体天数。旧节气或时令开关任一开启即恢复为开启,全部关闭才恢复为关闭;使用新的设置键使 Widget 旧摘要失效并重新生成。月历不再显示干支,也不提供对应开关;年月日干支只在启用黄历后的日期详情中显示,旧偏好与备份中的干支选项被忽略,其他选项保留。用户可自行启用或关闭,首次默认值保存后不随语言、地区变化重置;已有本机偏好及旧备份保持原选择。每周起始日首次采用 macOS 系统日历设置并保存,支持星期日到星期六,手动选择及备份恢复优先;后续系统设置变化不覆盖已保存的值,主应用月历与小组件使用同一周起始日。大陆节假日数据不能用于推断其他地区的法定安排。单日及整月小组件遵循日历选项,未启用大陆节假日时“明天上班吗”提示“仅支持中国大陆节假日”;黄历关闭后不再计算宜忌摘要。
|
||||
菜单栏日历在所有应用语言下均提供“日期与星期”“日期与农历”“农历”“自定义”显示方式;日期与农历按应用语言呈现,切换语言保留用户选择。自定义可自由组合 y/M/d 字段与分隔符,年份可省略,输入时显示预览并提示无效格式,不影响月历与单日详情的星期显示;支持农历字号与对比度、可选悬停展开,按分钟刷新,休眠或关闭功能后停止。日程和到期提醒默认关闭,通过明确的授权按钮请求 EventKit 权限,只在日历面板可见时查询当前 42 格日期范围;只读展示,不修改日程,也不向 Widget、设置备份或服务端传递内容。列表 ID 仅保存在本机,显示偏好参与设置备份。节假日覆盖年份来自内置 Tyme 数据并随应用更新;“假期倒计时与请假建议”仅在开启“中国大陆节假日与调休”后显示在该开关下方;关闭节假日时隐藏子选项并保留原选择。假期倒计时与最多 3 天的请假建议只基于已公布的中国调休与双休安排,跨到未知年份时停止推算。切换到未收录法定安排的年份时,不查询该年的法定节假日,不计算或沿用当前年份的假期概览与请假建议,仅提示缺少该年数据;返回有数据的年份后恢复展示。
|
||||
日历首次初始化按 macOS 地区设置决定默认值,不按设备销售地或应用语言判断:中国大陆(CN)默认启用农历、大陆节假日与调休、传统与公历节日、节气与时令及假期概览;其他地区(含台湾、香港、澳门)默认不启用这些地区相关的可选内容。各地区的月历和单日详情均固定显示星期,不提供独立开关;旧配置与备份中的星期开关被忽略,其他选择保留。藏历与回历各地区均默认关闭。“节气与时令”统一控制二十四节气、三伏、梅雨与数九;月历只显示节气当天、时令阶段起点及出梅,单日详情与单日小组件保留具体天数。旧节气或时令开关任一开启即恢复为开启,全部关闭才恢复为关闭;使用新的设置键使 Widget 旧摘要失效并重新生成。月历不再显示干支,也不提供对应开关;年月日干支只在日期详情中显示,旧偏好与备份中的干支选项被忽略,其他选项保留。用户可自行启用或关闭,首次默认值保存后不随语言、地区变化重置;已有本机偏好及旧备份保持原选择。每周起始日首次采用 macOS 系统日历设置并保存,支持星期日到星期六,手动选择及备份恢复优先;后续系统设置变化不覆盖已保存的值,主应用月历与小组件使用同一周起始日。大陆节假日数据不能用于推断其他地区的法定安排。单日及整月小组件遵循日历选项,未启用大陆节假日时“明天上班吗”提示“仅支持中国大陆节假日”;黄历在单日详情中固定显示,旧偏好与备份中的黄历开关被忽略;宜忌详情仍只为选中日期按需计算,小组件复用主应用预计算的八天摘要缓存。
|
||||
月历使用固定六行布局,日期 ID 在生成日期时计算一次。面板打开时测量初始高度,后续翻月、详情与日程内容变化在内部滚动,不从内容几何回调反向调整窗口,避免窗口高度与懒布局估算形成反馈循环。
|
||||
月历计算仅缓存最近一个月份(包含周起始日和时区),供实际面板、高度测量和重复打开共用;翻月替换缓存,避免浏览历史导致内存累积。日程日期标记在工作 actor 内随查询结果生成,界面不再逐格扫描日程列表;连续 EventKit 通知按 300 毫秒窗口合并,关闭面板时取消待执行刷新。
|
||||
用于正式签名的 Apple Developer 团队须为主应用和 Widget 扩展启用同一个 App Group;
|
||||
|
||||
@@ -14,7 +14,6 @@ extension Translations {
|
||||
星期日 Sunday
|
||||
农历 Lunar calendar
|
||||
黄历 Chinese almanac
|
||||
星期 Weekdays
|
||||
中国大陆节假日与调休 Mainland China holidays and makeup workdays
|
||||
传统与公历节日 Festivals
|
||||
节气与时令 Solar terms and seasons
|
||||
|
||||
@@ -1313,7 +1313,6 @@ Apple 智能 Apple Intelligence
|
||||
星期一 الاثنين
|
||||
星期日 الأحد
|
||||
农历 التقويم القمري الصيني
|
||||
星期 أيام الأسبوع
|
||||
中国大陆节假日与调休 العطلات وأيام العمل التعويضية في البر الرئيسي للصين
|
||||
传统与公历节日 المناسبات التقليدية والميلادية
|
||||
节气与时令 الفصول الشمسية والفترات الموسمية
|
||||
|
||||
|
@@ -1313,7 +1313,6 @@ Apple 智能 Apple Intelligence
|
||||
星期一 Montag
|
||||
星期日 Sonntag
|
||||
农历 Chinesischer Mondkalender
|
||||
星期 Wochentage
|
||||
中国大陆节假日与调休 Feiertage und Ausgleichsarbeitstage in Festlandchina
|
||||
传统与公历节日 Traditionelle und gregorianische Feste
|
||||
节气与时令 Sonnenperioden und Jahreszeiten
|
||||
|
||||
|
@@ -1313,7 +1313,6 @@ Apple 智能 Apple Intelligence
|
||||
星期一 Lunes
|
||||
星期日 Domingo
|
||||
农历 Calendario lunar chino
|
||||
星期 Días de la semana
|
||||
中国大陆节假日与调休 Festivos y días laborales compensatorios de China continental
|
||||
传统与公历节日 Fiestas tradicionales y gregorianas
|
||||
节气与时令 Términos solares y períodos estacionales
|
||||
|
||||
|
@@ -1313,7 +1313,6 @@ Apple 智能 Apple Intelligence
|
||||
星期一 Lundi
|
||||
星期日 Dimanche
|
||||
农历 Calendrier lunaire chinois
|
||||
星期 Jours de la semaine
|
||||
中国大陆节假日与调休 Jours fériés et jours de travail compensatoires en Chine continentale
|
||||
传统与公历节日 Fêtes traditionnelles et grégoriennes
|
||||
节气与时令 Termes solaires et périodes saisonnières
|
||||
|
||||
|
@@ -1313,7 +1313,6 @@ Apple 智能 Apple Intelligence
|
||||
星期一 月曜日
|
||||
星期日 日曜日
|
||||
农历 旧暦
|
||||
星期 曜日
|
||||
中国大陆节假日与调休 中国本土の祝日・振替出勤日
|
||||
传统与公历节日 伝統と西暦の祝祭日
|
||||
节气与时令 節気と季節の節目
|
||||
|
||||
|
@@ -1313,7 +1313,6 @@ Apple 智能 Apple Intelligence
|
||||
星期一 월요일
|
||||
星期日 일요일
|
||||
农历 음력
|
||||
星期 요일
|
||||
中国大陆节假日与调休 중국 본토 공휴일 및 대체 근무일
|
||||
传统与公历节日 전통 및 양력 기념일
|
||||
节气与时令 절기와 계절
|
||||
|
||||
|
@@ -1314,7 +1314,6 @@ Apple 智能 Apple Intelligence
|
||||
星期一 星期一
|
||||
星期日 星期日
|
||||
农历 農曆
|
||||
星期 星期
|
||||
中国大陆节假日与调休 中國大陸節假日與調休
|
||||
传统与公历节日 傳統與公曆節日
|
||||
节气与时令 節氣與時令
|
||||
|
||||
|
@@ -166,7 +166,7 @@ public struct WidgetSnapshot: Codable, Equatable, Sendable {
|
||||
public var showsLunar: Bool
|
||||
/// 可选以兼容旧版摘要;单日日历仅在用户开启时显示具体时令天数。
|
||||
public var showsSeasonal: Bool?
|
||||
/// nil 表示旧快照尚未按黄历开关生成,主应用会按需更新一次。
|
||||
/// 保留旧字段以识别黄历曾关闭的缓存;主应用固定写入 true,并重算旧的 nil / false 摘要。
|
||||
public var showsAlmanac: Bool?
|
||||
/// 可选以兼容旧版主应用写入的 App Group 摘要。
|
||||
public var calendarDays: [CalendarSummary]?
|
||||
|
||||
@@ -402,7 +402,6 @@ public final class AppController: NSObject, NSApplicationDelegate {
|
||||
_ = model.settings.language
|
||||
_ = model.settings.calendarFirstWeekday
|
||||
_ = model.settings.calendarFeatures
|
||||
_ = model.settings.calendarPreferences.showAlmanac
|
||||
_ = model.aiUsage.states
|
||||
_ = model.aiUsage.quotaStates
|
||||
_ = model.network.publicResults
|
||||
@@ -427,7 +426,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
|
||||
// 多预备几天,Widget 午夜换日时无需唤醒主应用;主应用运行期间按小时补足窗口。
|
||||
let calendarDays: [WidgetSnapshot.CalendarSummary] = {
|
||||
if reusesCalendar, previous.showsSeasonal != nil,
|
||||
previous.showsAlmanac == settings.calendarPreferences.showAlmanac,
|
||||
previous.showsAlmanac == true,
|
||||
previous.monthSummaries?.first?.featureKeys == monthFeatures, previous.calendarDays?.count == 8,
|
||||
previous.calendarDays?.first?.dateKey == todayKey {
|
||||
return previous.calendarDays ?? []
|
||||
@@ -435,8 +434,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
|
||||
return (0..<8).compactMap { offset in
|
||||
guard let date = calendar.date(byAdding: .day, value: offset, to: today),
|
||||
let day = CalendarEngine.today(at: date) else { return nil }
|
||||
return CalendarEngine.widgetSummary(for: day, features: settings.calendarFeatures,
|
||||
showsAlmanac: settings.calendarPreferences.showAlmanac)
|
||||
return CalendarEngine.widgetSummary(for: day, features: settings.calendarFeatures)
|
||||
}
|
||||
}()
|
||||
let monthSummaries: [WidgetSnapshot.MonthSummary] = (0...1).compactMap { offset in
|
||||
@@ -492,7 +490,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
|
||||
showsLunar: settings.calendarFeatures.contains(.lunar),
|
||||
calendarDays: calendarDays, monthSummaries: monthSummaries,
|
||||
showsSeasonal: settings.calendarFeatures.contains(.seasonalInfo),
|
||||
calendarDataVersion: dataVersion, showsAlmanac: settings.calendarPreferences.showAlmanac)
|
||||
calendarDataVersion: dataVersion, showsAlmanac: true)
|
||||
guard snapshot != previous, widgetStore.save(snapshot) else { return }
|
||||
for kind in snapshot.changedWidgetKinds(from: previous) {
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: kind)
|
||||
|
||||
@@ -46,8 +46,7 @@ extension CalendarEngine {
|
||||
"2026-05-04", "2026-05-05", "2026-10-05", "2026-10-06", "2026-10-07"
|
||||
]
|
||||
|
||||
static func widgetSummary(for day: CalendarDay, features: Set<CalendarFeature> = Set(CalendarFeature.allCases),
|
||||
showsAlmanac: Bool = true) -> WidgetSnapshot.CalendarSummary {
|
||||
static func widgetSummary(for day: CalendarDay, features: Set<CalendarFeature> = Set(CalendarFeature.allCases)) -> WidgetSnapshot.CalendarSummary {
|
||||
let schedule: WidgetSnapshot.CalendarSummary.Schedule
|
||||
if !features.contains(.holidays) || !hasHolidayData(year: day.year) {
|
||||
schedule = .unknown
|
||||
@@ -68,7 +67,7 @@ extension CalendarEngine {
|
||||
} else {
|
||||
schedule = day.isWeekend ? .weekend : .work
|
||||
}
|
||||
let almanac = showsAlmanac ? almanac(for: day) : nil
|
||||
let almanac = almanac(for: day)
|
||||
let seasons = features.contains(.seasonalInfo) ? [day.dogDays, day.plumRain, day.nineDays].compactMap { $0 } : []
|
||||
return .init(dateKey: day.id, festivals: features.contains(.festivals) ? day.festivals : [],
|
||||
solarTerm: features.contains(.seasonalInfo) ? day.solarTerm : nil,
|
||||
|
||||
@@ -41,7 +41,7 @@ struct CalendarAlmanacView: View {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = L10n.locale
|
||||
formatter.calendar = CalendarEngine.gregorian()
|
||||
formatter.setLocalizedDateFormatFromTemplate(features.contains(.weekdays) ? "yMMMMdEEEE" : "yMMMMd")
|
||||
formatter.setLocalizedDateFormatFromTemplate("yMMMMdEEEE")
|
||||
return formatter.string(from: day.date)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,19 +9,18 @@ import WidgetData
|
||||
|
||||
/// 公历是月历的坐标;其余信息可独立隐藏,设置以稳定 rawValue 保存和同步。
|
||||
public enum CalendarFeature: String, CaseIterable, Identifiable, Sendable {
|
||||
case lunar, weekdays, holidays, festivals, seasonalInfo, tibetan, hijri
|
||||
case lunar, holidays, festivals, seasonalInfo, tibetan, hijri
|
||||
public var id: String { rawValue }
|
||||
public static var defaults: Set<Self> { defaults(for: .current) }
|
||||
|
||||
/// 默认值依据使用地区,不依据应用语言或设备销售地;其他地区可自行启用。
|
||||
public static func defaults(for locale: Locale) -> Set<Self> {
|
||||
locale.region?.identifier == "CN" ? [.lunar, .weekdays, .holidays, .festivals, .seasonalInfo] : [.weekdays]
|
||||
locale.region?.identifier == "CN" ? [.lunar, .holidays, .festivals, .seasonalInfo] : []
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .lunar: tr("农历")
|
||||
case .weekdays: tr("星期")
|
||||
case .holidays: tr("中国大陆节假日与调休")
|
||||
case .festivals: tr("传统与公历节日")
|
||||
case .seasonalInfo: tr("节气与时令")
|
||||
@@ -32,7 +31,7 @@ public enum CalendarFeature: String, CaseIterable, Identifiable, Sendable {
|
||||
|
||||
/// 旧偏好与备份中的节气、时令开关合并;任一项曾开启就保留用户的显示意愿。
|
||||
static func restored(from rawValues: [String]) -> Set<Self> {
|
||||
// 已移除的月历干支(ganzhi)与未知选项一起忽略,不影响其他已保存的选择。
|
||||
// 星期固定显示;已移除的 weekdays、ganzhi 与未知选项一起忽略,不影响其他选择。
|
||||
var features = Set(rawValues.compactMap(Self.init(rawValue:)))
|
||||
// 使用新的设置键也使 Widget 的旧摘要失效,避免复用只包含节气或时令的旧缓存。
|
||||
if rawValues.contains(where: { ["solarTerms", "seasonal", "dogDays", "plumRain"].contains($0) }) {
|
||||
|
||||
@@ -126,7 +126,6 @@ struct CalendarPopover: View {
|
||||
refreshHolidayPlan(at: referenceDate ?? Date())
|
||||
}
|
||||
.onChange(of: selected?.id) { _, _ in refreshAlmanac() }
|
||||
.onChange(of: model.settings.calendarPreferences.showAlmanac) { _, _ in refreshAlmanac() }
|
||||
.onChange(of: model.settings.calendarFeatures) { _, _ in refreshHolidayPlan(at: referenceDate ?? Date()) }
|
||||
.onChange(of: model.settings.calendarPreferences.showHolidayOverview) { _, _ in
|
||||
refreshHolidayPlan(at: referenceDate ?? Date())
|
||||
@@ -217,18 +216,16 @@ struct CalendarPopover: View {
|
||||
let features = model.settings.calendarFeatures
|
||||
let firstWeekday = model.settings.calendarFirstWeekday
|
||||
return VStack(spacing: sizing.cellHeight < 50 ? 6 : 7) {
|
||||
if features.contains(.weekdays) {
|
||||
HStack(spacing: 4) {
|
||||
ForEach(0..<7) { offset in
|
||||
let weekday = (firstWeekday - 1 + offset) % 7 + 1
|
||||
Text(weekdayName(weekday))
|
||||
.font(.system(size: sizing.cellHeight < 50 ? 11 : 12, weight: .medium))
|
||||
.foregroundStyle(weekday == 1 || weekday == 7 ? Color.red : .secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
ForEach(0..<7) { offset in
|
||||
let weekday = (firstWeekday - 1 + offset) % 7 + 1
|
||||
Text(weekdayName(weekday))
|
||||
.font(.system(size: sizing.cellHeight < 50 ? 11 : 12, weight: .medium))
|
||||
.foregroundStyle(weekday == 1 || weekday == 7 ? Color.red : .secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.bottom, sizing.cellHeight < 50 ? 2 : 3)
|
||||
}
|
||||
.padding(.bottom, sizing.cellHeight < 50 ? 2 : 3)
|
||||
// 月历固定最多 42 格,无需懒布局的高度估算与反复放置。
|
||||
VStack(spacing: sizing.cellHeight < 50 ? 3 : 4) {
|
||||
ForEach(0..<6) { row in
|
||||
@@ -272,7 +269,7 @@ struct CalendarPopover: View {
|
||||
}
|
||||
|
||||
private func refreshAlmanac() {
|
||||
guard showsDayDetails, model.settings.calendarPreferences.showAlmanac, let selected else {
|
||||
guard showsDayDetails, let selected else {
|
||||
almanac = nil
|
||||
return
|
||||
}
|
||||
|
||||
@@ -26,19 +26,17 @@ public struct CalendarPreferences: Codable, Equatable, Sendable {
|
||||
public var strongerLunarText = false
|
||||
public var openOnHover = false
|
||||
public var showHolidayOverview: Bool
|
||||
public var showAlmanac: Bool
|
||||
public var showEvents = false
|
||||
public var showReminders = false
|
||||
|
||||
public init(locale: Locale = .current) {
|
||||
let isMainlandChina = locale.region?.identifier == "CN"
|
||||
showHolidayOverview = isMainlandChina
|
||||
showAlmanac = isMainlandChina
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case display, dateFormat, largeLunarText, strongerLunarText, openOnHover
|
||||
case showHolidayOverview, showAlmanac, showEvents, showReminders
|
||||
case showHolidayOverview, showEvents, showReminders
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
@@ -51,8 +49,6 @@ public struct CalendarPreferences: Codable, Equatable, Sendable {
|
||||
strongerLunarText = try values.decodeIfPresent(Bool.self, forKey: .strongerLunarText) ?? false
|
||||
openOnHover = try values.decodeIfPresent(Bool.self, forKey: .openOnHover) ?? false
|
||||
showHolidayOverview = try values.decodeIfPresent(Bool.self, forKey: .showHolidayOverview) ?? true
|
||||
// 旧偏好始终展示黄历,缺失新字段时保留已有行为;全新偏好使用地区默认值。
|
||||
showAlmanac = try values.decodeIfPresent(Bool.self, forKey: .showAlmanac) ?? true
|
||||
showEvents = try values.decodeIfPresent(Bool.self, forKey: .showEvents) ?? false
|
||||
showReminders = try values.decodeIfPresent(Bool.self, forKey: .showReminders) ?? false
|
||||
}
|
||||
|
||||
@@ -71,11 +71,6 @@ struct CalendarOptionsPopover: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
GroupRow {
|
||||
SettingRow(title: tr("黄历")) {
|
||||
DSToggle(isOn: $settings.calendarPreferences.showAlmanac, label: tr("黄历"))
|
||||
}
|
||||
}
|
||||
GroupRow {
|
||||
Text(tr("公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。"))
|
||||
.dsFont(.xs)
|
||||
|
||||
@@ -155,7 +155,7 @@ enum SnapshotRenderer {
|
||||
settings.calendarPreferences.display = .dateLunar
|
||||
write(CalendarOptionsPopover(), model: model, appearance: appearance,
|
||||
to: outputDirectory.appendingPathComponent("calendar-lunar-\(suffix).png"))
|
||||
// 黄历示例明确启用;普通日历与设置截图仍使用本机地区默认值。
|
||||
// 重置呈现偏好;黄历在日期详情中固定显示,其他选项仍按本机地区初始化。
|
||||
settings.calendarPreferences = CalendarPreferences(locale: Locale(identifier: "zh_CN"))
|
||||
write(CalendarPopover(referenceDate: september, showsDayDetails: true), model: model, appearance: appearance,
|
||||
to: outputDirectory.appendingPathComponent("calendar-almanac-\(suffix).png"))
|
||||
|
||||
@@ -10,20 +10,20 @@ struct CalendarAlmanacTests {
|
||||
@Test(arguments: [(2026, 9, 25), (2026, 9, 23), (2026, 12, 23), (2026, 10, 10)])
|
||||
func widgetsDoNotExposeDisabledRegionalFeatures(_ date: (Int, Int, Int)) throws {
|
||||
let day = try #require(CalendarEngine.day(year: date.0, month: date.1, day: date.2))
|
||||
let summary = CalendarEngine.widgetSummary(for: day, features: [.weekdays], showsAlmanac: false)
|
||||
let summary = CalendarEngine.widgetSummary(for: day, features: [])
|
||||
#expect(summary.festivals.isEmpty && summary.solarTerm == nil)
|
||||
#expect(summary.holidayName == nil && summary.schedule.needsWork == nil)
|
||||
#expect(summary.seasonalDescriptions == nil)
|
||||
#expect(summary.twelveStar == nil && summary.recommends.isEmpty && summary.avoids.isEmpty)
|
||||
#expect(summary.twelveStar != nil)
|
||||
}
|
||||
|
||||
@Test func manuallyEnabledFeaturesWorkOutsideDefaultRegions() throws {
|
||||
let day = try #require(CalendarEngine.day(year: 2026, month: 9, day: 25))
|
||||
let summary = CalendarEngine.widgetSummary(for: day, features: [.festivals, .holidays], showsAlmanac: false)
|
||||
let summary = CalendarEngine.widgetSummary(for: day, features: [.festivals, .holidays])
|
||||
#expect(!summary.festivals.isEmpty && summary.holidayName != nil)
|
||||
#expect(summary.schedule.needsWork == false)
|
||||
#expect(summary.twelveStar == nil)
|
||||
let almanacOnly = CalendarEngine.widgetSummary(for: day, features: [], showsAlmanac: true)
|
||||
#expect(summary.twelveStar != nil)
|
||||
let almanacOnly = CalendarEngine.widgetSummary(for: day, features: [])
|
||||
#expect(almanacOnly.twelveStar != nil && !almanacOnly.recommends.isEmpty)
|
||||
#expect(almanacOnly.schedule.needsWork == nil && almanacOnly.festivals.isEmpty)
|
||||
}
|
||||
|
||||
@@ -130,10 +130,19 @@ struct CalendarEnhancementTests {
|
||||
#expect(custom.largeLunarText)
|
||||
}
|
||||
|
||||
@Test(arguments: [true, false])
|
||||
func retiredAlmanacTogglePreservesOtherPreferences(enabled: Bool) throws {
|
||||
let data = try JSONSerialization.data(withJSONObject: ["showAlmanac": enabled, "display": "lunar", "showEvents": true])
|
||||
let preferences = try JSONDecoder().decode(CalendarPreferences.self, from: data)
|
||||
#expect(preferences.display == .lunar && preferences.showEvents)
|
||||
let exported = try #require(JSONSerialization.jsonObject(with: JSONEncoder().encode(preferences)) as? [String: Any])
|
||||
#expect(exported["showAlmanac"] == nil)
|
||||
}
|
||||
|
||||
@Test func calendarPreferencesKeepSafeDefaultsForOlderDocuments() throws {
|
||||
let old = try JSONDecoder().decode(CalendarPreferences.self, from: Data("{}".utf8))
|
||||
#expect(old == CalendarPreferences(locale: Locale(identifier: "zh_CN")))
|
||||
#expect(old.showAlmanac && old.showHolidayOverview)
|
||||
#expect(old.showHolidayOverview)
|
||||
#expect(!old.showEvents && !old.showReminders && !old.openOnHover)
|
||||
let future = try JSONDecoder().decode(CalendarPreferences.self, from: Data(#"{"display":"future"}"#.utf8))
|
||||
#expect(future.display == .standard)
|
||||
@@ -147,11 +156,12 @@ struct CalendarEnhancementTests {
|
||||
defer { defaults.removePersistentDomain(forName: name) }
|
||||
let locale = Locale(identifier: localeID)
|
||||
let mainland = locale.region?.identifier == "CN"
|
||||
let expected: Set<CalendarFeature> = mainland ? [.lunar, .weekdays, .holidays, .festivals, .seasonalInfo] : [.weekdays]
|
||||
let expected: Set<CalendarFeature> = mainland ? [.lunar, .holidays, .festivals, .seasonalInfo] : []
|
||||
let settings = AppSettings(defaults: defaults, calendarLocale: locale)
|
||||
#expect(settings.calendarFeatures == expected)
|
||||
#expect(settings.calendarPreferences.showHolidayOverview == mainland)
|
||||
#expect(settings.calendarPreferences.showAlmanac == mainland)
|
||||
let day = try #require(CalendarEngine.day(year: 2026, month: 9, day: 25, timeZone: zone))
|
||||
#expect(CalendarEngine.widgetSummary(for: day, features: settings.calendarFeatures).twelveStar != nil)
|
||||
#expect(settings.calendarFeatures.contains(.seasonalInfo) == mainland)
|
||||
|
||||
// 更改地区不应重置首次保存的默认值,明确关闭的选项和空集合也须保留。
|
||||
@@ -161,10 +171,9 @@ struct CalendarEnhancementTests {
|
||||
#expect(restored.calendarPreferences == settings.calendarPreferences)
|
||||
settings.calendarFeatures = []
|
||||
settings.calendarPreferences.showHolidayOverview = false
|
||||
settings.calendarPreferences.showAlmanac = false
|
||||
let disabled = AppSettings(defaults: defaults, calendarLocale: Locale(identifier: "zh_CN"))
|
||||
#expect(disabled.calendarFeatures.isEmpty)
|
||||
#expect(!disabled.calendarPreferences.showHolidayOverview && !disabled.calendarPreferences.showAlmanac)
|
||||
#expect(!disabled.calendarPreferences.showHolidayOverview)
|
||||
}
|
||||
|
||||
@Test(arguments: Array(1...7))
|
||||
@@ -219,13 +228,13 @@ struct CalendarEnhancementTests {
|
||||
let settings = AppSettings(defaults: defaults, calendarLocale: Locale(identifier: "en_US"))
|
||||
#expect(settings.calendarFeatures == [.lunar, .holidays, .seasonalInfo])
|
||||
#expect(settings.calendarPreferences.display == .lunar)
|
||||
#expect(settings.calendarPreferences.showHolidayOverview && settings.calendarPreferences.showAlmanac)
|
||||
#expect(settings.calendarPreferences.showHolidayOverview)
|
||||
let backup = try JSONDecoder().decode(SettingsDocument.self, from: JSONEncoder().encode(settings.exportDocument()))
|
||||
settings.calendarFeatures = [.weekdays]
|
||||
settings.calendarPreferences.showAlmanac = false
|
||||
settings.calendarFeatures = []
|
||||
settings.calendarPreferences.showHolidayOverview = false
|
||||
settings.apply(backup)
|
||||
#expect(settings.calendarFeatures == [.lunar, .holidays, .seasonalInfo])
|
||||
#expect(settings.calendarPreferences.showAlmanac)
|
||||
#expect(settings.calendarPreferences.showHolidayOverview)
|
||||
}
|
||||
|
||||
@Test func displayPreferencesRoundTripWithoutSyncingLocalCalendarIDs() throws {
|
||||
|
||||
@@ -183,6 +183,21 @@ struct CalendarTests {
|
||||
#expect(CalendarEngine.selection(old, in: CalendarMonth(year: 2024, month: 4), timeZone: newZone)?.id == "2024-04-01")
|
||||
}
|
||||
|
||||
@Test func retiredWeekdayTogglePreservesOtherCalendarChoices() throws {
|
||||
let name = "CalendarRetiredWeekdayTests.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: name))
|
||||
defer { defaults.removePersistentDomain(forName: name) }
|
||||
defaults.set(["weekdays", "lunar"], forKey: "calendarFeatures")
|
||||
let settings = AppSettings(defaults: defaults)
|
||||
#expect(!CalendarFeature.allCases.map(\.rawValue).contains("weekdays"))
|
||||
#expect(settings.calendarFeatures == [.lunar])
|
||||
var backup = SettingsDocument()
|
||||
backup.calendarFeatures = ["weekdays", "hijri"]
|
||||
settings.apply(backup)
|
||||
#expect(settings.calendarFeatures == [.hijri])
|
||||
#expect(settings.exportDocument().calendarFeatures == ["hijri"])
|
||||
}
|
||||
|
||||
@Test func retiredGanzhiSettingPreservesOtherCalendarChoices() throws {
|
||||
let name = "CalendarRetiredGanzhiTests.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: name))
|
||||
@@ -232,15 +247,15 @@ struct CalendarTests {
|
||||
let name = "CalendarMergedSeasonalTests.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: name))
|
||||
defer { defaults.removePersistentDomain(forName: name) }
|
||||
defaults.set(legacy + ["weekdays"], forKey: "calendarFeatures")
|
||||
defaults.set(legacy + ["lunar"], forKey: "calendarFeatures")
|
||||
let settings = AppSettings(defaults: defaults)
|
||||
let expected = Set(legacy.isEmpty ? ["weekdays"] : ["weekdays", "seasonalInfo"])
|
||||
let expected = Set(legacy.isEmpty ? ["lunar"] : ["lunar", "seasonalInfo"])
|
||||
#expect(Set(settings.calendarFeatures.map(\.rawValue)) == expected)
|
||||
#expect(!CalendarFeature.allCases.map(\.rawValue).contains("solarTerms"))
|
||||
#expect(!CalendarFeature.allCases.map(\.rawValue).contains("seasonal"))
|
||||
settings.calendarFeatures = []
|
||||
var backup = SettingsDocument()
|
||||
backup.calendarFeatures = legacy + ["weekdays"]
|
||||
backup.calendarFeatures = legacy + ["lunar"]
|
||||
settings.apply(backup)
|
||||
#expect(Set(settings.exportDocument().calendarFeatures ?? []) == expected)
|
||||
}
|
||||
@@ -272,7 +287,7 @@ struct CalendarTests {
|
||||
var oldBackup = SettingsDocument()
|
||||
oldBackup.calendarFeatures = ["plumRain", "weekdays"]
|
||||
settings.apply(oldBackup)
|
||||
#expect(settings.calendarFeatures == [.seasonalInfo, .weekdays])
|
||||
#expect(settings.exportDocument().calendarFeatures == ["seasonalInfo", "weekdays"])
|
||||
#expect(settings.calendarFeatures == [.seasonalInfo])
|
||||
#expect(settings.exportDocument().calendarFeatures == ["seasonalInfo"])
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user