feat: Add recipe image upload and camera capture support#190
feat: Add recipe image upload and camera capture support#190RaberAmin2 wants to merge 8 commits into
Conversation
Add the ability to upload images or take photos with the camera when creating or editing recipes. Images are compressed and scaled before upload to reduce bandwidth usage. Changes include: - New RecipeImageUpload data model for image upload payloads - RecipeImageCompressor utility for image compression and scaling - Camera capture integration with permission handling - Photo picker (gallery) integration via PickVisualMedia - FileProvider configuration for camera image URIs - Upload progress indicator and error handling in the UI - Image upload API integration in RecipeRepository - German translations for new strings - Updated FAQs documentation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
💤 Files with no reviewable changes (1)
📝 WalkthroughWalkthroughThis PR adds recipe image upload: domain model, compression, camera/gallery capture, UI wiring, ViewModel upload state/logic, repository WebDAV PUT uploads via OkHttp, DI wiring, Android FileProvider and file_paths, and HTTP 409 error mapping. ChangesRecipe Image Upload
Sequence DiagramsequenceDiagram
participant User
participant RecipeCreateScreen
participant CreateEditRecipeForm
participant RecipeImageCompressor
participant RecipeCreateEditViewModel
participant RecipeRepositoryImpl
participant Nextcloud
User->>RecipeCreateScreen: Open create recipe
User->>CreateEditRecipeForm: Select/take image
CreateEditRecipeForm->>RecipeCreateScreen: onUploadImage(uri)
RecipeCreateScreen->>RecipeCreateEditViewModel: uploadImage(uri, context)
RecipeCreateEditViewModel->>RecipeImageCompressor: compressRecipeImage(uri)
RecipeImageCompressor-->>RecipeCreateEditViewModel: RecipeImageUpload
RecipeCreateEditViewModel->>RecipeRepositoryImpl: uploadRecipeImage(image)
RecipeRepositoryImpl->>Nextcloud: MKCOL recipe_images/
RecipeRepositoryImpl->>Nextcloud: PUT image bytes
Nextcloud-->>RecipeRepositoryImpl: imagePath
RecipeRepositoryImpl-->>RecipeCreateEditViewModel: Resource.Success(path)
RecipeCreateEditViewModel->>RecipeCreateScreen: Update imageUploadError state
CreateEditRecipeForm->>User: Show uploaded image
🎯 3 (Moderate) | ⏱️ ~20 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/components/CreateEditRecipeForm.kt (1)
137-171: 🏗️ Heavy liftMove upload workflow/state into the ViewModel.
Compression/upload orchestration and error/loading state in this composable are business-flow concerns and should survive configuration changes.
As per coding guidelines, "Store UI state and business logic in ViewModels in the Presentation Layer".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/components/CreateEditRecipeForm.kt` around lines 137 - 171, Extract the compression and upload workflow out of the composable into the Recipe ViewModel: move the uploadImage(uri: Uri) logic (including compressRecipeImage call and the coroutine) into a ViewModel method (e.g., fun uploadImage(uri: Uri)). Replace the composable's local vars isImageUploading, imageUploadError and cameraImageUri with ViewModel-exposed state (StateFlow or LiveData like isImageUploading, imageUploadError, cameraImageUri or uploadedImageUri) and observe them in CreateEditRecipeForm; have the ViewModel call the existing onImageUpload repository/use-case instead of the composable. Ensure the composable invokes viewModel.uploadImage(uri) when needed and keep only UI rendering in the composable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/data/repository/RecipeRepositoryImpl.kt`:
- Around line 100-105: Add a fast-fail guard in
RecipeRepositoryImpl.uploadRecipeImage to validate the RecipeImageUpload payload
before any WebDAV or network work: check that image.filename and image.mimeType
are not blank and image.bytes is not empty, and if any check fails return
Resource.Error with an appropriate UiText.StringResource (create a string
resource like error_invalid_image_payload if needed); place this check near the
start of uploadRecipeImage (before account checks / before WebDAV calls) so
invalid images fail locally without making network requests.
In
`@app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/components/CreateEditRecipeForm.kt`:
- Around line 145-170: Wrap the image upload coroutine body in a
try/catch/finally inside coroutineScope.launch so that isImageUploading is
always reset in the finally block; specifically, keep the existing
compressRecipeImage(...) and onImageUpload(...) logic but move them into a try,
catch any thrown exception to set imageUploadError (e.g.,
UiText.StringResource(R.string.error_image_upload_failed) or the exception
message) and ensure isImageUploading = false runs in finally; reference the
coroutineScope.launch block, compressRecipeImage, onImageUpload,
isImageUploading, and imageUploadError when making the change.
- Around line 209-215: Remove the unnecessary CAMERA runtime permission gating
around intent-based capture: in requestCameraImage() remove the
ContextCompat.checkSelfPermission branch and always call launchCamera(); remove
the cameraPermissionLauncher declaration and any code that launches or handles
it (keep launchCamera() and the ActivityResultContracts.TakePicture/FileProvider
usage intact); and delete the <uses-permission
android:name="android.permission.CAMERA" /> entry from AndroidManifest.xml so
the system camera app can be used without requiring the CAMERA permission in the
calling app.
---
Nitpick comments:
In
`@app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/components/CreateEditRecipeForm.kt`:
- Around line 137-171: Extract the compression and upload workflow out of the
composable into the Recipe ViewModel: move the uploadImage(uri: Uri) logic
(including compressRecipeImage call and the coroutine) into a ViewModel method
(e.g., fun uploadImage(uri: Uri)). Replace the composable's local vars
isImageUploading, imageUploadError and cameraImageUri with ViewModel-exposed
state (StateFlow or LiveData like isImageUploading, imageUploadError,
cameraImageUri or uploadedImageUri) and observe them in CreateEditRecipeForm;
have the ViewModel call the existing onImageUpload repository/use-case instead
of the composable. Ensure the composable invokes viewModel.uploadImage(uri) when
needed and keep only UI rendering in the composable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 37e7f2c4-288c-4391-999f-2973df9a69cf
📒 Files selected for processing (16)
app/src/main/AndroidManifest.xmlapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/core/domain/repository/BaseRepository.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/di/RecipeModule.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/data/repository/RecipeRepositoryImpl.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/domain/model/RecipeImageUpload.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/domain/repository/RecipeRepository.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/components/CreateEditRecipeForm.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/components/RecipeImageCompressor.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/create/RecipeCreateScreen.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/detail/RecipeDetailScreen.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/edit/RecipeEditScreen.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/util/RecipeCreateEditViewModel.ktapp/src/main/res/values-de/strings.xmlapp/src/main/res/values/strings.xmlapp/src/main/res/xml/file_paths.xmldocs/faqs.md
Moved upload state to ViewModel, added payload validation, removed CAMERA permission.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/components/CreateEditRecipeForm.kt`:
- Around line 150-158: The launchCamera function currently swallows errors from
createRecipeImageUri; instead propagate failures to the UI by calling the
ViewModel error handler or setting its imageUploadError state. Replace the empty
onFailure block in launchCamera so that onFailure { ex ->
viewModel.setImageUploadError(ex) } (or viewModel.imageUploadError = ex.message
/ a mapped error object) is invoked, preserving the existing cameraImageUri and
takePictureLauncher logic in onSuccess; use the ViewModel method/property that
your ViewModel exposes for image upload errors.
In
`@app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/util/RecipeCreateEditViewModel.kt`:
- Around line 145-149: When handling Resource.Success in the image upload
response inside RecipeCreateEditViewModel, explicitly handle the case where
result.data == null by setting imageUploadError to an appropriate error message
(and clearing any loading state if applicable) so the user receives feedback;
locate the success branch that currently only calls
changeImageOrigin(result.data) and add a branch that sets imageUploadError (and
updates any related UI state) when data is null instead of doing nothing.
- Around line 155-157: The catch in uploadImage currently swallows
CancellationException by catching Exception and setting _imageUploadError;
update the error handling in uploadImage so that if the caught exception is a
CancellationException it is re-thrown immediately (preserving coroutine
cancellation), otherwise set _imageUploadError to
UiText.StringResource(R.string.error_image_upload_failed); locate the try/catch
around the upload logic in RecipeCreateEditViewModel.uploadImage and perform the
conditional rethrow before assigning the error state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 864d7f20-499a-42f7-954f-f7cdb85f45ae
📒 Files selected for processing (8)
app/src/main/AndroidManifest.xmlapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/data/repository/RecipeRepositoryImpl.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/components/CreateEditRecipeForm.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/create/RecipeCreateScreen.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/edit/RecipeEditScreen.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/util/RecipeCreateEditViewModel.ktapp/src/main/res/values-de/strings.xmlapp/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (1)
- app/src/main/res/values-de/strings.xml
🚧 Files skipped from review as they are similar to previous changes (2)
- app/src/main/res/values/strings.xml
- app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/data/repository/RecipeRepositoryImpl.kt
Handled null response, CancellationException, and local failures properly.
Added the missing onImageUploadError mock parameter to the preview, resolving build error.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
.idea/planningMode.xml (1)
1-13: ⚡ Quick winExclude IDE planning state from version control.
This is local IntelliJ metadata and will cause noisy diffs/merge conflicts. Please remove it from the PR and add it to
.gitignore.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.idea/planningMode.xml around lines 1 - 13, The PR accidentally includes local IntelliJ metadata (.idea/planningMode.xml containing the PlanningModeManager and approvalStates entries); remove this file from the commit, stop tracking it, and add an appropriate ignore rule so it doesn't recur. Specifically remove the file from the branch (git rm --cached .idea/planningMode.xml or delete from commit), add ".idea/planningMode.xml" (or the broader ".idea/") to .gitignore, and commit the .gitignore change so the PlanningModeManager/approvalStates metadata is no longer versioned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/create/RecipeCreateScreen.kt`:
- Line 30: The composable uses LocalContext.current (an Activity context) and
passes it into the ViewModel's long-lived upload coroutine (uploadImage in
viewModelScope), which can retain a destroyed Activity; change the code to use
applicationContext instead: obtain applicationContext from
LocalContext.current.applicationContext (or resolve a ContentResolver/required
data in the composable and pass only that) and pass that applicationContext into
uploadImage so the ViewModel no longer holds an Activity context; update any
call sites in RecipeCreateScreen that call uploadImage to pass the
applicationContext (or pre-fetched content data) and ensure uploadImage's
parameter name/signature reflects it.
In
`@app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/util/RecipeCreateEditViewModel.kt`:
- Around line 132-169: The uploadImage function can run concurrently and each
finally block clears _isImageUploading causing races; protect uploadImage by
serializing executions: add a coroutine Mutex (e.g., imageUploadMutex: Mutex =
Mutex()) and wrap the upload logic inside imageUploadMutex.withLock { ... } so
only one invocation runs at a time, set _isImageUploading = true inside the lock
before work and set it to false in the lock's finally block; update references
to uploadImage and _isImageUploading accordingly and import
kotlinx.coroutines.sync.Mutex and withLock.
---
Nitpick comments:
In @.idea/planningMode.xml:
- Around line 1-13: The PR accidentally includes local IntelliJ metadata
(.idea/planningMode.xml containing the PlanningModeManager and approvalStates
entries); remove this file from the commit, stop tracking it, and add an
appropriate ignore rule so it doesn't recur. Specifically remove the file from
the branch (git rm --cached .idea/planningMode.xml or delete from commit), add
".idea/planningMode.xml" (or the broader ".idea/") to .gitignore, and commit the
.gitignore change so the PlanningModeManager/approvalStates metadata is no
longer versioned.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fd53dc08-798f-4d41-9baf-fe0342db6168
📒 Files selected for processing (6)
.idea/markdown.xml.idea/planningMode.xmlapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/components/CreateEditRecipeForm.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/create/RecipeCreateScreen.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/edit/RecipeEditScreen.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/util/RecipeCreateEditViewModel.kt
✅ Files skipped from review due to trivial changes (1)
- .idea/markdown.xml
🚧 Files skipped from review as they are similar to previous changes (2)
- app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/edit/RecipeEditScreen.kt
- app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/components/CreateEditRecipeForm.kt
There was a problem hiding this comment.
🧹 Nitpick comments (1)
app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/util/RecipeCreateEditViewModel.kt (1)
136-145: ⚡ Quick winUse
applicationContextfor image processing inside the ViewModel.
uploadImagecurrently uses the passed UI context directly in coroutine work. Normalize tocontext.applicationContextbefore compression to avoid retaining a destroyed Activity/Fragment context during long-running upload/compression.Suggested patch
fun uploadImage(uri: Uri, context: Context) { viewModelScope.launch { imageUploadMutex.withLock { + val appContext = context.applicationContext _isImageUploading.value = true _imageUploadError.value = null try { val image = compressRecipeImage( - context = context, + context = appContext, uri = uri, )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/util/RecipeCreateEditViewModel.kt` around lines 136 - 145, The uploadImage function is passing the UI Context into background work; capture and use the application context instead to avoid retaining an Activity/Fragment. In uploadImage (inside viewModelScope.launch and before calling compressRecipeImage), obtain val appContext = context.applicationContext and pass appContext to compressRecipeImage (and any other background helpers) so all image compression/upload operations use the application context rather than the original UI context.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/util/RecipeCreateEditViewModel.kt`:
- Around line 136-145: The uploadImage function is passing the UI Context into
background work; capture and use the application context instead to avoid
retaining an Activity/Fragment. In uploadImage (inside viewModelScope.launch and
before calling compressRecipeImage), obtain val appContext =
context.applicationContext and pass appContext to compressRecipeImage (and any
other background helpers) so all image compression/upload operations use the
application context rather than the original UI context.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1e0e4bdc-e12e-4de7-a480-bb210f7bd4e9
📒 Files selected for processing (4)
.gitignoreapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/create/RecipeCreateScreen.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/edit/RecipeEditScreen.ktapp/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/util/RecipeCreateEditViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (2)
- app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/edit/RecipeEditScreen.kt
- app/src/main/java/de/lukasneugebauer/nextcloudcookbook/recipe/presentation/create/RecipeCreateScreen.kt
|
Thank you for your work @RaberAmin2! Would be great if you can fix the ktlint issues seen in CI. |
There was a problem hiding this comment.
I'd prefer if we do not handle OkHttp directly and instead use retrofit like with the other requests in the app. Some stuff - like the auth header - is already handled that way and the repository class is cleaner.
|
|
||
| private companion object { | ||
| const val HTTP_METHOD_NOT_ALLOWED = 405 | ||
| const val RECIPE_IMAGE_UPLOAD_FOLDER = "Cookbook uploads" |
There was a problem hiding this comment.
I'm not sure I like this hard coded folder name. Best case would be to mimic the same behaviour as in the webapp and only have a temporary file.
https://github.com/nextcloud/cookbook/blob/master/lib/Helper/DownloadHelper.php
But since thats probably not done easily and reliably it would at the very least be good to move the app upload folder into the selected directory from the user settings. That way we are already in a generated folder and don't interfer with anything or create a mess in the home directory.
| private val _isImageUploading = MutableStateFlow(false) | ||
| val isImageUploading: StateFlow<Boolean> = _isImageUploading | ||
|
|
||
| private val _imageUploadError = MutableStateFlow<UiText?>(null) | ||
| val imageUploadError: StateFlow<UiText?> = _imageUploadError |
There was a problem hiding this comment.
These properties could easily become part of the uiState so only one state needs to be consumed.
| private const val IMAGE_UPLOAD_MAX_SIZE = 1600 | ||
|
|
||
| suspend fun compressRecipeImage( | ||
| context: Context, |
There was a problem hiding this comment.
This file should be part of domain/data layer. That would also allow to inject the applicationContext.
Summary
Add the ability to upload images or take photos directly with the camera when creating or editing recipes. This addresses a long-requested feature where users had to manually manage image URLs.
Changes
New Files
Modified Files
onImageUploadcallbackuploadRecipeImage()API method with WebDAV uploadHow It Works
Screenshots
N/A (Android app - please test on device/emulator)
Testing
Summary by CodeRabbit
New Features
Bug Fixes
Documentation