fix(calendar): 保留农历选择并完善周起始日设置
This commit is contained in:
1 parent
678303187d
commit
222b2baa89
19 files changed
+116
-59
No files matched your search
+4
-1
@@ -180,7 +180,10 @@ not 24-hour intervals. Tibetan conversion is guarded to the upstream Gregorian c
|
||||
Plum-rain dates describe traditional calendar rules, not observed or forecast weather.
|
||||
|
||||
Calendar enablement, visible features and week start persist through `AppSettings` and the
|
||||
optional fields in `SettingsDocument`. Older backups preserve those settings. Additional
|
||||
optional fields in `SettingsDocument`. Week start initially uses `Calendar.current.firstWeekday`,
|
||||
then persists independently of later system changes; all seven weekdays are supported by the
|
||||
settings picker, backups, month grids and widgets. Menu-bar lunar modes remain available in
|
||||
every app language and use localized lunar dates. Older backups preserve those settings. Additional
|
||||
calendars default off and appear only in selected-day details. There is no event access or
|
||||
network request at runtime. Use `--snapshot <directory> --calendar-only` for deterministic
|
||||
light/dark calendar screenshots without starting unrelated samplers or scans.
|
||||
|
||||
+2
-2
@@ -54,8 +54,8 @@ GitHub Actions 只在分支 push 且 `server/**` 发生变化时构建并发布
|
||||
构建固定使用 `arm64`,仅支持 Apple Silicon Mac。
|
||||
有 Developer ID Application 证书时,Taskfile 会自动使用钥匙串中第一个证书的唯一指纹;否则由构建脚本逐层补齐 ad-hoc 签名与 entitlement。临时签名没有团队身份,不能加载启用团队验证的 Sparkle 动态库,因此仅此本地/CI 路径关闭 Hardened Runtime;正式 Developer ID 构建仍强制启用,且临时签名构建不能在线安装或操作特权辅助工具。
|
||||
桌面小组件与主应用通过 `group.work.12306.xstats` App Group 共享只读展示摘要。
|
||||
菜单栏日历提供“日期与星期”“自定义”显示方式;应用语言为简体中文或繁体中文(含自动检测为中文)时,额外提供“日期与农历”“农历”。切到非中文时,已保存的农历方式暂以“日期与星期”呈现,切回中文恢复原选择,不改写备份。自定义可自由组合 y/M/d 字段与分隔符,年份可省略,输入时显示预览并提示无效格式,独立于月历的星期开关;支持农历字号与对比度、可选悬停展开,按分钟刷新,休眠或关闭功能后停止。日程和到期提醒默认关闭,通过明确的授权按钮请求 EventKit 权限,只在日历面板可见时查询当前 42 格日期范围;只读展示,不修改日程,也不向 Widget、设置备份或服务端传递内容。列表 ID 仅保存在本机,显示偏好参与设置备份。节假日覆盖年份来自内置 Tyme 数据并随应用更新;假期倒计时与最多 3 天的请假建议只基于已公布的中国调休与双休安排,跨到未知年份时停止推算。
|
||||
日历首次初始化按 macOS 地区设置决定默认值,不按设备销售地或应用语言判断:中国大陆(CN)默认启用农历、星期、大陆节假日与调休、传统与公历节日、节气、黄历及假期概览;其他地区(含台湾、香港、澳门)默认只启用星期。干支、三伏/梅雨/数九、藏历与回历各地区均默认关闭。用户可自行启用或关闭,首次默认值保存后不随语言、地区变化重置;已有本机偏好及旧备份保持原选择。大陆节假日数据不能用于推断其他地区的法定安排。单日及整月小组件遵循日历选项,未启用大陆节假日时“明天上班吗”显示无法判断;黄历关闭后不再计算宜忌摘要。
|
||||
菜单栏日历在所有应用语言下均提供“日期与星期”“日期与农历”“农历”“自定义”显示方式;日期与农历按应用语言呈现,切换语言保留用户选择。自定义可自由组合 y/M/d 字段与分隔符,年份可省略,输入时显示预览并提示无效格式,独立于月历的星期开关;支持农历字号与对比度、可选悬停展开,按分钟刷新,休眠或关闭功能后停止。日程和到期提醒默认关闭,通过明确的授权按钮请求 EventKit 权限,只在日历面板可见时查询当前 42 格日期范围;只读展示,不修改日程,也不向 Widget、设置备份或服务端传递内容。列表 ID 仅保存在本机,显示偏好参与设置备份。节假日覆盖年份来自内置 Tyme 数据并随应用更新;假期倒计时与最多 3 天的请假建议只基于已公布的中国调休与双休安排,跨到未知年份时停止推算。
|
||||
日历首次初始化按 macOS 地区设置决定默认值,不按设备销售地或应用语言判断:中国大陆(CN)默认启用农历、星期、大陆节假日与调休、传统与公历节日、节气、黄历及假期概览;其他地区(含台湾、香港、澳门)默认只启用星期。干支、三伏/梅雨/数九、藏历与回历各地区均默认关闭。用户可自行启用或关闭,首次默认值保存后不随语言、地区变化重置;已有本机偏好及旧备份保持原选择。每周起始日首次采用 macOS 系统日历设置并保存,支持星期日到星期六,手动选择及备份恢复优先;后续系统设置变化不覆盖已保存的值,主应用月历与小组件使用同一周起始日。大陆节假日数据不能用于推断其他地区的法定安排。单日及整月小组件遵循日历选项,未启用大陆节假日时“明天上班吗”提示“仅支持中国大陆节假日”;黄历关闭后不再计算宜忌摘要。
|
||||
月历使用固定六行布局,日期 ID 在生成日期时计算一次。面板打开时测量初始高度,后续翻月、详情与日程内容变化在内部滚动,不从内容几何回调反向调整窗口,避免窗口高度与懒布局估算形成反馈循环。
|
||||
月历计算仅缓存最近一个月份(包含周起始日和时区),供实际面板、高度测量和重复打开共用;翻月替换缓存,避免浏览历史导致内存累积。日程日期标记在工作 actor 内随查询结果生成,界面不再逐格扫描日程列表;连续 EventKit 通知按 300 毫秒窗口合并,关闭面板时取消待执行刷新。
|
||||
用于正式签名的 Apple Developer 团队须为主应用和 Widget 扩展启用同一个 App Group;
|
||||
|
||||
@@ -54,7 +54,7 @@ extension Translations {
|
||||
上班 Workday
|
||||
不上班 Day off
|
||||
暂无安排 No schedule data
|
||||
暂无法判断 Cannot determine yet
|
||||
仅支持中国大陆节假日 Only Mainland China holidays are supported
|
||||
工作日 Weekday
|
||||
周末 Weekend
|
||||
节假日 Public holiday
|
||||
|
||||
@@ -1439,7 +1439,7 @@ Sub2API 账号平台与所选来源不匹配 منصة حساب Sub2API لا تط
|
||||
上班 يوم عمل
|
||||
不上班 يوم عطلة
|
||||
暂无安排 لا توجد بيانات
|
||||
暂无法判断 لا يمكن التحديد حاليًا
|
||||
仅支持中国大陆节假日 يدعم العطلات الرسمية في البر الرئيسي للصين فقط
|
||||
工作日 يوم عمل
|
||||
周末 عطلة نهاية الأسبوع
|
||||
节假日 عطلة رسمية
|
||||
|
||||
|
@@ -1439,7 +1439,7 @@ Sub2API 账号平台与所选来源不匹配 Die Plattform des Sub2API-Kontos st
|
||||
上班 Arbeitstag
|
||||
不上班 Frei
|
||||
暂无安排 Keine Daten
|
||||
暂无法判断 Derzeit nicht bestimmbar
|
||||
仅支持中国大陆节假日 Nur Feiertage in Festlandchina werden unterstützt
|
||||
工作日 Werktag
|
||||
周末 Wochenende
|
||||
节假日 Feiertag
|
||||
|
||||
|
@@ -1439,7 +1439,7 @@ Sub2API 账号平台与所选来源不匹配 La plataforma de la cuenta de Sub2A
|
||||
上班 Día laborable
|
||||
不上班 Día libre
|
||||
暂无安排 Sin datos
|
||||
暂无法判断 Aún no se puede determinar
|
||||
仅支持中国大陆节假日 Solo se admiten los festivos de China continental
|
||||
工作日 Día laborable
|
||||
周末 Fin de semana
|
||||
节假日 Festivo
|
||||
|
||||
|
@@ -1439,7 +1439,7 @@ Sub2API 账号平台与所选来源不匹配 La plateforme du compte Sub2API ne
|
||||
上班 Jour travaillé
|
||||
不上班 Repos
|
||||
暂无安排 Aucune donnée
|
||||
暂无法判断 Impossible à déterminer pour le moment
|
||||
仅支持中国大陆节假日 Seuls les jours fériés de Chine continentale sont pris en charge
|
||||
工作日 Jour ouvré
|
||||
周末 Week-end
|
||||
节假日 Jour férié
|
||||
|
||||
|
@@ -1439,7 +1439,7 @@ Sub2API 账号平台与所选来源不匹配 Sub2API アカウントのプラッ
|
||||
上班 勤務日
|
||||
不上班 休み
|
||||
暂无安排 予定データなし
|
||||
暂无法判断 判定できません
|
||||
仅支持中国大陆节假日 中国本土の祝日のみ対応
|
||||
工作日 平日
|
||||
周末 週末
|
||||
节假日 祝日
|
||||
|
||||
|
@@ -1439,7 +1439,7 @@ Sub2API 账号平台与所选来源不匹配 Sub2API 계정 플랫폼이 선택
|
||||
上班 근무
|
||||
不上班 쉬는 날
|
||||
暂无安排 일정 데이터 없음
|
||||
暂无法判断 아직 판단할 수 없음
|
||||
仅支持中国大陆节假日 중국 본토 공휴일만 지원
|
||||
工作日 평일
|
||||
周末 주말
|
||||
节假日 공휴일
|
||||
|
||||
|
@@ -1440,7 +1440,7 @@ Sub2API 账号平台与所选来源不匹配 Sub2API 帳號平台與所選來源
|
||||
上班 上班
|
||||
不上班 不上班
|
||||
暂无安排 暫無安排
|
||||
暂无法判断 暫時無法判斷
|
||||
仅支持中国大陆节假日 僅支援中國大陸節假日
|
||||
工作日 工作日
|
||||
周末 週末
|
||||
节假日 節假日
|
||||
|
||||
|
@@ -178,7 +178,7 @@ public struct WidgetSnapshot: Codable, Equatable, Sendable {
|
||||
localUsageEnabled: Bool? = nil,
|
||||
publicIPEnabled: Bool = false,
|
||||
addresses: [Address] = [], language: String = "system",
|
||||
calendarFirstWeekday: Int = 2, showsLunar: Bool = Locale.current.region?.identifier == "CN",
|
||||
calendarFirstWeekday: Int = Calendar.current.firstWeekday, showsLunar: Bool = Locale.current.region?.identifier == "CN",
|
||||
calendarDays: [CalendarSummary]? = nil, monthSummaries: [MonthSummary]? = nil,
|
||||
showsSeasonal: Bool? = nil, calendarDataVersion: String? = nil, showsAlmanac: Bool? = nil) {
|
||||
self.aiEnabled = aiEnabled
|
||||
|
||||
@@ -147,7 +147,7 @@ enum CalendarEngine {
|
||||
static func month(year: Int, month: Int, firstWeekday: Int,
|
||||
timeZone: TimeZone = .autoupdatingCurrent) -> [CalendarDay] {
|
||||
let requested = CalendarMonth(year: year, month: month)
|
||||
let weekStart = firstWeekday == 1 ? 1 : 2
|
||||
let weekStart = (1...7).contains(firstWeekday) ? firstWeekday : Calendar.current.firstWeekday
|
||||
// 固定自动更新时区的当前值,避免缓存里的时区也跟着系统变化,误判为可复用。
|
||||
let zone = TimeZone(identifier: timeZone.identifier) ?? timeZone
|
||||
if let cached = monthCache, cached.month == requested,
|
||||
|
||||
@@ -10,17 +10,6 @@ public struct CalendarPreferences: Codable, Equatable, 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("日期与星期")
|
||||
@@ -92,7 +81,7 @@ public struct CalendarPreferences: Codable, Equatable, Sendable {
|
||||
calendar.timeZone = timeZone
|
||||
formatter.calendar = calendar
|
||||
formatter.timeZone = timeZone
|
||||
switch display.effective(for: locale) {
|
||||
switch display {
|
||||
case .standard: formatter.setLocalizedDateFormatFromTemplate("MdEEE")
|
||||
case .custom:
|
||||
// 用户明确选择顺序,不能再用本地化模板把日月重新排回月日。
|
||||
|
||||
@@ -36,15 +36,19 @@ struct CalendarOptionsPopover: View {
|
||||
|
||||
var body: some View {
|
||||
@Bindable var settings = model.settings
|
||||
PageScroll {
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.locale = L10n.locale
|
||||
let weekdays = calendar.weekdaySymbols
|
||||
return PageScroll {
|
||||
VStack(spacing: DS.Space.s4) {
|
||||
presentationOptions
|
||||
SettingsGroup(caption: tr("日历")) {
|
||||
GroupRow(showsDivider: false) {
|
||||
SettingRow(title: tr("每周开始于")) {
|
||||
Picker(tr("每周开始于"), selection: $settings.calendarFirstWeekday) {
|
||||
Text(tr("星期一")).tag(2)
|
||||
Text(tr("星期日")).tag(1)
|
||||
ForEach(1...7, id: \.self) { weekday in
|
||||
Text(weekdays[weekday - 1]).tag(weekday)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.fixedSize()
|
||||
@@ -113,11 +117,8 @@ struct CalendarOptionsPopover: View {
|
||||
return SettingsGroup(caption: tr("菜单栏")) {
|
||||
GroupRow(showsDivider: false) {
|
||||
SettingRow(title: tr("显示方式")) {
|
||||
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) }
|
||||
Picker(tr("显示方式"), selection: $settings.calendarPreferences.display) {
|
||||
ForEach(CalendarPreferences.Display.allCases) { Text($0.title).tag($0) }
|
||||
}.labelsHidden().fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, calendarLocale: Locale = .current) {
|
||||
public init(defaults: UserDefaults = .standard, calendarLocale: Locale = .current, calendar: Calendar = .current) {
|
||||
self.defaults = defaults
|
||||
let isRestEnabled = defaults.bool(forKey: Keys.restEnabled)
|
||||
restEnabled = isRestEnabled
|
||||
@@ -758,7 +758,9 @@ public final class AppSettings {
|
||||
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 savedCalendarFirstWeekday = (defaults.object(forKey: Keys.calendarFirstWeekday) as? Int)
|
||||
.flatMap { (1...7).contains($0) ? $0 : nil }
|
||||
calendarFirstWeekday = savedCalendarFirstWeekday ?? calendar.firstWeekday
|
||||
let items = defaults.stringArray(forKey: Keys.menuBarItems)?.compactMap(MenuBarItem.init(rawValue:))
|
||||
menuBarItems = Set(items ?? [.cpu, .memory, .network])
|
||||
menuBarStyle = defaults.string(forKey: Keys.menuBarStyle).flatMap(MenuBarStyle.init(rawValue:)) ?? .stacked
|
||||
@@ -837,6 +839,10 @@ public final class AppSettings {
|
||||
if savedCalendarFeatures == nil {
|
||||
defaults.set(calendarFeatures.map(\.rawValue).sorted(), forKey: Keys.calendarFeatures)
|
||||
}
|
||||
// 首次采用系统的周起始日并保存,之后系统设置变化不覆盖用户选择。
|
||||
if savedCalendarFirstWeekday == nil {
|
||||
defaults.set(calendarFirstWeekday, forKey: Keys.calendarFirstWeekday)
|
||||
}
|
||||
}
|
||||
|
||||
/// 按固定顺序返回当前实际显示的菜单栏项目;可选模块关闭时保留展示偏好但不渲染。
|
||||
|
||||
@@ -144,7 +144,7 @@ extension AppSettings {
|
||||
assign(\.restSound, doc.restSound.flatMap(RestSound.init(rawValue:)))
|
||||
assign(\.restHUDStyle, doc.restHUDStyle.flatMap(RestHUDStyle.init(rawValue:)))
|
||||
assign(\.calendarFeatures, doc.calendarFeatures.map(CalendarFeature.restored(from:)))
|
||||
assign(\.calendarFirstWeekday, option(doc.calendarFirstWeekday, in: [1, 2]))
|
||||
assign(\.calendarFirstWeekday, option(doc.calendarFirstWeekday, in: Array(1...7)))
|
||||
assign(\.menuBarItems, doc.menuBarItems.map { Set($0.compactMap(MenuBarItem.init(rawValue:))) })
|
||||
assign(\.menuBarStyle, doc.menuBarStyle.flatMap(MenuBarStyle.init(rawValue:)))
|
||||
assign(\.networkStyle, doc.networkStyle.flatMap(NetworkMenuStyle.init(rawValue:)))
|
||||
|
||||
@@ -31,6 +31,15 @@ import WidgetData
|
||||
#expect(disabled.visibleAddresses.isEmpty)
|
||||
}
|
||||
|
||||
@Test(arguments: Array(1...7))
|
||||
func snapshotPreservesEveryWeekStartAndReloadsCalendars(firstWeekday: Int) throws {
|
||||
let previous = WidgetSnapshot(calendarFirstWeekday: firstWeekday % 7 + 1)
|
||||
let snapshot = WidgetSnapshot(calendarFirstWeekday: firstWeekday)
|
||||
let restored = try JSONDecoder().decode(WidgetSnapshot.self, from: JSONEncoder().encode(snapshot))
|
||||
#expect(restored.calendarFirstWeekday == firstWeekday)
|
||||
#expect(restored.changedWidgetKinds(from: previous) == [WidgetKind.calendar, WidgetKind.tomorrowWork, WidgetKind.calendarMonth])
|
||||
}
|
||||
|
||||
@Test func calendarSummaryUsesLocalCivilDayAndOldCacheStillDecodes() throws {
|
||||
let legacy = Data(#"{"aiEnabled":false,"quotas":[],"publicIPEnabled":false,"addresses":[],"language":"system","calendarFirstWeekday":2,"showsLunar":true}"#.utf8)
|
||||
let oldSnapshot = try JSONDecoder().decode(WidgetSnapshot.self, from: legacy)
|
||||
|
||||
@@ -63,29 +63,32 @@ 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 {
|
||||
@Test(arguments: ["en_CN", "en_US", "ja_JP", "ko_KR", "de_DE", "es_ES", "fr_FR", "ar_SA"])
|
||||
func manualLunarDisplaySurvivesEveryLanguage(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)
|
||||
let date = try #require(ISO8601DateFormatter().date(from: "2026-09-28T04:00:00Z"))
|
||||
let expected = try #require(CalendarEngine.lunarDateTitle(at: date, locale: locale, timeZone: zone))
|
||||
let standard = CalendarPreferences().title(at: date, locale: locale, timeZone: zone)
|
||||
var preferences = CalendarPreferences()
|
||||
preferences.display = .lunar
|
||||
#expect(preferences.title(at: date, locale: locale, timeZone: zone) == expected)
|
||||
#expect(preferences.title(at: date, locale: locale, timeZone: zone) != standard)
|
||||
preferences.display = .dateLunar
|
||||
#expect(preferences.title(at: date, locale: locale, timeZone: zone).contains(expected))
|
||||
let restored = try JSONDecoder().decode(CalendarPreferences.self, from: JSONEncoder().encode(preferences))
|
||||
#expect(restored.display == .dateLunar)
|
||||
#expect(restored.title(at: date, locale: locale, timeZone: zone) == preferences.title(at: date, locale: locale, timeZone: zone))
|
||||
}
|
||||
|
||||
@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: Array(1...7))
|
||||
func monthGridAndWidgetHonorEveryWeekStart(firstWeekday: Int) throws {
|
||||
let days = CalendarEngine.month(year: 2026, month: 9, firstWeekday: firstWeekday, timeZone: zone)
|
||||
let first = try #require(days.first)
|
||||
#expect(days.count == 42 && first.weekday == firstWeekday)
|
||||
let summary = CalendarEngine.widgetMonthSummary(year: 2026, month: 9, firstWeekday: firstWeekday,
|
||||
features: [], timeZone: zone)
|
||||
#expect(summary.firstWeekday == firstWeekday)
|
||||
#expect(summary.days.first?.dateKey == first.id)
|
||||
}
|
||||
|
||||
@Test(arguments: ["en_US", "en_GB", "zh_Hans_CN"])
|
||||
@@ -164,6 +167,49 @@ struct CalendarEnhancementTests {
|
||||
#expect(!disabled.calendarPreferences.showHolidayOverview && !disabled.calendarPreferences.showAlmanac)
|
||||
}
|
||||
|
||||
@Test(arguments: Array(1...7))
|
||||
func weekStartUsesSystemDefaultOnceAndPreservesManualChoice(firstWeekday: Int) throws {
|
||||
let name = "CalendarWeekStartTests.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: name))
|
||||
defer { defaults.removePersistentDomain(forName: name) }
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.firstWeekday = firstWeekday
|
||||
let settings = AppSettings(defaults: defaults, calendarLocale: Locale(identifier: "en_US"), calendar: calendar)
|
||||
#expect(settings.calendarFirstWeekday == firstWeekday)
|
||||
#expect(defaults.integer(forKey: "calendarFirstWeekday") == firstWeekday)
|
||||
|
||||
calendar.firstWeekday = firstWeekday % 7 + 1
|
||||
let restored = AppSettings(defaults: defaults, calendarLocale: Locale(identifier: "zh_CN"), calendar: calendar)
|
||||
#expect(restored.calendarFirstWeekday == firstWeekday)
|
||||
settings.calendarFirstWeekday = 3
|
||||
#expect(AppSettings(defaults: defaults, calendar: calendar).calendarFirstWeekday == 3)
|
||||
}
|
||||
|
||||
@Test(arguments: Array(1...7))
|
||||
func backupsRestoreEveryWeekStartAndRejectInvalidValues(firstWeekday: Int) throws {
|
||||
let name = "CalendarWeekBackupTests.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: name))
|
||||
defer { defaults.removePersistentDomain(forName: name) }
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.firstWeekday = 7
|
||||
defaults.set(99, forKey: "calendarFirstWeekday")
|
||||
let settings = AppSettings(defaults: defaults, calendar: calendar)
|
||||
#expect(settings.calendarFirstWeekday == 7)
|
||||
#expect(defaults.integer(forKey: "calendarFirstWeekday") == 7)
|
||||
|
||||
settings.calendarFirstWeekday = firstWeekday
|
||||
let document = try JSONDecoder().decode(SettingsDocument.self, from: JSONEncoder().encode(settings.exportDocument()))
|
||||
settings.calendarFirstWeekday = firstWeekday % 7 + 1
|
||||
settings.apply(document)
|
||||
#expect(settings.calendarFirstWeekday == firstWeekday)
|
||||
var invalid = SettingsDocument()
|
||||
invalid.calendarFirstWeekday = 99
|
||||
settings.apply(invalid)
|
||||
#expect(settings.calendarFirstWeekday == firstWeekday)
|
||||
settings.apply(SettingsDocument())
|
||||
#expect(settings.calendarFirstWeekday == firstWeekday)
|
||||
}
|
||||
|
||||
@Test func existingCalendarChoicesAndBackupsSurviveRegionalDefaults() throws {
|
||||
let name = "CalendarExistingRegionTests.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: name))
|
||||
|
||||
@@ -18,7 +18,8 @@ struct CalendarWidgetEntry: TimelineEntry {
|
||||
|
||||
struct CalendarWidgetProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> CalendarWidgetEntry {
|
||||
.init(date: .now, showsLunar: WidgetSnapshot().showsLunar, showsSeasonal: false, firstWeekday: 2,
|
||||
let snapshot = WidgetSnapshot()
|
||||
return .init(date: .now, showsLunar: snapshot.showsLunar, showsSeasonal: false, firstWeekday: snapshot.calendarFirstWeekday,
|
||||
today: nil, tomorrow: nil, month: nil)
|
||||
}
|
||||
|
||||
@@ -175,7 +176,9 @@ struct CalendarWidget: Widget {
|
||||
private struct MonthCalendarWidgetView: View {
|
||||
let entry: CalendarWidgetEntry
|
||||
|
||||
private var firstWeekday: Int { entry.firstWeekday == 1 ? 1 : 2 }
|
||||
private var firstWeekday: Int {
|
||||
(1...7).contains(entry.firstWeekday) ? entry.firstWeekday : Calendar.current.firstWeekday
|
||||
}
|
||||
|
||||
private var calendar: Calendar { WidgetSnapshot.keyCalendar }
|
||||
private var monthKey: String { WidgetSnapshot.monthKey(for: entry.date) }
|
||||
@@ -320,7 +323,7 @@ private struct TomorrowWorkWidgetView: View {
|
||||
.minimumScaleFactor(0.75)
|
||||
}
|
||||
} else {
|
||||
Text(tr("暂无法判断"))
|
||||
Text(tr("仅支持中国大陆节假日"))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user