mirror of
https://github.com/carey314/mio-plugin-worker.git
synced 2026-08-10 07:04:32 +00:00
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
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|