fix: Gitea issues #27, #28, #30
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s

#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) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-04 22:21:13 -05:00
parent fa3932c597
commit 55ef010370
6 changed files with 102 additions and 7 deletions

View File

@@ -24,6 +24,7 @@
A10067 /* TripImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10067 /* TripImageView.swift */; }; A10067 /* TripImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10067 /* TripImageView.swift */; };
A10068 /* TripPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10068 /* TripPlaceholderView.swift */; }; A10068 /* TripPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10068 /* TripPlaceholderView.swift */; };
A10069 /* TripDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10069 /* TripDetailView.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 */; }; A10006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10006 /* LoginView.swift */; };
A10007 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10007 /* HomeView.swift */; }; A10007 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10007 /* HomeView.swift */; };
A10008 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10008 /* HomeViewModel.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 = "<group>"; }; B10067 /* TripImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripImageView.swift; sourceTree = "<group>"; };
B10068 /* TripPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripPlaceholderView.swift; sourceTree = "<group>"; }; B10068 /* TripPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripPlaceholderView.swift; sourceTree = "<group>"; };
B10069 /* TripDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripDetailView.swift; sourceTree = "<group>"; }; B10069 /* TripDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripDetailView.swift; sourceTree = "<group>"; };
B10070 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
B10006 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; }; B10006 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
B10007 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; }; B10007 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
B10008 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; }; B10008 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
@@ -331,6 +333,7 @@
B10027 /* MacroRing.swift */, B10027 /* MacroRing.swift */,
B10028 /* MacroBar.swift */, B10028 /* MacroBar.swift */,
B10029 /* LoadingView.swift */, B10029 /* LoadingView.swift */,
B10070 /* CameraView.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -598,6 +601,7 @@
A10067 /* TripImageView.swift in Sources */, A10067 /* TripImageView.swift in Sources */,
A10068 /* TripPlaceholderView.swift in Sources */, A10068 /* TripPlaceholderView.swift in Sources */,
A10069 /* TripDetailView.swift in Sources */, A10069 /* TripDetailView.swift in Sources */,
A10070 /* CameraView.swift in Sources */,
A10006 /* LoginView.swift in Sources */, A10006 /* LoginView.swift in Sources */,
A10007 /* HomeView.swift in Sources */, A10007 /* HomeView.swift in Sources */,
A10008 /* HomeViewModel.swift in Sources */, A10008 /* HomeViewModel.swift in Sources */,

View File

@@ -3,6 +3,8 @@ import PhotosUI
struct AssistantChatView: View { struct AssistantChatView: View {
@State private var vm = AssistantViewModel() @State private var vm = AssistantViewModel()
@State private var showCamera = false
@State private var showPhotoPicker = false
var onFoodAdded: (() -> Void)? var onFoodAdded: (() -> Void)?
var body: some View { var body: some View {
@@ -83,19 +85,30 @@ struct AssistantChatView: View {
Divider() Divider()
// Input bar // Input bar
HStack(spacing: 10) { HStack(alignment: .bottom, spacing: 10) {
PhotosPicker(selection: $vm.selectedPhoto, matching: .images) { 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") Image(systemName: "camera.fill")
.font(.title3) .font(.title3)
.foregroundStyle(Color.accentWarm) .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) .textFieldStyle(.plain)
.onSubmit { .lineLimit(1...5)
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
Task { await vm.send() }
}
Button { Button {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 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) .disabled(vm.inputText.trimmingCharacters(in: .whitespaces).isEmpty || vm.isLoading)
.padding(.bottom, 4)
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 10) .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 // MARK: - Chat Bubble

View File

@@ -173,6 +173,23 @@ final class AssistantViewModel {
selectedPhoto = nil 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 // MARK: - Helpers
private func draftToDict(_ draft: FitnessDraft) -> [String: Any] { private func draftToDict(_ draft: FitnessDraft) -> [String: Any] {

View File

@@ -68,6 +68,10 @@ struct FitnessAPI {
try await api.get("\(basePath)/foods/\(id)") try await api.get("\(basePath)/foods/\(id)")
} }
func deleteFood(id: String) async throws -> SuccessResponse {
try await api.delete("\(basePath)/foods/\(id)")
}
// MARK: - Templates // MARK: - Templates
func getTemplates() async throws -> [MealTemplate] { func getTemplates() async throws -> [MealTemplate] {

View File

@@ -76,6 +76,16 @@ struct FoodLibraryView: View {
.padding(.horizontal, 20) .padding(.horizontal, 20)
.padding(.vertical, 10) .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) Divider().padding(.leading, 20)
} }
} }

View File

@@ -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)
}
}
}