1.2.2 - "Bug fixes and improvements"

This commit is contained in:
2025-10-01 21:34:22 -06:00
parent ba1a7117d9
commit 4e42985135
15 changed files with 150 additions and 158 deletions

View File

@@ -396,7 +396,7 @@
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12; CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -416,7 +416,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.1; MARKETING_VERSION = 1.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -439,7 +439,7 @@
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12; CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -459,7 +459,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.1; MARKETING_VERSION = 1.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -481,7 +481,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12; CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist; INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -492,7 +492,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.2.1; MARKETING_VERSION = 1.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -511,7 +511,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12; CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist; INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -522,7 +522,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.2.1; MARKETING_VERSION = 1.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;

View File

@@ -1,4 +1,3 @@
import Combine import Combine
import SwiftUI import SwiftUI
@@ -11,7 +10,7 @@ import SwiftUI
@State private var testResults: [String] = [] @State private var testResults: [String] = []
var body: some View { var body: some View {
NavigationView { NavigationStack {
List { List {
StatusSection() StatusSection()
@@ -364,7 +363,7 @@ import SwiftUI
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
var body: some View { var body: some View {
NavigationView { NavigationStack {
VStack(spacing: 30) { VStack(spacing: 30) {
Text("Icon Appearance Comparison") Text("Icon Appearance Comparison")
.font(.title2) .font(.title2)

View File

@@ -42,7 +42,7 @@ struct AddAttemptView: View {
} }
var body: some View { var body: some View {
NavigationView { NavigationStack {
Form { Form {
if !showingCreateProblem { if !showingCreateProblem {
ProblemSelectionSection() ProblemSelectionSection()
@@ -597,7 +597,7 @@ struct ProblemExpandedView: View {
@State private var selectedImageIndex = 0 @State private var selectedImageIndex = 0
var body: some View { var body: some View {
NavigationView { NavigationStack {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
// Images // Images
@@ -735,7 +735,7 @@ struct EditAttemptView: View {
} }
var body: some View { var body: some View {
NavigationView { NavigationStack {
Form { Form {
if !showingCreateProblem { if !showingCreateProblem {
ProblemSelectionSection() ProblemSelectionSection()

View File

@@ -1,4 +1,3 @@
import SwiftUI import SwiftUI
struct AddEditGymView: View { struct AddEditGymView: View {
@@ -34,7 +33,7 @@ struct AddEditGymView: View {
} }
var body: some View { var body: some View {
NavigationView { NavigationStack {
Form { Form {
BasicInfoSection() BasicInfoSection()
ClimbTypesSection() ClimbTypesSection()

View File

@@ -55,7 +55,7 @@ struct AddEditProblemView: View {
} }
var body: some View { var body: some View {
NavigationView { NavigationStack {
Form { Form {
GymSelectionSection() GymSelectionSection()
BasicInfoSection() BasicInfoSection()

View File

@@ -1,4 +1,3 @@
import SwiftUI import SwiftUI
struct AddEditSessionView: View { struct AddEditSessionView: View {
@@ -21,7 +20,7 @@ struct AddEditSessionView: View {
} }
var body: some View { var body: some View {
NavigationView { NavigationStack {
Form { Form {
GymSelectionSection() GymSelectionSection()
SessionDetailsSection() SessionDetailsSection()

View File

@@ -4,7 +4,7 @@ struct AnalyticsView: View {
@EnvironmentObject var dataManager: ClimbingDataManager @EnvironmentObject var dataManager: ClimbingDataManager
var body: some View { var body: some View {
NavigationView { NavigationStack {
ScrollView { ScrollView {
LazyVStack(spacing: 20) { LazyVStack(spacing: 20) {
OverallStatsSection() OverallStatsSection()

View File

@@ -420,7 +420,7 @@ struct ImageViewerView: View {
} }
var body: some View { var body: some View {
NavigationView { NavigationStack {
TabView(selection: $currentIndex) { TabView(selection: $currentIndex) {
ForEach(imagePaths.indices, id: \.self) { index in ForEach(imagePaths.indices, id: \.self) { index in
ProblemDetailImageFullView(imagePath: imagePaths[index]) ProblemDetailImageFullView(imagePath: imagePaths[index])

View File

@@ -9,24 +9,11 @@ struct SessionDetailView: View {
@State private var showingAddAttempt = false @State private var showingAddAttempt = false
@State private var editingAttempt: Attempt? @State private var editingAttempt: Attempt?
@State private var attemptToDelete: Attempt? @State private var attemptToDelete: Attempt?
@State private var currentTime = Date()
private var session: ClimbSession? { private var session: ClimbSession? {
dataManager.session(withId: sessionId) 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? { private var gym: Gym? {
guard let session = session else { return nil } guard let session = session else { return nil }
return dataManager.gym(withId: session.gymId) return dataManager.gym(withId: session.gymId)
@@ -47,14 +34,12 @@ struct SessionDetailView: View {
calculateSessionStats() calculateSessionStats()
} }
@State private var timer: Timer?
var body: some View { var body: some View {
ScrollView { ScrollView {
LazyVStack(spacing: 20) { LazyVStack(spacing: 20) {
if let session = session, let gym = gym { if let session = session, let gym = gym {
SessionHeaderCard( SessionHeaderCard(
session: session, gym: gym, stats: sessionStats, currentTime: currentTime) session: session, gym: gym, stats: sessionStats)
SessionStatsCard(stats: sessionStats) SessionStatsCard(stats: sessionStats)
@@ -69,12 +54,7 @@ struct SessionDetailView: View {
} }
.padding() .padding()
} }
.onAppear {
startTimer()
}
.onDisappear {
stopTimer()
}
.navigationTitle("Session Details") .navigationTitle("Session Details")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
@@ -182,7 +162,6 @@ struct SessionHeaderCard: View {
let session: ClimbSession let session: ClimbSession
let gym: Gym let gym: Gym
let stats: SessionStats let stats: SessionStats
let currentTime: Date
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
@@ -197,9 +176,13 @@ struct SessionHeaderCard: View {
if session.status == .active { if session.status == .active {
if let startTime = session.startTime { if let startTime = session.startTime {
Text("Duration: \(formatDuration(from: startTime, to: currentTime))") Text("Duration: ")
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
+ Text(timerInterval: startTime...Date.distantFuture, countsDown: false)
.font(.subheadline)
.foregroundColor(.secondary)
.monospacedDigit()
} }
} else if let duration = session.duration { } else if let duration = session.duration {
Text("Duration: \(duration) minutes") Text("Duration: \(duration) minutes")
@@ -246,20 +229,6 @@ struct SessionHeaderCard: View {
return formatter.string(from: date) 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 { struct SessionStatsCard: View {

View File

@@ -5,7 +5,7 @@ struct GymsView: View {
@State private var showingAddGym = false @State private var showingAddGym = false
var body: some View { var body: some View {
NavigationView { NavigationStack {
VStack { VStack {
if dataManager.gyms.isEmpty { if dataManager.gyms.isEmpty {
EmptyGymsView() EmptyGymsView()

View File

@@ -9,7 +9,7 @@ struct LiveActivityDebugView: View {
@State private var isTestRunning = false @State private var isTestRunning = false
var body: some View { var body: some View {
NavigationView { NavigationStack {
VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 20) {
// Header // Header

View File

@@ -6,6 +6,8 @@ struct ProblemsView: View {
@State private var selectedClimbType: ClimbType? @State private var selectedClimbType: ClimbType?
@State private var selectedGym: Gym? @State private var selectedGym: Gym?
@State private var searchText = "" @State private var searchText = ""
@State private var showingSearch = false
@FocusState private var isSearchFocused: Bool
private var filteredProblems: [Problem] { private var filteredProblems: [Problem] {
var filtered = dataManager.problems var filtered = dataManager.problems
@@ -38,29 +40,67 @@ struct ProblemsView: View {
} }
var body: some View { var body: some View {
NavigationView { NavigationStack {
VStack(spacing: 0) { Group {
if !dataManager.problems.isEmpty { VStack(spacing: 0) {
FilterSection( if showingSearch {
selectedClimbType: $selectedClimbType, HStack(spacing: 8) {
selectedGym: $selectedGym, Image(systemName: "magnifyingglass")
filteredProblems: filteredProblems .foregroundColor(.secondary)
) .font(.system(size: 16, weight: .medium))
.padding()
.background(.regularMaterial)
}
if filteredProblems.isEmpty { TextField("Search problems...", text: $searchText)
EmptyProblemsView( .textFieldStyle(.plain)
isEmpty: dataManager.problems.isEmpty, .font(.system(size: 16))
isFiltered: !dataManager.problems.isEmpty .focused($isSearchFocused)
) .submitLabel(.search)
} else { }
ProblemsList(problems: filteredProblems) .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") .navigationTitle("Problems")
.searchable(text: $searchText, prompt: "Search problems...") .navigationBarTitleDisplayMode(.automatic)
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) { ToolbarItemGroup(placement: .navigationBarTrailing) {
if dataManager.isSyncing { 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 { if !dataManager.gyms.isEmpty {
Button("Add") { Button("Add") {
showingAddProblem = true showingAddProblem = true

View File

@@ -6,7 +6,7 @@ struct SessionsView: View {
@State private var showingAddSession = false @State private var showingAddSession = false
var body: some View { var body: some View {
NavigationView { NavigationStack {
Group { Group {
if dataManager.sessions.isEmpty && dataManager.activeSession == nil { if dataManager.sessions.isEmpty && dataManager.activeSession == nil {
EmptySessionsView() EmptySessionsView()
@@ -53,7 +53,6 @@ struct SessionsView: View {
AddEditSessionView() AddEditSessionView()
} }
} }
.navigationViewStyle(.stack)
} }
} }
@@ -129,11 +128,8 @@ struct ActiveSessionBanner: View {
let session: ClimbSession let session: ClimbSession
let gym: Gym let gym: Gym
@EnvironmentObject var dataManager: ClimbingDataManager @EnvironmentObject var dataManager: ClimbingDataManager
@State private var currentTime = Date()
@State private var navigateToDetail = false @State private var navigateToDetail = false
@State private var timer: Timer?
var body: some View { var body: some View {
HStack { HStack {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
@@ -151,9 +147,10 @@ struct ActiveSessionBanner: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
if let startTime = session.startTime { if let startTime = session.startTime {
Text(formatDuration(from: startTime, to: currentTime)) Text(timerInterval: startTime...Date.distantFuture, countsDown: false)
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.monospacedDigit()
} }
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@@ -180,42 +177,12 @@ struct ActiveSessionBanner: View {
.fill(.green.opacity(0.1)) .fill(.green.opacity(0.1))
.stroke(.green.opacity(0.3), lineWidth: 1) .stroke(.green.opacity(0.3), lineWidth: 1)
) )
.onAppear {
startTimer()
}
.onDisappear {
stopTimer()
}
.navigationDestination(isPresented: $navigateToDetail) { .navigationDestination(isPresented: $navigateToDetail) {
SessionDetailView(sessionId: session.id) 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 { struct SessionRow: View {

View File

@@ -11,49 +11,52 @@ struct SettingsView: View {
@State private var activeSheet: SheetType? @State private var activeSheet: SheetType?
var body: some View { var body: some View {
List { NavigationStack {
SyncSection() List {
.environmentObject(dataManager.syncService) SyncSection()
.environmentObject(dataManager.syncService)
DataManagementSection( DataManagementSection(
activeSheet: $activeSheet activeSheet: $activeSheet
) )
AppInfoSection() AppInfoSection()
} }
.navigationTitle("Settings") .navigationTitle("Settings")
.toolbar { .navigationBarTitleDisplayMode(.automatic)
ToolbarItem(placement: .navigationBarTrailing) { .toolbar {
if dataManager.isSyncing { ToolbarItem(placement: .navigationBarTrailing) {
HStack(spacing: 2) { if dataManager.isSyncing {
ProgressView() HStack(spacing: 2) {
.progressViewStyle(CircularProgressViewStyle(tint: .blue)) ProgressView()
.scaleEffect(0.6) .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(
.sheet( item: Binding<SheetType?>(
item: Binding<SheetType?>( get: { activeSheet },
get: { activeSheet }, set: { activeSheet = $0 }
set: { activeSheet = $0 } )
) ) { sheetType in
) { sheetType in switch sheetType {
switch sheetType { case .export(let data):
case .export(let data): ExportDataView(data: data)
ExportDataView(data: data) case .importData:
case .importData: ImportDataView()
ImportDataView() }
} }
} }
} }
@@ -191,7 +194,7 @@ struct ExportDataView: View {
@State private var isCreatingFile = true @State private var isCreatingFile = true
var body: some View { var body: some View {
NavigationView { NavigationStack {
VStack(spacing: 30) { VStack(spacing: 30) {
if isCreatingFile { if isCreatingFile {
// Loading state - more prominent // Loading state - more prominent
@@ -498,7 +501,7 @@ struct SyncSettingsView: View {
@State private var testResultMessage = "" @State private var testResultMessage = ""
var body: some View { var body: some View {
NavigationView { NavigationStack {
Form { Form {
Section { Section {
TextField("Server URL", text: $serverURL) TextField("Server URL", text: $serverURL)
@@ -691,7 +694,7 @@ struct ImportDataView: View {
@State private var showingDocumentPicker = false @State private var showingDocumentPicker = false
var body: some View { var body: some View {
NavigationView { NavigationStack {
VStack(spacing: 20) { VStack(spacing: 20) {
Image(systemName: "square.and.arrow.down") Image(systemName: "square.and.arrow.down")
.font(.system(size: 60)) .font(.system(size: 60))