[Android] 1.9.2
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m29s
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m29s
This commit is contained in:
@@ -9,8 +9,26 @@ struct ProblemsView: View {
|
||||
@State private var showingSearch = false
|
||||
@FocusState private var isSearchFocused: Bool
|
||||
|
||||
private var filteredProblems: [Problem] {
|
||||
var filtered = dataManager.problems
|
||||
@State private var cachedFilteredProblems: [Problem] = []
|
||||
|
||||
private func updateFilteredProblems() {
|
||||
Task(priority: .userInitiated) {
|
||||
let result = await computeFilteredProblems()
|
||||
// Switch back to the main thread to update the UI
|
||||
await MainActor.run {
|
||||
cachedFilteredProblems = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func computeFilteredProblems() async -> [Problem] {
|
||||
// Capture dependencies for safe background processing
|
||||
let problems = dataManager.problems
|
||||
let searchText = self.searchText
|
||||
let selectedClimbType = self.selectedClimbType
|
||||
let selectedGym = self.selectedGym
|
||||
|
||||
var filtered = problems
|
||||
|
||||
// Apply search filter
|
||||
if !searchText.isEmpty {
|
||||
@@ -93,19 +111,19 @@ struct ProblemsView: View {
|
||||
FilterSection(
|
||||
selectedClimbType: $selectedClimbType,
|
||||
selectedGym: $selectedGym,
|
||||
filteredProblems: filteredProblems
|
||||
filteredProblems: cachedFilteredProblems
|
||||
)
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
}
|
||||
|
||||
if filteredProblems.isEmpty {
|
||||
if cachedFilteredProblems.isEmpty {
|
||||
EmptyProblemsView(
|
||||
isEmpty: dataManager.problems.isEmpty,
|
||||
isFiltered: !dataManager.problems.isEmpty
|
||||
)
|
||||
} else {
|
||||
ProblemsList(problems: filteredProblems)
|
||||
ProblemsList(problems: cachedFilteredProblems)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +176,21 @@ struct ProblemsView: View {
|
||||
AddEditProblemView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
updateFilteredProblems()
|
||||
}
|
||||
.onChange(of: dataManager.problems) {
|
||||
updateFilteredProblems()
|
||||
}
|
||||
.onChange(of: searchText) {
|
||||
updateFilteredProblems()
|
||||
}
|
||||
.onChange(of: selectedClimbType) {
|
||||
updateFilteredProblems()
|
||||
}
|
||||
.onChange(of: selectedGym) {
|
||||
updateFilteredProblems()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +302,7 @@ struct ProblemsList: View {
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
@State private var problemToDelete: Problem?
|
||||
@State private var problemToEdit: Problem?
|
||||
@State private var animationKey = 0
|
||||
|
||||
var body: some View {
|
||||
List(problems, id: \.id) { problem in
|
||||
@@ -309,8 +343,11 @@ struct ProblemsList: View {
|
||||
}
|
||||
.animation(
|
||||
.spring(response: 0.5, dampingFraction: 0.8, blendDuration: 0.1),
|
||||
value: problems.map { "\($0.id):\($0.isActive)" }.joined()
|
||||
value: animationKey
|
||||
)
|
||||
.onChange(of: problems) {
|
||||
animationKey += 1
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.scrollIndicators(.hidden)
|
||||
@@ -344,6 +381,12 @@ struct ProblemRow: View {
|
||||
dataManager.gym(withId: problem.gymId)
|
||||
}
|
||||
|
||||
private var isCompleted: Bool {
|
||||
dataManager.attempts.contains { attempt in
|
||||
attempt.problemId == problem.id && attempt.result.isSuccessful
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
@@ -361,10 +404,24 @@ struct ProblemRow: View {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text(problem.difficulty.grade)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
HStack(spacing: 8) {
|
||||
if !problem.imagePaths.isEmpty {
|
||||
Image(systemName: "photo")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
if isCompleted {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
|
||||
Text(problem.difficulty.grade)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
Text(problem.climbType.displayName)
|
||||
.font(.caption)
|
||||
@@ -396,17 +453,6 @@ struct ProblemRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
if !problem.imagePaths.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 8) {
|
||||
ForEach(problem.imagePaths.prefix(3), id: \.self) { imagePath in
|
||||
ProblemImageView(imagePath: imagePath)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
|
||||
if !problem.isActive {
|
||||
Text("Reset / No Longer Set")
|
||||
.font(.caption)
|
||||
@@ -478,17 +524,6 @@ struct EmptyProblemsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ProblemImageView: View {
|
||||
let imagePath: String
|
||||
|
||||
var body: some View {
|
||||
OrientationAwareImage.fill(imagePath: imagePath)
|
||||
.frame(width: 60, height: 60)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ProblemsView()
|
||||
.environmentObject(ClimbingDataManager.preview)
|
||||
|
||||
@@ -80,8 +80,7 @@ struct DataManagementSection: View {
|
||||
@Binding var activeSheet: SheetType?
|
||||
@State private var showingResetAlert = false
|
||||
@State private var isExporting = false
|
||||
@State private var isMigrating = false
|
||||
@State private var showingMigrationAlert = false
|
||||
|
||||
@State private var isDeletingImages = false
|
||||
@State private var showingDeleteImagesAlert = false
|
||||
|
||||
@@ -121,27 +120,6 @@ struct DataManagementSection: View {
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
|
||||
// Migrate Image Names
|
||||
Button(action: {
|
||||
showingMigrationAlert = true
|
||||
}) {
|
||||
HStack {
|
||||
if isMigrating {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
Text("Migrating Images...")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Image(systemName: "photo.badge.arrow.down")
|
||||
.foregroundColor(.orange)
|
||||
Text("Fix Image Names")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(isMigrating)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
// Delete All Images
|
||||
Button(action: {
|
||||
showingDeleteImagesAlert = true
|
||||
@@ -186,16 +164,7 @@ struct DataManagementSection: View {
|
||||
"Are you sure you want to reset all data? This will permanently delete:\n\n• All gyms and their information\n• All problems and their images\n• All climbing sessions\n• All attempts and progress data\n\nThis action cannot be undone. Consider exporting your data first."
|
||||
)
|
||||
}
|
||||
.alert("Fix Image Names", isPresented: $showingMigrationAlert) {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Fix Names") {
|
||||
migrateImageNames()
|
||||
}
|
||||
} message: {
|
||||
Text(
|
||||
"This will rename all existing image files to use a consistent naming system across devices.\n\nThis improves sync reliability between iOS and Android. Your images will not be lost, only renamed.\n\nThis is safe to run multiple times."
|
||||
)
|
||||
}
|
||||
|
||||
.alert("Delete All Images", isPresented: $showingDeleteImagesAlert) {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Delete", role: .destructive) {
|
||||
@@ -219,17 +188,6 @@ struct DataManagementSection: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func migrateImageNames() {
|
||||
isMigrating = true
|
||||
Task {
|
||||
await MainActor.run {
|
||||
ImageManager.shared.migrateImageNamesToDeterministic(dataManager: dataManager)
|
||||
isMigrating = false
|
||||
dataManager.successMessage = "Image names fixed successfully!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteAllImages() {
|
||||
isDeletingImages = true
|
||||
Task {
|
||||
|
||||
Reference in New Issue
Block a user