Verlorene Hunde: Melden-Button + Sheet analog zu Giftköder
Toolbar-Button rechts oben (exclamationmark.bubble), Sheet mit:
- Name (Pflicht), Rasse (optional), Eigener-Hund-Picker (optional)
- Beschreibung (Pflicht, min 3 Zeichen)
- PhotosPicker für ein Foto
- Standort vom OneShotLocation (read-only Anzeige)
- POST /api/lost, danach optional POST /api/lost/{id}/foto (multipart,
resized via ImageResize)
This commit is contained in:
parent
08069d6ea4
commit
fb00468c8c
1 changed files with 173 additions and 0 deletions
|
|
@ -1,16 +1,34 @@
|
|||
import SwiftUI
|
||||
import MapKit
|
||||
import PhotosUI
|
||||
|
||||
struct VerloreneHundeView: View {
|
||||
@State private var location = OneShotLocation()
|
||||
@State private var lostDogs: [LostDog] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var showReport = false
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.navigationTitle("Verlorene Hunde")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
if location.coordinate != nil {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
showReport = true
|
||||
} label: {
|
||||
Label("Melden", systemImage: "exclamationmark.bubble")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showReport) {
|
||||
if let coord = location.coordinate {
|
||||
ReportLostDogSheet(coord: coord) { await load() }
|
||||
}
|
||||
}
|
||||
.task {
|
||||
location.request()
|
||||
await load()
|
||||
|
|
@ -119,6 +137,161 @@ private struct LostDogRow: View {
|
|||
}
|
||||
}
|
||||
|
||||
private struct ReportLostDogSheet: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
let coord: CLLocationCoordinate2D
|
||||
let onSaved: () async -> Void
|
||||
|
||||
@State private var name = ""
|
||||
@State private var rasse = ""
|
||||
@State private var beschreibung = ""
|
||||
@State private var dogs: [Dog] = []
|
||||
@State private var ownDogId: Int?
|
||||
@State private var photoSelection: [PhotosPickerItem] = []
|
||||
@State private var photoData: Data?
|
||||
|
||||
@State private var saveState: SaveState = .idle
|
||||
@State private var errorMessage: String?
|
||||
|
||||
private enum SaveState: Equatable {
|
||||
case idle
|
||||
case savingEntry
|
||||
case uploadingPhoto
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("Hund") {
|
||||
TextField("Name (z. B. Bello)", text: $name)
|
||||
TextField("Rasse (optional)", text: $rasse)
|
||||
if !dogs.isEmpty {
|
||||
Picker("Eigener Hund", selection: $ownDogId) {
|
||||
Text("Nicht meiner").tag(Int?.none)
|
||||
ForEach(dogs) { dog in
|
||||
Text(dog.name).tag(Int?.some(dog.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Beschreibung") {
|
||||
TextField("Auffälligkeiten, Halsband, Verhalten — alles was beim Wiederfinden hilft.",
|
||||
text: $beschreibung, axis: .vertical)
|
||||
.lineLimit(3...8)
|
||||
}
|
||||
|
||||
Section {
|
||||
PhotosPicker(selection: $photoSelection, maxSelectionCount: 1, matching: .images) {
|
||||
Label(photoData == nil ? "Foto hinzufügen" : "Foto ändern",
|
||||
systemImage: "photo.badge.plus")
|
||||
}
|
||||
if let photoData, let img = UIImage(data: photoData) {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(maxHeight: 200)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
} header: {
|
||||
Text("Foto")
|
||||
} footer: {
|
||||
Text("Ein aktuelles Foto erhöht die Chance, dass jemand den Hund erkennt.")
|
||||
.font(.caption2)
|
||||
}
|
||||
|
||||
Section("Letzter bekannter Standort") {
|
||||
Text(String(format: "%.5f, %.5f", coord.latitude, coord.longitude))
|
||||
.font(.caption.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if let errorMessage {
|
||||
Section {
|
||||
Text(errorMessage).font(.footnote).foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Vermisst melden")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Abbrechen") { dismiss() }
|
||||
.disabled(saveState != .idle)
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
saveToolbarItem
|
||||
}
|
||||
}
|
||||
.onChange(of: photoSelection) { _, items in
|
||||
Task { await loadPhoto(from: items.first) }
|
||||
}
|
||||
.task { dogs = (try? await APIClient.shared.get("/api/dogs")) ?? [] }
|
||||
.interactiveDismissDisabled(saveState != .idle)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var saveToolbarItem: some View {
|
||||
switch saveState {
|
||||
case .idle:
|
||||
Button("Melden") { Task { await save() } }
|
||||
.disabled(name.trimmingCharacters(in: .whitespaces).isEmpty ||
|
||||
beschreibung.trimmingCharacters(in: .whitespaces).count < 3)
|
||||
case .savingEntry, .uploadingPhoto:
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadPhoto(from item: PhotosPickerItem?) async {
|
||||
guard let item else { return }
|
||||
photoData = try? await item.loadTransferable(type: Data.self)
|
||||
}
|
||||
|
||||
private func save() async {
|
||||
errorMessage = nil
|
||||
saveState = .savingEntry
|
||||
|
||||
let body = LostDogCreateBody(
|
||||
name: name.trimmingCharacters(in: .whitespaces),
|
||||
rasse: rasse.trimmingCharacters(in: .whitespaces).isEmpty ? nil : rasse,
|
||||
beschreibung: beschreibung.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
lat: coord.latitude,
|
||||
lon: coord.longitude,
|
||||
dogId: ownDogId
|
||||
)
|
||||
|
||||
let created: LostDog
|
||||
do {
|
||||
created = try await APIClient.shared.post("/api/lost", body: body)
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
saveState = .idle
|
||||
return
|
||||
}
|
||||
|
||||
if let photoData {
|
||||
saveState = .uploadingPhoto
|
||||
let resized = ImageResize.resizedJPEG(from: photoData)
|
||||
do {
|
||||
_ = try await APIClient.shared.uploadFile(
|
||||
"/api/lost/\(created.id)/foto",
|
||||
filename: "lost_\(created.id).jpg",
|
||||
data: resized
|
||||
)
|
||||
} catch {
|
||||
errorMessage = "Meldung ist gespeichert, aber Foto-Upload fehlgeschlagen: \(error.localizedDescription)"
|
||||
await onSaved()
|
||||
saveState = .idle
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await onSaved()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private struct LostDogDetailView: View {
|
||||
let dog: LostDog
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue