import SwiftUI import UniformTypeIdentifiers enum SheetType { case export(Data) case importData } struct SettingsView: View { @EnvironmentObject var dataManager: ClimbingDataManager @State private var activeSheet: SheetType? var body: some View { List { DataManagementSection( activeSheet: $activeSheet ) AppInfoSection() } .navigationTitle("Settings") .sheet( item: Binding( get: { activeSheet }, set: { activeSheet = $0 } ) ) { sheetType in switch sheetType { case .export(let data): ExportDataView(data: data) case .importData: ImportDataView() } } } } extension SheetType: Identifiable { var id: String { switch self { case .export: return "export" case .importData: return "import" } } } struct DataManagementSection: View { @EnvironmentObject var dataManager: ClimbingDataManager @Binding var activeSheet: SheetType? @State private var showingResetAlert = false @State private var isExporting = false var body: some View { Section("Data Management") { // Export Data Button(action: { exportDataAsync() }) { HStack { if isExporting { ProgressView() .scaleEffect(0.8) Text("Exporting...") .foregroundColor(.secondary) } else { Image(systemName: "square.and.arrow.up") .foregroundColor(.blue) Text("Export Data") } Spacer() } } .disabled(isExporting) .foregroundColor(.primary) // Import Data Button(action: { activeSheet = .importData }) { HStack { Image(systemName: "square.and.arrow.down") .foregroundColor(.green) Text("Import Data") Spacer() } } .foregroundColor(.primary) // Reset All Data Button(action: { showingResetAlert = true }) { HStack { Image(systemName: "trash") .foregroundColor(.red) Text("Reset All Data") Spacer() } } .foregroundColor(.red) } .alert("Reset All Data", isPresented: $showingResetAlert) { Button("Cancel", role: .cancel) {} Button("Reset", role: .destructive) { dataManager.resetAllData() } } message: { Text( "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." ) } } private func exportDataAsync() { isExporting = true Task { let data = await MainActor.run { dataManager.exportData() } isExporting = false if let data = data { activeSheet = .export(data) } } } } struct AppInfoSection: View { private var appVersion: String { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" } private var buildNumber: String { Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" } var body: some View { Section("App Information") { HStack { Image("MountainsIcon") .resizable() .frame(width: 24, height: 24) VStack(alignment: .leading) { Text("OpenClimb") .font(.headline) Text("Track your climbing progress") .font(.caption) .foregroundColor(.secondary) } Spacer() } HStack { Image(systemName: "info.circle") .foregroundColor(.blue) Text("Version") Spacer() Text("\(appVersion) (\(buildNumber))") .foregroundColor(.secondary) } } } } struct ExportDataView: View { let data: Data @Environment(\.dismiss) private var dismiss @State private var tempFileURL: URL? var body: some View { NavigationView { VStack(spacing: 20) { Image(systemName: "square.and.arrow.up") .font(.system(size: 60)) .foregroundColor(.blue) Text("Export Data") .font(.title) .fontWeight(.bold) Text( "Your climbing data has been prepared for export. Use the share button below to save or send your data." ) .multilineTextAlignment(.center) .padding(.horizontal) if let fileURL = tempFileURL { ShareLink( item: fileURL, preview: SharePreview( "OpenClimb Data Export", image: Image("MountainsIcon")) ) { Label("Share Data", systemImage: "square.and.arrow.up") .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding() .background( RoundedRectangle(cornerRadius: 12) .fill(.blue) ) } .padding(.horizontal) .buttonStyle(.plain) } else { Button(action: {}) { Label("Preparing Export...", systemImage: "hourglass") .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding() .background( RoundedRectangle(cornerRadius: 12) .fill(.gray) ) } .disabled(true) .padding(.horizontal) } Spacer() } .padding() .navigationTitle("Export") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Done") { dismiss() } } } .onAppear { if tempFileURL == nil { createTempFile() } } .onDisappear { // Delay cleanup to ensure sharing is complete DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { cleanupTempFile() } } } } private func createTempFile() { DispatchQueue.global(qos: .userInitiated).async { do { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] let isoString = formatter.string(from: Date()) let timestamp = isoString.replacingOccurrences(of: ":", with: "-") .replacingOccurrences(of: ".", with: "-") let filename = "openclimb_export_\(timestamp).zip" guard let documentsURL = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask ).first else { print("Could not access Documents directory") return } let fileURL = documentsURL.appendingPathComponent(filename) // Write the ZIP data to the file try data.write(to: fileURL) DispatchQueue.main.async { self.tempFileURL = fileURL } } catch { print("Failed to create export file: \(error)") } } } private func cleanupTempFile() { if let fileURL = tempFileURL { // Clean up after a delay to ensure sharing is complete DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { try? FileManager.default.removeItem(at: fileURL) print("Cleaned up export file: \(fileURL.lastPathComponent)") } } } } struct ImportDataView: View { @EnvironmentObject var dataManager: ClimbingDataManager @Environment(\.dismiss) private var dismiss @State private var isImporting = false @State private var importError: String? @State private var showingDocumentPicker = false var body: some View { NavigationView { VStack(spacing: 20) { Image(systemName: "square.and.arrow.down") .font(.system(size: 60)) .foregroundColor(.green) Text("Import Data") .font(.title) .fontWeight(.bold) VStack(spacing: 12) { Text("Import climbing data from a previously exported ZIP file.") .multilineTextAlignment(.center) Text("⚠️ Warning: This will replace all current data!") .font(.subheadline) .foregroundColor(.red) .multilineTextAlignment(.center) } .padding(.horizontal) Button(action: { showingDocumentPicker = true }) { if isImporting { HStack { ProgressView() .scaleEffect(0.8) Text("Importing...") } } else { Label("Select ZIP File to Import", systemImage: "folder.badge.plus") } } .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding() .background( RoundedRectangle(cornerRadius: 12) .fill(isImporting ? .gray : .green) ) .padding(.horizontal) .disabled(isImporting) if let error = importError { Text(error) .foregroundColor(.red) .padding() .background( RoundedRectangle(cornerRadius: 8) .fill(.red.opacity(0.1)) ) } Spacer() } .padding() .navigationTitle("Import Data") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Cancel") { dismiss() } } } .fileImporter( isPresented: $showingDocumentPicker, allowedContentTypes: [.zip, .archive], allowsMultipleSelection: false ) { result in switch result { case .success(let urls): if let url = urls.first { importData(from: url) } case .failure(let error): importError = "Failed to select file: \(error.localizedDescription)" } } } } private func importData(from url: URL) { isImporting = true importError = nil Task { do { // Access the security-scoped resource guard url.startAccessingSecurityScopedResource() else { await MainActor.run { isImporting = false importError = "Failed to access selected file" } return } defer { url.stopAccessingSecurityScopedResource() } let data = try Data(contentsOf: url) try dataManager.importData(from: data) await MainActor.run { isImporting = false // Auto-close after successful import DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { dismiss() } } } catch { await MainActor.run { isImporting = false importError = "Import failed: \(error.localizedDescription)" } } } } } #Preview { SettingsView() .environmentObject(ClimbingDataManager.preview) }