1.0.0 for iOS is ready to ship
This commit is contained in:
@@ -1,9 +1,3 @@
|
||||
//
|
||||
// ProblemsView.swift
|
||||
// OpenClimb
|
||||
//
|
||||
// Created by OpenClimb on 2025-01-17.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@@ -45,9 +39,13 @@ struct ProblemsView: View {
|
||||
NavigationView {
|
||||
VStack(spacing: 0) {
|
||||
if !dataManager.problems.isEmpty {
|
||||
FilterSection()
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
FilterSection(
|
||||
selectedClimbType: $selectedClimbType,
|
||||
selectedGym: $selectedGym,
|
||||
filteredProblems: filteredProblems
|
||||
)
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
}
|
||||
|
||||
if filteredProblems.isEmpty {
|
||||
@@ -79,8 +77,9 @@ struct ProblemsView: View {
|
||||
|
||||
struct FilterSection: View {
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
@State private var selectedClimbType: ClimbType?
|
||||
@State private var selectedGym: Gym?
|
||||
@Binding var selectedClimbType: ClimbType?
|
||||
@Binding var selectedGym: Gym?
|
||||
let filteredProblems: [Problem]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
@@ -154,19 +153,6 @@ struct FilterSection: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var filteredProblems: [Problem] {
|
||||
var filtered = dataManager.problems
|
||||
|
||||
if let climbType = selectedClimbType {
|
||||
filtered = filtered.filter { $0.climbType == climbType }
|
||||
}
|
||||
|
||||
if let gym = selectedGym {
|
||||
filtered = filtered.filter { $0.gymId == gym.id }
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
}
|
||||
|
||||
struct FilterChip: View {
|
||||
@@ -195,14 +181,47 @@ struct FilterChip: View {
|
||||
struct ProblemsList: View {
|
||||
let problems: [Problem]
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
@State private var problemToDelete: Problem?
|
||||
@State private var problemToEdit: Problem?
|
||||
|
||||
var body: some View {
|
||||
List(problems) { problem in
|
||||
NavigationLink(destination: ProblemDetailView(problemId: problem.id)) {
|
||||
ProblemRow(problem: problem)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button(role: .destructive) {
|
||||
problemToDelete = problem
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
|
||||
Button {
|
||||
problemToEdit = problem
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
.tint(.blue)
|
||||
}
|
||||
}
|
||||
.alert("Delete Problem", isPresented: .constant(problemToDelete != nil)) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
problemToDelete = nil
|
||||
}
|
||||
Button("Delete", role: .destructive) {
|
||||
if let problem = problemToDelete {
|
||||
dataManager.deleteProblem(problem)
|
||||
problemToDelete = nil
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text(
|
||||
"Are you sure you want to delete this problem? This will also delete all associated attempts."
|
||||
)
|
||||
}
|
||||
.sheet(item: $problemToEdit) { problem in
|
||||
AddEditProblemView(problemId: problem.id)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,19 +288,10 @@ struct ProblemRow: View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(problem.imagePaths.prefix(3), id: \.self) { imagePath in
|
||||
AsyncImage(url: URL(fileURLWithPath: imagePath)) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.gray.opacity(0.3))
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
ProblemImageView(imagePath: imagePath)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +302,7 @@ struct ProblemRow: View {
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,6 +366,70 @@ struct EmptyProblemsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ProblemImageView: View {
|
||||
let imagePath: String
|
||||
@State private var uiImage: UIImage?
|
||||
@State private var isLoading = true
|
||||
@State private var hasFailed = false
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let uiImage = uiImage {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 60, height: 60)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
} else if hasFailed {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.gray.opacity(0.2))
|
||||
.frame(width: 60, height: 60)
|
||||
.overlay {
|
||||
Image(systemName: "photo")
|
||||
.foregroundColor(.gray)
|
||||
.font(.title3)
|
||||
}
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.gray.opacity(0.3))
|
||||
.frame(width: 60, height: 60)
|
||||
.overlay {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadImage()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImage() {
|
||||
guard !imagePath.isEmpty else {
|
||||
hasFailed = true
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
if let data = ImageManager.shared.loadImageData(fromPath: imagePath),
|
||||
let image = UIImage(data: data)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.uiImage = image
|
||||
self.isLoading = false
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.hasFailed = true
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ProblemsView()
|
||||
.environmentObject(ClimbingDataManager.preview)
|
||||
|
||||
Reference in New Issue
Block a user