Import/export fixes, icon, and graphing

This commit is contained in:
2025-09-13 00:42:15 -06:00
parent 7da1893748
commit 61384623bd
11 changed files with 388 additions and 230 deletions

View File

@@ -1,35 +1,38 @@
{ {
"images" : [ "images": [
{ {
"idiom" : "universal", "filename": "app_icon_1024.png",
"platform" : "ios", "idiom": "universal",
"size" : "1024x1024" "platform": "ios",
"size": "1024x1024"
}, },
{ {
"appearances" : [ "appearances": [
{ {
"appearance" : "luminosity", "appearance": "luminosity",
"value" : "dark" "value": "dark"
} }
], ],
"idiom" : "universal", "filename": "app_icon_1024.png",
"platform" : "ios", "idiom": "universal",
"size" : "1024x1024" "platform": "ios",
"size": "1024x1024"
}, },
{ {
"appearances" : [ "appearances": [
{ {
"appearance" : "luminosity", "appearance": "luminosity",
"value" : "tinted" "value": "tinted"
} }
], ],
"idiom" : "universal", "filename": "app_icon_1024.png",
"platform" : "ios", "idiom": "universal",
"size" : "1024x1024" "platform": "ios",
"size": "1024x1024"
} }
], ],
"info" : { "info": {
"author" : "xcode", "author": "xcode",
"version" : 1 "version": 1
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,23 @@
{
"images": [
{
"filename": "mountains_icon_256.png",
"idiom": "universal",
"scale": "1x"
},
{
"filename": "mountains_icon_256.png",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "mountains_icon_256.png",
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -429,9 +429,6 @@ struct ZipUtils {
let endRecord = data.subdata(in: endOfCentralDirOffset..<endOfCentralDirOffset + 22) let endRecord = data.subdata(in: endOfCentralDirOffset..<endOfCentralDirOffset + 22)
let numEntries = endRecord.subdata(in: 8..<10).withUnsafeBytes { $0.load(as: UInt16.self) } let numEntries = endRecord.subdata(in: 8..<10).withUnsafeBytes { $0.load(as: UInt16.self) }
let centralDirSize = endRecord.subdata(in: 12..<16).withUnsafeBytes {
$0.load(as: UInt32.self)
}
let centralDirOffset = endRecord.subdata(in: 16..<20).withUnsafeBytes { let centralDirOffset = endRecord.subdata(in: 16..<20).withUnsafeBytes {
$0.load(as: UInt32.self) $0.load(as: UInt32.self)
} }

View File

@@ -76,10 +76,10 @@ struct AddAttemptView: View {
.onAppear { .onAppear {
setupInitialValues() setupInitialValues()
} }
.onChange(of: selectedClimbType) { _ in .onChange(of: selectedClimbType) {
updateDifficultySystem() updateDifficultySystem()
} }
.onChange(of: selectedDifficultySystem) { _ in .onChange(of: selectedDifficultySystem) {
resetGradeIfNeeded() resetGradeIfNeeded()
} }
} }
@@ -182,8 +182,12 @@ struct AddAttemptView: View {
} }
if selectedDifficultySystem == .custom { if selectedDifficultySystem == .custom {
TextField("Grade (Required)", text: $newProblemGrade) TextField("Grade (Required - numbers only)", text: $newProblemGrade)
.keyboardType(.numberPad) .keyboardType(.numberPad)
.onChange(of: newProblemGrade) {
// Filter out non-numeric characters
newProblemGrade = newProblemGrade.filter { $0.isNumber }
}
} else { } else {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("Grade (Required)") Text("Grade (Required)")
@@ -526,7 +530,7 @@ struct EditAttemptView: View {
} }
private func updateAttempt() { private func updateAttempt() {
guard let problem = selectedProblem else { return } guard selectedProblem != nil else { return }
let updatedAttempt = attempt.updated( let updatedAttempt = attempt.updated(
result: selectedResult, result: selectedResult,

View File

@@ -67,7 +67,7 @@ struct AddEditGymView: View {
.onAppear { .onAppear {
loadExistingGym() loadExistingGym()
} }
.onChange(of: selectedClimbTypes) { _ in .onChange(of: selectedClimbTypes) {
updateAvailableDifficultySystems() updateAvailableDifficultySystems()
} }
} }

View File

@@ -95,16 +95,16 @@ struct AddEditProblemView: View {
loadExistingProblem() loadExistingProblem()
setupInitialGym() setupInitialGym()
} }
.onChange(of: selectedGym) { _ in .onChange(of: selectedGym) {
updateAvailableOptions() updateAvailableOptions()
} }
.onChange(of: selectedClimbType) { _ in .onChange(of: selectedClimbType) {
updateDifficultySystem() updateDifficultySystem()
} }
.onChange(of: selectedDifficultySystem) { _ in .onChange(of: selectedDifficultySystem) {
resetGradeIfNeeded() resetGradeIfNeeded()
} }
.onChange(of: selectedPhotos) { _ in .onChange(of: selectedPhotos) {
Task { Task {
await loadSelectedPhotos() await loadSelectedPhotos()
} }
@@ -171,7 +171,7 @@ struct AddEditProblemView: View {
@ViewBuilder @ViewBuilder
private func ClimbTypeSection() -> some View { private func ClimbTypeSection() -> some View {
if let gym = selectedGym { if selectedGym != nil {
Section("Climb Type") { Section("Climb Type") {
ForEach(availableClimbTypes, id: \.self) { climbType in ForEach(availableClimbTypes, id: \.self) { climbType in
HStack { HStack {
@@ -227,8 +227,13 @@ struct AddEditProblemView: View {
.font(.headline) .font(.headline)
if selectedDifficultySystem == .custom || availableGrades.isEmpty { if selectedDifficultySystem == .custom || availableGrades.isEmpty {
TextField("Enter custom grade", text: $difficultyGrade) TextField("Enter custom grade (numbers only)", text: $difficultyGrade)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.keyboardType(.numberPad)
.onChange(of: difficultyGrade) {
// Filter out non-numeric characters
difficultyGrade = difficultyGrade.filter { $0.isNumber }
}
} else { } else {
Menu { Menu {
if !difficultyGrade.isEmpty { if !difficultyGrade.isEmpty {

View File

@@ -20,9 +20,11 @@ struct AnalyticsView: View {
ProgressChartSection() ProgressChartSection()
FavoriteGymSection() HStack(spacing: 16) {
FavoriteGymSection()
RecentActivitySection() RecentActivitySection()
}
} }
.padding() .padding()
} }
@@ -34,9 +36,9 @@ struct AnalyticsView: View {
struct HeaderSection: View { struct HeaderSection: View {
var body: some View { var body: some View {
HStack { HStack {
Image(systemName: "mountain.2.fill") Image("MountainsIcon")
.font(.title) .resizable()
.foregroundColor(.blue) .frame(width: 32, height: 32)
Text("Analytics") Text("Analytics")
.font(.title) .font(.title)
@@ -133,7 +135,13 @@ struct ProgressChartSection: View {
} }
private var usedSystems: [DifficultySystem] { private var usedSystems: [DifficultySystem] {
Set(progressData.map { $0.difficultySystem }).sorted { $0.rawValue < $1.rawValue } let uniqueSystems = Set(progressData.map { $0.difficultySystem })
return uniqueSystems.sorted {
let order: [DifficultySystem] = [.vScale, .font, .yds, .custom]
let firstIndex = order.firstIndex(of: $0) ?? order.count
let secondIndex = order.firstIndex(of: $1) ?? order.count
return firstIndex < secondIndex
}
} }
var body: some View { var body: some View {
@@ -148,20 +156,35 @@ struct ProgressChartSection: View {
if usedSystems.count > 1 { if usedSystems.count > 1 {
Menu { Menu {
ForEach(usedSystems, id: \.self) { system in ForEach(usedSystems, id: \.self) { system in
Button(system.displayName) { Button(action: {
selectedSystem = system selectedSystem = system
}) {
HStack {
Text(system.displayName)
if selectedSystem == system {
Spacer()
Image(systemName: "checkmark")
.foregroundColor(.blue)
}
}
} }
} }
} label: { } label: {
Text(selectedSystem.displayName) HStack(spacing: 4) {
.font(.caption) Text(selectedSystem.displayName)
.padding(.horizontal, 8) .font(.subheadline)
.padding(.vertical, 4) .fontWeight(.medium)
.background( Image(systemName: "chevron.down")
RoundedRectangle(cornerRadius: 8) .font(.caption)
.fill(.blue.opacity(0.1)) }
) .padding(.horizontal, 12)
.foregroundColor(.blue) .padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(.blue.opacity(0.1))
.stroke(.blue.opacity(0.3), lineWidth: 1)
)
.foregroundColor(.blue)
} }
} }
} }
@@ -169,37 +192,11 @@ struct ProgressChartSection: View {
let filteredData = progressData.filter { $0.difficultySystem == selectedSystem } let filteredData = progressData.filter { $0.difficultySystem == selectedSystem }
if !filteredData.isEmpty { if !filteredData.isEmpty {
VStack { LineChartView(data: filteredData, selectedSystem: selectedSystem)
// Simple text-based chart placeholder .frame(height: 200)
VStack(alignment: .leading, spacing: 8) {
ForEach(filteredData.indices.prefix(5), id: \.self) { index in
let point = filteredData[index]
HStack {
Text("Session \(index + 1)")
.font(.caption)
.frame(width: 80, alignment: .leading)
Rectangle()
.fill(.blue)
.frame(width: CGFloat(point.maxGradeNumeric * 5), height: 20)
Text(point.maxGrade)
.font(.caption)
.foregroundColor(.blue)
}
}
if filteredData.count > 5 {
Text("... and \(filteredData.count - 5) more sessions")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.frame(height: 200)
Text( Text(
"X: session number, Y: max \(selectedSystem.displayName.lowercased()) grade achieved" "Progress: max \(selectedSystem.displayName.lowercased()) grade achieved per session"
) )
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@@ -239,22 +236,28 @@ struct ProgressChartSection: View {
let attemptedProblemIds = sessionAttempts.map { $0.problemId } let attemptedProblemIds = sessionAttempts.map { $0.problemId }
let attemptedProblems = problems.filter { attemptedProblemIds.contains($0.id) } let attemptedProblems = problems.filter { attemptedProblemIds.contains($0.id) }
guard // Group problems by difficulty system
let highestGradeProblem = attemptedProblems.max(by: { let problemsBySystem = Dictionary(grouping: attemptedProblems) { $0.difficulty.system }
$0.difficulty.numericValue < $1.difficulty.numericValue
})
else {
return nil
}
return ProgressDataPoint( // Create data points for each system used in this session
date: session.date, return problemsBySystem.compactMap { (system, systemProblems) -> ProgressDataPoint? in
maxGrade: highestGradeProblem.difficulty.grade, guard
maxGradeNumeric: highestGradeProblem.difficulty.numericValue, let highestGradeProblem = systemProblems.max(by: {
climbType: highestGradeProblem.climbType, $0.difficulty.numericValue < $1.difficulty.numericValue
difficultySystem: highestGradeProblem.difficulty.system })
) else {
} return nil
}
return ProgressDataPoint(
date: session.date,
maxGrade: highestGradeProblem.difficulty.grade,
maxGradeNumeric: highestGradeProblem.difficulty.numericValue,
climbType: highestGradeProblem.climbType,
difficultySystem: system
)
}
}.flatMap { $0 }
} }
} }
@@ -275,27 +278,53 @@ struct FavoriteGymSection: View {
} }
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 16) {
Text("Favorite Gym") HStack {
.font(.title2) Image(systemName: "location.fill")
.fontWeight(.bold) .font(.title2)
.foregroundColor(.purple)
Text("Favorite Gym")
.font(.title2)
.fontWeight(.bold)
Spacer()
}
if let info = favoriteGymInfo { if let info = favoriteGymInfo {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 12) {
Text(info.gym.name) Text(info.gym.name)
.font(.title3) .font(.title3)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.primary)
Text("\(info.sessionCount) sessions") HStack {
.font(.subheadline) Image(systemName: "calendar")
.foregroundColor(.secondary) .font(.subheadline)
.foregroundColor(.purple)
Text("\(info.sessionCount) sessions")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
} }
} else { } else {
Text("No sessions yet") VStack(alignment: .leading, spacing: 8) {
.font(.subheadline) Text("No sessions yet")
.foregroundColor(.secondary) .font(.subheadline)
.foregroundColor(.secondary)
Text("Start climbing to see your favorite gym!")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
} }
} }
.frame(maxWidth: .infinity, minHeight: 120, alignment: .topLeading)
.padding() .padding()
.background( .background(
RoundedRectangle(cornerRadius: 16) RoundedRectangle(cornerRadius: 16)
@@ -311,21 +340,63 @@ struct RecentActivitySection: View {
dataManager.sessions.count dataManager.sessions.count
} }
private var totalAttempts: Int {
dataManager.attempts.count
}
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 16) {
Text("Recent Activity") HStack {
.font(.title2) Image(systemName: "clock.fill")
.fontWeight(.bold) .font(.title2)
.foregroundColor(.blue)
Text("Recent Activity")
.font(.title2)
.fontWeight(.bold)
Spacer()
}
if recentSessionsCount > 0 { if recentSessionsCount > 0 {
Text("You've had \(recentSessionsCount) sessions") VStack(alignment: .leading, spacing: 12) {
.font(.subheadline) HStack {
Image(systemName: "play.circle")
.font(.subheadline)
.foregroundColor(.blue)
Text("\(recentSessionsCount) sessions")
.font(.subheadline)
.foregroundColor(.secondary)
}
HStack {
Image(systemName: "hand.raised")
.font(.subheadline)
.foregroundColor(.green)
Text("\(totalAttempts) attempts")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
} else { } else {
Text("No recent activity") VStack(alignment: .leading, spacing: 8) {
.font(.subheadline) Text("No recent activity")
.foregroundColor(.secondary) .font(.subheadline)
.foregroundColor(.secondary)
Text("Start your first session!")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
} }
} }
.frame(maxWidth: .infinity, minHeight: 120, alignment: .topLeading)
.padding() .padding()
.background( .background(
RoundedRectangle(cornerRadius: 16) RoundedRectangle(cornerRadius: 16)
@@ -334,6 +405,131 @@ struct RecentActivitySection: View {
} }
} }
struct LineChartView: View {
let data: [ProgressDataPoint]
let selectedSystem: DifficultySystem
private var uniqueGrades: [String] {
if selectedSystem == .custom {
return Array(Set(data.map { $0.maxGrade })).sorted { grade1, grade2 in
return (Int(grade1) ?? 0) > (Int(grade2) ?? 0)
}
} else {
return Array(Set(data.map { $0.maxGrade })).sorted { grade1, grade2 in
let grade1Data = data.first(where: { $0.maxGrade == grade1 })
let grade2Data = data.first(where: { $0.maxGrade == grade2 })
return (grade1Data?.maxGradeNumeric ?? 0)
> (grade2Data?.maxGradeNumeric ?? 0)
}
}
}
private var minGrade: Int {
data.map { $0.maxGradeNumeric }.min() ?? 0
}
private var maxGrade: Int {
data.map { $0.maxGradeNumeric }.max() ?? 1
}
private var gradeRange: Int {
max(maxGrade - minGrade, 1)
}
var body: some View {
GeometryReader { geometry in
let chartWidth = geometry.size.width - 40
let chartHeight = geometry.size.height - 40
if data.isEmpty {
Rectangle()
.fill(.clear)
.overlay(
Text("No data")
.foregroundColor(.secondary)
)
} else {
HStack {
// Y-axis labels
VStack {
ForEach(0..<min(5, uniqueGrades.count), id: \.self) { i in
let gradeLabel = i < uniqueGrades.count ? uniqueGrades[i] : ""
Text(gradeLabel)
.font(.caption)
.foregroundColor(.secondary)
.frame(width: 30, alignment: .trailing)
if i < min(4, uniqueGrades.count - 1) {
Spacer()
}
}
}
.frame(height: chartHeight)
// Chart area
ZStack {
// Grid lines
ForEach(0..<5) { i in
let y = CGFloat(i) * chartHeight / 4
Rectangle()
.fill(.gray.opacity(0.2))
.frame(height: 0.5)
.offset(y: y - chartHeight / 2)
}
// Line chart
if data.count > 1 {
Path { path in
for (index, point) in data.enumerated() {
let x = CGFloat(index) * chartWidth / CGFloat(data.count - 1)
let normalizedY =
CGFloat(point.maxGradeNumeric - minGrade)
/ CGFloat(gradeRange)
let y = chartHeight - (normalizedY * chartHeight)
if index == 0 {
path.move(to: CGPoint(x: x, y: y))
} else {
path.addLine(to: CGPoint(x: x, y: y))
}
}
}
.stroke(.blue, lineWidth: 2)
}
// Data points
ForEach(data.indices, id: \.self) { index in
let point = data[index]
let x =
data.count == 1
? chartWidth / 2
: CGFloat(index) * chartWidth / CGFloat(data.count - 1)
let normalizedY =
CGFloat(point.maxGradeNumeric - minGrade) / CGFloat(gradeRange)
let y = chartHeight - (normalizedY * chartHeight)
Circle()
.fill(.blue)
.frame(width: 8, height: 8)
.position(x: x, y: y)
.overlay(
Circle()
.stroke(.white, lineWidth: 2)
.frame(width: 8, height: 8)
.position(x: x, y: y)
)
}
}
.frame(width: chartWidth, height: chartHeight)
}
}
}
.padding()
}
}
struct ProgressDataPoint { struct ProgressDataPoint {
let date: Date let date: Date
let maxGrade: String let maxGrade: String
@@ -344,63 +540,6 @@ struct ProgressDataPoint {
// MARK: - Helper Functions // MARK: - Helper Functions
func gradeToNumeric(_ system: DifficultySystem, _ grade: String) -> Int {
switch system {
case .vScale:
if grade == "VB" { return 0 }
return Int(grade.replacingOccurrences(of: "V", with: "")) ?? 0
case .font:
let fontMapping: [String: Int] = [
"3": 3, "4A": 4, "4B": 5, "4C": 6, "5A": 7, "5B": 8, "5C": 9,
"6A": 10, "6A+": 11, "6B": 12, "6B+": 13, "6C": 14, "6C+": 15,
"7A": 16, "7A+": 17, "7B": 18, "7B+": 19, "7C": 20, "7C+": 21,
"8A": 22, "8A+": 23, "8B": 24, "8B+": 25, "8C": 26, "8C+": 27,
]
return fontMapping[grade] ?? 0
case .yds:
let ydsMapping: [String: Int] = [
"5.0": 50, "5.1": 51, "5.2": 52, "5.3": 53, "5.4": 54, "5.5": 55,
"5.6": 56, "5.7": 57, "5.8": 58, "5.9": 59, "5.10a": 60, "5.10b": 61,
"5.10c": 62, "5.10d": 63, "5.11a": 64, "5.11b": 65, "5.11c": 66,
"5.11d": 67, "5.12a": 68, "5.12b": 69, "5.12c": 70, "5.12d": 71,
"5.13a": 72, "5.13b": 73, "5.13c": 74, "5.13d": 75, "5.14a": 76,
"5.14b": 77, "5.14c": 78, "5.14d": 79, "5.15a": 80, "5.15b": 81,
"5.15c": 82, "5.15d": 83,
]
return ydsMapping[grade] ?? 0
case .custom:
return Int(grade) ?? 0
}
}
func numericToGrade(_ system: DifficultySystem, _ numeric: Int) -> String {
switch system {
case .vScale:
return numeric == 0 ? "VB" : "V\(numeric)"
case .font:
let fontMapping: [Int: String] = [
3: "3", 4: "4A", 5: "4B", 6: "4C", 7: "5A", 8: "5B", 9: "5C",
10: "6A", 11: "6A+", 12: "6B", 13: "6B+", 14: "6C", 15: "6C+",
16: "7A", 17: "7A+", 18: "7B", 19: "7B+", 20: "7C", 21: "7C+",
22: "8A", 23: "8A+", 24: "8B", 25: "8B+", 26: "8C", 27: "8C+",
]
return fontMapping[numeric] ?? "\(numeric)"
case .yds:
let ydsMapping: [Int: String] = [
50: "5.0", 51: "5.1", 52: "5.2", 53: "5.3", 54: "5.4", 55: "5.5",
56: "5.6", 57: "5.7", 58: "5.8", 59: "5.9", 60: "5.10a", 61: "5.10b",
62: "5.10c", 63: "5.10d", 64: "5.11a", 65: "5.11b", 66: "5.11c",
67: "5.11d", 68: "5.12a", 69: "5.12b", 70: "5.12c", 71: "5.12d",
72: "5.13a", 73: "5.13b", 74: "5.13c", 75: "5.13d", 76: "5.14a",
77: "5.14b", 78: "5.14c", 79: "5.14d", 80: "5.15a", 81: "5.15b",
82: "5.15c", 83: "5.15d",
]
return ydsMapping[numeric] ?? "\(numeric)"
case .custom:
return "\(numeric)"
}
}
#Preview { #Preview {
AnalyticsView() AnalyticsView()
.environmentObject(ClimbingDataManager.preview) .environmentObject(ClimbingDataManager.preview)

View File

@@ -8,31 +8,53 @@
import SwiftUI import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
enum SheetType {
case export(Data)
case importData
}
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var dataManager: ClimbingDataManager @EnvironmentObject var dataManager: ClimbingDataManager
@State private var showingResetAlert = false @State private var activeSheet: SheetType?
@State private var showingExportSheet = false
@State private var showingImportSheet = false
@State private var exportData: Data?
var body: some View { var body: some View {
NavigationView { List {
List { DataManagementSection(
DataManagementSection() activeSheet: $activeSheet
)
AppInfoSection() AppInfoSection()
}
.navigationTitle("Settings")
.sheet(
item: Binding<SheetType?>(
get: { activeSheet },
set: { activeSheet = $0 }
)
) { sheetType in
switch sheetType {
case .export(let data):
ExportDataView(data: data)
case .importData:
ImportDataView()
} }
.navigationTitle("Settings") }
}
}
extension SheetType: Identifiable {
var id: String {
switch self {
case .export: return "export"
case .importData: return "import"
} }
} }
} }
struct DataManagementSection: View { struct DataManagementSection: View {
@EnvironmentObject var dataManager: ClimbingDataManager @EnvironmentObject var dataManager: ClimbingDataManager
@Binding var activeSheet: SheetType?
@State private var showingResetAlert = false @State private var showingResetAlert = false
@State private var showingExportSheet = false
@State private var showingImportSheet = false
@State private var exportData: Data?
@State private var isExporting = false @State private var isExporting = false
var body: some View { var body: some View {
@@ -60,7 +82,7 @@ struct DataManagementSection: View {
// Import Data // Import Data
Button(action: { Button(action: {
showingImportSheet = true activeSheet = .importData
}) { }) {
HStack { HStack {
Image(systemName: "square.and.arrow.down") Image(systemName: "square.and.arrow.down")
@@ -94,38 +116,15 @@ 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." "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."
) )
} }
.sheet(isPresented: $showingExportSheet) {
if let data = exportData {
ExportDataView(data: data)
} else {
Text("No export data available")
}
}
.sheet(isPresented: $showingImportSheet) {
ImportDataView()
}
} }
private func exportDataAsync() { private func exportDataAsync() {
isExporting = true isExporting = true
Task { Task {
let data = await withCheckedContinuation { continuation in let data = await MainActor.run { dataManager.exportData() }
DispatchQueue.global(qos: .userInitiated).async { isExporting = false
let result = dataManager.exportData() if let data = data {
continuation.resume(returning: result) activeSheet = .export(data)
}
}
await MainActor.run {
isExporting = false
if let data = data {
exportData = data
showingExportSheet = true
} else {
// Error message should already be set by dataManager
exportData = nil
}
} }
} }
} }
@@ -143,8 +142,9 @@ struct AppInfoSection: View {
var body: some View { var body: some View {
Section("App Information") { Section("App Information") {
HStack { HStack {
Image(systemName: "mountain.2.fill") Image("MountainsIcon")
.foregroundColor(.blue) .resizable()
.frame(width: 24, height: 24)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("OpenClimb") Text("OpenClimb")
.font(.headline) .font(.headline)
@@ -163,15 +163,6 @@ struct AppInfoSection: View {
Text("\(appVersion) (\(buildNumber))") Text("\(appVersion) (\(buildNumber))")
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
HStack {
Image(systemName: "person.fill")
.foregroundColor(.blue)
Text("Developer")
Spacer()
Text("OpenClimb Team")
.foregroundColor(.secondary)
}
} }
} }
} }
@@ -203,7 +194,7 @@ struct ExportDataView: View {
item: fileURL, item: fileURL,
preview: SharePreview( preview: SharePreview(
"OpenClimb Data Export", "OpenClimb Data Export",
image: Image(systemName: "mountain.2.fill")) image: Image("MountainsIcon"))
) { ) {
Label("Share Data", systemImage: "square.and.arrow.up") Label("Share Data", systemImage: "square.and.arrow.up")
.font(.headline) .font(.headline)
@@ -324,13 +315,6 @@ struct ImportDataView: View {
Text("Import climbing data from a previously exported ZIP file.") Text("Import climbing data from a previously exported ZIP file.")
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Text(
"Fully compatible with Android exports - identical ZIP format with images."
)
.font(.subheadline)
.foregroundColor(.blue)
.multilineTextAlignment(.center)
Text("⚠️ Warning: This will replace all current data!") Text("⚠️ Warning: This will replace all current data!")
.font(.subheadline) .font(.subheadline)
.foregroundColor(.red) .foregroundColor(.red)
@@ -423,7 +407,10 @@ struct ImportDataView: View {
await MainActor.run { await MainActor.run {
isImporting = false isImporting = false
dismiss() // Auto-close after successful import
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
dismiss()
}
} }
} catch { } catch {
await MainActor.run { await MainActor.run {