224 lines
7.1 KiB
Swift
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())
|
|
}
|
|
}
|
|
}
|