Files
Gaze/Gaze/Views/MenuBar/MenuBarContentView.swift

330 lines
10 KiB
Swift

//
// MenuBarContentView.swift
// Gaze
//
// Created by Mike Freno on 1/7/26.
//
import SwiftUI
// Hover button style for menubar items
struct MenuBarButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(
RoundedRectangle(cornerRadius: 6)
.fill(
configuration.isPressed
? Color.accentColor.opacity(0.2) : Color.gray.opacity(0.1)
)
.opacity(configuration.isPressed ? 1 : 0)
)
.contentShape(Rectangle())
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
struct MenuBarHoverButtonStyle: ButtonStyle {
@State private var isHovered = false
func makeBody(configuration: Configuration) -> some View {
configuration.label
.glassEffect(
isHovered ? .regular.tint(.accentColor.opacity(0.5)).interactive() : .regular,
in: .rect(cornerRadius: 6)
)
.contentShape(Rectangle())
.onHover { hovering in
isHovered = hovering
}
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
.animation(.easeInOut(duration: 0.1), value: isHovered)
.animation(.easeInOut(duration: 0.05), value: configuration.isPressed)
}
}
struct MenuBarContentView: View {
@ObservedObject var timerEngine: TimerEngine
@ObservedObject var settingsManager: SettingsManager
var onQuit: () -> Void
var onOpenSettings: () -> Void
var onOpenSettingsTab: (Int) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Header
HStack {
Image(systemName: "eye.fill")
.font(.title2)
.foregroundColor(.accentColor)
Text("Gaze")
.font(.title2)
.fontWeight(.semibold)
}
.padding()
Divider()
// Timer Status
VStack(alignment: .leading, spacing: 12) {
Text("Active Timers")
.font(.caption)
.foregroundColor(.secondary)
.padding(.horizontal)
.padding(.top, 8)
ForEach(TimerType.allCases) { timerType in
if let state = timerEngine.timerStates[timerType] {
TimerStatusRow(
type: timerType,
state: state,
onSkip: {
timerEngine.skipNext(type: timerType)
},
onDevTrigger: {
timerEngine.triggerReminder(for: timerType)
},
onTap: {
onOpenSettingsTab(timerType.tabIndex)
}
)
} else {
InactiveTimerRow(
type: timerType,
onTap: {
onOpenSettingsTab(timerType.tabIndex)
}
)
}
}
}
.padding(.bottom, 8)
Divider()
// Controls
VStack(spacing: 4) {
Button(action: {
if timerEngine.timerStates.values.first?.isPaused == true {
timerEngine.resume()
} else {
timerEngine.pause()
}
}) {
HStack {
Image(systemName: isPaused ? "play.circle" : "pause.circle")
Text(isPaused ? "Resume All Timers" : "Pause All Timers")
Spacer()
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.buttonStyle(MenuBarHoverButtonStyle())
Button(action: {
onOpenSettings()
}) {
HStack {
Image(systemName: "gearshape")
Text("Settings...")
Spacer()
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.buttonStyle(MenuBarHoverButtonStyle())
}
.padding(.vertical, 8)
.padding(.horizontal, 8)
Divider()
// Quit
Button(action: onQuit) {
HStack {
Image(systemName: "power")
.foregroundColor(.red)
Text("Quit Gaze")
Spacer()
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.buttonStyle(MenuBarHoverButtonStyle())
.padding(.horizontal, 8)
.padding(.vertical, 8)
}
.frame(width: 300)
}
private var isPaused: Bool {
timerEngine.timerStates.values.first?.isPaused ?? false
}
}
struct TimerStatusRow: View {
let type: TimerType
let state: TimerState
var onSkip: () -> Void
var onDevTrigger: (() -> Void)? = nil
var onTap: (() -> Void)? = nil
@State private var isHoveredSkip = false
@State private var isHoveredDevTrigger = false
@State private var isHoveredBody = false
var body: some View {
HStack {
HStack {
Image(systemName: type.iconName)
.foregroundColor(iconColor)
.frame(width: 20)
VStack(alignment: .leading, spacing: 2) {
Text(type.displayName)
.font(.subheadline)
.fontWeight(.medium)
Text(timeRemaining)
.font(.caption)
.foregroundColor(.secondary)
.monospacedDigit()
}
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
onTap?()
}
#if DEBUG
if let onDevTrigger = onDevTrigger {
Button(action: onDevTrigger) {
Image(systemName: "bolt.fill")
.font(.caption)
.foregroundColor(.yellow)
.padding(6)
}
.buttonStyle(.plain)
.glassEffect(
isHoveredDevTrigger ? .regular.tint(.yellow.opacity(0.5)) : .regular,
in: .circle
)
.help("Trigger \(type.displayName) reminder now (dev)")
.onHover { hovering in
isHoveredDevTrigger = hovering
}
}
#endif
Button(action: onSkip) {
Image(systemName: "forward.fill")
.font(.caption)
.foregroundColor(.accentColor)
.padding(6)
}
.buttonStyle(.plain)
.glassEffect(
isHoveredSkip ? .regular.tint(.accentColor.opacity(0.5)) : .regular,
in: .circle
)
.help("Skip to next \(type.displayName) reminder")
.onHover { hovering in
isHoveredSkip = hovering
}
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
.glassEffect(
isHoveredBody ? .regular.tint(.accentColor.opacity(0.5)) : .regular,
in: .rect(cornerRadius: 6)
)
.padding(.horizontal, 8)
.onHover { hovering in
isHoveredBody = hovering
}
.help(tooltipText)
}
private var tooltipText: String {
type.tooltipText
}
private var iconColor: Color {
switch type {
case .lookAway: return .accentColor
case .blink: return .green
case .posture: return .orange
}
}
private var timeRemaining: String {
let seconds = state.remainingSeconds
let minutes = seconds / 60
let remainingSeconds = seconds % 60
if minutes >= 60 {
let hours = minutes / 60
let remainingMinutes = minutes % 60
return String(format: "%dh %dm", hours, remainingMinutes)
} else if minutes > 0 {
return String(format: "%dm %ds", minutes, remainingSeconds)
} else {
return String(format: "%ds", remainingSeconds)
}
}
}
struct InactiveTimerRow: View {
let type: TimerType
var onTap: () -> Void
@State private var isHovered = false
var body: some View {
Button(action: onTap) {
HStack {
Image(systemName: type.iconName)
.foregroundColor(.secondary)
.frame(width: 20)
VStack(alignment: .leading, spacing: 2) {
Text(type.displayName)
.font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "plus.circle")
.font(.title3)
.foregroundColor(.accentColor)
.padding(6)
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.buttonStyle(.plain)
.glassEffect(
isHovered ? .regular.tint(.accentColor.opacity(0.5)) : .regular,
in: .rect(cornerRadius: 6)
)
.padding(.horizontal, 8)
.onHover { hovering in
isHovered = hovering
}
.help("Enable \(type.displayName) reminders")
}
}
#Preview("Menu Bar Content") {
let settingsManager = SettingsManager.shared
let timerEngine = TimerEngine(settingsManager: settingsManager)
MenuBarContentView(
timerEngine: timerEngine,
settingsManager: settingsManager,
onQuit: {},
onOpenSettings: {},
onOpenSettingsTab: { _ in }
)
}