From 1e0ff1236e0419ef2cb4ecc6384d43e312c0a6c5 Mon Sep 17 00:00:00 2001 From: ysicing Date: Wed, 30 Sep 2026 20:59:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor(calendar):=20=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E8=8A=82=E6=B0=94=E4=B8=8E=E6=97=B6=E4=BB=A4=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ARCHITECTURE.md | 10 ++-- DEVELOPMENT.md | 2 +- .../Localization/CalendarTranslations.swift | 3 +- .../Sources/Localization/Resources/ar.tsv | 3 +- .../Sources/Localization/Resources/de.tsv | 3 +- .../Sources/Localization/Resources/es.tsv | 3 +- .../Sources/Localization/Resources/fr.tsv | 3 +- .../Sources/Localization/Resources/ja.tsv | 3 +- .../Sources/Localization/Resources/ko.tsv | 3 +- .../Localization/Resources/zh-Hant.tsv | 3 +- .../XStatsUI/AppKit/AppController.swift | 2 +- .../XStatsUI/Calendar/CalendarAlmanac.swift | 4 +- .../Calendar/CalendarAlmanacView.swift | 11 +++-- .../XStatsUI/Calendar/CalendarEngine.swift | 18 +++---- .../LocalizationTests/LocalizationTests.swift | 2 +- .../CalendarEnhancementTests.swift | 8 ++-- .../Tests/XStatsUITests/CalendarTests.swift | 48 ++++++++++++++++--- 17 files changed, 80 insertions(+), 49 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 704ab2f..4128c77 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -36,11 +36,13 @@ its displayed count resets at local midnight even if the main app has not refres The tomorrow-work widget uses the official 2026 holiday schedule to distinguish statutory holidays, adjusted days off, ordinary weekends, and makeup workdays; later years need their own confirmed schedule before receiving those specific labels. -The single seasonal-calendar setting covers dog days, traditional plum-rain days, and the -nine cold periods. Month cells and the large month widget show only phase starts and the +The combined solar-term and seasonal setting covers solar terms, dog days, traditional +plum-rain days, and the nine cold periods. Month cells and the large month widget show solar +terms on their dates, phase starts and the plum-rain exit; the small daily widget and selected-day details keep the exact day count. -Existing dog-day or plum-rain -preferences migrate to the combined setting. +Existing solar-term, seasonal, dog-day or plum-rain preferences migrate to the combined +`seasonalInfo` setting if any were enabled. The new key invalidates old widget summaries +so both solar terms and seasonal annotations are regenerated. Things that are easy to get wrong and are handled on purpose: diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index cd82671..ce9feae 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -55,7 +55,7 @@ GitHub Actions 只在分支 push 且 `server/**` 发生变化时构建并发布 有 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)默认启用农历、星期、大陆节假日与调休、传统与公历节日、节气、黄历及假期概览;其他地区(含台湾、香港、澳门)默认只启用星期。三伏/梅雨/数九、藏历与回历各地区均默认关闭。月历不再显示干支,也不提供对应开关;年月日干支只在启用黄历后的日期详情中显示,旧偏好与备份中的干支选项被忽略,其他选项保留。用户可自行启用或关闭,首次默认值保存后不随语言、地区变化重置;已有本机偏好及旧备份保持原选择。每周起始日首次采用 macOS 系统日历设置并保存,支持星期日到星期六,手动选择及备份恢复优先;后续系统设置变化不覆盖已保存的值,主应用月历与小组件使用同一周起始日。大陆节假日数据不能用于推断其他地区的法定安排。单日及整月小组件遵循日历选项,未启用大陆节假日时“明天上班吗”提示“仅支持中国大陆节假日”;黄历关闭后不再计算宜忌摘要。 +日历首次初始化按 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 27f1ac7..d93afdf 100644 --- a/Packages/XStatsKit/Sources/Localization/CalendarTranslations.swift +++ b/Packages/XStatsKit/Sources/Localization/CalendarTranslations.swift @@ -17,10 +17,9 @@ extension Translations { 星期 Weekdays 中国大陆节假日与调休 Mainland China holidays and makeup workdays 传统与公历节日 Festivals -二十四节气 Solar terms +节气与时令 Solar terms and seasons 三伏天 Dog days 梅雨天 Plum-rain days -三伏·梅雨·数九 Fu · Plum rain · Nines 初伏 Early Fu 中伏 Middle Fu 末伏 Late Fu diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv index e5903b0..959b048 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv @@ -1316,10 +1316,9 @@ Apple 智能 Apple Intelligence 星期 أيام الأسبوع 中国大陆节假日与调休 العطلات وأيام العمل التعويضية في البر الرئيسي للصين 传统与公历节日 المناسبات التقليدية والميلادية -二十四节气 الفصول الشمسية الأربعة والعشرون +节气与时令 الفصول الشمسية والفترات الموسمية 三伏天 أيام القيظ 梅雨天 أيام أمطار البرقوق التقليدية -三伏·梅雨·数九 فو · ميو · التسعات 初伏 فو الأولى 中伏 فو الوسطى 末伏 فو الأخيرة diff --git a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv index d2451d6..86b4dd7 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv @@ -1316,10 +1316,9 @@ Apple 智能 Apple Intelligence 星期 Wochentage 中国大陆节假日与调休 Feiertage und Ausgleichsarbeitstage in Festlandchina 传统与公历节日 Traditionelle und gregorianische Feste -二十四节气 24 Sonnenperioden +节气与时令 Sonnenperioden und Jahreszeiten 三伏天 Hundstage 梅雨天 Traditionelle Pflaumenregenzeit -三伏·梅雨·数九 Fu · Meiyu · Neuner 初伏 Fu 1 中伏 Fu 2 末伏 Fu 3 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv index 694181c..ffd3d19 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv @@ -1316,10 +1316,9 @@ Apple 智能 Apple Intelligence 星期 Días de la semana 中国大陆节假日与调休 Festivos y días laborales compensatorios de China continental 传统与公历节日 Fiestas tradicionales y gregorianas -二十四节气 24 términos solares +节气与时令 Términos solares y períodos estacionales 三伏天 Días caniculares 梅雨天 Lluvias de ciruela tradicionales -三伏·梅雨·数九 Fu · Meiyu · Nueves 初伏 Fu inicial 中伏 Fu medio 末伏 Fu final diff --git a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv index d4555aa..ce4a1e7 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv @@ -1316,10 +1316,9 @@ Apple 智能 Apple Intelligence 星期 Jours de la semaine 中国大陆节假日与调休 Jours fériés et jours de travail compensatoires en Chine continentale 传统与公历节日 Fêtes traditionnelles et grégoriennes -二十四节气 24 périodes solaires +节气与时令 Termes solaires et périodes saisonnières 三伏天 Jours caniculaires 梅雨天 Pluies des prunes traditionnelles -三伏·梅雨·数九 Fu · Meiyu · Neuvaines 初伏 Fu initial 中伏 Fu médian 末伏 Fu final diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv index d76fac8..8e29e33 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv @@ -1316,10 +1316,9 @@ Apple 智能 Apple Intelligence 星期 曜日 中国大陆节假日与调休 中国本土の祝日・振替出勤日 传统与公历节日 伝統と西暦の祝祭日 -二十四节气 二十四節気 +节气与时令 節気と季節の節目 三伏天 三伏 梅雨天 入梅・出梅 -三伏·梅雨·数九 三伏・梅雨・数九 初伏 初伏 中伏 中伏 末伏 末伏 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv index 5e11881..904108e 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv @@ -1316,10 +1316,9 @@ Apple 智能 Apple Intelligence 星期 요일 中国大陆节假日与调休 중국 본토 공휴일 및 대체 근무일 传统与公历节日 전통 및 양력 기념일 -二十四节气 24절기 +节气与时令 절기와 계절 三伏天 삼복 梅雨天 입매·출매 -三伏·梅雨·数九 삼복·매우·구구 初伏 초복 中伏 중복 末伏 말복 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv index 90d3974..4b74667 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv @@ -1317,10 +1317,9 @@ Apple 智能 Apple Intelligence 星期 星期 中国大陆节假日与调休 中國大陸節假日與調休 传统与公历节日 傳統與公曆節日 -二十四节气 二十四節氣 +节气与时令 節氣與時令 三伏天 三伏天 梅雨天 梅雨天 -三伏·梅雨·数九 三伏·梅雨·數九 初伏 初伏 中伏 中伏 末伏 末伏 diff --git a/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift b/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift index 9243e13..9cbf630 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift @@ -491,7 +491,7 @@ public final class AppController: NSObject, NSApplicationDelegate { calendarFirstWeekday: settings.calendarFirstWeekday, showsLunar: settings.calendarFeatures.contains(.lunar), calendarDays: calendarDays, monthSummaries: monthSummaries, - showsSeasonal: settings.calendarFeatures.contains(.seasonal), + showsSeasonal: settings.calendarFeatures.contains(.seasonalInfo), calendarDataVersion: dataVersion, showsAlmanac: settings.calendarPreferences.showAlmanac) guard snapshot != previous, widgetStore.save(snapshot) else { return } for kind in snapshot.changedWidgetKinds(from: previous) { diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanac.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanac.swift index 2eff79d..30d8e5a 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanac.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanac.swift @@ -69,9 +69,9 @@ extension CalendarEngine { schedule = day.isWeekend ? .weekend : .work } let almanac = showsAlmanac ? almanac(for: day) : nil - let seasons = features.contains(.seasonal) ? [day.dogDays, day.plumRain, day.nineDays].compactMap { $0 } : [] + 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(.solarTerms) ? day.solarTerm : nil, + solarTerm: features.contains(.seasonalInfo) ? day.solarTerm : nil, schedule: schedule, holidayName: features.contains(.holidays) ? day.holiday?.name : nil, twelveStar: almanac?.twelveStar, isEcliptic: almanac?.twelveStarIsLucky ?? false, recommends: Array(almanac?.recommends.prefix(3) ?? []), diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanacView.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanacView.swift index 4e3bf9a..448d882 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanacView.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarAlmanacView.swift @@ -193,7 +193,7 @@ struct CalendarAlmanacView: View { Text(row.value).frame(maxWidth: .infinity, alignment: .trailing) } } - if features.contains(.seasonal), day.plumRain != nil { + if features.contains(.seasonalInfo), day.plumRain != nil { Text(tr("按传统历法推算,并非天气预报")).font(.caption2).foregroundStyle(.secondary) } } @@ -211,10 +211,11 @@ struct CalendarAlmanacView: View { if features.contains(.festivals), !day.festivals.isEmpty { rows.append(.init(feature: .festivals, value: day.festivals.map { tr($0) }.joined(separator: " · "))) } - if features.contains(.solarTerms), let value = day.solarTerm { rows.append(.init(feature: .solarTerms, value: tr(value))) } - if features.contains(.seasonal) { - let seasons = [day.dogDays, day.plumRain, day.nineDays].compactMap { $0 } - if !seasons.isEmpty { rows.append(.init(feature: .seasonal, value: seasons.map(tr).joined(separator: " · "))) } + if features.contains(.seasonalInfo) { + let values = [day.solarTerm, day.dogDays, day.plumRain, day.nineDays].compactMap { $0 } + if !values.isEmpty { + rows.append(.init(feature: .seasonalInfo, value: values.map(tr).joined(separator: " · "))) + } } return rows + CalendarEngine.additionalCalendars(for: day, features: features) } diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarEngine.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarEngine.swift index e0ea7e5..4a64d57 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarEngine.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarEngine.swift @@ -9,13 +9,13 @@ import WidgetData /// 公历是月历的坐标;其余信息可独立隐藏,设置以稳定 rawValue 保存和同步。 public enum CalendarFeature: String, CaseIterable, Identifiable, Sendable { - case lunar, weekdays, holidays, festivals, solarTerms, seasonal, tibetan, hijri + case lunar, weekdays, 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, .solarTerms] : [.weekdays] + locale.region?.identifier == "CN" ? [.lunar, .weekdays, .holidays, .festivals, .seasonalInfo] : [.weekdays] } var title: String { @@ -24,18 +24,20 @@ public enum CalendarFeature: String, CaseIterable, Identifiable, Sendable { case .weekdays: tr("星期") case .holidays: tr("中国大陆节假日与调休") case .festivals: tr("传统与公历节日") - case .solarTerms: tr("二十四节气") - case .seasonal: tr("三伏·梅雨·数九") + case .seasonalInfo: tr("节气与时令") case .tibetan: tr("藏历") case .hijri: tr("回历") } } - /// 旧偏好和备份里的两个独立开关合并为一个;任一项曾开启就保留用户的显示意愿。 + /// 旧偏好与备份中的节气、时令开关合并;任一项曾开启就保留用户的显示意愿。 static func restored(from rawValues: [String]) -> Set { // 已移除的月历干支(ganzhi)与未知选项一起忽略,不影响其他已保存的选择。 var features = Set(rawValues.compactMap(Self.init(rawValue:))) - if rawValues.contains("dogDays") || rawValues.contains("plumRain") { features.insert(.seasonal) } + // 使用新的设置键也使 Widget 的旧摘要失效,避免复用只包含节气或时令的旧缓存。 + if rawValues.contains(where: { ["solarTerms", "seasonal", "dogDays", "plumRain"].contains($0) }) { + features.insert(.seasonalInfo) + } return features } } @@ -79,9 +81,9 @@ struct CalendarDay: Identifiable, Equatable { /// 每格仅显示一条主注释,所有启用的信息在选中日期详情中保留。 func subtitle(features: Set) -> String { // 一九与冬至同日,阶段起点优先展示;节日和节气仍可在单日详情查看。 - if features.contains(.seasonal), let seasonalBoundary { return seasonalBoundary } + if features.contains(.seasonalInfo), let seasonalBoundary { return seasonalBoundary } if features.contains(.festivals), let festival = festivals.first { return festival } - if features.contains(.solarTerms), let solarTerm { return solarTerm } + if features.contains(.seasonalInfo), let solarTerm { return solarTerm } return features.contains(.lunar) ? lunarLabel : "" } } diff --git a/Packages/XStatsKit/Tests/LocalizationTests/LocalizationTests.swift b/Packages/XStatsKit/Tests/LocalizationTests/LocalizationTests.swift index f5a36f5..71741af 100644 --- a/Packages/XStatsKit/Tests/LocalizationTests/LocalizationTests.swift +++ b/Packages/XStatsKit/Tests/LocalizationTests/LocalizationTests.swift @@ -12,7 +12,7 @@ import Testing #expect(table.translate("最近刷新:20:53") == "Last refresh: 20:53") #expect(table.translate("12 个进程") == "12 processes") #expect(table.translate("1 个进程") == "1 process") - #expect(table.translate("三伏·梅雨·数九") == "Fu · Plum rain · Nines") + #expect(table.translate("节气与时令") == "Solar terms and seasons") #expect(table.translate("一九第2天") == "Nine 1, day 2") #expect(table.translate("不存在的文案") == nil) } diff --git a/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift b/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift index 8a2dbab..62f8b53 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift @@ -147,12 +147,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, .solarTerms] : [.weekdays] + let expected: Set = mainland ? [.lunar, .weekdays, .holidays, .festivals, .seasonalInfo] : [.weekdays] let settings = AppSettings(defaults: defaults, calendarLocale: locale) #expect(settings.calendarFeatures == expected) #expect(settings.calendarPreferences.showHolidayOverview == mainland) #expect(settings.calendarPreferences.showAlmanac == mainland) - #expect(!settings.calendarFeatures.contains(.seasonal)) + #expect(settings.calendarFeatures.contains(.seasonalInfo) == mainland) // 更改地区不应重置首次保存的默认值,明确关闭的选项和空集合也须保留。 let otherRegion = Locale(identifier: mainland ? "zh_TW" : "en_CN") @@ -217,14 +217,14 @@ struct CalendarEnhancementTests { defaults.set(["lunar", "holidays", "dogDays"], forKey: "calendarFeatures") defaults.set(Data(#"{"display":"lunar","showHolidayOverview":true}"#.utf8), forKey: "calendarPreferences") let settings = AppSettings(defaults: defaults, calendarLocale: Locale(identifier: "en_US")) - #expect(settings.calendarFeatures == [.lunar, .holidays, .seasonal]) + #expect(settings.calendarFeatures == [.lunar, .holidays, .seasonalInfo]) #expect(settings.calendarPreferences.display == .lunar) #expect(settings.calendarPreferences.showHolidayOverview && settings.calendarPreferences.showAlmanac) let backup = try JSONDecoder().decode(SettingsDocument.self, from: JSONEncoder().encode(settings.exportDocument())) settings.calendarFeatures = [.weekdays] settings.calendarPreferences.showAlmanac = false settings.apply(backup) - #expect(settings.calendarFeatures == [.lunar, .holidays, .seasonal]) + #expect(settings.calendarFeatures == [.lunar, .holidays, .seasonalInfo]) #expect(settings.calendarPreferences.showAlmanac) } diff --git a/Packages/XStatsKit/Tests/XStatsUITests/CalendarTests.swift b/Packages/XStatsKit/Tests/XStatsUITests/CalendarTests.swift index 0ef8828..3fbffd7 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/CalendarTests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/CalendarTests.swift @@ -120,15 +120,15 @@ struct CalendarTests { ] for (year, month, day, label) in examples { let date = try #require(CalendarEngine.day(year: year, month: month, day: day, timeZone: zone)) - #expect(date.subtitle(features: [.seasonal]) == label) - #expect(date.subtitle(features: [.seasonal, .festivals, .solarTerms, .lunar]) == label) + #expect(date.subtitle(features: [.seasonalInfo]) == label) + #expect(date.subtitle(features: [.seasonalInfo, .festivals, .lunar]) == label) } for (year, month, day) in [(2012, 8, 8), (2024, 6, 12), (2020, 12, 22)] { let date = try #require(CalendarEngine.day(year: year, month: month, day: day, timeZone: zone)) - #expect(date.subtitle(features: [.seasonal]).isEmpty) + #expect(date.subtitle(features: [.seasonalInfo]).isEmpty) } let june = CalendarEngine.widgetMonthSummary(year: 2024, month: 6, firstWeekday: 2, - features: [.seasonal], timeZone: zone) + features: [.seasonalInfo], timeZone: zone) #expect(june.days.first { $0.dateKey == "2024-06-11" }?.subtitle == "入梅") #expect(june.days.first { $0.dateKey == "2024-06-12" }?.subtitle == "") } @@ -227,18 +227,52 @@ struct CalendarTests { #expect(restored.calendarFirstWeekday == 1) } + @Test(arguments: [[], ["solarTerms"], ["seasonal"], ["solarTerms", "seasonal"], ["dogDays"], ["plumRain"], ["seasonalInfo"]]) + func mergedSeasonalSettingPreservesEitherLegacyChoice(legacy: [String]) throws { + let name = "CalendarMergedSeasonalTests.\(UUID())" + let defaults = try #require(UserDefaults(suiteName: name)) + defer { defaults.removePersistentDomain(forName: name) } + defaults.set(legacy + ["weekdays"], forKey: "calendarFeatures") + let settings = AppSettings(defaults: defaults) + let expected = Set(legacy.isEmpty ? ["weekdays"] : ["weekdays", "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"] + settings.apply(backup) + #expect(Set(settings.exportDocument().calendarFeatures ?? []) == expected) + } + + @Test(arguments: ["solarTerms", "seasonal"]) + func mergedSeasonalChoiceControlsTermsAndSeasonalSummaries(legacy: String) throws { + let enabled = CalendarFeature.restored(from: [legacy]) + let term = try #require(CalendarEngine.day(year: 2026, month: 9, day: 23, timeZone: zone)) + let season = try #require(CalendarEngine.day(year: 2024, month: 6, day: 11, timeZone: zone)) + #expect(CalendarEngine.widgetSummary(for: term, features: enabled).solarTerm == term.solarTerm) + let seasonalDescription = try #require(season.plumRain) + #expect(CalendarEngine.widgetSummary(for: season, features: enabled).seasonalDescriptions == [seasonalDescription]) + #expect(CalendarEngine.widgetSummary(for: term, features: []).solarTerm == nil) + #expect(CalendarEngine.widgetSummary(for: season, features: []).seasonalDescriptions == nil) + #expect(term.subtitle(features: enabled) == term.solarTerm) + #expect(season.subtitle(features: enabled) == season.seasonalBoundary) + let summary = CalendarEngine.widgetMonthSummary(year: 2026, month: 9, firstWeekday: 2, features: enabled, timeZone: zone) + #expect(summary.featureKeys == ["seasonalInfo"]) + } + @Test func oldSeasonalTogglesRestoreAsOneSetting() throws { let name = "CalendarSeasonalTests.\(UUID().uuidString)" let defaults = try #require(UserDefaults(suiteName: name)) defer { defaults.removePersistentDomain(forName: name) } defaults.set(["dogDays", "plumRain", "lunar"], forKey: "calendarFeatures") let settings = AppSettings(defaults: defaults) - #expect(settings.calendarFeatures == [.seasonal, .lunar]) + #expect(settings.calendarFeatures == [.seasonalInfo, .lunar]) var oldBackup = SettingsDocument() oldBackup.calendarFeatures = ["plumRain", "weekdays"] settings.apply(oldBackup) - #expect(settings.calendarFeatures == [.seasonal, .weekdays]) - #expect(settings.exportDocument().calendarFeatures == ["seasonal", "weekdays"]) + #expect(settings.calendarFeatures == [.seasonalInfo, .weekdays]) + #expect(settings.exportDocument().calendarFeatures == ["seasonalInfo", "weekdays"]) } }