import SwiftUI import PhotosUI struct RouteDetailView: View { let routeId: Int let fallbackName: String @State private var detail: RouteDetail? @State private var isLoading = false @State private var errorMessage: String? @State private var newPhotoSelection: [PhotosPickerItem] = [] @State private var isUploadingPhoto = false @State private var photoUploadProgress: (done: Int, total: Int) = (0, 0) @State private var photoErrorMessage: String? var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { if let detail { MiniRouteMap(track: detail.gpsTrack, lineWidth: 4) .frame(height: 320) .clipShape(RoundedRectangle(cornerRadius: 16)) .padding(.horizontal) HStack(spacing: 12) { StatTile(value: formatKm(detail.distanzKm), label: "Distanz", icon: "ruler") StatTile(value: formatMin(detail.dauerMin), label: "Dauer", icon: "clock") StatTile(value: "\(detail.gpsTrack.count)", label: "Punkte", icon: "point.3.connected.trianglepath.dotted") } .padding(.horizontal) if let beschreibung = detail.beschreibung, !beschreibung.isEmpty { Text(beschreibung) .font(.body) .padding(.horizontal) } photosSection(for: detail) .padding(.horizontal) Spacer(minLength: 24) } else if isLoading { ProgressView().padding(.top, 80) } else if let error = errorMessage { ContentUnavailableView( "Fehler", systemImage: "exclamationmark.triangle", description: Text(error) ) .padding(.top, 60) } } } .navigationTitle(detail?.name ?? fallbackName) .navigationBarTitleDisplayMode(.inline) .task { await load() } .onChange(of: newPhotoSelection) { _, items in guard !items.isEmpty else { return } Task { await uploadSelected(items: items) } } } @ViewBuilder private func photosSection(for detail: RouteDetail) -> some View { let urls = detail.fotoUrls ?? [] VStack(alignment: .leading, spacing: 8) { HStack { Text(urls.isEmpty ? "Fotos" : "Fotos (\(urls.count))") .font(.headline) Spacer() PhotosPicker( selection: $newPhotoSelection, maxSelectionCount: 5, matching: .images ) { Label("Foto hinzufügen", systemImage: "photo.badge.plus") .font(.subheadline) } .disabled(isUploadingPhoto) } if isUploadingPhoto { HStack(spacing: 8) { ProgressView() Text("Lade \(photoUploadProgress.done + 1)/\(photoUploadProgress.total)…") .font(.caption) .foregroundStyle(.secondary) } } if let photoErrorMessage { Text(photoErrorMessage) .font(.caption) .foregroundStyle(.red) } if !urls.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(urls, id: \.self) { path in photoThumb(path) } } } } else if !isUploadingPhoto { Text("Noch keine Fotos. Über den Button oben kannst du welche hinzufügen.") .font(.footnote) .foregroundStyle(.secondary) } } } private func photoThumb(_ path: String) -> some View { let url = URL(string: "https://banyaro.app\(path)") return AsyncImage(url: url) { phase in switch phase { case .success(let img): img.resizable().scaledToFill() default: Rectangle().fill(.gray.opacity(0.15)) } } .frame(width: 160, height: 160) .clipShape(RoundedRectangle(cornerRadius: 10)) } private func load() async { isLoading = true errorMessage = nil defer { isLoading = false } do { detail = try await APIClient.shared.get("/api/routes/\(routeId)") } catch { errorMessage = error.localizedDescription } } private func uploadSelected(items: [PhotosPickerItem]) async { isUploadingPhoto = true photoErrorMessage = nil photoUploadProgress = (0, items.count) defer { isUploadingPhoto = false newPhotoSelection = [] } for (index, item) in items.enumerated() { photoUploadProgress = (index, items.count) guard let raw = try? await item.loadTransferable(type: Data.self) else { continue } let resized = ImageResize.resizedJPEG(from: raw) do { try await APIClient.shared.uploadFile( "/api/routes/\(routeId)/photo", filename: "photo_\(Int(Date.now.timeIntervalSince1970))_\(index + 1).jpg", data: resized ) } catch { photoErrorMessage = "Foto \(index + 1) konnte nicht hochgeladen werden: \(error.localizedDescription)" break } } // Refresh detail to pick up the new foto_urls. await load() } private func formatKm(_ km: Double?) -> String { guard let km else { return "—" } return String(format: "%.2f km", km) } private func formatMin(_ mins: Int?) -> String { guard let mins else { return "—" } if mins >= 60 { return "\(mins / 60) h \(mins % 60) min" } return "\(mins) min" } } private struct StatTile: View { let value: String let label: String let icon: String var body: some View { VStack(spacing: 6) { Image(systemName: icon) .foregroundStyle(Color.accentColor) Text(value) .font(.headline) Text(label) .font(.caption) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity) .padding(.vertical, 12) .background(.background.secondary, in: RoundedRectangle(cornerRadius: 12)) } }