349 lines
12 KiB
Swift
349 lines
12 KiB
Swift
import Combine
|
|
import SwiftUI
|
|
|
|
enum SessionViewMode: String {
|
|
case list
|
|
case calendar
|
|
}
|
|
|
|
struct SessionsView: View {
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
@State private var showingAddSession = false
|
|
@AppStorage("sessionViewMode") private var viewMode: SessionViewMode = .list
|
|
@State private var selectedMonth = Date()
|
|
@State private var selectedDate: Date? = nil
|
|
@State private var selectedSessionId: UUID? = nil
|
|
|
|
private var completedSessions: [ClimbSession] {
|
|
dataManager.sessions
|
|
.filter { $0.status == .completed }
|
|
.sorted { $0.date > $1.date }
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if dataManager.sessions.isEmpty && dataManager.activeSession == nil {
|
|
EmptySessionsView()
|
|
} else {
|
|
if viewMode == .list {
|
|
SessionsList(onNavigateToSession: { sessionId in
|
|
selectedSessionId = sessionId
|
|
})
|
|
} else {
|
|
CalendarView(
|
|
sessions: completedSessions,
|
|
selectedMonth: $selectedMonth,
|
|
selectedDate: $selectedDate,
|
|
onNavigateToSession: { sessionId in
|
|
selectedSessionId = sessionId
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
.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.sessions.isEmpty || dataManager.activeSession != nil {
|
|
Button(action: {
|
|
withAnimation(.easeInOut(duration: 0.2)) {
|
|
viewMode = viewMode == .list ? .calendar : .list
|
|
selectedDate = nil
|
|
}
|
|
}) {
|
|
Image(systemName: viewMode == .list ? "calendar" : "list.bullet")
|
|
.font(.body)
|
|
.fontWeight(.semibold)
|
|
}
|
|
}
|
|
|
|
if dataManager.gyms.isEmpty {
|
|
EmptyView()
|
|
} else if dataManager.activeSession == nil {
|
|
Button("Start Session") {
|
|
if dataManager.gyms.count == 1 {
|
|
Task {
|
|
await dataManager.startSession(gymId: dataManager.gyms.first!.id)
|
|
}
|
|
} else {
|
|
showingAddSession = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingAddSession) {
|
|
AddEditSessionView()
|
|
}
|
|
.navigationDestination(isPresented: .constant(selectedSessionId != nil)) {
|
|
if let sessionId = selectedSessionId {
|
|
SessionDetailView(sessionId: sessionId)
|
|
.onDisappear {
|
|
selectedSessionId = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SessionsList: View {
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
@State private var sessionToDelete: ClimbSession?
|
|
var onNavigateToSession: (UUID) -> Void
|
|
|
|
private var completedSessions: [ClimbSession] {
|
|
dataManager.sessions
|
|
.filter { $0.status == .completed }
|
|
.sorted { $0.date > $1.date }
|
|
}
|
|
|
|
var body: some View {
|
|
List {
|
|
if let activeSession = dataManager.activeSession,
|
|
let gym = dataManager.gym(withId: activeSession.gymId)
|
|
{
|
|
Section {
|
|
ActiveSessionBanner(
|
|
session: activeSession,
|
|
gym: gym,
|
|
onNavigateToSession: onNavigateToSession
|
|
)
|
|
.padding(.horizontal, 16)
|
|
.listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0))
|
|
.listRowBackground(Color.clear)
|
|
.listRowSeparator(.hidden)
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
.tint(.red)
|
|
}
|
|
}
|
|
} header: {
|
|
if dataManager.activeSession != nil {
|
|
Text("Previous Sessions")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
.confirmationDialog(
|
|
"Delete Session",
|
|
isPresented: .init(
|
|
get: { sessionToDelete != nil },
|
|
set: { if !$0 { sessionToDelete = nil } }
|
|
),
|
|
titleVisibility: .visible
|
|
) {
|
|
Button("Delete", role: .destructive) {
|
|
if let session = sessionToDelete {
|
|
dataManager.deleteSession(session)
|
|
sessionToDelete = nil
|
|
}
|
|
}
|
|
Button("Cancel", role: .cancel) {
|
|
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
|
|
var onNavigateToSession: (UUID) -> Void
|
|
|
|
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 {
|
|
onNavigateToSession(session.id)
|
|
}
|
|
|
|
Button(action: {
|
|
Task {
|
|
await 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)
|
|
)
|
|
|
|
}
|
|
}
|
|
|
|
struct SessionRow: View {
|
|
let session: ClimbSession
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
|
|
private static let dateFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .medium
|
|
return formatter
|
|
}()
|
|
|
|
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(Self.dateFormatter.string(from: 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)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
Task {
|
|
await dataManager.startSession(gymId: dataManager.gyms.first!.id)
|
|
}
|
|
} else {
|
|
showingAddSession = true
|
|
}
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.controlSize(.large)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.sheet(isPresented: $showingAddSession) {
|
|
AddEditSessionView()
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
SessionsView()
|
|
.environmentObject(ClimbingDataManager.preview)
|
|
}
|