// // ClockoutView.swift // 摸鱼侠 plugin v0.1 // // 下班 — 距离下班 X时Y分. Red bar fills as the day progresses. // import SwiftUI struct ClockoutView: View { @ObservedObject var store: WorkerStore @State private var editing: Bool = false @State private var editHour: Int = 18 @State private var editMinute: Int = 0 var body: some View { ZStack(alignment: .top) { VStack(spacing: 18) { if store.isWeekend { weekendHero weekendTipText Spacer(minLength: 0) } else { statusBadge countdownDisplay progressBar controls Divider() .background(WorkerTheme.overlay08) .padding(.horizontal, 24) timeRow tipText Spacer(minLength: 0) } } .padding(.top, 8) // Celebration banner — overlays the panel for ~5s after the // user crosses today's clockout boundary. Subtle gold→lime // shimmer so it pops without being kindergarten-style confetti. if store.clockoutCelebrationActive { celebrationBanner .transition( .asymmetric( insertion: .move(edge: .top).combined(with: .opacity), removal: .opacity ) ) } } .animation(.spring(response: 0.35, dampingFraction: 0.78), value: store.clockoutCelebrationActive) } private var celebrationBanner: some View { VStack(spacing: 4) { Text("🎉 今日打卡下班 🎉") .font(.system(size: 16, weight: .bold)) .foregroundColor(.black) Text("辛苦了,到点了,关 IDE 下班!") .font(.system(size: 11, weight: .medium)) .foregroundColor(.black.opacity(0.7)) } .frame(maxWidth: .infinity) .padding(.vertical, 14) .background( LinearGradient( colors: [WorkerTheme.lime, WorkerTheme.tomato.opacity(0.95)], startPoint: .leading, endPoint: .trailing ) ) .clipShape(RoundedRectangle(cornerRadius: 14)) .shadow(color: WorkerTheme.lime.opacity(0.45), radius: 16, x: 0, y: 4) .padding(.horizontal, 16) .padding(.top, 4) } // MARK: - Status private var statusBadge: some View { let isOff = store.clockoutRemainingSec >= 86400 - 60 // basically never let almost = store.clockoutProgress >= 0.95 let dotColor: Color = almost ? WorkerTheme.alertRed : WorkerTheme.lime return HStack(spacing: 8) { Circle() .fill(dotColor) .frame(width: 8, height: 8) .shadow(color: dotColor.opacity(0.7), radius: 4) Text(isOff ? "下班时间未设置" : (almost ? "马上就能下班!" : "今日工作中")) .font(.system(size: 12.5, weight: .semibold)) .foregroundColor(WorkerTheme.fg85) } .padding(.horizontal, 12) .frame(height: 26) .background(Capsule().fill(WorkerTheme.overlay06)) } // MARK: - Countdown private var countdownDisplay: some View { let total = store.clockoutRemainingSec let h = total / 3600 let m = (total % 3600) / 60 let s = total % 60 return VStack(spacing: 6) { Text("距离下班") .font(.system(size: 12)) .foregroundColor(WorkerTheme.fg55) HStack(alignment: .firstTextBaseline, spacing: 6) { bigPart(value: h, suffix: "时") bigPart(value: m, suffix: "分") bigPart(value: s, suffix: "秒", small: true) } Text(targetLabel) .font(.system(size: 11)) .foregroundColor(WorkerTheme.fg45) } .frame(maxWidth: .infinity) .padding(.vertical, 18) .background( RoundedRectangle(cornerRadius: 14) .fill(WorkerTheme.overlay04) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(WorkerTheme.overlay08, lineWidth: 0.5) ) ) .padding(.horizontal, 16) } private func bigPart(value: Int, suffix: String, small: Bool = false) -> some View { HStack(alignment: .firstTextBaseline, spacing: 2) { Text("\(value)") .font(.system(size: small ? 28 : 44, weight: .semibold, design: .rounded)) .foregroundColor(WorkerTheme.fgPrimary) .monospacedDigit() Text(suffix) .font(.system(size: small ? 12 : 14, weight: .medium)) .foregroundColor(WorkerTheme.fg55) } } private var targetLabel: String { String(format: "目标 %02d:%02d", store.clockoutHour, store.clockoutMinute) } // MARK: - Progress bar private var progressBar: some View { GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 6) .fill(WorkerTheme.overlay06) .frame(height: 10) RoundedRectangle(cornerRadius: 6) .fill(LinearGradient( colors: [WorkerTheme.tomato, WorkerTheme.alertRed], startPoint: .leading, endPoint: .trailing )) .frame( width: max(0, geo.size.width * CGFloat(store.clockoutProgress)), height: 10 ) .animation(.linear(duration: 0.3), value: store.clockoutProgress) } } .frame(height: 10) .padding(.horizontal, 16) } // MARK: - Controls private var controls: some View { HStack(spacing: 10) { Button(action: { editHour = store.clockoutHour editMinute = store.clockoutMinute editing.toggle() }) { HStack(spacing: 6) { Image(systemName: editing ? "checkmark" : "clock") .font(.system(size: 11, weight: .semibold)) Text(editing ? "完成" : "调整下班时间") .font(.system(size: 12.5, weight: .semibold)) } .foregroundColor(Color(red: 0x0B/255, green: 0x0B/255, blue: 0x0B/255)) .padding(.horizontal, 16) .frame(height: 32) .background(Capsule().fill(WorkerTheme.lime)) } .buttonStyle(.plain) } } // MARK: - Time row (only shown when editing) @ViewBuilder private var timeRow: some View { if editing { HStack(spacing: 10) { stepperPill( title: "时", value: editHour, onMinus: { editHour = max(0, editHour - 1) store.clockoutSet(hour: editHour, minute: editMinute) }, onPlus: { editHour = min(23, editHour + 1) store.clockoutSet(hour: editHour, minute: editMinute) } ) stepperPill( title: "分", value: editMinute, onMinus: { editMinute = max(0, editMinute - 5) store.clockoutSet(hour: editHour, minute: editMinute) }, onPlus: { editMinute = min(55, editMinute + 5) store.clockoutSet(hour: editHour, minute: editMinute) } ) } .padding(.horizontal, 16) } else { EmptyView() } } private func stepperPill(title: String, value: Int, onMinus: @escaping () -> Void, onPlus: @escaping () -> Void) -> some View { HStack(spacing: 0) { Text(title) .font(.system(size: 11)) .foregroundColor(WorkerTheme.fg55) .padding(.leading, 10) Spacer() Button(action: onMinus) { Image(systemName: "minus") .font(.system(size: 10, weight: .semibold)) .foregroundColor(WorkerTheme.fg70) .frame(width: 22, height: 22) .background(Circle().fill(WorkerTheme.overlay06)) } .buttonStyle(.plain) Text(String(format: "%02d", value)) .font(.system(size: 13, weight: .semibold)) .foregroundColor(WorkerTheme.fgPrimary) .monospacedDigit() .frame(minWidth: 36) Button(action: onPlus) { Image(systemName: "plus") .font(.system(size: 10, weight: .semibold)) .foregroundColor(WorkerTheme.fg70) .frame(width: 22, height: 22) .background(Circle().fill(WorkerTheme.overlay06)) } .buttonStyle(.plain) .padding(.trailing, 8) } .frame(height: 32) .background( RoundedRectangle(cornerRadius: 8) .fill(WorkerTheme.overlay04) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(WorkerTheme.overlay08, lineWidth: 0.5) ) ) } // MARK: - Weekend mode /// Weekend-mode hero — replaces the countdown card with a purple /// "today's a weekend" panel. The clockout countdown still works /// in the background (anyone working Saturday's edge case), but /// the default UI no longer rubs in the fact that there's /// theoretically a clockout time on a day no one's working. private var weekendHero: some View { VStack(spacing: 8) { HStack(spacing: 8) { Circle() .fill(WorkerTheme.weekendPurple) .frame(width: 8, height: 8) .shadow(color: WorkerTheme.weekendPurple.opacity(0.7), radius: 4) Text("今天不上班") .font(.system(size: 12.5, weight: .semibold)) .foregroundColor(WorkerTheme.fg85) } .padding(.horizontal, 12) .frame(height: 26) .background(Capsule().fill(WorkerTheme.overlay06)) Text("🌴") .font(.system(size: 64)) .padding(.top, 8) Text("今天是周末") .font(.system(size: 22, weight: .bold)) .foregroundColor(WorkerTheme.fgPrimary) Text("去做你想做的事") .font(.system(size: 13)) .foregroundColor(WorkerTheme.fg55) } .frame(maxWidth: .infinity) .padding(.vertical, 28) .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) .padding(.top, 12) } private var weekendTipText: some View { Text("如果你周末也上班,记得在 周末 tab 取消休息时调整设置。\n(暂时还没做这个 toggle — 反正下周一会自动恢复倒计时。)") .font(.system(size: 11)) .foregroundColor(WorkerTheme.fg45) .padding(.horizontal, 20) .padding(.top, 8) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) } private var tipText: some View { Text("到点会有通知 + 一段 Glass 音效庆祝下班\n每日只触发一次,重启插件不会重复响铃。") .font(.system(size: 11)) .foregroundColor(WorkerTheme.fg45) .padding(.horizontal, 20) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) } }