2026-03-28 22:45:29 -04:00
|
|
|
// MenuBarView.swift — Menu bar popover content
|
|
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
struct MenuBarView: View {
|
|
|
|
|
@Environment(AuthManager.self) private var auth
|
|
|
|
|
@Environment(SessionManager.self) private var session
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
|
|
|
// Session status
|
|
|
|
|
sessionStatusSection
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
|
|
|
|
// Actions
|
|
|
|
|
actionsSection
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
2026-04-01 16:10:30 -05:00
|
|
|
// Settings
|
|
|
|
|
settingsSection
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
2026-03-28 22:45:29 -04:00
|
|
|
// Bottom
|
|
|
|
|
HStack {
|
|
|
|
|
Text(auth.currentUser?.displayName ?? auth.currentUser?.email ?? "LockInBro")
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
Spacer()
|
|
|
|
|
Button("Sign Out") { auth.logout() }
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 12)
|
|
|
|
|
.padding(.vertical, 8)
|
|
|
|
|
}
|
|
|
|
|
.frame(width: 280)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var sessionStatusSection: some View {
|
|
|
|
|
HStack(spacing: 10) {
|
|
|
|
|
Circle()
|
|
|
|
|
.fill(session.isSessionActive ? Color.green : Color.secondary.opacity(0.4))
|
|
|
|
|
.frame(width: 8, height: 8)
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text(session.isSessionActive ? "Session Active" : "No Active Session")
|
|
|
|
|
.font(.subheadline.bold())
|
|
|
|
|
|
|
|
|
|
if let task = session.activeTask {
|
|
|
|
|
Text(task.title)
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
} else if session.isSessionActive {
|
|
|
|
|
Text("No task selected")
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
if session.isSessionActive, session.distractionCount > 0 {
|
|
|
|
|
Label("\(session.distractionCount)", systemImage: "exclamationmark.triangle.fill")
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.orange)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 12)
|
|
|
|
|
.padding(.vertical, 10)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var actionsSection: some View {
|
|
|
|
|
VStack(spacing: 2) {
|
|
|
|
|
if session.isSessionActive {
|
|
|
|
|
MenuBarButton(
|
|
|
|
|
icon: "stop.circle",
|
|
|
|
|
title: "End Session",
|
|
|
|
|
color: .red
|
|
|
|
|
) {
|
|
|
|
|
Task { await session.endSession() }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let step = session.currentStep {
|
|
|
|
|
MenuBarButton(
|
|
|
|
|
icon: "checkmark.circle",
|
|
|
|
|
title: "Mark '\(step.title.prefix(25))…' Done",
|
|
|
|
|
color: .green
|
|
|
|
|
) {
|
|
|
|
|
Task { await session.completeCurrentStep() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MenuBarButton(
|
|
|
|
|
icon: "arrow.uturn.backward.circle",
|
|
|
|
|
title: "Show Resume Card",
|
|
|
|
|
color: .blue
|
|
|
|
|
) {
|
|
|
|
|
Task { await session.fetchResumeCard() }
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
MenuBarButton(
|
|
|
|
|
icon: "play.circle",
|
|
|
|
|
title: "Start Focus Session",
|
|
|
|
|
color: .blue
|
|
|
|
|
) {
|
|
|
|
|
// Opens main window — user picks task there
|
|
|
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
NSApp.windows.first?.makeKeyAndOrderFront(nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MenuBarButton(
|
|
|
|
|
icon: "macwindow",
|
|
|
|
|
title: "Open LockInBro",
|
|
|
|
|
color: .primary
|
|
|
|
|
) {
|
|
|
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
NSApp.windows.first?.makeKeyAndOrderFront(nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(.vertical, 4)
|
|
|
|
|
}
|
2026-04-01 16:10:30 -05:00
|
|
|
private var settingsSection: some View {
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
Image(systemName: "bell.badge")
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.frame(width: 16)
|
|
|
|
|
Text("Nudge after")
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
Spacer()
|
|
|
|
|
Picker("", selection: Binding(
|
|
|
|
|
get: { Int(session.distractionThresholdSeconds) },
|
|
|
|
|
set: { session.distractionThresholdSeconds = TimeInterval($0) }
|
|
|
|
|
)) {
|
|
|
|
|
Text("1 min").tag(60)
|
|
|
|
|
Text("2 min").tag(120)
|
|
|
|
|
Text("3 min").tag(180)
|
|
|
|
|
Text("5 min").tag(300)
|
|
|
|
|
}
|
|
|
|
|
.pickerStyle(.menu)
|
|
|
|
|
.frame(width: 80)
|
|
|
|
|
.font(.caption)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 12)
|
|
|
|
|
.padding(.vertical, 6)
|
|
|
|
|
}
|
2026-03-28 22:45:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Menu Bar Button
|
|
|
|
|
|
|
|
|
|
private struct MenuBarButton: View {
|
|
|
|
|
let icon: String
|
|
|
|
|
let title: String
|
|
|
|
|
let color: Color
|
|
|
|
|
let action: () -> Void
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
Button(action: action) {
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
Image(systemName: icon)
|
|
|
|
|
.foregroundStyle(color)
|
|
|
|
|
.frame(width: 16)
|
|
|
|
|
Text(title)
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
Spacer()
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 12)
|
|
|
|
|
.padding(.vertical, 7)
|
|
|
|
|
.contentShape(.rect)
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
.background(Color.clear)
|
|
|
|
|
.hoverEffect()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// hoverEffect for macOS (no-op style that adds highlight on hover)
|
|
|
|
|
private extension View {
|
|
|
|
|
@ViewBuilder
|
|
|
|
|
func hoverEffect() -> some View {
|
|
|
|
|
self.onHover { _ in } // triggers redraw; real hover highlight handled below
|
|
|
|
|
}
|
|
|
|
|
}
|