2026-04-27 12:21:12 +00:00
|
|
|
|
//
|
|
|
|
|
|
// WeekendView.swift
|
|
|
|
|
|
// 摸鱼侠 plugin v0.1
|
|
|
|
|
|
//
|
|
|
|
|
|
// 周末 — countdown to next Saturday 00:00.
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
|
|
struct WeekendView: View {
|
|
|
|
|
|
@ObservedObject var store: WorkerStore
|
|
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
2026-05-20 01:59:06 +00:00
|
|
|
|
ScrollView {
|
|
|
|
|
|
VStack(spacing: 14) {
|
|
|
|
|
|
statusBadge
|
2026-04-27 12:21:12 +00:00
|
|
|
|
|
2026-05-20 01:59:06 +00:00
|
|
|
|
heroCountdown
|
2026-04-27 12:21:12 +00:00
|
|
|
|
|
2026-05-20 01:59:06 +00:00
|
|
|
|
partsRow
|
2026-04-27 12:21:12 +00:00
|
|
|
|
|
2026-05-20 01:59:06 +00:00
|
|
|
|
if store.nextHoliday != nil {
|
|
|
|
|
|
holidayCard
|
|
|
|
|
|
}
|
2026-04-27 12:21:12 +00:00
|
|
|
|
|
2026-05-20 01:59:06 +00:00
|
|
|
|
Divider()
|
|
|
|
|
|
.background(WorkerTheme.overlay08)
|
|
|
|
|
|
.padding(.horizontal, 24)
|
2026-04-27 12:21:12 +00:00
|
|
|
|
|
2026-05-20 01:59:06 +00:00
|
|
|
|
quoteCard
|
|
|
|
|
|
|
|
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(.top, 8)
|
|
|
|
|
|
.padding(.bottom, 12)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Holiday card (next 法定假)
|
|
|
|
|
|
|
|
|
|
|
|
/// Distance-to-next-statutory-holiday card. Splits visual weight
|
|
|
|
|
|
/// with the weekend hero so users on a Friday don't just see "1 天
|
|
|
|
|
|
/// 2 时" and miss that 国庆放 8 天 is next week.
|
|
|
|
|
|
private var holidayCard: some View {
|
|
|
|
|
|
Group {
|
|
|
|
|
|
if let h = store.nextHoliday {
|
|
|
|
|
|
holidayContent(h)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(.horizontal, 16)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
|
|
private func holidayContent(_ h: UpcomingHoliday) -> some View {
|
|
|
|
|
|
HStack(alignment: .center, spacing: 12) {
|
|
|
|
|
|
// Left side: name + days-off line
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
|
|
|
|
HStack(spacing: 6) {
|
|
|
|
|
|
Image(systemName: "party.popper.fill")
|
|
|
|
|
|
.font(.system(size: 11, weight: .semibold))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.tomato)
|
|
|
|
|
|
Text(h.isOngoing ? "假期中" : "下个法定假")
|
|
|
|
|
|
.font(.system(size: 10.5, weight: .medium))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fg55)
|
|
|
|
|
|
.tracking(0.3)
|
|
|
|
|
|
}
|
|
|
|
|
|
Text(h.name)
|
|
|
|
|
|
.font(.system(size: 15, weight: .bold))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fgPrimary)
|
|
|
|
|
|
Text("放 \(h.days) 天")
|
|
|
|
|
|
.font(.system(size: 11))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fg55)
|
|
|
|
|
|
}
|
2026-04-27 12:21:12 +00:00
|
|
|
|
Spacer(minLength: 0)
|
2026-05-20 01:59:06 +00:00
|
|
|
|
// Right side: distance / "假期中"
|
|
|
|
|
|
VStack(alignment: .trailing, spacing: 2) {
|
|
|
|
|
|
if h.isOngoing {
|
|
|
|
|
|
Text("🎉")
|
|
|
|
|
|
.font(.system(size: 28))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Text("\(h.daysUntil)")
|
|
|
|
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.tomato)
|
|
|
|
|
|
.monospacedDigit()
|
|
|
|
|
|
Text("天后")
|
|
|
|
|
|
.font(.system(size: 10))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fg55)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-27 12:21:12 +00:00
|
|
|
|
}
|
2026-05-20 01:59:06 +00:00
|
|
|
|
.padding(.horizontal, 14)
|
|
|
|
|
|
.padding(.vertical, 12)
|
|
|
|
|
|
.background(
|
|
|
|
|
|
RoundedRectangle(cornerRadius: 12)
|
|
|
|
|
|
.fill(
|
|
|
|
|
|
LinearGradient(
|
|
|
|
|
|
colors: [
|
|
|
|
|
|
WorkerTheme.tomato.opacity(0.10),
|
|
|
|
|
|
WorkerTheme.tomato.opacity(0.02)
|
|
|
|
|
|
],
|
|
|
|
|
|
startPoint: .topLeading, endPoint: .bottomTrailing
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.overlay(
|
|
|
|
|
|
RoundedRectangle(cornerRadius: 12)
|
|
|
|
|
|
.stroke(WorkerTheme.tomato.opacity(0.30), lineWidth: 0.5)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-04-27 12:21:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Status
|
|
|
|
|
|
|
|
|
|
|
|
private var statusBadge: some View {
|
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
|
Circle()
|
|
|
|
|
|
.fill(WorkerTheme.weekendPurple)
|
|
|
|
|
|
.frame(width: 8, height: 8)
|
|
|
|
|
|
.shadow(color: WorkerTheme.weekendPurple.opacity(0.7), radius: 4)
|
|
|
|
|
|
Text(badgeText)
|
|
|
|
|
|
.font(.system(size: 12.5, weight: .semibold))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fg85)
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(.horizontal, 12)
|
|
|
|
|
|
.frame(height: 26)
|
|
|
|
|
|
.background(Capsule().fill(WorkerTheme.overlay06))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private var badgeText: String {
|
|
|
|
|
|
if store.weekendDays == 0 && store.weekendHours < 12 {
|
|
|
|
|
|
return "周末就在眼前 ✨"
|
|
|
|
|
|
}
|
|
|
|
|
|
return "距离周末"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Hero countdown
|
|
|
|
|
|
|
|
|
|
|
|
private var heroCountdown: some View {
|
|
|
|
|
|
VStack(spacing: 4) {
|
|
|
|
|
|
Text("还有")
|
|
|
|
|
|
.font(.system(size: 12))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fg55)
|
|
|
|
|
|
Text(WorkerFormat.dhm(
|
|
|
|
|
|
days: store.weekendDays,
|
|
|
|
|
|
hours: store.weekendHours,
|
|
|
|
|
|
minutes: store.weekendMinutes
|
|
|
|
|
|
))
|
|
|
|
|
|
.font(.system(size: 36, weight: .semibold, design: .rounded))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.weekendPurple)
|
|
|
|
|
|
.monospacedDigit()
|
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
|
.minimumScaleFactor(0.7)
|
|
|
|
|
|
.padding(.horizontal, 16)
|
|
|
|
|
|
Text("下个周六 00:00")
|
|
|
|
|
|
.font(.system(size: 11))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fg45)
|
|
|
|
|
|
}
|
|
|
|
|
|
.frame(maxWidth: .infinity)
|
|
|
|
|
|
.padding(.vertical, 22)
|
|
|
|
|
|
.background(
|
|
|
|
|
|
RoundedRectangle(cornerRadius: 14)
|
|
|
|
|
|
.fill(
|
|
|
|
|
|
LinearGradient(
|
|
|
|
|
|
colors: [
|
|
|
|
|
|
WorkerTheme.weekendPurple.opacity(0.18),
|
|
|
|
|
|
WorkerTheme.weekendPurple.opacity(0.04)
|
|
|
|
|
|
],
|
|
|
|
|
|
startPoint: .topLeading, endPoint: .bottomTrailing
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.overlay(
|
|
|
|
|
|
RoundedRectangle(cornerRadius: 14)
|
|
|
|
|
|
.stroke(WorkerTheme.weekendPurple.opacity(0.35), lineWidth: 0.5)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.padding(.horizontal, 16)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Parts row
|
|
|
|
|
|
|
|
|
|
|
|
private var partsRow: some View {
|
|
|
|
|
|
HStack(spacing: 10) {
|
|
|
|
|
|
partCard(label: "天", value: store.weekendDays)
|
|
|
|
|
|
partCard(label: "时", value: store.weekendHours)
|
|
|
|
|
|
partCard(label: "分", value: store.weekendMinutes)
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(.horizontal, 16)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func partCard(label: String, value: Int) -> some View {
|
|
|
|
|
|
VStack(spacing: 4) {
|
|
|
|
|
|
Text("\(value)")
|
|
|
|
|
|
.font(.system(size: 24, weight: .semibold, design: .rounded))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fgPrimary)
|
|
|
|
|
|
.monospacedDigit()
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.font(.system(size: 10.5))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fg55)
|
|
|
|
|
|
}
|
|
|
|
|
|
.frame(maxWidth: .infinity)
|
|
|
|
|
|
.padding(.vertical, 12)
|
|
|
|
|
|
.background(
|
|
|
|
|
|
RoundedRectangle(cornerRadius: 10)
|
|
|
|
|
|
.fill(WorkerTheme.overlay04)
|
|
|
|
|
|
.overlay(
|
|
|
|
|
|
RoundedRectangle(cornerRadius: 10)
|
|
|
|
|
|
.stroke(WorkerTheme.overlay08, lineWidth: 0.5)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Quote card
|
|
|
|
|
|
|
|
|
|
|
|
private var quoteCard: some View {
|
|
|
|
|
|
let q = quoteForToday()
|
|
|
|
|
|
return VStack(alignment: .leading, spacing: 6) {
|
|
|
|
|
|
Text("今日打工语录")
|
|
|
|
|
|
.font(.system(size: 10.5))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fg55)
|
|
|
|
|
|
Text(q)
|
|
|
|
|
|
.font(.system(size: 12.5))
|
|
|
|
|
|
.foregroundColor(WorkerTheme.fg85)
|
|
|
|
|
|
.lineSpacing(2)
|
|
|
|
|
|
}
|
|
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
|
.padding(.horizontal, 14)
|
|
|
|
|
|
.padding(.vertical, 12)
|
|
|
|
|
|
.background(
|
|
|
|
|
|
RoundedRectangle(cornerRadius: 10)
|
|
|
|
|
|
.fill(WorkerTheme.overlay04)
|
|
|
|
|
|
.overlay(
|
|
|
|
|
|
RoundedRectangle(cornerRadius: 10)
|
|
|
|
|
|
.stroke(WorkerTheme.overlay08, lineWidth: 0.5)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.padding(.horizontal, 16)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func quoteForToday() -> String {
|
|
|
|
|
|
// Stable per-day rotation. Day-of-year as the index keeps the
|
|
|
|
|
|
// quote consistent for the whole day.
|
|
|
|
|
|
let quotes = [
|
|
|
|
|
|
"周末是充电的理由,工作日是放电的代价。",
|
|
|
|
|
|
"上班是为了更好地下班。",
|
|
|
|
|
|
"再坚持一下,咖啡就在转角等你。",
|
|
|
|
|
|
"生活不止眼前的KPI,还有诗和周末的咖啡馆。",
|
|
|
|
|
|
"每一秒倒计时都是给未来周末的铺垫。",
|
|
|
|
|
|
"工作再忙,水也要喝完八杯。",
|
|
|
|
|
|
"下班的钟声永远比开会的提示音动听。"
|
|
|
|
|
|
]
|
|
|
|
|
|
let cal = Calendar(identifier: .gregorian)
|
|
|
|
|
|
let day = cal.ordinality(of: .day, in: .year, for: Date()) ?? 0
|
|
|
|
|
|
return quotes[day % quotes.count]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|