chore: bump v0.3.0 build 3 + holiday data + cross-day water dedupe fix

Three things squashed into one commit since they're all release-prep:

1. Bump CFBundleVersion 2 → 3, CFBundleShortVersionString 0.2.0 →
   0.3.0, WorkerPlugin.version & ExpandedView footer string to match.
   Aligns with the 5-feature v0.3.0 ship.

2. fix(water): lastWaterReminderHr was an Int hour (0-23) so a
   yesterday-fired 17:00 blocked today's 17:00 reminder. Now stores
   "yyyy-MM-dd HH" composite key so dedupe is scoped per-day.

3. Holidays.swift: 2026 entry table re-calibrated per typical
   State Council 调休 pattern. Notable changes:
   - 春节 starts on 除夕 02-16 (not 初一 02-17) per 2024 policy
   - 中秋 + 国庆 merged into 10-day 09-25 ~ 10-04 window (since
     2026 中秋 09-25 is 6 days before 国庆 10-01)
   - 劳动节 renamed from "五一" for clarity
   Disclaimer still in header: verify against 国务院 announcement
   when published, edit + re-release if 调休 differs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
徐翔宇 2026-05-20 10:12:40 +08:00
parent 51cc5bb0cc
commit dc0d22700c
5 changed files with 52 additions and 31 deletions

View File

@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>BNDL</string> <string>BNDL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.2.0</string> <string>0.3.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>2</string> <string>3</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>WorkerPlugin.WorkerPlugin</string> <string>WorkerPlugin.WorkerPlugin</string>
<!-- <!--

View File

@ -13,7 +13,7 @@ final class WorkerPlugin: NSObject, MioPlugin {
var id: String { "worker" } var id: String { "worker" }
var name: String { "摸鱼侠" } var name: String { "摸鱼侠" }
var icon: String { "fish.fill" } var icon: String { "fish.fill" }
var version: String { "0.2.0" } var version: String { "0.3.0" }
func activate() { func activate() {
WorkerDebugLog.write("plugin activate") WorkerDebugLog.write("plugin activate")

View File

@ -2,15 +2,26 @@
// Holidays.swift // Holidays.swift
// 摸鱼侠 plugin v0.3.0 // 摸鱼侠 plugin v0.3.0
// //
// China State Council statutory holidays 2026. Estimated from prior- // China State Council statutory holidays for 2026. Calibrated to the
// year patterns (公历日期固定假 + 农历节假) — exact dates and // 公历日期 + 农历对应日 + typical 调休 pattern; **the official 国务院
// adjusted-workday days may shift when the State Council announces // 办公厅 announcement (usually released Nov of prior year)** is the
// each year's calendar (typically Q4 of the prior year). // ultimate source of truth. If a date here disagrees with 国务院
// notice when published, edit this file and re-release.
//
// Reference dates used (公历 yyyy-MM-dd → 农历):
// 2026-02-17 → 农历 正月 初一 (春节)
// 2026-04-05 → 清明 (公历固定)
// 2026-06-19 → 农历 五月 初五 (端午)
// 2026-09-25 → 农历 八月 十五 (中秋)
//
// In years where 中秋 falls within 6 days of 国庆, the State Council
// typically merges them into a single 8-10 day window. 2026 has
// 中秋 09-25 (Fri) and 国庆 10-01 (Thu) → window 09-25 ~ 10-04 is the
// most likely pattern (with 10-10 Saturday or earlier 09-20 Sunday
// for makeup work day).
// //
// Data structure is a flat list rather than a JSON resource so the // Data structure is a flat list rather than a JSON resource so the
// plugin bundle stays single-binary and dependency-free. To update: // plugin bundle stays single-binary and dependency-free.
// edit this file when the next year's official 国务院办公厅
// announcement publishes.
// //
import Foundation import Foundation
@ -20,8 +31,7 @@ struct Holiday {
let name: String let name: String
/// First day of the holiday window (yyyy-MM-dd, Asia/Shanghai). /// First day of the holiday window (yyyy-MM-dd, Asia/Shanghai).
let startDate: String let startDate: String
/// Total consecutive days off, including the trigger day. e.g. /// Total consecutive days off, including the trigger day.
/// 国庆 = 8 covers 10/01-10/08 (when paired with 中秋 in 2026).
let days: Int let days: Int
} }
@ -37,17 +47,27 @@ struct UpcomingHoliday {
} }
enum HolidayDatabase { enum HolidayDatabase {
/// 2026 法定假期 — estimated from typical State Council pattern. /// 2026 法定假期 — best estimate per typical State Council调休 pattern.
/// **Verify against the official 国务院办公厅 announcement.** /// VERIFY against the official 国务院办公厅 公告 when published.
///
/// 春节 starts on 除夕 (the day before 初一) per 2024 policy update.
/// 中秋 + 国庆 merged because 2026 中秋 09-25 falls 6 days before
/// 国庆 10-01 — same pattern as 2017 / 2020 when they were close.
static let entries2026: [Holiday] = [ static let entries2026: [Holiday] = [
// 元旦 周四 — 单日不调休(2024 政策起元旦只放 1 天)
Holiday(name: "元旦", startDate: "2026-01-01", days: 1), Holiday(name: "元旦", startDate: "2026-01-01", days: 1),
Holiday(name: "春节", startDate: "2026-02-17", days: 7), // 春节 02-16 除夕 ~ 02-22 初七,7 天。调休 02-14 周六 + 02-28 周六上班
Holiday(name: "春节", startDate: "2026-02-16", days: 7),
// 清明 04-05 周日,调休 04-04 ~ 04-06 共 3 天
Holiday(name: "清明", startDate: "2026-04-04", days: 3), Holiday(name: "清明", startDate: "2026-04-04", days: 3),
Holiday(name: "五一", startDate: "2026-05-01", days: 5), // 五一 05-01 周五 ~ 05-05 周二,5 天。调休 04-26 周日上班
Holiday(name: "劳动节", startDate: "2026-05-01", days: 5),
// 端午 06-19 周五 ~ 06-21 周日,3 天(自然连周末,无调休)
Holiday(name: "端午", startDate: "2026-06-19", days: 3), Holiday(name: "端午", startDate: "2026-06-19", days: 3),
Holiday(name: "中秋·国庆", startDate: "2026-10-01", days: 8), // 中秋 + 国庆 合并 09-25 周五 ~ 10-04 周日,10 天
// Roll-over to early 2027 so late-Dec users still see a next // 调休 09-19 / 10-10 周六上班
// holiday after the 2026 calendar runs out. Holiday(name: "中秋·国庆", startDate: "2026-09-25", days: 10),
// 跨年 roll-over — 元旦 2027 估计 01-01 周五 ~ 01-03 周日,3 天
Holiday(name: "元旦", startDate: "2027-01-01", days: 3), Holiday(name: "元旦", startDate: "2027-01-01", days: 3),
] ]

View File

@ -45,7 +45,7 @@ private enum K {
static let waterGoal = "water.goal" // Int, default 8 static let waterGoal = "water.goal" // Int, default 8
static let waterAutoFromPomo = "water.autoFromPomo" // Bool, default true static let waterAutoFromPomo = "water.autoFromPomo" // Bool, default true
static let waterHourlyReminder = "water.hourlyReminder" // Bool, default true static let waterHourlyReminder = "water.hourlyReminder" // Bool, default true
static let lastWaterReminderHr = "water.lastReminderHour" // Int, hour 0-23 last fired (per day) static let lastWaterReminderHr = "water.lastReminderHour" // String "yyyy-MM-dd HH" — last hour that fired, scoped per-day
static let clockoutHHmm = "clockout.hhmm" // String "18:00" static let clockoutHHmm = "clockout.hhmm" // String "18:00"
static let lastClockoutCelebDate = "clockout.lastCelebDate" // String yyyy-MM-dd, dedupe per-day static let lastClockoutCelebDate = "clockout.lastCelebDate" // String yyyy-MM-dd, dedupe per-day
} }
@ -357,12 +357,13 @@ final class WorkerStore: ObservableObject {
guard (9...18).contains(hour) else { return } guard (9...18).contains(hour) else { return }
// Already reached goal — no nag. // Already reached goal — no nag.
guard waterCupsToday < waterGoal else { return } guard waterCupsToday < waterGoal else { return }
let lastFiredHour = defaults.integer(forKey: K.lastWaterReminderHr) // Scope dedupe by date+hour so yesterday's 17:00 doesn't block
// Sentinel: 0 means "never fired" — first run at any hour >= 9 // today's 17:00. Prior int-only key blocked the same hour
// will pass the != check. We accept one wasted re-fire on the // forever within calendar 24h cycles.
// boundary case where last == hour 0 from a fresh install. let key = "\(Self.dateKey(now, calendar: calendar)) \(String(format: "%02d", hour))"
guard lastFiredHour != hour else { return } let lastFiredKey = defaults.string(forKey: K.lastWaterReminderHr) ?? ""
defaults.set(hour, forKey: K.lastWaterReminderHr) guard lastFiredKey != key else { return }
defaults.set(key, forKey: K.lastWaterReminderHr)
let remaining = waterGoal - waterCupsToday let remaining = waterGoal - waterCupsToday
WorkerNotificationCenter.shared.notify( WorkerNotificationCenter.shared.notify(
title: "💧 喝水时间到", title: "💧 喝水时间到",

View File

@ -148,7 +148,7 @@ struct ExpandedView: View {
Text(footerLeftText) Text(footerLeftText)
} }
Spacer() Spacer()
Text("v0.1 · 本地运行") Text("v0.3.0 · 本地运行")
} }
.font(.system(size: 11)) .font(.system(size: 11))
.foregroundColor(WorkerTheme.fg40) .foregroundColor(WorkerTheme.fg40)