diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 038ff16..8f16915 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -2,6 +2,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/example/mycrib/platform/ImagePicker.android.kt b/composeApp/src/androidMain/kotlin/com/example/mycrib/platform/ImagePicker.android.kt index 6593611..0ebed24 100644 --- a/composeApp/src/androidMain/kotlin/com/example/mycrib/platform/ImagePicker.android.kt +++ b/composeApp/src/androidMain/kotlin/com/example/mycrib/platform/ImagePicker.android.kt @@ -1,11 +1,15 @@ package com.mycrib.platform +import android.content.Context import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext +import androidx.core.content.FileProvider +import java.io.File @Composable actual fun rememberImagePicker( @@ -50,6 +54,46 @@ actual fun rememberImagePicker( } } +@Composable +actual fun rememberCameraPicker( + onImageCaptured: (ImageData) -> Unit +): () -> Unit { + val context = LocalContext.current + + // Create a temp file URI for the camera to save to + val photoUri = remember { + val photoFile = File(context.cacheDir, "camera_photo_${System.currentTimeMillis()}.jpg") + FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + photoFile + ) + } + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture() + ) { success: Boolean -> + if (success) { + try { + val inputStream = context.contentResolver.openInputStream(photoUri) + val bytes = inputStream?.readBytes() + inputStream?.close() + + if (bytes != null) { + val fileName = "camera_${System.currentTimeMillis()}.jpg" + onImageCaptured(ImageData(bytes, fileName)) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + return { + launcher.launch(photoUri) + } +} + private fun getFileNameFromUri(context: android.content.Context, uri: Uri): String { var fileName = "image_${System.currentTimeMillis()}.jpg" diff --git a/composeApp/src/androidMain/res/xml/file_paths.xml b/composeApp/src/androidMain/res/xml/file_paths.xml new file mode 100644 index 0000000..d894287 --- /dev/null +++ b/composeApp/src/androidMain/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/platform/ImagePicker.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/platform/ImagePicker.kt index 8dc9835..87309f9 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/platform/ImagePicker.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/platform/ImagePicker.kt @@ -29,3 +29,8 @@ data class ImageData( expect fun rememberImagePicker( onImagesPicked: (List) -> Unit ): () -> Unit + +@Composable +expect fun rememberCameraPicker( + onImageCaptured: (ImageData) -> Unit +): () -> Unit diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt index 543df2d..475af27 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt @@ -8,12 +8,14 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.mycrib.shared.models.TaskCompletionCreateRequest import com.mycrib.platform.ImageData import com.mycrib.platform.rememberImagePicker +import com.mycrib.platform.rememberCameraPicker import kotlinx.datetime.* @OptIn(ExperimentalMaterial3Api::class) @@ -34,6 +36,10 @@ fun CompleteTaskDialog( selectedImages = images } + val cameraPicker = rememberCameraPicker { image -> + selectedImages = selectedImages + image + } + AlertDialog( onDismissRequest = onDismiss, title = { Text("Complete Task: $taskTitle") }, @@ -89,11 +95,23 @@ fun CompleteTaskDialog( ) Spacer(modifier = Modifier.height(4.dp)) - OutlinedButton( - onClick = { imagePicker() }, - modifier = Modifier.fillMaxWidth() + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Text("Select Images (up to 5)") + OutlinedButton( + onClick = { cameraPicker() }, + modifier = Modifier.weight(1f) + ) { + Text("Take Photo") + } + + OutlinedButton( + onClick = { imagePicker() }, + modifier = Modifier.weight(1f) + ) { + Text("Choose from Library") + } } // Display selected images diff --git a/composeApp/src/iosMain/kotlin/com/example/mycrib/platform/ImagePicker.ios.kt b/composeApp/src/iosMain/kotlin/com/example/mycrib/platform/ImagePicker.ios.kt index 1486e0e..25ff72a 100644 --- a/composeApp/src/iosMain/kotlin/com/example/mycrib/platform/ImagePicker.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/example/mycrib/platform/ImagePicker.ios.kt @@ -101,6 +101,53 @@ private fun NSData.toByteArray(): ByteArray { } } +@OptIn(ExperimentalForeignApi::class) +@Composable +actual fun rememberCameraPicker( + onImageCaptured: (ImageData) -> Unit +): () -> Unit { + val viewController = LocalUIViewController.current + + val cameraDelegate = remember { + object : NSObject(), UIImagePickerControllerDelegateProtocol, UINavigationControllerDelegateProtocol { + override fun imagePickerController( + picker: UIImagePickerController, + didFinishPickingMediaWithInfo: Map + ) { + picker.dismissViewControllerAnimated(true, null) + + val image = didFinishPickingMediaWithInfo[UIImagePickerControllerOriginalImage] as? UIImage + if (image != null) { + // Convert to JPEG + val jpegData = UIImageJPEGRepresentation(image, 0.8) + if (jpegData != null) { + val bytes = jpegData.toByteArray() + val fileName = "camera_${NSDate().timeIntervalSince1970.toLong()}.jpg" + onImageCaptured(ImageData(bytes, fileName)) + } + } + } + + override fun imagePickerControllerDidCancel(picker: UIImagePickerController) { + picker.dismissViewControllerAnimated(true, null) + } + } + } + + return { + // Check if camera is available + if (UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypeCamera)) { + val picker = UIImagePickerController().apply { + setSourceType(UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypeCamera) + setDelegate(cameraDelegate) + setCameraCaptureMode(UIImagePickerControllerCameraCaptureMode.UIImagePickerControllerCameraCaptureModePhoto) + } + + viewController.presentViewController(picker, animated = true, completion = null) + } + } +} + @Suppress("UNCHECKED_CAST") private fun synchronized(lock: Any, block: () -> T): T { return block() diff --git a/composeApp/src/jsMain/kotlin/com/example/mycrib/platform/ImagePicker.js.kt b/composeApp/src/jsMain/kotlin/com/example/mycrib/platform/ImagePicker.js.kt index bdd0a8d..1453bee 100644 --- a/composeApp/src/jsMain/kotlin/com/example/mycrib/platform/ImagePicker.js.kt +++ b/composeApp/src/jsMain/kotlin/com/example/mycrib/platform/ImagePicker.js.kt @@ -13,3 +13,15 @@ actual fun rememberImagePicker( println("Image picker not yet implemented for web") } } + +@Composable +actual fun rememberCameraPicker( + onImageCaptured: (ImageData) -> Unit +): () -> Unit { + // Web camera picker would require HTML5 media capture + // This is a placeholder implementation + return { + // TODO: Implement web camera capture + println("Camera picker not yet implemented for web") + } +} diff --git a/composeApp/src/jvmMain/kotlin/com/example/mycrib/platform/ImagePicker.jvm.kt b/composeApp/src/jvmMain/kotlin/com/example/mycrib/platform/ImagePicker.jvm.kt index 3ed8520..c58c44d 100644 --- a/composeApp/src/jvmMain/kotlin/com/example/mycrib/platform/ImagePicker.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/com/example/mycrib/platform/ImagePicker.jvm.kt @@ -13,3 +13,15 @@ actual fun rememberImagePicker( println("Image picker not yet implemented for desktop") } } + +@Composable +actual fun rememberCameraPicker( + onImageCaptured: (ImageData) -> Unit +): () -> Unit { + // Desktop camera picker would require platform-specific camera access + // This is a placeholder implementation + return { + // TODO: Implement desktop camera capture + println("Camera picker not yet implemented for desktop") + } +} diff --git a/composeApp/src/wasmJsMain/kotlin/com/example/mycrib/platform/ImagePicker.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/com/example/mycrib/platform/ImagePicker.wasmJs.kt index bb76d91..9e64743 100644 --- a/composeApp/src/wasmJsMain/kotlin/com/example/mycrib/platform/ImagePicker.wasmJs.kt +++ b/composeApp/src/wasmJsMain/kotlin/com/example/mycrib/platform/ImagePicker.wasmJs.kt @@ -13,3 +13,15 @@ actual fun rememberImagePicker( println("Image picker not yet implemented for WASM") } } + +@Composable +actual fun rememberCameraPicker( + onImageCaptured: (ImageData) -> Unit +): () -> Unit { + // WASM camera picker would require HTML5 media capture + // This is a placeholder implementation + return { + // TODO: Implement WASM camera capture + println("Camera picker not yet implemented for WASM") + } +} diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index dab9d2c..34c7155 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -15,5 +15,9 @@ com.mycrib.app + NSCameraUsageDescription + MyCrib needs access to your camera to take photos of completed tasks + NSPhotoLibraryUsageDescription + MyCrib needs access to your photo library to select photos of completed tasks diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index 2d56aed..8b2a1e2 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -150,7 +150,7 @@ struct ResidenceDetailView: View { } } .sheet(item: $selectedTaskForComplete) { task in - CompleteTaskView(task: task, isPresented: .constant(true)) { + CompleteTaskView(task: task) { selectedTaskForComplete = nil loadResidenceTasks() } diff --git a/iosApp/iosApp/Task/AllTasksView.swift b/iosApp/iosApp/Task/AllTasksView.swift index b382b85..35a5155 100644 --- a/iosApp/iosApp/Task/AllTasksView.swift +++ b/iosApp/iosApp/Task/AllTasksView.swift @@ -155,7 +155,7 @@ struct AllTasksView: View { } } .sheet(item: $selectedTaskForComplete) { task in - CompleteTaskView(task: task, isPresented: .constant(true)) { + CompleteTaskView(task: task) { selectedTaskForComplete = nil loadAllTasks() } diff --git a/iosApp/iosApp/Task/CompleteTaskView.swift b/iosApp/iosApp/Task/CompleteTaskView.swift index 9563a50..44c5b54 100644 --- a/iosApp/iosApp/Task/CompleteTaskView.swift +++ b/iosApp/iosApp/Task/CompleteTaskView.swift @@ -4,9 +4,9 @@ import ComposeApp struct CompleteTaskView: View { let task: TaskDetail - @Binding var isPresented: Bool let onComplete: () -> Void + @Environment(\.dismiss) private var dismiss @StateObject private var taskViewModel = TaskViewModel() @State private var completedByName: String = "" @State private var actualCost: String = "" @@ -17,6 +17,7 @@ struct CompleteTaskView: View { @State private var isSubmitting: Bool = false @State private var showError: Bool = false @State private var errorMessage: String = "" + @State private var showCamera: Bool = false var body: some View { NavigationStack { @@ -127,17 +128,28 @@ struct CompleteTaskView: View { // Images Section Section { VStack(alignment: .leading, spacing: 12) { - PhotosPicker( - selection: $selectedItems, - maxSelectionCount: 5, - matching: .images, - photoLibrary: .shared() - ) { - Label("Add Photos", systemImage: "photo.on.rectangle.angled") - .frame(maxWidth: .infinity) - .foregroundStyle(.blue) + HStack(spacing: 12) { + Button(action: { + showCamera = true + }) { + Label("Take Photo", systemImage: "camera") + .frame(maxWidth: .infinity) + .foregroundStyle(.blue) + } + .buttonStyle(.bordered) + + PhotosPicker( + selection: $selectedItems, + maxSelectionCount: 5, + matching: .images, + photoLibrary: .shared() + ) { + Label("Library", systemImage: "photo.on.rectangle.angled") + .frame(maxWidth: .infinity) + .foregroundStyle(.blue) + } + .buttonStyle(.bordered) } - .buttonStyle(.bordered) .onChange(of: selectedItems) { newItems in Task { selectedImages = [] @@ -200,7 +212,7 @@ struct CompleteTaskView: View { .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - isPresented = false + dismiss() } } } @@ -209,6 +221,13 @@ struct CompleteTaskView: View { } message: { Text(errorMessage) } + .sheet(isPresented: $showCamera) { + CameraPickerView { image in + if selectedImages.count < 5 { + selectedImages.append(image) + } + } + } } } @@ -265,7 +284,7 @@ struct CompleteTaskView: View { DispatchQueue.main.async { if result is ApiResultSuccess { self.isSubmitting = false - self.isPresented = false + self.dismiss() self.onComplete() } else if let errorResult = result as? ApiResultError { self.errorMessage = errorResult.message @@ -322,3 +341,41 @@ struct ImageThumbnailView: View { } } } + +// Camera Picker View Component +struct CameraPickerView: UIViewControllerRepresentable { + @Environment(\.dismiss) var dismiss + let onImageCaptured: (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(self) + } + + class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + let parent: CameraPickerView + + init(_ parent: CameraPickerView) { + self.parent = parent + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + if let image = info[.originalImage] as? UIImage { + parent.onImageCaptured(image) + } + parent.dismiss() + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + parent.dismiss() + } + } +}