Files
platform/ios/Platform/Platform/ContentView.swift
Yusuf Suleman c74f36a94d
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
fix: widen speed gaps — Slow 1.0x, Med 2.0x, Fast 3.5x (was 1.5/1.75/2.0)
2026-04-04 10:31:55 -05:00

224 lines
7.1 KiB
Swift

import SwiftUI
import ConfettiSwiftUI
struct ContentView: View {
@Environment(AuthManager.self) private var auth
var body: some View {
Group {
if auth.isCheckingAuth {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.canvas)
} else if auth.isLoggedIn {
MainTabView()
} else {
LoginView()
}
}
.task {
await auth.checkAuth()
}
}
}
struct MainTabView: View {
@Environment(AuthManager.self) private var auth
@State private var selectedTab = 0
@State private var showAssistant = false
@State private var confettiTrigger = 0
@State private var readerVM = ReaderViewModel()
@State private var isAutoScrolling = false
@State private var scrollSpeed: Double = 1.5
@State private var speedLevel = 0 // 0=slow, 1=med, 2=fast
@State private var previousTab = 0
private var showReader: Bool {
auth.currentUser?.id != 4
}
private let speedLevels: [(String, String, Double)] = [
("Slow", "hare", 1.0),
("Med", "figure.walk", 2.0),
("Fast", "bolt.fill", 3.5),
]
// Dynamic icon + label for the search-role tab
private var actionIcon: String {
if selectedTab == 2 && isAutoScrolling {
return speedLevels[speedLevel].1
}
if selectedTab == 2 {
return "play.fill"
}
return "plus"
}
private var actionLabel: String {
if selectedTab == 2 && isAutoScrolling {
return speedLevels[speedLevel].0
}
if selectedTab == 2 {
return "Play"
}
return "Add"
}
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Tab("Home", systemImage: "house.fill", value: 0) {
HomeView(selectedTab: $selectedTab)
}
Tab("Fitness", systemImage: "flame.fill", value: 1) {
FitnessTabView()
}
if showReader {
Tab("Reader", systemImage: "newspaper.fill", value: 2) {
ReaderTabView(vm: readerVM, isAutoScrolling: $isAutoScrolling, scrollSpeed: $scrollSpeed)
}
}
// Action button separated circle on trailing side of tab bar
// Home/Fitness: quick add food (+)
// Reader: play/pause auto-scroll
Tab(value: 3, role: .search) {
Color.clear
} label: {
Label(actionLabel, systemImage: actionIcon)
}
}
.tint(Color.accentWarm)
.tabBarMinimizeBehavior(.onScrollDown)
// Feedback button (not on Reader)
if selectedTab != 2 {
VStack {
Spacer()
HStack {
FeedbackButton()
.padding(.leading, 20)
Spacer()
}
.padding(.bottom, 70)
}
}
}
.confettiCannon(
trigger: $confettiTrigger,
num: 80,
confettis: [.shape(.circle), .shape(.roundedCross), .shape(.slimRectangle)],
colors: [.red, .orange, .yellow, .green, .blue, .purple, .pink],
confettiSize: 8,
rainHeight: 600,
openingAngle: Angle.degrees(40),
closingAngle: Angle.degrees(140),
radius: 300
)
.sheet(isPresented: $showAssistant) {
AssistantSheetView(onFoodAdded: foodAdded)
}
.task {
guard showReader else { return }
let renderer = ArticleRenderer.shared
renderer.attachToWindow()
await readerVM.loadInitial()
}
.onChange(of: selectedTab) { oldTab, newTab in
if newTab == 3 {
// Action tab tapped handle based on previous tab
handleActionTap(from: oldTab)
} else {
previousTab = newTab
if newTab != 2 { isAutoScrolling = false }
}
}
}
private func handleActionTap(from sourceTab: Int) {
if sourceTab == 2 {
if !isAutoScrolling {
// First tap: start at Slow
speedLevel = 0
scrollSpeed = speedLevels[0].2
isAutoScrolling = true
} else {
// Cycle: Slow Med Fast Slow
speedLevel = (speedLevel + 1) % speedLevels.count
scrollSpeed = speedLevels[speedLevel].2
}
selectedTab = 2
} else {
// Home/Fitness: open food assistant, return to previous tab
showAssistant = true
selectedTab = sourceTab
}
}
private func foodAdded() {
showAssistant = false
selectedTab = 1
confettiTrigger += 1
UINotificationFeedbackGenerator().notificationOccurred(.success)
}
}
// MARK: - Assistant Sheet
struct AssistantSheetView: View {
@State private var selectedMode = 0
var onFoodAdded: () -> Void = {}
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 12) {
Capsule()
.fill(Color.textTertiary.opacity(0.3))
.frame(width: 36, height: 5)
.padding(.top, 8)
Text("Add Food")
.font(.headline)
.foregroundStyle(Color.textPrimary)
HStack(spacing: 4) {
tabButton("AI Chat", icon: "sparkles", index: 0)
tabButton("Quick Add", icon: "magnifyingglass", index: 1)
}
.padding(4)
.background(Color.textTertiary.opacity(0.08))
.clipShape(Capsule())
.padding(.horizontal, 40)
}
.padding(.bottom, 12)
.background(Color.canvas)
if selectedMode == 0 {
AssistantChatView(onFoodAdded: onFoodAdded)
} else {
FoodSearchView(isSheet: true, onFoodAdded: onFoodAdded)
}
}
.background(Color.canvas)
.presentationDetents([.large])
}
private func tabButton(_ title: String, icon: String, index: Int) -> some View {
Button {
withAnimation(.easeInOut(duration: 0.2)) { selectedMode = index }
} label: {
HStack(spacing: 5) {
Image(systemName: icon).font(.caption2)
Text(title).font(.subheadline.weight(selectedMode == index ? .semibold : .regular))
}
.foregroundStyle(selectedMode == index ? Color.textPrimary : Color.textTertiary)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(selectedMode == index ? Color.surfaceCard : Color.clear)
.clipShape(Capsule())
}
}
}