mirror of
https://github.com/carey314/mio-plugin-worker.git
synced 2026-08-10 07:04:32 +00:00
Office-survival toolkit. Five tabs in the notch: - 🍅 番茄钟: 25/5 focus/break with circular ring timer + tally. - 🪑 久坐: count-up since last break, dock-bounce alert at 45min. - 💧 喝水: animated cup, daily goal tracking with progress dots. - 🏃 下班: H:M:S countdown to clock-out hour with gradient bar. - 🎉 周末: 天/时/分 to next Saturday + rotating 打工语录. 100% local. UNUserNotificationCenter for alerts, dock-bounce fallback when authorization denied. UserDefaults persistence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
198 lines
6.1 KiB
Swift
198 lines
6.1 KiB
Swift
//
|
||
// ExpandedView.swift
|
||
// 摸鱼侠 plugin v0.1
|
||
//
|
||
// Top-level panel container — 380×620 (40pt notch + 580pt content).
|
||
// Renders title bar + 5-tab pill strip + active tab body + footer.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
struct ExpandedView: View {
|
||
@ObservedObject var store: WorkerStore = .shared
|
||
|
||
enum Tab: String, CaseIterable, Identifiable {
|
||
case pomodoro = "番茄"
|
||
case sit = "久坐"
|
||
case water = "喝水"
|
||
case clockout = "下班"
|
||
case weekend = "周末"
|
||
var id: String { rawValue }
|
||
|
||
var emoji: String {
|
||
switch self {
|
||
case .pomodoro: return "🍅"
|
||
case .sit: return "🪑"
|
||
case .water: return "💧"
|
||
case .clockout: return "🏃"
|
||
case .weekend: return "🎉"
|
||
}
|
||
}
|
||
}
|
||
|
||
@State private var tab: Tab = .pomodoro
|
||
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
// Notch reservation (40pt) — same pattern as 看盘侠 / Music
|
||
// Player. Host's floating back-chevron lives here.
|
||
Color.clear.frame(height: 40)
|
||
topBar
|
||
tabStrip
|
||
body_
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||
footer
|
||
}
|
||
.frame(width: 380, height: 620)
|
||
.background(
|
||
ZStack {
|
||
WorkerTheme.panelBg
|
||
RadialGradient(
|
||
colors: [Color.white.opacity(0.04), Color.clear],
|
||
center: .top,
|
||
startRadius: 4, endRadius: 220
|
||
)
|
||
}
|
||
)
|
||
.clipShape(
|
||
UnevenRoundedRectangle(
|
||
cornerRadii: .init(topLeading: 0, bottomLeading: 28, bottomTrailing: 28, topTrailing: 0)
|
||
)
|
||
)
|
||
.onAppear {
|
||
store.start()
|
||
}
|
||
}
|
||
|
||
// MARK: - Top bar
|
||
|
||
private var topBar: some View {
|
||
HStack(spacing: 10) {
|
||
HStack(spacing: 8) {
|
||
Circle()
|
||
.fill(WorkerTheme.lime)
|
||
.frame(width: 7, height: 7)
|
||
.shadow(color: WorkerTheme.lime.opacity(0.6), radius: 4)
|
||
Text("摸鱼侠")
|
||
.font(.system(size: 14, weight: .semibold))
|
||
.foregroundColor(WorkerTheme.fgPrimary)
|
||
}
|
||
Spacer()
|
||
// Notification status dot — green = authorized, dim = falling
|
||
// back to in-panel pulses.
|
||
Circle()
|
||
.fill(store.notificationsAuthorized ? WorkerTheme.lime.opacity(0.7) : WorkerTheme.fg35)
|
||
.frame(width: 6, height: 6)
|
||
.help(store.notificationsAuthorized ? "通知已开启" : "通知未开启 · 仅面板提示")
|
||
}
|
||
.padding(.horizontal, 16)
|
||
.padding(.top, 14)
|
||
.padding(.bottom, 8)
|
||
}
|
||
|
||
// MARK: - Tabs
|
||
|
||
private var tabStrip: some View {
|
||
// 5 tabs at 380pt panel — give them equal share with tight pad.
|
||
HStack(spacing: 6) {
|
||
ForEach(Tab.allCases) { t in
|
||
tabPill(tab: t, selected: tab == t) { tab = t }
|
||
}
|
||
}
|
||
.padding(.horizontal, 12)
|
||
.padding(.bottom, 10)
|
||
}
|
||
|
||
private func tabPill(tab t: Tab, selected: Bool, action: @escaping () -> Void) -> some View {
|
||
Button(action: action) {
|
||
HStack(spacing: 4) {
|
||
Text(t.emoji).font(.system(size: 11))
|
||
Text(t.rawValue)
|
||
.font(.system(size: 11.5, weight: .semibold))
|
||
}
|
||
.foregroundColor(selected ? Color(red: 0x0B/255, green: 0x0B/255, blue: 0x0B/255) : WorkerTheme.fg70)
|
||
.padding(.horizontal, 10)
|
||
.frame(height: 28)
|
||
.frame(maxWidth: .infinity)
|
||
.background(
|
||
Capsule()
|
||
.fill(selected ? WorkerTheme.lime : WorkerTheme.overlay04)
|
||
.overlay(
|
||
Capsule()
|
||
.stroke(selected ? Color.clear : WorkerTheme.overlay08, lineWidth: 0.5)
|
||
)
|
||
)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
|
||
// MARK: - Body
|
||
|
||
@ViewBuilder
|
||
private var body_: some View {
|
||
switch tab {
|
||
case .pomodoro: PomodoroView(store: store)
|
||
case .sit: SitView(store: store)
|
||
case .water: WaterView(store: store)
|
||
case .clockout: ClockoutView(store: store)
|
||
case .weekend: WeekendView(store: store)
|
||
}
|
||
}
|
||
|
||
// MARK: - Footer
|
||
|
||
private var footer: some View {
|
||
HStack {
|
||
HStack(spacing: 6) {
|
||
LiveDot()
|
||
Text(footerLeftText)
|
||
}
|
||
Spacer()
|
||
Text("v0.1 · 本地运行")
|
||
}
|
||
.font(.system(size: 11))
|
||
.foregroundColor(WorkerTheme.fg40)
|
||
.padding(.horizontal, 16)
|
||
.padding(.vertical, 10)
|
||
.background(
|
||
Rectangle()
|
||
.fill(Color.clear)
|
||
.overlay(
|
||
Rectangle()
|
||
.fill(WorkerTheme.overlay04)
|
||
.frame(height: 0.5),
|
||
alignment: .top
|
||
)
|
||
)
|
||
}
|
||
|
||
private var footerLeftText: String {
|
||
switch tab {
|
||
case .pomodoro: return "今日 \(store.pomodoroTodayCount) 个番茄"
|
||
case .sit: return store.sitStart == nil ? "未开始计时" : "久坐监控中"
|
||
case .water: return "目标 \(store.waterGoal) 杯"
|
||
case .clockout: return String(format: "%02d:%02d 下班", store.clockoutHour, store.clockoutMinute)
|
||
case .weekend: return "下个周六"
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Live dot
|
||
|
||
private struct LiveDot: View {
|
||
@State private var pulse = false
|
||
var body: some View {
|
||
Circle()
|
||
.fill(WorkerTheme.lime)
|
||
.frame(width: 6, height: 6)
|
||
.shadow(color: WorkerTheme.lime.opacity(0.7), radius: 4)
|
||
.scaleEffect(pulse ? 0.8 : 1.0)
|
||
.opacity(pulse ? 0.4 : 1.0)
|
||
.onAppear {
|
||
withAnimation(.easeInOut(duration: 1.6).repeatForever(autoreverses: true)) {
|
||
pulse = true
|
||
}
|
||
}
|
||
}
|
||
}
|