1.5.0 Initial run as iOS in a monorepo
This commit is contained in:
243
ios/OpenClimb/Views/SessionsView.swift
Normal file
243
ios/OpenClimb/Views/SessionsView.swift
Normal file
@@ -0,0 +1,243 @@
|
||||
//
|
||||
// SessionsView.swift
|
||||
// OpenClimb
|
||||
//
|
||||
// Created by OpenClimb on 2025-01-17.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct SessionsView: View {
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
@State private var showingAddSession = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 0) {
|
||||
// Active session banner
|
||||
if let activeSession = dataManager.activeSession,
|
||||
let gym = dataManager.gym(withId: activeSession.gymId)
|
||||
{
|
||||
VStack(spacing: 8) {
|
||||
ActiveSessionBanner(session: activeSession, gym: gym)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
// Sessions list
|
||||
if dataManager.sessions.isEmpty && dataManager.activeSession == nil {
|
||||
EmptySessionsView()
|
||||
} else {
|
||||
SessionsList()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Sessions")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
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 ActiveSessionBanner: View {
|
||||
let session: ClimbSession
|
||||
let gym: Gym
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
@State private var currentTime = Date()
|
||||
|
||||
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: SessionDetailView(sessionId: session.id)) {
|
||||
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(formatDuration(from: startTime, to: currentTime))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("End") {
|
||||
dataManager.endSession(session.id)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(.green.opacity(0.1))
|
||||
.stroke(.green.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onReceive(timer) { _ in
|
||||
currentTime = Date()
|
||||
}
|
||||
}
|
||||
|
||||
private func formatDuration(from start: Date, to end: Date) -> String {
|
||||
let interval = end.timeIntervalSince(start)
|
||||
let hours = Int(interval) / 3600
|
||||
let minutes = Int(interval) % 3600 / 60
|
||||
let seconds = Int(interval) % 60
|
||||
|
||||
if hours > 0 {
|
||||
return String(format: "%dh %dm %ds", hours, minutes, seconds)
|
||||
} else if minutes > 0 {
|
||||
return String(format: "%dm %ds", minutes, seconds)
|
||||
} else {
|
||||
return String(format: "%ds", seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionsList: View {
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
|
||||
private var completedSessions: [ClimbSession] {
|
||||
dataManager.sessions
|
||||
.filter { $0.status == .completed }
|
||||
.sorted { $0.date > $1.date }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List(completedSessions) { session in
|
||||
NavigationLink(destination: SessionDetailView(sessionId: session.id)) {
|
||||
SessionRow(session: session)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
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, 4)
|
||||
}
|
||||
|
||||
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