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()
+ }
+ }
+}