diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 84e2bb6..6ae8aeb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 946174e..e432e8c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -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; diff --git a/Packages/XStatsKit/Sources/Localization/CalendarTranslations.swift b/Packages/XStatsKit/Sources/Localization/CalendarTranslations.swift index d93afdf..c2cc42a 100644 --- a/Packages/XStatsKit/Sources/Localization/CalendarTranslations.swift +++ b/Packages/XStatsKit/Sources/Localization/CalendarTranslations.swift @@ -14,7 +14,6 @@ extension Translations { 星期日 Sunday 农历 Lunar calendar 黄历 Chinese almanac -星期 Weekdays 中国大陆节假日与调休 Mainland China holidays and makeup workdays 传统与公历节日 Festivals 节气与时令 Solar terms and seasons diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv index 959b048..c95f6be 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv @@ -1313,7 +1313,6 @@ Apple 智能 Apple Intelligence 星期一 الاثنين 星期日 الأحد 农历 التقويم القمري الصيني -星期 أيام الأسبوع 中国大陆节假日与调休 العطلات وأيام العمل التعويضية في البر الرئيسي للصين 传统与公历节日 المناسبات التقليدية والميلادية 节气与时令 الفصول الشمسية والفترات الموسمية diff --git a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv index 86b4dd7..e88d9ae 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv @@ -1313,7 +1313,6 @@ Apple 智能 Apple Intelligence 星期一 Montag 星期日 Sonntag 农历 Chinesischer Mondkalender -星期 Wochentage 中国大陆节假日与调休 Feiertage und Ausgleichsarbeitstage in Festlandchina 传统与公历节日 Traditionelle und gregorianische Feste 节气与时令 Sonnenperioden und Jahreszeiten diff --git a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv index ffd3d19..07937d1 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv @@ -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 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv index ce4a1e7..b34296e 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv @@ -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 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv index 8e29e33..ec44d56 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv @@ -1313,7 +1313,6 @@ Apple 智能 Apple Intelligence 星期一 月曜日 星期日 日曜日 农历 旧暦 -星期 曜日 中国大陆节假日与调休 中国本土の祝日・振替出勤日 传统与公历节日 伝統と西暦の祝祭日 节气与时令 節気と季節の節目 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv index 904108e..91a2c80 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv @@ -1313,7 +1313,6 @@ Apple 智能 Apple Intelligence 星期一 월요일 星期日 일요일 农历 음력 -星期 요일 中国大陆节假日与调休 중국 본토 공휴일 및 대체 근무일 传统与公历节日 전통 및 양력 기념일 节气与时令 절기와 계절 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv index 4b74667..d732e83 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv @@ -1314,7 +1314,6 @@ Apple 智能 Apple Intelligence 星期一 星期一 星期日 星期日 农历 農曆 -星期 星期 中国大陆节假日与调休 中國大陸節假日與調休 传统与公历节日 傳統與公曆節日 节气与时令 節氣與時令 diff --git a/Packages/XStatsKit/Sources/WidgetData/WidgetSnapshot.swift b/Packages/XStatsKit/Sources/WidgetData/WidgetSnapshot.swift index f405f57..3f59368 100644 --- a/Packages/XStatsKit/Sources/WidgetData/WidgetSnapshot.swift +++ b/Packages/XStatsKit/Sources/WidgetData/WidgetSnapshot.swift @@ -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]? diff --git a/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift b/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift index 9cbf630..366a1f1 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift @@ -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) diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanac.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanac.swift index 30d8e5a..6820781 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanac.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanac.swift @@ -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 = Set(CalendarFeature.allCases), - showsAlmanac: Bool = true) -> WidgetSnapshot.CalendarSummary { + static func widgetSummary(for day: CalendarDay, features: Set = 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, diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanacView.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanacView.swift index 448d882..66c0b78 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanacView.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanacView.swift @@ -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) } diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarEngine.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarEngine.swift index a613a39..6e6ee91 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarEngine.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarEngine.swift @@ -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 { defaults(for: .current) } /// 默认值依据使用地区,不依据应用语言或设备销售地;其他地区可自行启用。 public static func defaults(for locale: Locale) -> Set { - 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 { - // 已移除的月历干支(ganzhi)与未知选项一起忽略,不影响其他已保存的选择。 + // 星期固定显示;已移除的 weekdays、ganzhi 与未知选项一起忽略,不影响其他选择。 var features = Set(rawValues.compactMap(Self.init(rawValue:))) // 使用新的设置键也使 Widget 的旧摘要失效,避免复用只包含节气或时令的旧缓存。 if rawValues.contains(where: { ["solarTerms", "seasonal", "dogDays", "plumRain"].contains($0) }) { diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPopover.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPopover.swift index d277dc6..d8f9f4c 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPopover.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPopover.swift @@ -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 } diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPreferences.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPreferences.swift index 564b15b..26be6a5 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPreferences.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPreferences.swift @@ -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 } diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift index 7613cf7..f794ccc 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift @@ -71,11 +71,6 @@ struct CalendarOptionsPopover: View { } } } - GroupRow { - SettingRow(title: tr("黄历")) { - DSToggle(isOn: $settings.calendarPreferences.showAlmanac, label: tr("黄历")) - } - } GroupRow { Text(tr("公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。")) .dsFont(.xs) diff --git a/Packages/XStatsKit/Sources/XStatsUI/Snapshot/SnapshotRenderer.swift b/Packages/XStatsKit/Sources/XStatsUI/Snapshot/SnapshotRenderer.swift index 68e1fa7..5c664bd 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Snapshot/SnapshotRenderer.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Snapshot/SnapshotRenderer.swift @@ -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")) diff --git a/Packages/XStatsKit/Tests/XStatsUITests/CalendarAlmanacTests.swift b/Packages/XStatsKit/Tests/XStatsUITests/CalendarAlmanacTests.swift index 1f319d5..ac7ea25 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/CalendarAlmanacTests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/CalendarAlmanacTests.swift @@ -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) } diff --git a/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift b/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift index 8d48758..9a096e2 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift @@ -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 = mainland ? [.lunar, .weekdays, .holidays, .festivals, .seasonalInfo] : [.weekdays] + let expected: Set = 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 { diff --git a/Packages/XStatsKit/Tests/XStatsUITests/CalendarTests.swift b/Packages/XStatsKit/Tests/XStatsUITests/CalendarTests.swift index 3fbffd7..1ae8176 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/CalendarTests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/CalendarTests.swift @@ -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"]) } }