diff --git a/ios/Sources/ProductPhotoView.swift b/ios/Sources/ProductPhotoView.swift index 822eb92..432d4b6 100644 --- a/ios/Sources/ProductPhotoView.swift +++ b/ios/Sources/ProductPhotoView.swift @@ -196,7 +196,7 @@ struct ProductPhotoSection: View { } private func upload(_ image: UIImage) async { - guard let data = Self.prepared(image) else { return } + guard let data = productImageJPEG(image) else { return } busy = true defer { busy = false } error = nil @@ -223,22 +223,24 @@ struct ProductPhotoSection: View { } } - /// Foto verkleinern und als JPEG unter die Server-Grenze (2 MB) drücken. - private static func prepared(_ image: UIImage, maxDim: CGFloat = 1600) -> Data? { - let size = image.size - let scale = min(1, maxDim / max(size.width, size.height)) - let target = CGSize(width: size.width * scale, height: size.height * scale) - let scaled = UIGraphicsImageRenderer(size: target).image { _ in - image.draw(in: CGRect(origin: .zero, size: target)) - } - var quality: CGFloat = 0.8 - var data = scaled.jpegData(compressionQuality: quality) - while let d = data, d.count > 1_800_000, quality > 0.3 { - quality -= 0.15 - data = scaled.jpegData(compressionQuality: quality) - } - return data +} + +/// Foto verkleinern und als JPEG unter die Server-Grenze (2 MB) drücken. +/// Frei (nicht an eine View gebunden), damit auch das Anlege-Formular es nutzt. +func productImageJPEG(_ image: UIImage, maxDim: CGFloat = 1600) -> Data? { + let size = image.size + let scale = min(1, maxDim / max(size.width, size.height)) + let target = CGSize(width: size.width * scale, height: size.height * scale) + let scaled = UIGraphicsImageRenderer(size: target).image { _ in + image.draw(in: CGRect(origin: .zero, size: target)) } + var quality: CGFloat = 0.8 + var data = scaled.jpegData(compressionQuality: quality) + while let d = data, d.count > 1_800_000, quality > 0.3 { + quality -= 0.15 + data = scaled.jpegData(compressionQuality: quality) + } + return data } /// Kamera über UIImagePickerController – SwiftUI hat keine eigene Kamera-Ansicht. diff --git a/ios/Sources/ProductViews.swift b/ios/Sources/ProductViews.swift index cc2aa3c..e71b4c0 100644 --- a/ios/Sources/ProductViews.swift +++ b/ios/Sources/ProductViews.swift @@ -1,4 +1,5 @@ import SwiftUI +import PhotosUI /// Artikel per Namenssuche auswählen (Alternative zum Scannen). struct ProductSearchView: View { @@ -84,6 +85,10 @@ struct ProductFormView: View { @State private var newGroupShown = false @State private var busy = false @State private var error: String? + // Foto schon beim Anlegen: erst nach dem Anlegen hochladen (ID nötig). + @State private var pendingImage: UIImage? + @State private var showCamera = false + @State private var pickerItem: PhotosPickerItem? private let baseUnits = [("piece", "Stück"), ("gram", "Gramm"), ("milliliter", "Milliliter")] // Beschriftungen kommen sonst aus DisplaySettings.baseUnitLabel. @@ -174,6 +179,25 @@ struct ProductFormView: View { } } } + // Foto direkt beim Anlegen – z.B. wenn Open Food/Products Facts keins + // liefert. Wird nach dem Anlegen hochgeladen (dann gibt es eine ID). + Section("Foto") { + if let pendingImage { + Image(uiImage: pendingImage) + .resizable().scaledToFit() + .frame(maxWidth: .infinity).frame(maxHeight: 200) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + Button { showCamera = true } label: { Label("Foto machen", systemImage: "camera") } + PhotosPicker(selection: $pickerItem, matching: .images) { + Label("Aus Galerie", systemImage: "photo.on.rectangle") + } + if pendingImage != nil { + Button(role: .destructive) { pendingImage = nil } label: { + Label("Foto entfernen", systemImage: "trash") + } + } + } if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } } @@ -184,6 +208,20 @@ struct ProductFormView: View { } .navigationTitle("Neuer Artikel") .navigationBarTitleDisplayMode(.inline) + .fullScreenCover(isPresented: $showCamera) { + CameraPicker(isPresented: $showCamera) { image in pendingImage = image } + .ignoresSafeArea() + } + .onChange(of: pickerItem) { item in + guard let item else { return } + Task { + if let data = try? await item.loadTransferable(type: Data.self), + let ui = UIImage(data: data) { + pendingImage = ui + } + pickerItem = nil + } + } .onAppear(perform: prefill) .task { groups = (try? await APIClient.shared.groups()) ?? [] @@ -242,6 +280,12 @@ struct ProductFormView: View { bulk: modus == "object" ? (objMode == "bulk") : nil ) ) + // Vorgemerktes Foto hochladen – ein Fehler darf das Anlegen nicht + // zunichtemachen, der Artikel existiert bereits. + if let pendingImage, let data = productImageJPEG(pendingImage) { + _ = try? await APIClient.shared.uploadProductImage( + id: product.id, data: data, contentType: "image/jpeg") + } onCreated(product) dismiss() } catch {