Skip to content

feat(recipe): upload recipe photos via Supabase Storage#159

Open
plusmobileapps wants to merge 7 commits into
mainfrom
129-upload-photo
Open

feat(recipe): upload recipe photos via Supabase Storage#159
plusmobileapps wants to merge 7 commits into
mainfrom
129-upload-photo

Conversation

@plusmobileapps
Copy link
Copy Markdown
Collaborator

@plusmobileapps plusmobileapps commented May 8, 2026

Summary

  • Adds an image picker on the edit recipe screen and uploads the selected photo to a recipe-photos Supabase Storage bucket, setting the returned public URL on the recipe (issue add functionality to upload photo for recipe #129).
  • New cross-platform rememberImagePickerLauncher (client/util/public): Android PickVisualMedia, iOS PHPickerViewController, JVM JFileChooser.
  • New RecipePhotoStorage (public interface in client/recipe/data/public) backed by Supabase Storage; uploads land under <userId>/<uuid>.<ext> so RLS policies can scope per-user.
  • EditRecipeViewModel gains upload progress + error state, surfaced through EditRecipeBloc.Model and a non-blocking dialog on the edit screen. Existing Image URL field is kept for manual entry.
  • Added FakeRecipePhotoStorage plus tests for the success and failure paths in EditRecipeViewModelTest.

Setup notes

  • Create a Supabase Storage bucket named recipe-photos (public read or signed URLs as you prefer). The client expects bucket.publicUrl(path) to be reachable by Coil, so for a private bucket, swap that call for createSignedUrl(...).
  • Add an RLS policy that allows authenticated users to insert/select objects under auth.uid()::text || '/'.

Test plan

  • ./gradlew ktfmtCheck
  • ./gradlew :client:recipe:core:impl:allTests (covers the new upload tests)
  • Android: tap Upload photo, verify picker opens, image renders inline, recipe saves with the public URL.
  • iOS: same flow via PHPicker.
  • Desktop: same flow via JFileChooser.
  • Force a failure (e.g., bad bucket name) and confirm the error dialog shows and clears.

🤖 Generated with Claude Code

plusmobileapps and others added 6 commits May 10, 2026 14:10
Adds an image picker on the edit recipe screen and uploads the
selected photo to a "recipe-photos" Supabase Storage bucket, then
sets the public URL as the recipe's image. Includes:
- Multiplatform expect/actual image picker (Android PickVisualMedia,
  iOS PHPickerViewController, JVM JFileChooser).
- RecipePhotoStorage interface with a Supabase-backed implementation
  scoped per signed-in user.
- ViewModel state + tests for upload progress and failures.

Closes #129

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Captures the bucket config and RLS policies that pair with the new
SupabaseRecipePhotoStorage so the setup can be re-applied across
environments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Holds picked image bytes in the ViewModel and only hits Supabase
Storage during save, so cancelled edits and replaced photos no
longer leak orphaned files in the bucket. The local bytes drive
the inline preview via Coil's ByteArray model, the dirty check
treats a pending photo as unsaved changes, and an upload failure
during save aborts the save and surfaces the existing error
dialog instead of writing a half-saved row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Falls back to a shared "anonymous/" folder when no user is signed in
so the upload flow works without an account, matching how recipes
themselves are already created locally without auth. Adds a matching
RLS policy for the anon role that scopes their writes to that folder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a full-screen Compose crop overlay that opens after the picker
returns. The user pans/zooms the image behind a fixed centered 1:1
frame and confirms; the resulting source rectangle is cropped and
downscaled to 1024px via Bitmap on Android and Skia on iOS/JVM
before the bytes land in the ViewModel for upload.

Also adds an after-delete trigger on the recipes table that pulls
the storage path out of image_url and removes the matching object,
so deleting a recipe no longer leaves an orphaned photo in the
recipe-photos bucket.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wraps the crop UI in androidx.compose.ui.window.Dialog so it lives in
its own window: back press dismisses just the crop step (not the whole
edit screen), touches don't bleed through to the form behind, and the
overlay is a real system-level modal rather than an inline overlay.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds RecipePhotoStorage.deletePhoto() and wires RecipeRepositoryImpl to
call it during deleteRecipe (covers the signed-out / never-synced case
where no remote row exists for the Postgres trigger to fire on) and
during updateRecipe whenever image_url changes (covers replacing a photo
on a recipe that hasn't synced yet).

Also adds an after-update trigger on recipes that mirrors the existing
delete trigger, so direct admin edits or other clients still clean up
the previous photo when image_url is swapped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant