feat(calendar): 按语言与地区调整日历显示和默认选项
This commit is contained in:
1 parent
e8b151d205
commit
678303187d
25 files changed
+248
-73
No files matched your search
+2
-1
@@ -54,7 +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 天的请假建议只基于已公布的中国调休与双休安排,跨到未知年份时停止推算。
|
||||
菜单栏日历提供“日期与星期”“自定义”显示方式;应用语言为简体中文或繁体中文(含自动检测为中文)时,额外提供“日期与农历”“农历”。切到非中文时,已保存的农历方式暂以“日期与星期”呈现,切回中文恢复原选择,不改写备份。自定义可自由组合 y/M/d 字段与分隔符,年份可省略,输入时显示预览并提示无效格式,独立于月历的星期开关;支持农历字号与对比度、可选悬停展开,按分钟刷新,休眠或关闭功能后停止。日程和到期提醒默认关闭,通过明确的授权按钮请求 EventKit 权限,只在日历面板可见时查询当前 42 格日期范围;只读展示,不修改日程,也不向 Widget、设置备份或服务端传递内容。列表 ID 仅保存在本机,显示偏好参与设置备份。节假日覆盖年份来自内置 Tyme 数据并随应用更新;假期倒计时与最多 3 天的请假建议只基于已公布的中国调休与双休安排,跨到未知年份时停止推算。
|
||||
日历首次初始化按 macOS 地区设置决定默认值,不按设备销售地或应用语言判断:中国大陆(CN)默认启用农历、星期、大陆节假日与调休、传统与公历节日、节气、黄历及假期概览;其他地区(含台湾、香港、澳门)默认只启用星期。干支、三伏/梅雨/数九、藏历与回历各地区均默认关闭。用户可自行启用或关闭,首次默认值保存后不随语言、地区变化重置;已有本机偏好及旧备份保持原选择。大陆节假日数据不能用于推断其他地区的法定安排。单日及整月小组件遵循日历选项,未启用大陆节假日时“明天上班吗”显示无法判断;黄历关闭后不再计算宜忌摘要。
|
||||
月历使用固定六行布局,日期 ID 在生成日期时计算一次。面板打开时测量初始高度,后续翻月、详情与日程内容变化在内部滚动,不从内容几何回调反向调整窗口,避免窗口高度与懒布局估算形成反馈循环。
|
||||
月历计算仅缓存最近一个月份(包含周起始日和时区),供实际面板、高度测量和重复打开共用;翻月替换缓存,避免浏览历史导致内存累积。日程日期标记在工作 actor 内随查询结果生成,界面不再逐格扫描日程列表;连续 EventKit 通知按 300 毫秒窗口合并,关闭面板时取消待执行刷新。
|
||||
用于正式签名的 Apple Developer 团队须为主应用和 Widget 扩展启用同一个 App Group;
|
||||
|
||||
@@ -13,8 +13,9 @@ extension Translations {
|
||||
星期一 Monday
|
||||
星期日 Sunday
|
||||
农历 Lunar calendar
|
||||
黄历 Chinese almanac
|
||||
星期 Weekdays
|
||||
法定节假日与调休 Holidays and makeup workdays
|
||||
中国大陆节假日与调休 Mainland China holidays and makeup workdays
|
||||
传统与公历节日 Festivals
|
||||
二十四节气 Solar terms
|
||||
干支 Sexagenary cycle
|
||||
|
||||
@@ -1314,7 +1314,7 @@ Apple 智能 Apple Intelligence
|
||||
星期日 الأحد
|
||||
农历 التقويم القمري الصيني
|
||||
星期 أيام الأسبوع
|
||||
法定节假日与调休 العطلات وأيام العمل التعويضية
|
||||
中国大陆节假日与调休 العطلات وأيام العمل التعويضية في البر الرئيسي للصين
|
||||
传统与公历节日 المناسبات التقليدية والميلادية
|
||||
二十四节气 الفصول الشمسية الأربعة والعشرون
|
||||
干支 الدورة الستينية
|
||||
@@ -1623,3 +1623,4 @@ GitHub 项目 مشروع GitHub
|
||||
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 يفتح صفحة عمليات XStats عند تفعيل الميزة، أو مراقب نشاط النظام عند إيقافها.
|
||||
不能在 XStats 内卸载或退出 XStats 自身 لا يمكن لهذه الأداة إلغاء تثبيت XStats نفسه أو إنهاؤه
|
||||
应用本体未能移动;已将 {} 项残留移到废纸篓,约 {}。请解决错误后重试。 تعذر نقل التطبيق. نُقلت {} من العناصر المرتبطة إلى سلة المهملات (نحو {}). عالج الخطأ ثم أعد المحاولة.
|
||||
黄历 التقويم الصيني التقليدي
|
||||
|
@@ -1314,7 +1314,7 @@ Apple 智能 Apple Intelligence
|
||||
星期日 Sonntag
|
||||
农历 Chinesischer Mondkalender
|
||||
星期 Wochentage
|
||||
法定节假日与调休 Feiertage und Ausgleichsarbeitstage
|
||||
中国大陆节假日与调休 Feiertage und Ausgleichsarbeitstage in Festlandchina
|
||||
传统与公历节日 Traditionelle und gregorianische Feste
|
||||
二十四节气 24 Sonnenperioden
|
||||
干支 Sechzigjahreszyklus
|
||||
@@ -1623,3 +1623,4 @@ GitHub 项目 GitHub-Projekt
|
||||
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 Öffnet bei aktivierter Prozessfunktion die XStats-Prozessseite, andernfalls die macOS-Aktivitätsanzeige.
|
||||
不能在 XStats 内卸载或退出 XStats 自身 XStats kann sich mit diesem Werkzeug nicht selbst deinstallieren oder beenden
|
||||
应用本体未能移动;已将 {} 项残留移到废纸篓,约 {}。请解决错误后重试。 Die App konnte nicht verschoben werden. Zugehörige Objekte im Papierkorb: {} (ca. {}). Beheben Sie den Fehler und versuchen Sie es erneut.
|
||||
黄历 Chinesischer Almanach
|
||||
|
@@ -1314,7 +1314,7 @@ Apple 智能 Apple Intelligence
|
||||
星期日 Domingo
|
||||
农历 Calendario lunar chino
|
||||
星期 Días de la semana
|
||||
法定节假日与调休 Festivos y días laborales compensatorios
|
||||
中国大陆节假日与调休 Festivos y días laborales compensatorios de China continental
|
||||
传统与公历节日 Fiestas tradicionales y gregorianas
|
||||
二十四节气 24 términos solares
|
||||
干支 Ciclo sexagenario
|
||||
@@ -1623,3 +1623,4 @@ GitHub 项目 Proyecto en GitHub
|
||||
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 Abre Procesos de XStats si la función está activada; si está desactivada, abre el Monitor de Actividad de macOS.
|
||||
不能在 XStats 内卸载或退出 XStats 自身 XStats no puede desinstalarse ni cerrarse desde esta herramienta
|
||||
应用本体未能移动;已将 {} 项残留移到废纸篓,约 {}。请解决错误后重试。 No se pudo mover la app. Se movieron {} elementos relacionados a la Papelera (aprox. {}). Resuelve el error e inténtalo de nuevo.
|
||||
黄历 Almanaque chino
|
||||
|
@@ -1314,7 +1314,7 @@ Apple 智能 Apple Intelligence
|
||||
星期日 Dimanche
|
||||
农历 Calendrier lunaire chinois
|
||||
星期 Jours de la semaine
|
||||
法定节假日与调休 Jours fériés et jours de travail compensatoires
|
||||
中国大陆节假日与调休 Jours fériés et jours de travail compensatoires en Chine continentale
|
||||
传统与公历节日 Fêtes traditionnelles et grégoriennes
|
||||
二十四节气 24 périodes solaires
|
||||
干支 Cycle sexagésimal
|
||||
@@ -1623,3 +1623,4 @@ GitHub 项目 Projet GitHub
|
||||
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 Ouvre les processus XStats si la fonction est activée, sinon le Moniteur d’activité de macOS.
|
||||
不能在 XStats 内卸载或退出 XStats 自身 XStats ne peut pas se désinstaller ou se quitter depuis cet outil
|
||||
应用本体未能移动;已将 {} 项残留移到废纸篓,约 {}。请解决错误后重试。 L’app n’a pas pu être déplacée. Éléments associés placés dans la Corbeille : {} (environ {}). Corrigez l’erreur et réessayez.
|
||||
黄历 Almanach chinois
|
||||
|
@@ -1314,7 +1314,7 @@ Apple 智能 Apple Intelligence
|
||||
星期日 日曜日
|
||||
农历 旧暦
|
||||
星期 曜日
|
||||
法定节假日与调休 祝日と振替出勤日
|
||||
中国大陆节假日与调休 中国本土の祝日・振替出勤日
|
||||
传统与公历节日 伝統と西暦の祝祭日
|
||||
二十四节气 二十四節気
|
||||
干支 干支
|
||||
@@ -1623,3 +1623,4 @@ GitHub 项目 GitHub プロジェクト
|
||||
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 プロセス機能がオンの場合は XStats のプロセスページを、オフの場合はシステムのアクティビティモニタを開きます。
|
||||
不能在 XStats 内卸载或退出 XStats 自身 このツールからXStats自身をアンインストールまたは終了することはできません
|
||||
应用本体未能移动;已将 {} 项残留移到废纸篓,约 {}。请解决错误后重试。 アプリ本体を移動できませんでした。関連項目を{}件(約{})ゴミ箱に移動しました。エラーを解決して再試行してください。
|
||||
黄历 中国の伝統暦
|
||||
|
@@ -1314,7 +1314,7 @@ Apple 智能 Apple Intelligence
|
||||
星期日 일요일
|
||||
农历 음력
|
||||
星期 요일
|
||||
法定节假日与调休 공휴일 및 대체 근무일
|
||||
中国大陆节假日与调休 중국 본토 공휴일 및 대체 근무일
|
||||
传统与公历节日 전통 및 양력 기념일
|
||||
二十四节气 24절기
|
||||
干支 간지
|
||||
@@ -1623,3 +1623,4 @@ GitHub 项目 GitHub 프로젝트
|
||||
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 프로세스 기능이 켜져 있으면 XStats 프로세스 페이지를, 꺼져 있으면 시스템 활성 상태 보기를 엽니다.
|
||||
不能在 XStats 内卸载或退出 XStats 自身 이 도구에서 XStats 자체를 제거하거나 종료할 수 없습니다
|
||||
应用本体未能移动;已将 {} 项残留移到废纸篓,约 {}。请解决错误后重试。 앱 본체를 이동하지 못했습니다. 관련 항목 {}개(약 {})를 휴지통으로 이동했습니다. 오류를 해결한 후 다시 시도하세요.
|
||||
黄历 중국 전통 역서
|
||||
|
@@ -1315,7 +1315,7 @@ Apple 智能 Apple Intelligence
|
||||
星期日 星期日
|
||||
农历 農曆
|
||||
星期 星期
|
||||
法定节假日与调休 法定節假日與調休
|
||||
中国大陆节假日与调休 中國大陸節假日與調休
|
||||
传统与公历节日 傳統與公曆節日
|
||||
二十四节气 二十四節氣
|
||||
干支 干支
|
||||
@@ -1624,3 +1624,4 @@ GitHub 项目 GitHub 專案
|
||||
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 程序功能開啟時開啟 XStats 程序頁,關閉時開啟系統活動監視器。
|
||||
不能在 XStats 内卸载或退出 XStats 自身 不能在 XStats 內卸載或退出 XStats 自身
|
||||
应用本体未能移动;已将 {} 项残留移到废纸篓,约 {}。请解决错误后重试。 應用本體未能移動;已將 {} 項殘留移到廢紙簍,約 {}。請解決錯誤後重試。
|
||||
黄历 黃曆
|
||||
|
@@ -166,6 +166,8 @@ public struct WidgetSnapshot: Codable, Equatable, Sendable {
|
||||
public var showsLunar: Bool
|
||||
/// 可选以兼容旧版摘要;单日日历仅在用户开启时显示具体时令天数。
|
||||
public var showsSeasonal: Bool?
|
||||
/// nil 表示旧快照尚未按黄历开关生成,主应用会按需更新一次。
|
||||
public var showsAlmanac: Bool?
|
||||
/// 可选以兼容旧版主应用写入的 App Group 摘要。
|
||||
public var calendarDays: [CalendarSummary]?
|
||||
/// 当前与下个月的六周网格;旧版缓存缺失时 Widget 仍显示公历日期。
|
||||
@@ -176,9 +178,9 @@ public struct WidgetSnapshot: Codable, Equatable, Sendable {
|
||||
localUsageEnabled: Bool? = nil,
|
||||
publicIPEnabled: Bool = false,
|
||||
addresses: [Address] = [], language: String = "system",
|
||||
calendarFirstWeekday: Int = 2, showsLunar: Bool = true,
|
||||
calendarFirstWeekday: Int = 2, showsLunar: Bool = Locale.current.region?.identifier == "CN",
|
||||
calendarDays: [CalendarSummary]? = nil, monthSummaries: [MonthSummary]? = nil,
|
||||
showsSeasonal: Bool? = nil, calendarDataVersion: String? = nil) {
|
||||
showsSeasonal: Bool? = nil, calendarDataVersion: String? = nil, showsAlmanac: Bool? = nil) {
|
||||
self.aiEnabled = aiEnabled
|
||||
self.quotas = quotas
|
||||
self.dailyTokens = dailyTokens
|
||||
@@ -189,6 +191,7 @@ public struct WidgetSnapshot: Codable, Equatable, Sendable {
|
||||
self.calendarFirstWeekday = calendarFirstWeekday
|
||||
self.showsLunar = showsLunar
|
||||
self.showsSeasonal = showsSeasonal
|
||||
self.showsAlmanac = showsAlmanac
|
||||
self.calendarDays = calendarDays
|
||||
self.monthSummaries = monthSummaries
|
||||
self.calendarDataVersion = calendarDataVersion
|
||||
|
||||
@@ -402,6 +402,7 @@ 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
|
||||
@@ -422,19 +423,22 @@ public final class AppController: NSObject, NSApplicationDelegate {
|
||||
let todayKey = CalendarEngine.today(at: today)?.id
|
||||
let dataVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
|
||||
let reusesCalendar = previous.calendarDataVersion == dataVersion
|
||||
let monthFeatures = settings.calendarFeatures.map(\.rawValue).sorted()
|
||||
// 多预备几天,Widget 午夜换日时无需唤醒主应用;主应用运行期间按小时补足窗口。
|
||||
let calendarDays: [WidgetSnapshot.CalendarSummary] = {
|
||||
if reusesCalendar, previous.showsSeasonal != nil, previous.calendarDays?.count == 8,
|
||||
if reusesCalendar, previous.showsSeasonal != nil,
|
||||
previous.showsAlmanac == settings.calendarPreferences.showAlmanac,
|
||||
previous.monthSummaries?.first?.featureKeys == monthFeatures, previous.calendarDays?.count == 8,
|
||||
previous.calendarDays?.first?.dateKey == todayKey {
|
||||
return previous.calendarDays ?? []
|
||||
}
|
||||
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)
|
||||
return CalendarEngine.widgetSummary(for: day, features: settings.calendarFeatures,
|
||||
showsAlmanac: settings.calendarPreferences.showAlmanac)
|
||||
}
|
||||
}()
|
||||
let monthFeatures = settings.calendarFeatures.map(\.rawValue).sorted()
|
||||
let monthSummaries: [WidgetSnapshot.MonthSummary] = (0...1).compactMap { offset in
|
||||
guard let date = calendar.date(byAdding: .month, value: offset, to: today) else { return nil }
|
||||
let parts = calendar.dateComponents([.year, .month], from: date)
|
||||
@@ -488,7 +492,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
|
||||
showsLunar: settings.calendarFeatures.contains(.lunar),
|
||||
calendarDays: calendarDays, monthSummaries: monthSummaries,
|
||||
showsSeasonal: settings.calendarFeatures.contains(.seasonal),
|
||||
calendarDataVersion: dataVersion)
|
||||
calendarDataVersion: dataVersion, showsAlmanac: settings.calendarPreferences.showAlmanac)
|
||||
guard snapshot != previous, widgetStore.save(snapshot) else { return }
|
||||
for kind in snapshot.changedWidgetKinds(from: previous) {
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: kind)
|
||||
|
||||
@@ -46,9 +46,10 @@ extension CalendarEngine {
|
||||
"2026-05-04", "2026-05-05", "2026-10-05", "2026-10-06", "2026-10-07"
|
||||
]
|
||||
|
||||
static func widgetSummary(for day: CalendarDay) -> WidgetSnapshot.CalendarSummary {
|
||||
static func widgetSummary(for day: CalendarDay, features: Set<CalendarFeature> = Set(CalendarFeature.allCases),
|
||||
showsAlmanac: Bool = true) -> WidgetSnapshot.CalendarSummary {
|
||||
let schedule: WidgetSnapshot.CalendarSummary.Schedule
|
||||
if !hasHolidayData(year: day.year) {
|
||||
if !features.contains(.holidays) || !hasHolidayData(year: day.year) {
|
||||
schedule = .unknown
|
||||
} else if let holiday = day.holiday {
|
||||
if holiday.isWork {
|
||||
@@ -67,10 +68,11 @@ extension CalendarEngine {
|
||||
} else {
|
||||
schedule = day.isWeekend ? .weekend : .work
|
||||
}
|
||||
let almanac = almanac(for: day)
|
||||
let seasons = [day.dogDays, day.plumRain, day.nineDays].compactMap { $0 }
|
||||
return .init(dateKey: day.id, festivals: day.festivals,
|
||||
solarTerm: day.solarTerm, schedule: schedule, holidayName: day.holiday?.name,
|
||||
let almanac = showsAlmanac ? almanac(for: day) : nil
|
||||
let seasons = features.contains(.seasonal) ? [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,
|
||||
schedule: schedule, holidayName: features.contains(.holidays) ? day.holiday?.name : nil,
|
||||
twelveStar: almanac?.twelveStar, isEcliptic: almanac?.twelveStarIsLucky ?? false,
|
||||
recommends: Array(almanac?.recommends.prefix(3) ?? []),
|
||||
avoids: Array(almanac?.avoids.prefix(3) ?? []),
|
||||
|
||||
@@ -5,10 +5,10 @@ import Foundation
|
||||
import Localization
|
||||
import SwiftUI
|
||||
|
||||
/// 传统黄历详情:以分格表呈现同一日期的数据,正文保留 Tyme 的传统名称。
|
||||
/// 日期详情保留已启用的历法信息;黄历开启时才展示传统宜忌表。
|
||||
struct CalendarAlmanacView: View {
|
||||
let day: CalendarDay
|
||||
let almanac: CalendarAlmanac
|
||||
let almanac: CalendarAlmanac?
|
||||
let features: Set<CalendarFeature>
|
||||
private let heading = Color.dynamic(light: 0xAE743F, dark: 0xDBAC79)
|
||||
private let lucky = Color.dynamic(light: 0x288B3B, dark: 0x63CC76)
|
||||
@@ -20,13 +20,19 @@ struct CalendarAlmanacView: View {
|
||||
if features.contains(.lunar) {
|
||||
Text(day.lunarSummary).font(.system(size: 21)).foregroundStyle(.red)
|
||||
}
|
||||
Text(almanac.ganzhiSummary).font(.system(size: 13)).foregroundStyle(.secondary)
|
||||
if let almanac {
|
||||
Text(almanac.ganzhiSummary).font(.system(size: 13)).foregroundStyle(.secondary)
|
||||
} else if features.contains(.ganzhi) {
|
||||
Text(day.ganzhiSummary).font(.system(size: 13)).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
advice(tr("宜"), entries: almanac.recommends, color: lucky)
|
||||
advice(tr("忌"), entries: almanac.avoids, color: .red)
|
||||
if let almanac {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
advice(tr("宜"), entries: almanac.recommends, color: lucky)
|
||||
advice(tr("忌"), entries: almanac.avoids, color: .red)
|
||||
}
|
||||
almanacTable(almanac)
|
||||
}
|
||||
almanacTable
|
||||
supplementaryDetails
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
@@ -53,7 +59,7 @@ struct CalendarAlmanacView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var almanacTable: some View {
|
||||
private func almanacTable(_ almanac: CalendarAlmanac) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
tile(tr("纳音")) { Text(almanac.sound) }
|
||||
@@ -69,7 +75,7 @@ struct CalendarAlmanacView: View {
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Divider()
|
||||
hourTable
|
||||
hourTable(almanac)
|
||||
Divider()
|
||||
HStack(spacing: 0) {
|
||||
sideTile(tr("建除十二神"), value: almanac.duty)
|
||||
@@ -125,7 +131,7 @@ struct CalendarAlmanacView: View {
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
|
||||
private var hourTable: some View {
|
||||
private func hourTable(_ almanac: CalendarAlmanac) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Text(tr("时辰吉凶"))
|
||||
.font(.system(size: 15)).foregroundStyle(heading)
|
||||
|
||||
@@ -11,13 +11,18 @@ import WidgetData
|
||||
public enum CalendarFeature: String, CaseIterable, Identifiable, Sendable {
|
||||
case lunar, weekdays, holidays, festivals, solarTerms, ganzhi, seasonal, tibetan, hijri
|
||||
public var id: String { rawValue }
|
||||
public static let defaults: Set<Self> = [.lunar, .weekdays, .holidays, .festivals, .solarTerms]
|
||||
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, .solarTerms] : [.weekdays]
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .lunar: tr("农历")
|
||||
case .weekdays: tr("星期")
|
||||
case .holidays: tr("法定节假日与调休")
|
||||
case .holidays: tr("中国大陆节假日与调休")
|
||||
case .festivals: tr("传统与公历节日")
|
||||
case .solarTerms: tr("二十四节气")
|
||||
case .ganzhi: tr("干支")
|
||||
|
||||
@@ -56,10 +56,10 @@ struct CalendarPopover: View {
|
||||
_yearInput = State(initialValue: String(month.year))
|
||||
_selected = State(initialValue: today)
|
||||
_showsDayDetails = State(initialValue: showsDayDetails)
|
||||
_almanac = State(initialValue: showsDayDetails ? today.flatMap { CalendarEngine.almanac(for: $0) } : nil)
|
||||
_almanac = State(initialValue: nil)
|
||||
_lastTodayID = State(initialValue: today?.id)
|
||||
_days = State(initialValue: CalendarEngine.month(year: month.year, month: month.month, firstWeekday: firstWeekday))
|
||||
_holidayPlan = State(initialValue: CalendarEngine.holidayPlan(from: referenceDate ?? Date()))
|
||||
_holidayPlan = State(initialValue: nil)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -91,7 +91,7 @@ struct CalendarPopover: View {
|
||||
month = CalendarMonth(year: today.year, month: today.month)
|
||||
}
|
||||
lastTodayID = newID
|
||||
holidayPlan = CalendarEngine.holidayPlan(from: referenceDate ?? context.date)
|
||||
refreshHolidayPlan(at: referenceDate ?? context.date)
|
||||
}
|
||||
}
|
||||
.frame(width: isSnapshot ? sizing.width : nil)
|
||||
@@ -103,6 +103,8 @@ struct CalendarPopover: View {
|
||||
.overlay { RoundedRectangle(cornerRadius: DS.Radius.xl).strokeBorder(DS.Palette.border) }
|
||||
.onAppear {
|
||||
reload()
|
||||
refreshAlmanac()
|
||||
refreshHolidayPlan(at: referenceDate ?? Date())
|
||||
if !isSnapshot { model.calendarAgenda.refreshAuthorization() }
|
||||
}
|
||||
.task(id: agendaQuery) {
|
||||
@@ -123,10 +125,15 @@ struct CalendarPopover: View {
|
||||
reload()
|
||||
}
|
||||
.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())
|
||||
}
|
||||
.onChange(of: model.settings.calendarFirstWeekday) { _, _ in reload() }
|
||||
.onReceive(NotificationCenter.default.publisher(for: .NSSystemTimeZoneDidChange)) { _ in
|
||||
reload()
|
||||
holidayPlan = CalendarEngine.holidayPlan(from: referenceDate ?? Date())
|
||||
refreshHolidayPlan(at: referenceDate ?? Date())
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .NSCalendarDayChanged)) { _ in
|
||||
guard referenceDate == nil, let today = CalendarEngine.today() else { return }
|
||||
@@ -137,7 +144,7 @@ struct CalendarPopover: View {
|
||||
|
||||
private func calendarContent(today: CalendarDay?) -> some View {
|
||||
VStack(spacing: sizing.sectionSpacing) {
|
||||
if showsDayDetails, let selected, let almanac {
|
||||
if showsDayDetails, let selected {
|
||||
CalendarAlmanacView(day: selected, almanac: almanac, features: model.settings.calendarFeatures)
|
||||
if model.settings.calendarPreferences.showEvents || model.settings.calendarPreferences.showReminders {
|
||||
CalendarAgendaView(day: selected)
|
||||
@@ -263,10 +270,22 @@ struct CalendarPopover: View {
|
||||
}
|
||||
|
||||
private func refreshAlmanac() {
|
||||
guard showsDayDetails, let selected, almanac?.id != selected.id else { return }
|
||||
guard showsDayDetails, model.settings.calendarPreferences.showAlmanac, let selected else {
|
||||
almanac = nil
|
||||
return
|
||||
}
|
||||
guard almanac?.id != selected.id else { return }
|
||||
almanac = CalendarEngine.almanac(for: selected)
|
||||
}
|
||||
|
||||
private func refreshHolidayPlan(at date: Date) {
|
||||
guard model.settings.calendarFeatures.contains(.holidays), model.settings.calendarPreferences.showHolidayOverview else {
|
||||
holidayPlan = nil
|
||||
return
|
||||
}
|
||||
holidayPlan = CalendarEngine.holidayPlan(from: date)
|
||||
}
|
||||
|
||||
private func reload() {
|
||||
selected = CalendarEngine.selection(selected, in: month)
|
||||
days = CalendarEngine.month(year: month.year, month: month.month, firstWeekday: model.settings.calendarFirstWeekday)
|
||||
|
||||
@@ -9,6 +9,18 @@ public struct CalendarPreferences: Codable, Equatable, Sendable {
|
||||
public enum Display: String, Codable, CaseIterable, Identifiable, Sendable {
|
||||
case standard, dateLunar, lunar, custom
|
||||
public var id: String { rawValue }
|
||||
|
||||
static func available(for locale: Locale) -> [Self] {
|
||||
locale.language.languageCode?.identifier == "zh" ? allCases : [.standard, .custom]
|
||||
}
|
||||
|
||||
/// 切换语言只改变呈现,不覆盖已保存的农历选择,切回中文后可恢复。
|
||||
func effective(for locale: Locale) -> Self {
|
||||
if (self == .dateLunar || self == .lunar), locale.language.languageCode?.identifier != "zh" {
|
||||
return .standard
|
||||
}
|
||||
return self
|
||||
}
|
||||
var title: String {
|
||||
switch self {
|
||||
case .standard: tr("日期与星期")
|
||||
@@ -24,15 +36,20 @@ public struct CalendarPreferences: Codable, Equatable, Sendable {
|
||||
public var largeLunarText = false
|
||||
public var strongerLunarText = false
|
||||
public var openOnHover = false
|
||||
public var showHolidayOverview = true
|
||||
public var showHolidayOverview: Bool
|
||||
public var showAlmanac: Bool
|
||||
public var showEvents = false
|
||||
public var showReminders = false
|
||||
|
||||
public init() {}
|
||||
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, showEvents, showReminders
|
||||
case showHolidayOverview, showAlmanac, showEvents, showReminders
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
@@ -45,6 +62,8 @@ 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
|
||||
}
|
||||
@@ -73,7 +92,7 @@ public struct CalendarPreferences: Codable, Equatable, Sendable {
|
||||
calendar.timeZone = timeZone
|
||||
formatter.calendar = calendar
|
||||
formatter.timeZone = timeZone
|
||||
switch display {
|
||||
switch display.effective(for: locale) {
|
||||
case .standard: formatter.setLocalizedDateFormatFromTemplate("MdEEE")
|
||||
case .custom:
|
||||
// 用户明确选择顺序,不能再用本地化模板把日月重新排回月日。
|
||||
|
||||
@@ -60,6 +60,11 @@ struct CalendarOptionsPopover: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
GroupRow {
|
||||
SettingRow(title: tr("黄历")) {
|
||||
DSToggle(isOn: $settings.calendarPreferences.showAlmanac, label: tr("黄历"))
|
||||
}
|
||||
}
|
||||
GroupRow {
|
||||
Text(tr("公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。"))
|
||||
.dsFont(.xs)
|
||||
@@ -108,8 +113,11 @@ struct CalendarOptionsPopover: View {
|
||||
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(for: L10n.locale) },
|
||||
set: { settings.calendarPreferences.display = $0 }
|
||||
)) {
|
||||
ForEach(CalendarPreferences.Display.available(for: L10n.locale)) { Text($0.title).tag($0) }
|
||||
}.labelsHidden().fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,8 @@ enum SnapshotRenderer {
|
||||
settings.calendarPreferences.display = .dateLunar
|
||||
write(CalendarOptionsPopover(), model: model, appearance: appearance,
|
||||
to: outputDirectory.appendingPathComponent("calendar-lunar-\(suffix).png"))
|
||||
settings.calendarPreferences = CalendarPreferences()
|
||||
// 黄历示例明确启用;普通日历与设置截图仍使用本机地区默认值。
|
||||
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"))
|
||||
settings.calendarFeatures = Set(CalendarFeature.allCases)
|
||||
|
||||
@@ -733,7 +733,7 @@ public final class AppSettings {
|
||||
public static let alertLoadOptions = [70, 80, 90]
|
||||
public static let cpuChartOptions = [60, 180, 300]
|
||||
|
||||
public init(defaults: UserDefaults = .standard) {
|
||||
public init(defaults: UserDefaults = .standard, calendarLocale: Locale = .current) {
|
||||
self.defaults = defaults
|
||||
let isRestEnabled = defaults.bool(forKey: Keys.restEnabled)
|
||||
restEnabled = isRestEnabled
|
||||
@@ -750,12 +750,14 @@ public final class AppSettings {
|
||||
restSound = defaults.string(forKey: Keys.restSound).flatMap(RestSound.init(rawValue:)) ?? .off
|
||||
restHUDStyle = defaults.string(forKey: Keys.restHUDStyle).flatMap(RestHUDStyle.init(rawValue:)) ?? .countdown
|
||||
calendarEnabled = defaults.bool(forKey: Keys.calendarEnabled)
|
||||
calendarPreferences = defaults.data(forKey: Keys.calendarPreferences)
|
||||
.flatMap { try? JSONDecoder().decode(CalendarPreferences.self, from: $0) } ?? CalendarPreferences()
|
||||
let savedCalendarPreferences = defaults.data(forKey: Keys.calendarPreferences)
|
||||
calendarPreferences = savedCalendarPreferences
|
||||
.flatMap { try? JSONDecoder().decode(CalendarPreferences.self, from: $0) } ?? CalendarPreferences(locale: calendarLocale)
|
||||
calendarEventSourceIDs = defaults.stringArray(forKey: "calendarEventSourceIDs").map { Set($0) }
|
||||
calendarReminderSourceIDs = defaults.stringArray(forKey: "calendarReminderSourceIDs").map { Set($0) }
|
||||
calendarFeatures = defaults.stringArray(forKey: Keys.calendarFeatures)
|
||||
.map(CalendarFeature.restored(from:)) ?? CalendarFeature.defaults
|
||||
let savedCalendarFeatures = defaults.stringArray(forKey: Keys.calendarFeatures)
|
||||
calendarFeatures = savedCalendarFeatures
|
||||
.map(CalendarFeature.restored(from:)) ?? CalendarFeature.defaults(for: calendarLocale)
|
||||
calendarFirstWeekday = defaults.integer(forKey: Keys.calendarFirstWeekday) == 1 ? 1 : 2
|
||||
let items = defaults.stringArray(forKey: Keys.menuBarItems)?.compactMap(MenuBarItem.init(rawValue:))
|
||||
menuBarItems = Set(items ?? [.cpu, .memory, .network])
|
||||
@@ -828,6 +830,13 @@ public final class AppSettings {
|
||||
? defaults.integer(forKey: Keys.alertCPULoad) : 80
|
||||
cpuChartSeconds = Self.cpuChartOptions.contains(defaults.integer(forKey: Keys.cpuChartSeconds))
|
||||
? defaults.integer(forKey: Keys.cpuChartSeconds) : 60
|
||||
// 仅在没有偏好时确定默认值;后续语言、地区变化及备份恢复不重置用户选择。
|
||||
if savedCalendarPreferences == nil {
|
||||
defaults.set(try? JSONEncoder().encode(calendarPreferences), forKey: Keys.calendarPreferences)
|
||||
}
|
||||
if savedCalendarFeatures == nil {
|
||||
defaults.set(calendarFeatures.map(\.rawValue).sorted(), forKey: Keys.calendarFeatures)
|
||||
}
|
||||
}
|
||||
|
||||
/// 按固定顺序返回当前实际显示的菜单栏项目;可选模块关闭时保留展示偏好但不渲染。
|
||||
|
||||
@@ -86,7 +86,8 @@ struct LanguageSupportTests {
|
||||
let catalog = try #require(Translations.catalog(for: language))
|
||||
#expect(catalog.sourceKeys == Translations.shared.sourceKeys)
|
||||
for key in ["语言", "应用 UI 语言", "自动检测", "搜索语言", "没有匹配的语言",
|
||||
"设置", "内存", "下载并应用", "XStats 基于 OpenStats 开发,感谢原项目的开源贡献 · MIT License"] {
|
||||
"设置", "内存", "下载并应用", "中国大陆节假日与调休", "黄历",
|
||||
"XStats 基于 OpenStats 开发,感谢原项目的开源贡献 · MIT License"] {
|
||||
#expect(catalog.translate(key)?.isEmpty == false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import WidgetData
|
||||
publicIPEnabled: true,
|
||||
addresses: [.init(family: "v4", ip: "203.0.113.1", countryCode: "CN", city: nil,
|
||||
organization: nil, purityScore: 82, purityGrade: "A")],
|
||||
language: "japanese", calendarFirstWeekday: 1, showsLunar: false)
|
||||
language: "japanese", calendarFirstWeekday: 1, showsLunar: false, showsAlmanac: false)
|
||||
#expect(store.save(snapshot))
|
||||
#expect(store.load() == snapshot)
|
||||
#expect(snapshot.currentQuotas(at: now).count == 1)
|
||||
@@ -169,7 +169,7 @@ import WidgetData
|
||||
organization: nil, purityScore: 82, purityGrade: "A")
|
||||
let base = WidgetSnapshot(aiEnabled: true, quotas: [quota(fetchedAt: now)],
|
||||
dailyTokens: [.init(provider: "codex", day: now, tokens: 10)],
|
||||
publicIPEnabled: true, addresses: [address])
|
||||
publicIPEnabled: true, addresses: [address], showsLunar: true)
|
||||
|
||||
// 重新抓取但额度未变:抓取时间不显示在 Widget 上,不应消耗重载预算
|
||||
var refetched = base
|
||||
|
||||
@@ -7,6 +7,27 @@ import Testing
|
||||
|
||||
@MainActor
|
||||
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)
|
||||
#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)
|
||||
}
|
||||
|
||||
@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)
|
||||
#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(almanacOnly.twelveStar != nil && !almanacOnly.recommends.isEmpty)
|
||||
#expect(almanacOnly.schedule.needsWork == nil && almanacOnly.festivals.isEmpty)
|
||||
}
|
||||
|
||||
@Test func referenceDateMatchesAlmanacFields() throws {
|
||||
let day = try #require(CalendarEngine.day(year: 2026, month: 9, day: 23))
|
||||
let detail = try #require(CalendarEngine.almanac(for: day))
|
||||
|
||||
@@ -63,6 +63,31 @@ struct CalendarEnhancementTests {
|
||||
#expect(CalendarEngine.lunarDateTitle(at: newYear, locale: Locale(identifier: "zh_Hans_CN"), timeZone: zone) == "正月初一")
|
||||
}
|
||||
|
||||
@Test(arguments: ["zh_Hans_CN", "zh_Hans_SG", "zh_Hant_TW", "zh_Hant_HK"])
|
||||
func chineseLanguagesOfferLunarDisplayRegardlessOfRegion(localeID: String) throws {
|
||||
let locale = Locale(identifier: localeID)
|
||||
#expect(CalendarPreferences.Display.available(for: locale) == [.standard, .dateLunar, .lunar, .custom])
|
||||
#expect(CalendarPreferences.Display.lunar.effective(for: locale) == .lunar)
|
||||
#expect(CalendarPreferences.Display.dateLunar.effective(for: locale) == .dateLunar)
|
||||
}
|
||||
|
||||
@Test(arguments: ["en_CN", "en_US", "ja_JP", "ko_KR", "de_DE", "es_ES", "fr_FR", "ar_SA"])
|
||||
func nonChineseLanguagesTemporarilyUseStandardDisplay(localeID: String) throws {
|
||||
let locale = Locale(identifier: localeID)
|
||||
#expect(CalendarPreferences.Display.available(for: locale) == [.standard, .custom])
|
||||
let date = try #require(ISO8601DateFormatter().date(from: "2026-09-28T04:00:00Z"))
|
||||
let standard = CalendarPreferences().title(at: date, locale: locale, timeZone: zone)
|
||||
for display in [CalendarPreferences.Display.lunar, .dateLunar] {
|
||||
var preferences = CalendarPreferences()
|
||||
preferences.display = display
|
||||
#expect(preferences.title(at: date, locale: locale, timeZone: zone) == standard)
|
||||
// 呈现回退不应被写入备份,切回繁体中文仍使用原选择。
|
||||
let restored = try JSONDecoder().decode(CalendarPreferences.self, from: JSONEncoder().encode(preferences))
|
||||
#expect(restored.display == display)
|
||||
#expect(restored.display.effective(for: Locale(identifier: "zh_Hant_TW")) == display)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(arguments: ["en_US", "en_GB", "zh_Hans_CN"])
|
||||
func customDateKeepsUserOrderAndSeparators(localeID: String) throws {
|
||||
var preferences = CalendarPreferences()
|
||||
@@ -104,12 +129,59 @@ struct CalendarEnhancementTests {
|
||||
|
||||
@Test func calendarPreferencesKeepSafeDefaultsForOlderDocuments() throws {
|
||||
let old = try JSONDecoder().decode(CalendarPreferences.self, from: Data("{}".utf8))
|
||||
#expect(old == CalendarPreferences())
|
||||
#expect(old == CalendarPreferences(locale: Locale(identifier: "zh_CN")))
|
||||
#expect(old.showAlmanac && 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)
|
||||
}
|
||||
|
||||
@Test(arguments: ["en_US", "en_CN", "zh_Hans_CN", "zh_Hant_CN", "zh_Hant_TW", "zh_Hant_HK", "zh_Hant_MO",
|
||||
"zh_Hans_SG", "ja_JP", "ko_KR", "ar_SA", "fr_FR"])
|
||||
func freshCalendarDefaultsFollowRegionAndRemainStable(localeID: String) throws {
|
||||
let name = "CalendarRegionTests.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: name))
|
||||
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, .solarTerms] : [.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))
|
||||
|
||||
// 更改地区不应重置首次保存的默认值,明确关闭的选项和空集合也须保留。
|
||||
let otherRegion = Locale(identifier: mainland ? "zh_TW" : "en_CN")
|
||||
let restored = AppSettings(defaults: defaults, calendarLocale: otherRegion)
|
||||
#expect(restored.calendarFeatures == expected)
|
||||
#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)
|
||||
}
|
||||
|
||||
@Test func existingCalendarChoicesAndBackupsSurviveRegionalDefaults() throws {
|
||||
let name = "CalendarExistingRegionTests.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: name))
|
||||
defer { defaults.removePersistentDomain(forName: name) }
|
||||
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.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.calendarPreferences.showAlmanac)
|
||||
}
|
||||
|
||||
@Test func displayPreferencesRoundTripWithoutSyncingLocalCalendarIDs() throws {
|
||||
let name = "CalendarEnhancementTests.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: name))
|
||||
|
||||
@@ -61,7 +61,7 @@ struct CalendarTests {
|
||||
|
||||
@Test func widgetMonthContainsSixWeeksAndOfficialHolidayNames() throws {
|
||||
let month = CalendarEngine.widgetMonthSummary(year: 2026, month: 9, firstWeekday: 2,
|
||||
features: CalendarFeature.defaults, timeZone: zone)
|
||||
features: CalendarFeature.defaults(for: Locale(identifier: "zh_CN")), timeZone: zone)
|
||||
#expect(month.days.count == 42)
|
||||
#expect(month.days.first?.dateKey == "2026-08-31")
|
||||
#expect(month.days.last?.dateKey == "2026-10-11")
|
||||
@@ -71,7 +71,7 @@ struct CalendarTests {
|
||||
#expect(midAutumn.isWork == false)
|
||||
|
||||
let sundayFirst = CalendarEngine.widgetMonthSummary(year: 2026, month: 9, firstWeekday: 1,
|
||||
features: CalendarFeature.defaults, timeZone: zone)
|
||||
features: CalendarFeature.defaults(for: Locale(identifier: "zh_CN")), timeZone: zone)
|
||||
#expect(sundayFirst.days.first?.dateKey == "2026-08-30")
|
||||
|
||||
let plain = CalendarEngine.widgetMonthSummary(year: 2026, month: 9, firstWeekday: 2,
|
||||
|
||||
+12
-16
@@ -18,7 +18,7 @@ struct CalendarWidgetEntry: TimelineEntry {
|
||||
|
||||
struct CalendarWidgetProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> CalendarWidgetEntry {
|
||||
.init(date: .now, showsLunar: true, showsSeasonal: false, firstWeekday: 2,
|
||||
.init(date: .now, showsLunar: WidgetSnapshot().showsLunar, showsSeasonal: false, firstWeekday: 2,
|
||||
today: nil, tomorrow: nil, month: nil)
|
||||
}
|
||||
|
||||
@@ -116,26 +116,22 @@ private struct CalendarWidgetView: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Rectangle().fill(.quaternary).frame(width: 1)
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
if let today = entry.today {
|
||||
if let star = today.twelveStar {
|
||||
HStack(spacing: 5) {
|
||||
Text(tr(today.isEcliptic ? "黄道日" : "黑道日"))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(today.isEcliptic ? .green : .secondary)
|
||||
Text(star).font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
Divider()
|
||||
if let today = entry.today, let star = today.twelveStar {
|
||||
Rectangle().fill(.quaternary).frame(width: 1)
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
HStack(spacing: 5) {
|
||||
Text(tr(today.isEcliptic ? "黄道日" : "黑道日"))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(today.isEcliptic ? .green : .secondary)
|
||||
Text(star).font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
Divider()
|
||||
advice(tr("宜"), values: today.recommends)
|
||||
advice(tr("忌"), values: today.avoids)
|
||||
} else {
|
||||
Text(tr("暂无安排")).font(.caption).foregroundStyle(.secondary)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(12)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user