From 55ef01037041f03941610882bcd2466e3f728edd Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Sat, 4 Apr 2026 22:21:13 -0500 Subject: [PATCH] fix: Gitea issues #27, #28, #30 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #28 — Return key now inserts new line in AI chat: TextField replaced with TextField(axis: .vertical) + lineLimit(1...5). Return = new line, send button submits. Removed .onSubmit. #27 — Delete foods in food library: Long-press context menu with "Delete" option on each food item. Calls DELETE /api/fitness/foods/{id} and reloads. #30 — Camera + photo library options: Replaced PhotosPicker with a Menu offering "Take Photo" (camera) and "Photo Library" (picker). CameraView wraps UIImagePickerController. handleCameraImage() in ViewModel resizes and sends same as photo picker. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Platform.xcodeproj/project.pbxproj | 4 ++ .../Assistant/AssistantChatView.swift | 35 +++++++++++++---- .../Assistant/AssistantViewModel.swift | 17 ++++++++ .../Features/Fitness/API/FitnessAPI.swift | 4 ++ .../Fitness/Views/FoodLibraryView.swift | 10 +++++ .../Shared/Components/CameraView.swift | 39 +++++++++++++++++++ 6 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 ios/Platform/Platform/Shared/Components/CameraView.swift diff --git a/ios/Platform/Platform.xcodeproj/project.pbxproj b/ios/Platform/Platform.xcodeproj/project.pbxproj index e9283f5..68c4635 100644 --- a/ios/Platform/Platform.xcodeproj/project.pbxproj +++ b/ios/Platform/Platform.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ A10067 /* TripImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10067 /* TripImageView.swift */; }; A10068 /* TripPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10068 /* TripPlaceholderView.swift */; }; A10069 /* TripDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10069 /* TripDetailView.swift */; }; + A10070 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10070 /* CameraView.swift */; }; A10006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10006 /* LoginView.swift */; }; A10007 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10007 /* HomeView.swift */; }; A10008 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10008 /* HomeViewModel.swift */; }; @@ -110,6 +111,7 @@ B10067 /* TripImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripImageView.swift; sourceTree = ""; }; B10068 /* TripPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripPlaceholderView.swift; sourceTree = ""; }; B10069 /* TripDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripDetailView.swift; sourceTree = ""; }; + B10070 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; B10006 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; B10007 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; B10008 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; @@ -331,6 +333,7 @@ B10027 /* MacroRing.swift */, B10028 /* MacroBar.swift */, B10029 /* LoadingView.swift */, + B10070 /* CameraView.swift */, ); path = Components; sourceTree = ""; @@ -598,6 +601,7 @@ A10067 /* TripImageView.swift in Sources */, A10068 /* TripPlaceholderView.swift in Sources */, A10069 /* TripDetailView.swift in Sources */, + A10070 /* CameraView.swift in Sources */, A10006 /* LoginView.swift in Sources */, A10007 /* HomeView.swift in Sources */, A10008 /* HomeViewModel.swift in Sources */, diff --git a/ios/Platform/Platform/Features/Assistant/AssistantChatView.swift b/ios/Platform/Platform/Features/Assistant/AssistantChatView.swift index 8c1b169..77ee4dc 100644 --- a/ios/Platform/Platform/Features/Assistant/AssistantChatView.swift +++ b/ios/Platform/Platform/Features/Assistant/AssistantChatView.swift @@ -3,6 +3,8 @@ import PhotosUI struct AssistantChatView: View { @State private var vm = AssistantViewModel() + @State private var showCamera = false + @State private var showPhotoPicker = false var onFoodAdded: (() -> Void)? var body: some View { @@ -83,19 +85,30 @@ struct AssistantChatView: View { Divider() // Input bar - HStack(spacing: 10) { - PhotosPicker(selection: $vm.selectedPhoto, matching: .images) { + HStack(alignment: .bottom, spacing: 10) { + Menu { + Button { + showCamera = true + } label: { + Label("Take Photo", systemImage: "camera") + } + + Button { + showPhotoPicker = true + } label: { + Label("Photo Library", systemImage: "photo.on.rectangle") + } + } label: { Image(systemName: "camera.fill") .font(.title3) .foregroundStyle(Color.accentWarm) } + .padding(.bottom, 4) - TextField("Describe your food...", text: $vm.inputText) + // Multiline text input — Return = new line, send button submits + TextField("Describe your food...", text: $vm.inputText, axis: .vertical) .textFieldStyle(.plain) - .onSubmit { - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - Task { await vm.send() } - } + .lineLimit(1...5) Button { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) @@ -110,6 +123,7 @@ struct AssistantChatView: View { ) } .disabled(vm.inputText.trimmingCharacters(in: .whitespaces).isEmpty || vm.isLoading) + .padding(.bottom, 4) } .padding(.horizontal, 16) .padding(.vertical, 10) @@ -126,6 +140,13 @@ struct AssistantChatView: View { } } } + .photosPicker(isPresented: $showPhotoPicker, selection: $vm.selectedPhoto, matching: .images) + .fullScreenCover(isPresented: $showCamera) { + CameraView { image in + showCamera = false + Task { await vm.handleCameraImage(image) } + } + } } // MARK: - Chat Bubble diff --git a/ios/Platform/Platform/Features/Assistant/AssistantViewModel.swift b/ios/Platform/Platform/Features/Assistant/AssistantViewModel.swift index 67bd143..dc7fb7e 100644 --- a/ios/Platform/Platform/Features/Assistant/AssistantViewModel.swift +++ b/ios/Platform/Platform/Features/Assistant/AssistantViewModel.swift @@ -173,6 +173,23 @@ final class AssistantViewModel { selectedPhoto = nil } + func handleCameraImage(_ image: UIImage) async { + let maxDim: CGFloat = 800 + let scale = min(maxDim / image.size.width, maxDim / image.size.height, 1.0) + let newSize = CGSize(width: image.size.width * scale, height: image.size.height * scale) + let renderer = UIGraphicsImageRenderer(size: newSize) + let resized = renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: newSize)) + } + + if let jpegData = resized.jpegData(compressionQuality: 0.7) { + let base64 = jpegData.base64EncodedString() + imageDataUrl = "data:image/jpeg;base64,\(base64)" + messages.append(ChatMessage(role: "user", content: "[Photo attached]")) + await doSend(action: "chat") + } + } + // MARK: - Helpers private func draftToDict(_ draft: FitnessDraft) -> [String: Any] { diff --git a/ios/Platform/Platform/Features/Fitness/API/FitnessAPI.swift b/ios/Platform/Platform/Features/Fitness/API/FitnessAPI.swift index 51ac183..eb32971 100644 --- a/ios/Platform/Platform/Features/Fitness/API/FitnessAPI.swift +++ b/ios/Platform/Platform/Features/Fitness/API/FitnessAPI.swift @@ -68,6 +68,10 @@ struct FitnessAPI { try await api.get("\(basePath)/foods/\(id)") } + func deleteFood(id: String) async throws -> SuccessResponse { + try await api.delete("\(basePath)/foods/\(id)") + } + // MARK: - Templates func getTemplates() async throws -> [MealTemplate] { diff --git a/ios/Platform/Platform/Features/Fitness/Views/FoodLibraryView.swift b/ios/Platform/Platform/Features/Fitness/Views/FoodLibraryView.swift index e5303cc..52f4850 100644 --- a/ios/Platform/Platform/Features/Fitness/Views/FoodLibraryView.swift +++ b/ios/Platform/Platform/Features/Fitness/Views/FoodLibraryView.swift @@ -76,6 +76,16 @@ struct FoodLibraryView: View { .padding(.horizontal, 20) .padding(.vertical, 10) } + .contextMenu { + Button(role: .destructive) { + Task { + _ = try? await FitnessAPI().deleteFood(id: food.id) + await vm.loadInitial() + } + } label: { + Label("Delete", systemImage: "trash") + } + } Divider().padding(.leading, 20) } } diff --git a/ios/Platform/Platform/Shared/Components/CameraView.swift b/ios/Platform/Platform/Shared/Components/CameraView.swift new file mode 100644 index 0000000..51881db --- /dev/null +++ b/ios/Platform/Platform/Shared/Components/CameraView.swift @@ -0,0 +1,39 @@ +import SwiftUI +import UIKit + +/// UIImagePickerController wrapper for taking photos with the camera. +struct CameraView: UIViewControllerRepresentable { + let onCapture: (UIImage) -> Void + + func makeUIViewController(context: Context) -> UIImagePickerController { + let picker = UIImagePickerController() + picker.sourceType = .camera + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(onCapture: onCapture) + } + + class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + let onCapture: (UIImage) -> Void + + init(onCapture: @escaping (UIImage) -> Void) { + self.onCapture = onCapture + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + if let image = info[.originalImage] as? UIImage { + onCapture(image) + } + picker.dismiss(animated: true) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) + } + } +}