1.2.2 - "Bug fixes and improvements"
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Binary file not shown.
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ struct AddEditProblemView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
GymSelectionSection()
|
GymSelectionSection()
|
||||||
BasicInfoSection()
|
BasicInfoSection()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user