265 lines
8.3 KiB
Swift
265 lines
8.3 KiB
Swift
// ── Menu Bar Popover ──────────────────────────────────────────
|
|
// Main popover view shown when clicking the menu bar icon
|
|
|
|
import SwiftUI
|
|
|
|
struct MenuBarPopover: View {
|
|
@EnvironmentObject var store: MacTimerStore
|
|
@EnvironmentObject var menuBar: MenuBarState
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Header
|
|
header
|
|
|
|
Divider().background(CMColors.border)
|
|
|
|
// Quick timer creation
|
|
if menuBar.showQuickTimer {
|
|
quickTimerForm
|
|
Divider().background(CMColors.border)
|
|
}
|
|
|
|
// Timer list
|
|
if store.activeTimers.isEmpty && !menuBar.showQuickTimer {
|
|
emptyState
|
|
} else {
|
|
timerList
|
|
}
|
|
|
|
Divider().background(CMColors.border)
|
|
|
|
// Footer
|
|
footer
|
|
}
|
|
.frame(width: 320)
|
|
.background(CMColors.bg)
|
|
}
|
|
|
|
// MARK: - Header
|
|
|
|
private var header: some View {
|
|
HStack {
|
|
Text("ChronoMind")
|
|
.font(CMFonts.display(size: 14))
|
|
.foregroundStyle(CMColors.text)
|
|
|
|
Spacer()
|
|
|
|
Button {
|
|
menuBar.showQuickTimer.toggle()
|
|
} label: {
|
|
Image(systemName: menuBar.showQuickTimer ? "xmark" : "plus")
|
|
.font(.body.weight(.semibold))
|
|
.foregroundStyle(CMColors.accent)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.keyboardShortcut("t", modifiers: [.command, .shift])
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 10)
|
|
}
|
|
|
|
// MARK: - Quick Timer Form
|
|
|
|
private var quickTimerForm: some View {
|
|
VStack(spacing: 8) {
|
|
TextField("Timer label", text: $menuBar.quickTimerLabel)
|
|
.textFieldStyle(.plain)
|
|
.font(CMFonts.body(size: 14))
|
|
.foregroundStyle(CMColors.text)
|
|
.padding(8)
|
|
.background(CMColors.surface)
|
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
|
|
HStack {
|
|
Text("\(Int(menuBar.quickTimerMinutes))m")
|
|
.font(CMFonts.mono(size: 13, weight: .semibold))
|
|
.foregroundStyle(CMColors.accent)
|
|
.frame(width: 40)
|
|
|
|
Slider(value: $menuBar.quickTimerMinutes, in: 1...120, step: 1)
|
|
.tint(CMColors.accent)
|
|
}
|
|
|
|
HStack(spacing: 8) {
|
|
// Preset buttons
|
|
ForEach([5, 15, 25, 60], id: \.self) { minutes in
|
|
Button("\(minutes)m") {
|
|
menuBar.quickTimerMinutes = Double(minutes)
|
|
}
|
|
.font(CMFonts.body(size: 11, weight: .medium))
|
|
.foregroundStyle(menuBar.quickTimerMinutes == Double(minutes) ? .white : CMColors.textSecondary)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(menuBar.quickTimerMinutes == Double(minutes) ? CMColors.accent : CMColors.surfaceHover)
|
|
.clipShape(Capsule())
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button("Start") {
|
|
let label = menuBar.quickTimerLabel.isEmpty ? "Timer" : menuBar.quickTimerLabel
|
|
store.addCountdown(
|
|
label: label,
|
|
durationSeconds: menuBar.quickTimerMinutes * 60
|
|
)
|
|
menuBar.resetQuickTimer()
|
|
}
|
|
.font(CMFonts.body(size: 13, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 6)
|
|
.background(CMColors.accent)
|
|
.clipShape(Capsule())
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(12)
|
|
}
|
|
|
|
// MARK: - Timer List
|
|
|
|
private var timerList: some View {
|
|
ScrollView {
|
|
LazyVStack(spacing: 1) {
|
|
ForEach(store.activeTimers.sorted(by: { $0.targetTime < $1.targetTime })) { timer in
|
|
MacTimerRow(timer: timer, now: store.now)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxHeight: 300)
|
|
}
|
|
|
|
// MARK: - Empty State
|
|
|
|
private var emptyState: some View {
|
|
VStack(spacing: 8) {
|
|
Image(systemName: "clock")
|
|
.font(.system(size: 28))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
Text("No active timers")
|
|
.font(CMFonts.body(size: 13))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
Text("Press ⌘⇧T to create one")
|
|
.font(CMFonts.body(size: 11))
|
|
.foregroundStyle(CMColors.textMuted.opacity(0.7))
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 24)
|
|
}
|
|
|
|
// MARK: - Footer
|
|
|
|
private var footer: some View {
|
|
HStack {
|
|
Text("\(store.activeTimers.count) active")
|
|
.font(CMFonts.body(size: 11))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
|
|
Spacer()
|
|
|
|
if #available(macOS 14.0, *) {
|
|
SettingsLink {
|
|
Text("Settings")
|
|
.font(CMFonts.body(size: 11))
|
|
.foregroundStyle(CMColors.textSecondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
Button("Quit") {
|
|
NSApplication.shared.terminate(nil)
|
|
}
|
|
.font(CMFonts.body(size: 11))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
.buttonStyle(.plain)
|
|
.keyboardShortcut("q")
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
}
|
|
}
|
|
|
|
// MARK: - Mac Timer Row
|
|
|
|
struct MacTimerRow: View {
|
|
let timer: CMTimer
|
|
let now: Date
|
|
|
|
@EnvironmentObject var store: MacTimerStore
|
|
|
|
var body: some View {
|
|
HStack(spacing: 8) {
|
|
// Urgency dot
|
|
Circle()
|
|
.fill(CMColors.urgencyColor(timer.urgency))
|
|
.frame(width: 6, height: 6)
|
|
|
|
// Label
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(timer.label)
|
|
.font(CMFonts.body(size: 13, weight: .medium))
|
|
.foregroundStyle(CMColors.text)
|
|
.lineLimit(1)
|
|
|
|
Text(formatTime(timer.targetTime))
|
|
.font(CMFonts.body(size: 11))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Countdown
|
|
let remaining = getRemainingSeconds(timer, now: now)
|
|
Text(formatDuration(remaining))
|
|
.font(CMFonts.mono(size: 13, weight: .semibold))
|
|
.foregroundStyle(stateColor)
|
|
|
|
// Actions
|
|
if timer.state == .firing {
|
|
Button {
|
|
store.snooze(timer.id, minutes: 5)
|
|
} label: {
|
|
Image(systemName: "moon.zzz")
|
|
.font(.caption)
|
|
.foregroundStyle(CMColors.textMuted)
|
|
}
|
|
.buttonStyle(.plain)
|
|
|
|
Button {
|
|
store.dismiss(timer.id)
|
|
} label: {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(CMColors.error)
|
|
}
|
|
.buttonStyle(.plain)
|
|
} else {
|
|
Button {
|
|
store.removeTimer(timer.id)
|
|
} label: {
|
|
Image(systemName: "xmark")
|
|
.font(.caption2.weight(.bold))
|
|
.foregroundStyle(CMColors.textMuted.opacity(0.5))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
.background(timer.state == .firing ? CMColors.urgencyBg(timer.urgency) : Color.clear)
|
|
.contentShape(Rectangle())
|
|
}
|
|
|
|
private var stateColor: Color {
|
|
switch timer.state {
|
|
case .firing: return CMColors.urgencyColor(timer.urgency)
|
|
case .warning: return CMColors.important
|
|
case .paused: return CMColors.textMuted
|
|
default: return CMColors.accent
|
|
}
|
|
}
|
|
}
|