Moved to Ascently
All checks were successful
Ascently Docker Deploy / build-and-push (push) Successful in 2m31s
All checks were successful
Ascently Docker Deploy / build-and-push (push) Successful in 2m31s
This commit is contained in:
284
ios/Ascently/Views/SessionsView.swift
Normal file
284
ios/Ascently/Views/SessionsView.swift
Normal file
@@ -0,0 +1,284 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user