Files
LockInBroMacOS/LockInBro/ContentView.swift

236 lines
7.1 KiB
Swift
Raw Normal View History

// ContentView.swift Auth gate + main tab navigation
2026-03-28 14:53:40 -04:00
import SwiftUI
struct ContentView: View {
@Environment(AuthManager.self) private var auth
@Environment(SessionManager.self) private var session
@State private var selectedTab: AppTab = .tasks
@State private var showingSettings = false
@AppStorage("geminiApiKey") private var geminiApiKey = ""
enum AppTab: String, CaseIterable {
case tasks = "Tasks"
case brainDump = "Brain Dump"
case focusSession = "Focus"
var systemImage: String {
switch self {
case .tasks: return "checklist"
case .brainDump: return "brain.head.profile"
case .focusSession: return "target"
}
}
}
2026-03-28 14:53:40 -04:00
var body: some View {
if !auth.isLoggedIn {
LoginView()
} else {
mainContent
}
}
private var mainContent: some View {
NavigationSplitView {
// Sidebar
2026-03-29 00:58:22 -04:00
VStack(spacing: 0) {
List(AppTab.allCases, id: \.self, selection: $selectedTab) { tab in
Label(tab.rawValue, systemImage: tab.systemImage)
.badge(tab == .focusSession && session.isSessionActive ? "" : nil)
}
Divider()
HStack {
Button {
showingSettings = true
} label: {
2026-03-29 00:58:22 -04:00
Label("Settings", systemImage: "gear")
.font(.subheadline)
}
2026-03-29 00:58:22 -04:00
.buttonStyle(.plain)
.foregroundStyle(.secondary)
Spacer()
Button {
auth.logout()
} label: {
Image(systemName: "rectangle.portrait.and.arrow.right")
}
2026-03-29 00:58:22 -04:00
.buttonStyle(.plain)
.foregroundStyle(.secondary)
.help("Sign out")
}
2026-03-29 00:58:22 -04:00
.padding(.horizontal, 12)
.padding(.vertical, 10)
}
2026-03-29 00:58:22 -04:00
.navigationSplitViewColumnWidth(min: 160, ideal: 180)
.navigationTitle("LockInBro")
.sheet(isPresented: $showingSettings) {
SettingsSheet(geminiApiKey: $geminiApiKey)
}
} detail: {
NavigationStack {
detailView
.navigationTitle(selectedTab.rawValue)
}
}
.frame(minWidth: 700, minHeight: 500)
// Auto-navigate to Focus tab when a session becomes active
.onChange(of: session.isSessionActive) { _, isActive in
if isActive { selectedTab = .focusSession }
}
// Active session banner at bottom
.safeAreaInset(edge: .bottom) {
if session.isSessionActive {
sessionBanner
}
2026-03-28 14:53:40 -04:00
}
}
@ViewBuilder
private var detailView: some View {
switch selectedTab {
case .tasks:
TaskBoardView()
case .brainDump:
BrainDumpView(onGoToTasks: { selectedTab = .tasks })
case .focusSession:
if session.isSessionActive {
FocusSessionView()
} else {
StartSessionPlaceholder {
selectedTab = .tasks
}
}
}
}
private var sessionBanner: some View {
HStack(spacing: 12) {
Circle()
.fill(.green)
.frame(width: 8, height: 8)
.overlay(
Circle()
.fill(.green.opacity(0.3))
.frame(width: 16, height: 16)
)
if let task = session.activeTask {
Text("Focusing on: \(task.title)")
.font(.subheadline.bold())
.lineLimit(1)
} else {
Text("Focus session active")
.font(.subheadline.bold())
}
if let step = session.currentStep {
Text("· \(step.title)")
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer()
Button("View Session") {
selectedTab = .focusSession
}
.buttonStyle(.bordered)
.controlSize(.small)
Button {
Task { await session.endSession() }
} label: {
Image(systemName: "stop.circle.fill")
.foregroundStyle(.red)
}
.buttonStyle(.plain)
.help("End session")
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(.thinMaterial)
.overlay(alignment: .top) { Divider() }
2026-03-28 14:53:40 -04:00
}
}
// MARK: - Settings Sheet
private struct SettingsSheet: View {
@Binding var geminiApiKey: String
@Environment(\.dismiss) private var dismiss
@State private var draft = ""
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text("Settings")
.font(.title2.bold())
VStack(alignment: .leading, spacing: 6) {
Text("Gemini API Key")
.font(.subheadline.bold())
Text("Required for the VLM screen analysis agent.")
.font(.caption)
.foregroundStyle(.secondary)
SecureField("AIza…", text: $draft)
.textFieldStyle(.roundedBorder)
.font(.system(.body, design: .monospaced))
}
HStack {
Spacer()
Button("Cancel") { dismiss() }
.keyboardShortcut(.escape)
Button("Save") {
geminiApiKey = draft.trimmingCharacters(in: .whitespaces)
dismiss()
}
.buttonStyle(.borderedProminent)
.keyboardShortcut(.return)
.disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty)
}
}
.padding(24)
.frame(width: 420)
.onAppear { draft = geminiApiKey }
}
}
// MARK: - Start Session Placeholder
private struct StartSessionPlaceholder: View {
let onGoToTasks: () -> Void
var body: some View {
VStack(spacing: 20) {
Image(systemName: "target")
.font(.system(size: 64))
.foregroundStyle(.secondary)
Text("No active session")
.font(.title2.bold())
Text("Go to your task board and tap the play button to start a focus session on a task.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 320)
Button {
onGoToTasks()
} label: {
Label("Go to Tasks", systemImage: "checklist")
.frame(width: 160)
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
2026-03-28 14:53:40 -04:00
}