1.2.2 - "Bug fixes and improvements"
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
@@ -11,7 +10,7 @@ import SwiftUI
|
||||
@State private var testResults: [String] = []
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
List {
|
||||
StatusSection()
|
||||
|
||||
@@ -364,7 +363,7 @@ import SwiftUI
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
VStack(spacing: 30) {
|
||||
Text("Icon Appearance Comparison")
|
||||
.font(.title2)
|
||||
|
||||
@@ -42,7 +42,7 @@ struct AddAttemptView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
Form {
|
||||
if !showingCreateProblem {
|
||||
ProblemSelectionSection()
|
||||
@@ -597,7 +597,7 @@ struct ProblemExpandedView: View {
|
||||
@State private var selectedImageIndex = 0
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// Images
|
||||
@@ -735,7 +735,7 @@ struct EditAttemptView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
Form {
|
||||
if !showingCreateProblem {
|
||||
ProblemSelectionSection()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AddEditGymView: View {
|
||||
@@ -34,7 +33,7 @@ struct AddEditGymView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
Form {
|
||||
BasicInfoSection()
|
||||
ClimbTypesSection()
|
||||
|
||||
@@ -55,7 +55,7 @@ struct AddEditProblemView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
Form {
|
||||
GymSelectionSection()
|
||||
BasicInfoSection()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AddEditSessionView: View {
|
||||
@@ -21,7 +20,7 @@ struct AddEditSessionView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
Form {
|
||||
GymSelectionSection()
|
||||
SessionDetailsSection()
|
||||
|
||||
@@ -4,7 +4,7 @@ struct AnalyticsView: View {
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 20) {
|
||||
OverallStatsSection()
|
||||
|
||||
@@ -420,7 +420,7 @@ struct ImageViewerView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
TabView(selection: $currentIndex) {
|
||||
ForEach(imagePaths.indices, id: \.self) { index in
|
||||
ProblemDetailImageFullView(imagePath: imagePaths[index])
|
||||
|
||||
@@ -9,24 +9,11 @@ struct SessionDetailView: View {
|
||||
@State private var showingAddAttempt = false
|
||||
@State private var editingAttempt: Attempt?
|
||||
@State private var attemptToDelete: Attempt?
|
||||
@State private var currentTime = Date()
|
||||
|
||||
private var session: ClimbSession? {
|
||||
dataManager.session(withId: sessionId)
|
||||
}
|
||||
|
||||
private func startTimer() {
|
||||
// Update every 5 seconds instead of 1 second for better performance
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
|
||||
currentTime = Date()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopTimer() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
private var gym: Gym? {
|
||||
guard let session = session else { return nil }
|
||||
return dataManager.gym(withId: session.gymId)
|
||||
@@ -47,14 +34,12 @@ struct SessionDetailView: View {
|
||||
calculateSessionStats()
|
||||
}
|
||||
|
||||
@State private var timer: Timer?
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 20) {
|
||||
if let session = session, let gym = gym {
|
||||
SessionHeaderCard(
|
||||
session: session, gym: gym, stats: sessionStats, currentTime: currentTime)
|
||||
session: session, gym: gym, stats: sessionStats)
|
||||
|
||||
SessionStatsCard(stats: sessionStats)
|
||||
|
||||
@@ -69,12 +54,7 @@ struct SessionDetailView: View {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.onAppear {
|
||||
startTimer()
|
||||
}
|
||||
.onDisappear {
|
||||
stopTimer()
|
||||
}
|
||||
|
||||
.navigationTitle("Session Details")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
@@ -182,7 +162,6 @@ struct SessionHeaderCard: View {
|
||||
let session: ClimbSession
|
||||
let gym: Gym
|
||||
let stats: SessionStats
|
||||
let currentTime: Date
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
@@ -197,9 +176,13 @@ struct SessionHeaderCard: View {
|
||||
|
||||
if session.status == .active {
|
||||
if let startTime = session.startTime {
|
||||
Text("Duration: \(formatDuration(from: startTime, to: currentTime))")
|
||||
Text("Duration: ")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
+ Text(timerInterval: startTime...Date.distantFuture, countsDown: false)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.monospacedDigit()
|
||||
}
|
||||
} else if let duration = session.duration {
|
||||
Text("Duration: \(duration) minutes")
|
||||
@@ -246,20 +229,6 @@ struct SessionHeaderCard: View {
|
||||
return formatter.string(from: 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 SessionStatsCard: View {
|
||||
|
||||
@@ -5,7 +5,7 @@ struct GymsView: View {
|
||||
@State private var showingAddGym = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
if dataManager.gyms.isEmpty {
|
||||
EmptyGymsView()
|
||||
|
||||
@@ -9,7 +9,7 @@ struct LiveActivityDebugView: View {
|
||||
@State private var isTestRunning = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
|
||||
// Header
|
||||
|
||||
@@ -6,6 +6,8 @@ struct ProblemsView: View {
|
||||
@State private var selectedClimbType: ClimbType?
|
||||
@State private var selectedGym: Gym?
|
||||
@State private var searchText = ""
|
||||
@State private var showingSearch = false
|
||||
@FocusState private var isSearchFocused: Bool
|
||||
|
||||
private var filteredProblems: [Problem] {
|
||||
var filtered = dataManager.problems
|
||||
@@ -38,29 +40,67 @@ struct ProblemsView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 0) {
|
||||
if !dataManager.problems.isEmpty {
|
||||
FilterSection(
|
||||
selectedClimbType: $selectedClimbType,
|
||||
selectedGym: $selectedGym,
|
||||
filteredProblems: filteredProblems
|
||||
)
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
}
|
||||
NavigationStack {
|
||||
Group {
|
||||
VStack(spacing: 0) {
|
||||
if showingSearch {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
|
||||
if filteredProblems.isEmpty {
|
||||
EmptyProblemsView(
|
||||
isEmpty: dataManager.problems.isEmpty,
|
||||
isFiltered: !dataManager.problems.isEmpty
|
||||
)
|
||||
} else {
|
||||
ProblemsList(problems: filteredProblems)
|
||||
TextField("Search problems...", text: $searchText)
|
||||
.textFieldStyle(.plain)
|
||||
.font(.system(size: 16))
|
||||
.focused($isSearchFocused)
|
||||
.submitLabel(.search)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 10)
|
||||
.background {
|
||||
if #available(iOS 18.0, *) {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(.regularMaterial)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.quaternary, lineWidth: 0.5)
|
||||
}
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color(.systemGray6))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color(.systemGray4), lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
.animation(.easeInOut(duration: 0.3), value: showingSearch)
|
||||
}
|
||||
|
||||
if !dataManager.problems.isEmpty && !showingSearch {
|
||||
FilterSection(
|
||||
selectedClimbType: $selectedClimbType,
|
||||
selectedGym: $selectedGym,
|
||||
filteredProblems: filteredProblems
|
||||
)
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
}
|
||||
|
||||
if filteredProblems.isEmpty {
|
||||
EmptyProblemsView(
|
||||
isEmpty: dataManager.problems.isEmpty,
|
||||
isFiltered: !dataManager.problems.isEmpty
|
||||
)
|
||||
} else {
|
||||
ProblemsList(problems: filteredProblems)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Problems")
|
||||
.searchable(text: $searchText, prompt: "Search problems...")
|
||||
.navigationBarTitleDisplayMode(.automatic)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
if dataManager.isSyncing {
|
||||
@@ -81,6 +121,22 @@ struct ProblemsView: View {
|
||||
)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
showingSearch.toggle()
|
||||
if showingSearch {
|
||||
isSearchFocused = true
|
||||
} else {
|
||||
searchText = ""
|
||||
isSearchFocused = false
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Image(systemName: showingSearch ? "xmark.circle.fill" : "magnifyingglass")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(showingSearch ? .secondary : .blue)
|
||||
}
|
||||
|
||||
if !dataManager.gyms.isEmpty {
|
||||
Button("Add") {
|
||||
showingAddProblem = true
|
||||
|
||||
@@ -6,7 +6,7 @@ struct SessionsView: View {
|
||||
@State private var showingAddSession = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if dataManager.sessions.isEmpty && dataManager.activeSession == nil {
|
||||
EmptySessionsView()
|
||||
@@ -53,7 +53,6 @@ struct SessionsView: View {
|
||||
AddEditSessionView()
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,11 +128,8 @@ struct ActiveSessionBanner: View {
|
||||
let session: ClimbSession
|
||||
let gym: Gym
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
@State private var currentTime = Date()
|
||||
@State private var navigateToDetail = false
|
||||
|
||||
@State private var timer: Timer?
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
@@ -151,9 +147,10 @@ struct ActiveSessionBanner: View {
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let startTime = session.startTime {
|
||||
Text(formatDuration(from: startTime, to: currentTime))
|
||||
Text(timerInterval: startTime...Date.distantFuture, countsDown: false)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -180,42 +177,12 @@ struct ActiveSessionBanner: View {
|
||||
.fill(.green.opacity(0.1))
|
||||
.stroke(.green.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
.onAppear {
|
||||
startTimer()
|
||||
}
|
||||
.onDisappear {
|
||||
stopTimer()
|
||||
}
|
||||
|
||||
.navigationDestination(isPresented: $navigateToDetail) {
|
||||
SessionDetailView(sessionId: session.id)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private func startTimer() {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
|
||||
currentTime = Date()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopTimer() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionRow: View {
|
||||
|
||||
@@ -11,49 +11,52 @@ struct SettingsView: View {
|
||||
@State private var activeSheet: SheetType?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
SyncSection()
|
||||
.environmentObject(dataManager.syncService)
|
||||
NavigationStack {
|
||||
List {
|
||||
SyncSection()
|
||||
.environmentObject(dataManager.syncService)
|
||||
|
||||
DataManagementSection(
|
||||
activeSheet: $activeSheet
|
||||
)
|
||||
DataManagementSection(
|
||||
activeSheet: $activeSheet
|
||||
)
|
||||
|
||||
AppInfoSection()
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if dataManager.isSyncing {
|
||||
HStack(spacing: 2) {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
|
||||
.scaleEffect(0.6)
|
||||
AppInfoSection()
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.navigationBarTitleDisplayMode(.automatic)
|
||||
.toolbar {
|
||||
ToolbarItem(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
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 3)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(.regularMaterial)
|
||||
)
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
.animation(
|
||||
.easeInOut(duration: 0.2), value: dataManager.isSyncing
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(
|
||||
item: Binding<SheetType?>(
|
||||
get: { activeSheet },
|
||||
set: { activeSheet = $0 }
|
||||
)
|
||||
) { sheetType in
|
||||
switch sheetType {
|
||||
case .export(let data):
|
||||
ExportDataView(data: data)
|
||||
case .importData:
|
||||
ImportDataView()
|
||||
.sheet(
|
||||
item: Binding<SheetType?>(
|
||||
get: { activeSheet },
|
||||
set: { activeSheet = $0 }
|
||||
)
|
||||
) { sheetType in
|
||||
switch sheetType {
|
||||
case .export(let data):
|
||||
ExportDataView(data: data)
|
||||
case .importData:
|
||||
ImportDataView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,7 +194,7 @@ struct ExportDataView: View {
|
||||
@State private var isCreatingFile = true
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
VStack(spacing: 30) {
|
||||
if isCreatingFile {
|
||||
// Loading state - more prominent
|
||||
@@ -498,7 +501,7 @@ struct SyncSettingsView: View {
|
||||
@State private var testResultMessage = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
TextField("Server URL", text: $serverURL)
|
||||
@@ -691,7 +694,7 @@ struct ImportDataView: View {
|
||||
@State private var showingDocumentPicker = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
.font(.system(size: 60))
|
||||
|
||||
Reference in New Issue
Block a user