285 lines
9.4 KiB
Swift
285 lines
9.4 KiB
Swift
import Combine
|
|
import SwiftUI
|
|
|
|
struct SessionsView: View {
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
@State private var showingAddSession = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if dataManager.sessions.isEmpty && dataManager.activeSession == nil {
|
|
EmptySessionsView()
|
|
} else {
|
|
SessionsList()
|
|
}
|
|
}
|
|
.navigationTitle("Sessions")
|
|
.navigationBarTitleDisplayMode(.automatic)
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
|
if dataManager.isSyncing {
|
|
HStack(spacing: 2) {
|
|
ProgressView()
|
|
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
|
|
.scaleEffect(0.6)
|
|
}
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 3)
|
|
.background(
|
|
Circle()
|
|
.fill(.regularMaterial)
|
|
)
|
|
.transition(.scale.combined(with: .opacity))
|
|
.animation(
|
|
.easeInOut(duration: 0.2), value: dataManager.isSyncing
|
|
)
|
|
}
|
|
|
|
if dataManager.gyms.isEmpty {
|
|
EmptyView()
|
|
} else if dataManager.activeSession == nil {
|
|
Button("Start Session") {
|
|
if dataManager.gyms.count == 1 {
|
|
dataManager.startSession(gymId: dataManager.gyms.first!.id)
|
|
} else {
|
|
showingAddSession = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingAddSession) {
|
|
AddEditSessionView()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SessionsList: View {
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
@State private var sessionToDelete: ClimbSession?
|
|
|
|
private var completedSessions: [ClimbSession] {
|
|
dataManager.sessions
|
|
.filter { $0.status == .completed }
|
|
.sorted { $0.date > $1.date }
|
|
}
|
|
|
|
var body: some View {
|
|
List {
|
|
// Active session banner section
|
|
if let activeSession = dataManager.activeSession,
|
|
let gym = dataManager.gym(withId: activeSession.gymId)
|
|
{
|
|
Section {
|
|
ActiveSessionBanner(session: activeSession, gym: gym)
|
|
.padding(.horizontal, 16)
|
|
.listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0))
|
|
.listRowBackground(Color.clear)
|
|
.listRowSeparator(.hidden)
|
|
}
|
|
}
|
|
|
|
// Completed sessions section
|
|
if !completedSessions.isEmpty {
|
|
Section {
|
|
ForEach(completedSessions) { session in
|
|
NavigationLink(destination: SessionDetailView(sessionId: session.id)) {
|
|
SessionRow(session: session)
|
|
}
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
Button(role: .destructive) {
|
|
sessionToDelete = session
|
|
} label: {
|
|
Label("Delete", systemImage: "trash")
|
|
}
|
|
}
|
|
}
|
|
} header: {
|
|
if dataManager.activeSession != nil {
|
|
Text("Previous Sessions")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
.alert("Delete Session", isPresented: .constant(sessionToDelete != nil)) {
|
|
Button("Cancel", role: .cancel) {
|
|
sessionToDelete = nil
|
|
}
|
|
Button("Delete", role: .destructive) {
|
|
if let session = sessionToDelete {
|
|
dataManager.deleteSession(session)
|
|
sessionToDelete = nil
|
|
}
|
|
}
|
|
} message: {
|
|
Text(
|
|
"Are you sure you want to delete this session? This will also delete all attempts associated with this session."
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ActiveSessionBanner: View {
|
|
let session: ClimbSession
|
|
let gym: Gym
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
@State private var navigateToDetail = false
|
|
|
|
var body: some View {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack {
|
|
Image(systemName: "play.fill")
|
|
.foregroundColor(.green)
|
|
.font(.caption)
|
|
Text("Active Session")
|
|
.font(.headline)
|
|
.fontWeight(.bold)
|
|
}
|
|
|
|
Text(gym.name)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
if let startTime = session.startTime {
|
|
Text(timerInterval: startTime...Date.distantFuture, countsDown: false)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.monospacedDigit()
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
navigateToDetail = true
|
|
}
|
|
|
|
Button(action: {
|
|
dataManager.endSession(session.id)
|
|
}) {
|
|
Image(systemName: "stop.fill")
|
|
.font(.system(size: 16, weight: .bold))
|
|
.foregroundColor(.white)
|
|
.frame(width: 32, height: 32)
|
|
.background(Color.red)
|
|
.clipShape(Circle())
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(.green.opacity(0.1))
|
|
.stroke(.green.opacity(0.3), lineWidth: 1)
|
|
)
|
|
|
|
.navigationDestination(isPresented: $navigateToDetail) {
|
|
SessionDetailView(sessionId: session.id)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
struct SessionRow: View {
|
|
let session: ClimbSession
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
|
|
private var gym: Gym? {
|
|
dataManager.gym(withId: session.gymId)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text(gym?.name ?? "Unknown Gym")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
|
|
Spacer()
|
|
|
|
Text(formatDate(session.date))
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
if let duration = session.duration {
|
|
Text("Duration: \(duration) minutes")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
if let notes = session.notes, !notes.isEmpty {
|
|
Text(notes)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.lineLimit(2)
|
|
}
|
|
}
|
|
.padding(.vertical, 8)
|
|
}
|
|
|
|
private func formatDate(_ date: Date) -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .medium
|
|
return formatter.string(from: date)
|
|
}
|
|
}
|
|
|
|
struct EmptySessionsView: View {
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
@State private var showingAddSession = false
|
|
|
|
var body: some View {
|
|
VStack(spacing: 20) {
|
|
Spacer()
|
|
|
|
Image(systemName: "figure.climbing")
|
|
.font(.system(size: 60))
|
|
.foregroundColor(.secondary)
|
|
|
|
VStack(spacing: 8) {
|
|
Text(dataManager.gyms.isEmpty ? "No Gyms Available" : "No Sessions Yet")
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
|
|
Text(
|
|
dataManager.gyms.isEmpty
|
|
? "Add a gym first to start tracking your climbing sessions!"
|
|
: "Start your first climbing session!"
|
|
)
|
|
.font(.body)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
if !dataManager.gyms.isEmpty {
|
|
Button("Start Session") {
|
|
if dataManager.gyms.count == 1 {
|
|
dataManager.startSession(gymId: dataManager.gyms.first!.id)
|
|
} else {
|
|
showingAddSession = true
|
|
}
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.controlSize(.large)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.sheet(isPresented: $showingAddSession) {
|
|
AddEditSessionView()
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
SessionsView()
|
|
.environmentObject(ClimbingDataManager.preview)
|
|
}
|