Initial commit: KisaniCal iOS app
Task management, Eisenhower matrix, workout logging with persistence, and calendar with 6 view modes (Month, List, Year, Week, 3 Day, Day). Custom floating tab bar, dynamic calendar icon, and workout data seeded.
This commit is contained in:
765
KisaniCal/Views/SettingsView.swift
Normal file
765
KisaniCal/Views/SettingsView.swift
Normal file
@@ -0,0 +1,765 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.colorScheme) var cs
|
||||
@EnvironmentObject var workoutVM: WorkoutViewModel
|
||||
|
||||
// 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 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) {
|
||||
|
||||
Text("Settings")
|
||||
.font(AppFonts.sans(15, weight: .semibold))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
.padding(.top, 10).padding(.bottom, 4)
|
||||
|
||||
// ── Profile ──
|
||||
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: 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()
|
||||
.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: $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: - 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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user