Tapping the profile card opens a full stats sheet showing: Tasks — completed this week, overdue count, done rate, 7-day bar chart, open count by Eisenhower quadrant. Workouts — streak, sets today, today % progress, weekly schedule grid (dumbbell = scheduled, checkmark = done), active program summary.
1015 lines
54 KiB
Swift
1015 lines
54 KiB
Swift
import SwiftUI
|
||
|
||
struct SettingsView: View {
|
||
@Environment(\.colorScheme) var cs
|
||
@EnvironmentObject var workoutVM: WorkoutViewModel
|
||
@EnvironmentObject var taskVM: TaskViewModel
|
||
|
||
// Persisted settings
|
||
@AppStorage("appearanceMode") private var appearanceMode = 0
|
||
@AppStorage("soundsOn") private var soundsOn = true
|
||
@AppStorage("healthOn") private var healthOn = false
|
||
@AppStorage("showMatrixTab") private var showMatrixTab = true
|
||
@AppStorage("showWorkoutTab") private var showWorkoutTab = true
|
||
@AppStorage("dateFormat") private var dateFormat = 0
|
||
@AppStorage("firstDayMonday") private var firstDayMonday = false
|
||
@AppStorage("showCompleted") private var showCompleted = false
|
||
@AppStorage("weightUnit") private var weightUnit = 0
|
||
@AppStorage("defaultSets") private var defaultSets = 3
|
||
@AppStorage("defaultReps") private var defaultReps = 10
|
||
@AppStorage("iCloudSync") private var iCloudSync = true
|
||
@AppStorage("googleCalSync") private var googleCalSync = false
|
||
@AppStorage("iCloudCalSync") private var iCloudCalSync = true
|
||
|
||
// Sheet presentation
|
||
@State private var showProfile = false
|
||
@State private var showTabBar = false
|
||
@State private var showAppearance = false
|
||
@State private var showDateTime = false
|
||
@State private var showWidgets = false
|
||
@State private var showGeneral = false
|
||
@State private var showImport = false
|
||
@State private var showBackup = false
|
||
@State private var showWorkoutSettings = false
|
||
@State private var showRestTimer = false
|
||
@State private var showHelp = false
|
||
@State private var showFollow = false
|
||
@State private var showAbout = false
|
||
|
||
private var appearanceLabel: String { ["System", "Light", "Dark"][appearanceMode] }
|
||
private var restLabel: String { "\(workoutVM.restTimerSeconds)s" }
|
||
private var weightLabel: String { weightUnit == 0 ? "kg" : "lbs" }
|
||
private var dateLabel: String { ["MMM d", "MM/DD", "DD/MM"][dateFormat] }
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
AppColors.background(cs).ignoresSafeArea()
|
||
ScrollView(showsIndicators: false) {
|
||
VStack(spacing: 0) {
|
||
Color.clear.frame(height: 0).trackScrollForTabBar()
|
||
|
||
Text("Settings")
|
||
.font(AppFonts.sans(15, weight: .semibold))
|
||
.foregroundColor(AppColors.text(cs))
|
||
.padding(.top, 10).padding(.bottom, 4)
|
||
|
||
// ── Profile ──
|
||
Button { showProfile = true } label: {
|
||
HStack(spacing: 12) {
|
||
ZStack {
|
||
Circle().fill(AppColors.accentSoft)
|
||
Text("RK")
|
||
.font(AppFonts.sans(15, weight: .bold))
|
||
.foregroundColor(AppColors.accent)
|
||
}
|
||
.frame(width: 44, height: 44)
|
||
.overlay(Circle().stroke(AppColors.accent.opacity(0.18), lineWidth: 1.5))
|
||
|
||
VStack(alignment: .leading, spacing: 5) {
|
||
Text("Robin Kutesa")
|
||
.font(AppFonts.sans(14, weight: .semibold))
|
||
.foregroundColor(AppColors.text(cs))
|
||
HStack(spacing: 5) {
|
||
AppBadge(
|
||
text: workoutVM.streakDays > 0 ? "\(workoutVM.streakDays)d streak 🔥" : "No streak yet",
|
||
bg: AppColors.accentSoft, fg: AppColors.accent
|
||
)
|
||
AppBadge(
|
||
text: "\(taskVM.tasks.filter { $0.isComplete }.count) done",
|
||
bg: AppColors.greenSoft, fg: AppColors.green
|
||
)
|
||
AppBadge(
|
||
text: weightUnit == 0 ? "kg" : "lbs",
|
||
bg: AppColors.surface3(cs), fg: AppColors.text2(cs)
|
||
)
|
||
}
|
||
}
|
||
Spacer()
|
||
Image(systemName: "chevron.right")
|
||
.font(.system(size: 10))
|
||
.foregroundColor(AppColors.text3(cs))
|
||
}
|
||
.padding(14).cardStyle()
|
||
}
|
||
.buttonStyle(.plain)
|
||
.padding(.horizontal, 14).padding(.top, 10).padding(.bottom, 14)
|
||
|
||
// ── CUSTOMIZE ──
|
||
SttSection(label: "Customize") {
|
||
SttNavRow(icon: "menucard", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Tab Bar", value: "\(showMatrixTab && showWorkoutTab ? "5" : showMatrixTab || showWorkoutTab ? "4" : "3") tabs") { showTabBar = true }
|
||
SttNavRow(icon: "paintpalette", bg: AppColors.blueSoft, fg: AppColors.blue, label: "Appearance", value: appearanceLabel) { showAppearance = true }
|
||
SttToggleRow(icon: "bell", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Sounds & Notifications", isOn: $soundsOn)
|
||
SttNavRow(icon: "clock", bg: AppColors.greenSoft, fg: AppColors.green, label: "Date & Time", value: dateLabel) { showDateTime = true }
|
||
SttNavRow(icon: "puzzlepiece", bg: AppColors.yellowSoft, fg: AppColors.yellow, label: "Widgets") { showWidgets = true }
|
||
SttNavRow(icon: "gearshape", bg: AppColors.surface3(cs), label: "General", isLast: true) { showGeneral = true }
|
||
}
|
||
|
||
// ── DATA ──
|
||
SttSection(label: "Data") {
|
||
SttNavRow(icon: "arrow.up.right", bg: AppColors.greenSoft, fg: AppColors.green, label: "Connected Sources", value: googleCalSync || iCloudCalSync ? "Connected" : nil) { showImport = true }
|
||
SttNavRow(icon: "cloud", bg: AppColors.blueSoft, fg: AppColors.blue, label: "Library Sync", value: iCloudSync ? "● Synced" : "Off", valueColor: iCloudSync ? AppColors.green : nil, isLast: true) { showBackup = true }
|
||
}
|
||
|
||
// ── FITNESS ──
|
||
SttSection(label: "Fitness") {
|
||
SttNavRow(icon: "dumbbell", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Workout Settings", value: weightLabel) { showWorkoutSettings = true }
|
||
SttNavRow(icon: "figure.run", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Rest Timer", value: restLabel) { showRestTimer = true }
|
||
SttToggleRow(icon: "heart", bg: AppColors.greenSoft, fg: AppColors.green, label: "Health Integration", isOn: $healthOn, isLast: true)
|
||
}
|
||
|
||
// ── SUPPORT ──
|
||
SttSection(label: "Support", secondary: true) {
|
||
SttNavRow(icon: "star", bg: AppColors.yellowSoft, fg: AppColors.yellow, label: "Help & Feedback") { showHelp = true }
|
||
SttNavRow(icon: "message", bg: AppColors.blueSoft, fg: AppColors.blue, label: "Follow Us", value: "𝕏 👾 📸") { showFollow = true }
|
||
SttNavRow(icon: "info.circle", bg: AppColors.surface3(cs), label: "About", value: "v1.0.0", isLast: true) { showAbout = true }
|
||
}
|
||
|
||
Spacer().frame(height: 40)
|
||
}
|
||
}
|
||
}
|
||
.sheet(isPresented: $showProfile) { ProfileStatsSheet().environmentObject(taskVM).environmentObject(workoutVM).presentationDetents([.large]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showTabBar) { TabBarSheet(showMatrix: $showMatrixTab, showWorkout: $showWorkoutTab).presentationDetents([.height(320)]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showAppearance) { AppearanceSheet(mode: $appearanceMode).presentationDetents([.height(290)]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showDateTime) { DateTimeSheet(format: $dateFormat, mondayFirst: $firstDayMonday).presentationDetents([.height(300)]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showWidgets) { WidgetsSheet().presentationDetents([.height(320)]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showGeneral) { GeneralSheet(showCompleted: $showCompleted, mondayFirst: $firstDayMonday).presentationDetents([.height(260)]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showImport) { ImportSheet(googleCal: $googleCalSync, iCloudCal: $iCloudCalSync).presentationDetents([.height(280)]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showBackup) { BackupSheet(iCloudSync: $iCloudSync).presentationDetents([.height(280)]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showWorkoutSettings) { WorkoutSettingsSheet(unit: $weightUnit, sets: $defaultSets, reps: $defaultReps).presentationDetents([.large]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showRestTimer) { RestTimerSheet(seconds: $workoutVM.restTimerSeconds).presentationDetents([.height(340)]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showHelp) { HelpSheet().presentationDetents([.height(300)]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showFollow) { FollowSheet().presentationDetents([.height(280)]).presentationDragIndicator(.visible) }
|
||
.sheet(isPresented: $showAbout) { AboutSheet().presentationDetents([.height(340)]).presentationDragIndicator(.visible) }
|
||
}
|
||
}
|
||
|
||
// MARK: - Section
|
||
private struct SttSection<Content: View>: View {
|
||
@Environment(\.colorScheme) private var cs
|
||
let label: String
|
||
var secondary: Bool = false
|
||
@ViewBuilder let content: Content
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 0) {
|
||
Text(label)
|
||
.font(AppFonts.sans(secondary ? 9.5 : 10, weight: .semibold))
|
||
.foregroundColor(secondary ? AppColors.text3(cs) : AppColors.text2(cs))
|
||
.padding(.horizontal, 18).padding(.bottom, 5)
|
||
VStack(spacing: 0) { content }
|
||
.cardStyle().padding(.horizontal, 14).padding(.bottom, secondary ? 6 : 8)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Nav Row
|
||
private struct SttNavRow: View {
|
||
@Environment(\.colorScheme) private var cs
|
||
let icon: String; let bg: Color
|
||
var fg: Color? = nil
|
||
let label: String
|
||
var value: String? = nil; var valueColor: Color? = nil; var isLast: Bool = false
|
||
let action: () -> Void
|
||
var body: some View {
|
||
Button(action: action) {
|
||
VStack(spacing: 0) {
|
||
HStack(spacing: 11) {
|
||
Image(systemName: icon).font(.system(size: 14))
|
||
.foregroundColor(fg ?? AppColors.text2(cs))
|
||
.frame(width: 28, height: 28).background(bg).cornerRadius(8)
|
||
Text(label).font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
if let v = value {
|
||
Text(v).font(AppFonts.mono(11))
|
||
.foregroundColor(valueColor ?? AppColors.text2(cs)).lineLimit(1)
|
||
}
|
||
Image(systemName: "chevron.right").font(.system(size: 10))
|
||
.foregroundColor(AppColors.text3(cs))
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 11)
|
||
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 53) }
|
||
}.contentShape(Rectangle())
|
||
}.buttonStyle(.plain)
|
||
}
|
||
}
|
||
|
||
// MARK: - Toggle Row
|
||
private struct SttToggleRow: View {
|
||
@Environment(\.colorScheme) private var cs
|
||
let icon: String; let bg: Color
|
||
var fg: Color? = nil
|
||
let label: String
|
||
@Binding var isOn: Bool; var isLast: Bool = false
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
HStack(spacing: 11) {
|
||
Image(systemName: icon).font(.system(size: 14))
|
||
.foregroundColor(fg ?? AppColors.text2(cs))
|
||
.frame(width: 28, height: 28).background(bg).cornerRadius(8)
|
||
Text(label).font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
Toggle("", isOn: $isOn).labelsHidden().tint(AppColors.accent)
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 11)
|
||
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 53) }
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Shared Header
|
||
private struct SheetHeader: View {
|
||
@Environment(\.colorScheme) private var cs
|
||
let title: String; let onDone: () -> Void
|
||
var body: some View {
|
||
HStack {
|
||
Text(title).font(AppFonts.sans(17, weight: .bold)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
Button("Done", action: onDone).font(AppFonts.sans(13, weight: .semibold))
|
||
.foregroundColor(AppColors.accent).buttonStyle(.plain)
|
||
}
|
||
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 16)
|
||
}
|
||
}
|
||
|
||
// MARK: - Tab Bar Sheet
|
||
private struct TabBarSheet: View {
|
||
@Binding var showMatrix: Bool
|
||
@Binding var showWorkout: Bool
|
||
@Environment(\.dismiss) var dismiss
|
||
@Environment(\.colorScheme) var cs
|
||
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "Tab Bar") { dismiss() }
|
||
VStack(spacing: 0) {
|
||
TabToggleRow(icon: "checkmark", label: "Tasks", isOn: .constant(true), locked: true)
|
||
TabToggleRow(icon: "calendar", label: "Calendar", isOn: .constant(true), locked: true)
|
||
TabToggleRow(icon: "square.grid.2x2", label: "Matrix", isOn: $showMatrix, locked: false)
|
||
TabToggleRow(icon: "dumbbell", label: "Workout", isOn: $showWorkout, locked: false)
|
||
TabToggleRow(icon: "gearshape", label: "Settings", isOn: .constant(true), locked: true, isLast: true)
|
||
}
|
||
.cardStyle().padding(.horizontal, 20)
|
||
Text("Tasks, Calendar, and Settings are always visible.")
|
||
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
|
||
.multilineTextAlignment(.center).padding(.horizontal, 20).padding(.top, 12)
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|
||
|
||
private struct TabToggleRow: View {
|
||
@Environment(\.colorScheme) private var cs
|
||
let icon: String; let label: String
|
||
@Binding var isOn: Bool; let locked: Bool; var isLast: Bool = false
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
HStack(spacing: 12) {
|
||
Image(systemName: icon).font(.system(size: 13)).foregroundColor(locked ? AppColors.text3(cs) : AppColors.text(cs)).frame(width: 22)
|
||
Text(label).font(AppFonts.sans(13)).foregroundColor(locked ? AppColors.text3(cs) : AppColors.text(cs))
|
||
Spacer()
|
||
if locked {
|
||
Text("Always on").font(AppFonts.mono(9.5)).foregroundColor(AppColors.text3(cs))
|
||
} else {
|
||
Toggle("", isOn: $isOn).labelsHidden().tint(AppColors.accent)
|
||
}
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 12)
|
||
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 48) }
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Appearance Sheet
|
||
private struct AppearanceSheet: View {
|
||
@Binding var mode: Int
|
||
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
|
||
private let options = [("System", "circle.lefthalf.filled"), ("Light", "sun.max"), ("Dark", "moon.stars")]
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "Appearance") { dismiss() }
|
||
VStack(spacing: 8) {
|
||
ForEach(options.indices, id: \.self) { i in
|
||
Button { withAnimation(KisaniSpring.micro) { mode = i } } label: {
|
||
HStack(spacing: 14) {
|
||
Image(systemName: options[i].1).font(.system(size: 15, weight: .medium))
|
||
.foregroundColor(mode == i ? AppColors.accent : AppColors.text2(cs)).frame(width: 24)
|
||
Text(options[i].0).font(AppFonts.sans(14, weight: mode == i ? .semibold : .regular))
|
||
.foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
if mode == i { Image(systemName: "checkmark").font(.system(size: 12, weight: .bold)).foregroundColor(AppColors.accent) }
|
||
}
|
||
.padding(.horizontal, 16).padding(.vertical, 14)
|
||
.background(mode == i ? AppColors.accentSoft : AppColors.surface2(cs))
|
||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.medium))
|
||
.overlay(RoundedRectangle(cornerRadius: AppRadius.medium).stroke(mode == i ? AppColors.accent.opacity(0.35) : AppColors.border(cs), lineWidth: 1))
|
||
}.buttonStyle(.plain)
|
||
}
|
||
}
|
||
.padding(.horizontal, 20)
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|
||
|
||
// MARK: - Date & Time Sheet
|
||
private struct DateTimeSheet: View {
|
||
@Binding var format: Int
|
||
@Binding var mondayFirst: Bool
|
||
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
|
||
private let formats = ["MMM d", "MM/DD/YYYY", "DD/MM/YYYY"]
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "Date & Time") { dismiss() }
|
||
VStack(alignment: .leading, spacing: 20) {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text("DATE FORMAT").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||
HStack(spacing: 8) {
|
||
ForEach(formats.indices, id: \.self) { i in
|
||
Button { withAnimation { format = i } } label: {
|
||
Text(formats[i]).font(AppFonts.mono(11, weight: .bold))
|
||
.foregroundColor(format == i ? AppColors.accent : AppColors.text2(cs))
|
||
.frame(maxWidth: .infinity).padding(.vertical, 10)
|
||
.background(format == i ? AppColors.accentSoft : AppColors.surface2(cs))
|
||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(format == i ? AppColors.accent : AppColors.border(cs), lineWidth: 1))
|
||
}.buttonStyle(.plain)
|
||
}
|
||
}
|
||
}
|
||
VStack(spacing: 0) {
|
||
HStack {
|
||
Text("First day of week").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
Text(mondayFirst ? "Monday" : "Sunday").font(AppFonts.mono(11)).foregroundColor(AppColors.text3(cs))
|
||
Toggle("", isOn: $mondayFirst).labelsHidden().tint(AppColors.accent)
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 12)
|
||
}
|
||
.cardStyle()
|
||
}
|
||
.padding(.horizontal, 20)
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|
||
|
||
// MARK: - Widgets Sheet
|
||
private struct WidgetsSheet: View {
|
||
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
|
||
private let widgets = [("Small", "1×1 — Today's task count", "square.grid.2x2"), ("Medium", "2×1 — Upcoming tasks list", "rectangle.grid.1x2"), ("Large", "2×2 — Full task view", "square.split.2x2")]
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "Widgets") { dismiss() }
|
||
VStack(spacing: 10) {
|
||
ForEach(widgets, id: \.0) { w in
|
||
HStack(spacing: 14) {
|
||
Image(systemName: w.2).font(.system(size: 20)).foregroundColor(AppColors.accent)
|
||
.frame(width: 44, height: 44).background(AppColors.accentSoft)
|
||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(w.0).font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
|
||
Text(w.1).font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
|
||
}
|
||
Spacer()
|
||
}
|
||
.padding(12).cardStyle()
|
||
}
|
||
Text("Long-press your home screen → tap + → search Kisani Cal to add widgets.")
|
||
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
|
||
.multilineTextAlignment(.center).padding(.top, 4)
|
||
}
|
||
.padding(.horizontal, 20)
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|
||
|
||
// MARK: - General Sheet
|
||
private struct GeneralSheet: View {
|
||
@Binding var showCompleted: Bool
|
||
@Binding var mondayFirst: Bool
|
||
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "General") { dismiss() }
|
||
VStack(spacing: 0) {
|
||
HStack {
|
||
Text("Show completed tasks").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
Toggle("", isOn: $showCompleted).labelsHidden().tint(AppColors.accent)
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 12)
|
||
Divider().background(AppColors.border(cs)).padding(.leading, 14)
|
||
HStack {
|
||
Text("Week starts on Monday").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
Toggle("", isOn: $mondayFirst).labelsHidden().tint(AppColors.accent)
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 12)
|
||
}
|
||
.cardStyle().padding(.horizontal, 20)
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|
||
|
||
// MARK: - Import Sheet
|
||
private struct ImportSheet: View {
|
||
@Binding var googleCal: Bool
|
||
@Binding var iCloudCal: Bool
|
||
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "Import & Integration") { dismiss() }
|
||
VStack(spacing: 0) {
|
||
HStack(spacing: 12) {
|
||
Image(systemName: "calendar.badge.plus").font(.system(size: 16))
|
||
.foregroundColor(AppColors.blue).frame(width: 28, height: 28)
|
||
.background(AppColors.blueSoft).cornerRadius(8)
|
||
Text("Google Calendar").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
Toggle("", isOn: $googleCal).labelsHidden().tint(AppColors.accent)
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 12)
|
||
Divider().background(AppColors.border(cs)).padding(.leading, 53)
|
||
HStack(spacing: 12) {
|
||
Image(systemName: "cloud").font(.system(size: 16))
|
||
.foregroundColor(AppColors.blue).frame(width: 28, height: 28)
|
||
.background(AppColors.blueSoft).cornerRadius(8)
|
||
Text("iCloud Calendar").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
Toggle("", isOn: $iCloudCal).labelsHidden().tint(AppColors.accent)
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 12)
|
||
}
|
||
.cardStyle().padding(.horizontal, 20)
|
||
Text("Enabling integrations will sync events to your Kisani calendar.")
|
||
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
|
||
.multilineTextAlignment(.center).padding(.horizontal, 20).padding(.top, 12)
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|
||
|
||
// MARK: - Backup Sheet
|
||
private struct BackupSheet: View {
|
||
@Binding var iCloudSync: Bool
|
||
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "Backup & Sync") { dismiss() }
|
||
VStack(spacing: 14) {
|
||
VStack(spacing: 0) {
|
||
HStack(spacing: 12) {
|
||
Image(systemName: "icloud").font(.system(size: 16))
|
||
.foregroundColor(AppColors.blue).frame(width: 28, height: 28)
|
||
.background(AppColors.blueSoft).cornerRadius(8)
|
||
Text("iCloud Sync").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
Toggle("", isOn: $iCloudSync).labelsHidden().tint(AppColors.accent)
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 12)
|
||
if iCloudSync {
|
||
Divider().background(AppColors.border(cs)).padding(.leading, 53)
|
||
HStack {
|
||
Text("Last synced").font(AppFonts.sans(12)).foregroundColor(AppColors.text2(cs))
|
||
Spacer()
|
||
Text("Today · just now").font(AppFonts.mono(11)).foregroundColor(AppColors.green)
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 10)
|
||
}
|
||
}
|
||
.cardStyle().padding(.horizontal, 20)
|
||
.animation(KisaniSpring.snappy, value: iCloudSync)
|
||
|
||
if iCloudSync {
|
||
Button {
|
||
// trigger sync
|
||
} label: {
|
||
Text("Sync Now")
|
||
.font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.accent)
|
||
.frame(maxWidth: .infinity).padding(13)
|
||
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.accent, lineWidth: 1.5))
|
||
}
|
||
.buttonStyle(.plain).padding(.horizontal, 20)
|
||
}
|
||
}
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|
||
|
||
// MARK: - Workout Settings Sheet
|
||
private struct WorkoutSettingsSheet: View {
|
||
@Binding var unit: Int
|
||
@Binding var sets: Int
|
||
@Binding var reps: Int
|
||
@AppStorage("bodyWeightKg") private var bodyWeightKg: Double = 80
|
||
@AppStorage("heightCm") private var heightCm: Double = 175
|
||
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
|
||
|
||
@State private var bwStr = ""
|
||
@State private var htStr = ""
|
||
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "Workout Settings") { dismiss() }
|
||
ScrollView(showsIndicators: false) {
|
||
VStack(alignment: .leading, spacing: 20) {
|
||
|
||
// ── Body Profile ──
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text("BODY PROFILE").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||
VStack(spacing: 0) {
|
||
HStack {
|
||
Text("Body Weight").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
HStack(spacing: 4) {
|
||
TextField("80", text: $bwStr)
|
||
.font(AppFonts.mono(13, weight: .semibold))
|
||
.foregroundColor(AppColors.text(cs))
|
||
.multilineTextAlignment(.trailing)
|
||
.keyboardType(.decimalPad)
|
||
.frame(width: 56)
|
||
.onChange(of: bwStr) { val in
|
||
let clean = String(val.filter { $0.isNumber || $0 == "." }.prefix(5))
|
||
if clean != val { bwStr = clean }
|
||
if let d = Double(clean) { bodyWeightKg = d }
|
||
}
|
||
Text("kg")
|
||
.font(AppFonts.mono(10, weight: .bold))
|
||
.foregroundColor(AppColors.text3(cs))
|
||
}
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 11)
|
||
Divider().background(AppColors.border(cs)).padding(.leading, 14)
|
||
HStack {
|
||
Text("Height").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
HStack(spacing: 4) {
|
||
TextField("175", text: $htStr)
|
||
.font(AppFonts.mono(13, weight: .semibold))
|
||
.foregroundColor(AppColors.text(cs))
|
||
.multilineTextAlignment(.trailing)
|
||
.keyboardType(.numberPad)
|
||
.frame(width: 56)
|
||
.onChange(of: htStr) { val in
|
||
let clean = String(val.filter { $0.isNumber }.prefix(3))
|
||
if clean != val { htStr = clean }
|
||
if let d = Double(clean) { heightCm = d }
|
||
}
|
||
Text("cm")
|
||
.font(AppFonts.mono(10, weight: .bold))
|
||
.foregroundColor(AppColors.text3(cs))
|
||
}
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 11)
|
||
}
|
||
.cardStyle()
|
||
}
|
||
|
||
// ── Weight Unit ──
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text("WEIGHT UNIT").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||
HStack(spacing: 10) {
|
||
ForEach(["kg", "lbs"].indices, id: \.self) { i in
|
||
Button { withAnimation(KisaniSpring.micro) { unit = i } } label: {
|
||
Text(["kg", "lbs"][i]).font(AppFonts.mono(13, weight: .bold))
|
||
.foregroundColor(unit == i ? AppColors.accent : AppColors.text2(cs))
|
||
.frame(maxWidth: .infinity).padding(.vertical, 12)
|
||
.background(unit == i ? AppColors.accentSoft : AppColors.surface2(cs))
|
||
.clipShape(RoundedRectangle(cornerRadius: 9))
|
||
.overlay(RoundedRectangle(cornerRadius: 9).stroke(unit == i ? AppColors.accent : AppColors.border(cs), lineWidth: unit == i ? 1.5 : 1))
|
||
}.buttonStyle(.plain)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Defaults ──
|
||
VStack(spacing: 0) {
|
||
StepperRow(label: "Default Sets", value: $sets, range: 1...10)
|
||
Divider().background(AppColors.border(cs)).padding(.leading, 14)
|
||
StepperRow(label: "Default Reps", value: $reps, range: 1...30, isLast: true)
|
||
}
|
||
.cardStyle()
|
||
}
|
||
.padding(.horizontal, 20)
|
||
.padding(.bottom, 20)
|
||
}
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
.onAppear {
|
||
bwStr = bodyWeightKg > 0 ? (bodyWeightKg.truncatingRemainder(dividingBy: 1) == 0 ? "\(Int(bodyWeightKg))" : String(format: "%.1f", bodyWeightKg)) : ""
|
||
htStr = heightCm > 0 ? "\(Int(heightCm))" : ""
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct StepperRow: View {
|
||
@Environment(\.colorScheme) private var cs
|
||
let label: String; @Binding var value: Int; let range: ClosedRange<Int>; var isLast: Bool = false
|
||
var body: some View {
|
||
HStack {
|
||
Text(label).font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||
Spacer()
|
||
HStack(spacing: 0) {
|
||
Button { if value > range.lowerBound { value -= 1 } } label: {
|
||
Image(systemName: "minus").font(.system(size: 13, weight: .semibold))
|
||
.foregroundColor(value > range.lowerBound ? AppColors.accent : AppColors.text3(cs))
|
||
.frame(width: 32, height: 32)
|
||
}
|
||
.buttonStyle(.plain)
|
||
Text("\(value)").font(AppFonts.mono(13, weight: .bold))
|
||
.foregroundColor(AppColors.text(cs)).frame(width: 32).multilineTextAlignment(.center)
|
||
Button { if value < range.upperBound { value += 1 } } label: {
|
||
Image(systemName: "plus").font(.system(size: 13, weight: .semibold))
|
||
.foregroundColor(value < range.upperBound ? AppColors.accent : AppColors.text3(cs))
|
||
.frame(width: 32, height: 32)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
.background(AppColors.surface2(cs)).clipShape(RoundedRectangle(cornerRadius: 8))
|
||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(AppColors.border(cs), lineWidth: 1))
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 10)
|
||
}
|
||
}
|
||
|
||
// MARK: - Rest Timer Sheet
|
||
private struct RestTimerSheet: View {
|
||
@Binding var seconds: Int
|
||
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
|
||
private let options = [30, 45, 60, 90, 120, 180]
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "Rest Timer") { dismiss() }
|
||
Text("\(seconds)s").font(AppFonts.mono(52, weight: .bold)).foregroundColor(AppColors.accent).padding(.bottom, 20)
|
||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 10), count: 3), spacing: 10) {
|
||
ForEach(options, id: \.self) { opt in
|
||
Button { withAnimation(KisaniSpring.micro) { seconds = opt } } label: {
|
||
Text("\(opt)s").font(AppFonts.mono(14, weight: .bold))
|
||
.foregroundColor(seconds == opt ? AppColors.accent : AppColors.text2(cs))
|
||
.frame(maxWidth: .infinity).padding(.vertical, 13)
|
||
.background(seconds == opt ? AppColors.accentSoft : AppColors.surface2(cs))
|
||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
|
||
.overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(seconds == opt ? AppColors.accent : AppColors.border(cs), lineWidth: 1))
|
||
}.buttonStyle(.plain)
|
||
}
|
||
}
|
||
.padding(.horizontal, 20)
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|
||
|
||
// MARK: - Help & Feedback Sheet
|
||
private struct HelpSheet: View {
|
||
@Environment(\.dismiss) var dismiss
|
||
@Environment(\.colorScheme) var cs
|
||
@Environment(\.openURL) var openURL
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "Help & Feedback") { dismiss() }
|
||
VStack(spacing: 10) {
|
||
HelpRow(icon: "envelope", label: "Send Feedback", sub: "Report a bug or suggest a feature") {
|
||
if let url = URL(string: "mailto:feedback@kisanicaI.app?subject=Kisani%20Cal%20Feedback") { openURL(url) }
|
||
}
|
||
HelpRow(icon: "questionmark.circle", label: "FAQ", sub: "Frequently asked questions") {}
|
||
HelpRow(icon: "book", label: "User Guide", sub: "Learn how to use Kisani Cal") {}
|
||
}
|
||
.padding(.horizontal, 20)
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|
||
|
||
private struct HelpRow: View {
|
||
@Environment(\.colorScheme) private var cs
|
||
let icon: String; let label: String; let sub: String; let action: () -> Void
|
||
var body: some View {
|
||
Button(action: action) {
|
||
HStack(spacing: 14) {
|
||
Image(systemName: icon).font(.system(size: 16)).foregroundColor(AppColors.accent)
|
||
.frame(width: 40, height: 40).background(AppColors.accentSoft)
|
||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(label).font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
|
||
Text(sub).font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
|
||
}
|
||
Spacer()
|
||
Image(systemName: "chevron.right").font(.system(size: 10)).foregroundColor(AppColors.text3(cs))
|
||
}
|
||
.padding(12).cardStyle()
|
||
}.buttonStyle(.plain)
|
||
}
|
||
}
|
||
|
||
// MARK: - Follow Us Sheet
|
||
private struct FollowSheet: View {
|
||
@Environment(\.dismiss) var dismiss
|
||
@Environment(\.colorScheme) var cs
|
||
@Environment(\.openURL) var openURL
|
||
private let socials: [(String, String, String, String)] = [
|
||
("𝕏 Twitter / X", "Share your streaks", "at.circle", "https://x.com"),
|
||
("Discord", "Join the community", "bubble.left.and.bubble.right", "https://discord.com"),
|
||
("Instagram", "Behind the scenes", "camera", "https://instagram.com"),
|
||
]
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "Follow Us") { dismiss() }
|
||
VStack(spacing: 8) {
|
||
ForEach(socials, id: \.0) { s in
|
||
Button { if let url = URL(string: s.3) { openURL(url) } } label: {
|
||
HStack(spacing: 14) {
|
||
Image(systemName: s.2).font(.system(size: 16)).foregroundColor(AppColors.accent)
|
||
.frame(width: 40, height: 40).background(AppColors.accentSoft)
|
||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(s.0).font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
|
||
Text(s.1).font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
|
||
}
|
||
Spacer()
|
||
Image(systemName: "arrow.up.right").font(.system(size: 10)).foregroundColor(AppColors.text3(cs))
|
||
}
|
||
.padding(12).cardStyle()
|
||
}.buttonStyle(.plain)
|
||
}
|
||
}
|
||
.padding(.horizontal, 20)
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|
||
|
||
// MARK: - Profile Stats Sheet
|
||
private struct ProfileStatsSheet: View {
|
||
@Environment(\.dismiss) var dismiss
|
||
@Environment(\.colorScheme) var cs
|
||
@EnvironmentObject var taskVM: TaskViewModel
|
||
@EnvironmentObject var workoutVM: WorkoutViewModel
|
||
|
||
// ── Task metrics ──
|
||
private var totalTasks: Int { taskVM.tasks.count }
|
||
private var doneTasks: Int { taskVM.tasks.filter { $0.isComplete }.count }
|
||
private var overdueCount: Int { taskVM.overdueTasks.count }
|
||
private var completionRate: Int {
|
||
guard totalTasks > 0 else { return 0 }
|
||
return Int(Double(doneTasks) / Double(totalTasks) * 100)
|
||
}
|
||
private var completedThisWeek: Int {
|
||
let start = Calendar.current.date(byAdding: .day, value: -6, to: Calendar.current.startOfDay(for: Date()))!
|
||
return taskVM.tasks.filter { $0.isComplete && ($0.completedAt ?? .distantPast) >= start }.count
|
||
}
|
||
private var urgentOpen: Int {
|
||
taskVM.tasks.filter { !$0.isComplete && $0.quadrant == .urgent }.count
|
||
}
|
||
|
||
// ── 7-day task activity ──
|
||
private var last7: [(Date, Int)] {
|
||
let cal = Calendar.current
|
||
return (0..<7).reversed().map { ago -> (Date, Int) in
|
||
let day = cal.date(byAdding: .day, value: -ago, to: cal.startOfDay(for: Date()))!
|
||
let next = cal.date(byAdding: .day, value: 1, to: day)!
|
||
let n = taskVM.tasks.filter { t in
|
||
guard t.isComplete, let at = t.completedAt else { return false }
|
||
return at >= day && at < next
|
||
}.count
|
||
return (day, n)
|
||
}
|
||
}
|
||
private var maxDay: Int { max(1, last7.map { $0.1 }.max() ?? 1) }
|
||
|
||
// ── Workout metrics ──
|
||
private var scheduledDays: Int { workoutVM.schedule.count }
|
||
private let weekRow: [(Int, String)] = [(2,"M"),(3,"T"),(4,"W"),(5,"T"),(6,"F"),(7,"S"),(1,"S")]
|
||
private var todayWD: Int { Calendar.current.component(.weekday, from: Date()) }
|
||
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
HStack {
|
||
Spacer()
|
||
Button("Done") { dismiss() }
|
||
.font(AppFonts.sans(13, weight: .semibold))
|
||
.foregroundColor(AppColors.accent).buttonStyle(.plain)
|
||
}
|
||
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 4)
|
||
|
||
ScrollView(showsIndicators: false) {
|
||
VStack(spacing: 14) {
|
||
// ── Avatar ──
|
||
VStack(spacing: 8) {
|
||
ZStack {
|
||
Circle().fill(AppColors.accentSoft)
|
||
Text("RK").font(AppFonts.sans(24, weight: .bold)).foregroundColor(AppColors.accent)
|
||
}
|
||
.frame(width: 68, height: 68)
|
||
.overlay(Circle().stroke(AppColors.accent.opacity(0.25), lineWidth: 2))
|
||
|
||
Text("Robin Kutesa")
|
||
.font(AppFonts.sans(18, weight: .bold)).foregroundColor(AppColors.text(cs))
|
||
Text("kisaniCAL.")
|
||
.font(AppFonts.mono(11)).foregroundColor(AppColors.text3(cs))
|
||
}
|
||
.frame(maxWidth: .infinity).padding(.vertical, 14)
|
||
|
||
// ── Tasks card ──
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
Text("TASKS").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||
|
||
HStack(spacing: 8) {
|
||
PStatCell(value: "\(completedThisWeek)", label: "this week", color: AppColors.green)
|
||
PStatCell(value: "\(overdueCount)", label: "overdue", color: overdueCount > 0 ? AppColors.accent : AppColors.text3(cs))
|
||
PStatCell(value: "\(completionRate)%", label: "done rate", color: AppColors.blue)
|
||
}
|
||
|
||
// 7-day bar chart
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
Text("COMPLETED LAST 7 DAYS")
|
||
.font(AppFonts.mono(7.5, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||
HStack(alignment: .bottom, spacing: 5) {
|
||
ForEach(last7, id: \.0) { day, count in
|
||
VStack(spacing: 3) {
|
||
if count > 0 {
|
||
Text("\(count)")
|
||
.font(AppFonts.mono(7.5, weight: .bold))
|
||
.foregroundColor(AppColors.green)
|
||
}
|
||
RoundedRectangle(cornerRadius: 3)
|
||
.fill(count > 0 ? AppColors.green : AppColors.borderHi(cs))
|
||
.frame(height: count > 0 ? max(8, CGFloat(count) / CGFloat(maxDay) * 44) : 8)
|
||
Text(dayLetter(day))
|
||
.font(AppFonts.mono(7.5, weight: .bold))
|
||
.foregroundColor(Calendar.current.isDateInToday(day) ? AppColors.accent : AppColors.text3(cs))
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
}
|
||
.frame(height: 62)
|
||
}
|
||
|
||
// Quadrant breakdown
|
||
HStack(spacing: 8) {
|
||
QuadStat(label: "Urgent", count: taskVM.tasks.filter { $0.quadrant == .urgent && !$0.isComplete }.count, color: AppColors.accent)
|
||
QuadStat(label: "Schedule", count: taskVM.tasks.filter { $0.quadrant == .schedule && !$0.isComplete }.count, color: AppColors.yellow)
|
||
QuadStat(label: "Delegate", count: taskVM.tasks.filter { $0.quadrant == .delegate_ && !$0.isComplete }.count, color: AppColors.blue)
|
||
QuadStat(label: "Eliminate",count: taskVM.tasks.filter { $0.quadrant == .eliminate && !$0.isComplete }.count, color: AppColors.green)
|
||
}
|
||
}
|
||
.padding(14).background(AppColors.surface(cs))
|
||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
|
||
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1))
|
||
|
||
// ── Workouts card ──
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
Text("WORKOUTS").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||
|
||
HStack(spacing: 8) {
|
||
PStatCell(value: "\(workoutVM.streakDays)", label: "day streak", color: workoutVM.streakDays > 0 ? AppColors.accent : AppColors.text3(cs), suffix: workoutVM.streakDays > 0 ? "🔥" : "")
|
||
PStatCell(value: "\(workoutVM.doneSets)/\(workoutVM.totalSets)", label: "sets today", color: AppColors.blue)
|
||
PStatCell(value: "\(Int(workoutVM.progress * 100))%", label: "today", color: workoutVM.progress >= 1 ? AppColors.green : AppColors.text2(cs))
|
||
}
|
||
|
||
// Weekly schedule grid
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
Text("THIS WEEK'S SCHEDULE")
|
||
.font(AppFonts.mono(7.5, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||
HStack(spacing: 5) {
|
||
ForEach(weekRow, id: \.0) { wd, letter in
|
||
let scheduled = workoutVM.schedule[wd] != nil
|
||
let isToday = wd == todayWD
|
||
let done = isToday && workoutVM.progress >= 1
|
||
VStack(spacing: 4) {
|
||
ZStack {
|
||
Circle()
|
||
.fill(scheduled
|
||
? (done ? AppColors.greenSoft : (isToday ? AppColors.accentSoft : AppColors.surface2(cs)))
|
||
: AppColors.surface2(cs))
|
||
.frame(width: 30, height: 30)
|
||
if scheduled {
|
||
if done {
|
||
Image(systemName: "checkmark")
|
||
.font(.system(size: 9, weight: .bold))
|
||
.foregroundColor(AppColors.green)
|
||
} else {
|
||
Image(systemName: "dumbbell.fill")
|
||
.font(.system(size: 8))
|
||
.foregroundColor(isToday ? AppColors.accent : AppColors.text3(cs))
|
||
}
|
||
}
|
||
}
|
||
.overlay(Circle().stroke(isToday ? AppColors.accent : Color.clear, lineWidth: 1.5))
|
||
Text(letter)
|
||
.font(AppFonts.mono(7.5, weight: .bold))
|
||
.foregroundColor(isToday ? AppColors.accent : AppColors.text3(cs))
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Programs summary
|
||
HStack {
|
||
Image(systemName: "list.bullet.clipboard")
|
||
.font(.system(size: 13))
|
||
.foregroundColor(AppColors.blue)
|
||
.frame(width: 28, height: 28)
|
||
.background(AppColors.blueSoft)
|
||
.clipShape(RoundedRectangle(cornerRadius: 7))
|
||
VStack(alignment: .leading, spacing: 1) {
|
||
Text(workoutVM.workoutTitle)
|
||
.font(AppFonts.sans(12, weight: .semibold))
|
||
.foregroundColor(AppColors.text(cs))
|
||
Text("\(workoutVM.activeSections.count) sections · \(workoutVM.totalSets) sets total")
|
||
.font(AppFonts.mono(9)).foregroundColor(AppColors.text3(cs))
|
||
}
|
||
Spacer()
|
||
Text("ACTIVE").font(AppFonts.mono(8, weight: .bold))
|
||
.foregroundColor(AppColors.accent)
|
||
.padding(.horizontal, 7).padding(.vertical, 3)
|
||
.background(AppColors.accentSoft).cornerRadius(5)
|
||
}
|
||
.padding(10)
|
||
.background(AppColors.surface2(cs))
|
||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
|
||
}
|
||
.padding(14).background(AppColors.surface(cs))
|
||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
|
||
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1))
|
||
}
|
||
.padding(.horizontal, 14).padding(.bottom, 30)
|
||
}
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
|
||
private func dayLetter(_ date: Date) -> String {
|
||
let f = DateFormatter(); f.dateFormat = "E"
|
||
return String(f.string(from: date).prefix(1))
|
||
}
|
||
}
|
||
|
||
private struct PStatCell: View {
|
||
@Environment(\.colorScheme) private var cs
|
||
let value: String; let label: String; let color: Color; var suffix: String = ""
|
||
var body: some View {
|
||
VStack(spacing: 4) {
|
||
HStack(spacing: 3) {
|
||
Text(value).font(AppFonts.mono(17, weight: .bold)).foregroundColor(color)
|
||
if !suffix.isEmpty { Text(suffix).font(.system(size: 13)) }
|
||
}
|
||
Text(label).font(AppFonts.mono(8)).foregroundColor(AppColors.text3(cs)).multilineTextAlignment(.center)
|
||
}
|
||
.frame(maxWidth: .infinity).padding(.vertical, 12)
|
||
.background(AppColors.surface2(cs)).clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
|
||
}
|
||
}
|
||
|
||
private struct QuadStat: View {
|
||
@Environment(\.colorScheme) private var cs
|
||
let label: String; let count: Int; let color: Color
|
||
var body: some View {
|
||
VStack(spacing: 3) {
|
||
Text("\(count)").font(AppFonts.mono(14, weight: .bold)).foregroundColor(count > 0 ? color : AppColors.text3(cs))
|
||
Text(label).font(AppFonts.mono(7, weight: .bold)).foregroundColor(AppColors.text3(cs)).lineLimit(1).minimumScaleFactor(0.7)
|
||
}
|
||
.frame(maxWidth: .infinity).padding(.vertical, 8)
|
||
.background(count > 0 ? color.opacity(0.08) : AppColors.surface2(cs))
|
||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
|
||
.overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(count > 0 ? color.opacity(0.25) : AppColors.border(cs), lineWidth: 1))
|
||
}
|
||
}
|
||
|
||
// MARK: - About Sheet
|
||
private struct AboutSheet: View {
|
||
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
SheetHeader(title: "About") { dismiss() }
|
||
VStack(spacing: 16) {
|
||
Image(systemName: "calendar.badge.clock")
|
||
.font(.system(size: 48, weight: .light)).foregroundColor(AppColors.accent)
|
||
VStack(spacing: 4) {
|
||
Text("Kisani Cal").font(AppFonts.sans(20, weight: .bold)).foregroundColor(AppColors.text(cs))
|
||
Text("Version 1.0.0").font(AppFonts.mono(11)).foregroundColor(AppColors.text3(cs))
|
||
}
|
||
Text("A calm, focused productivity app for tasks, calendar, and fitness.")
|
||
.font(AppFonts.sans(13)).foregroundColor(AppColors.text2(cs))
|
||
.multilineTextAlignment(.center).padding(.horizontal, 20)
|
||
}
|
||
.padding(.top, 10)
|
||
Spacer()
|
||
}
|
||
.background(AppColors.background(cs).ignoresSafeArea())
|
||
}
|
||
}
|