Refactor dish image pipeline around PocketBase originals #15

Closed
opened 2026-05-04 20:11:56 +02:00 by ben · 0 comments
Owner

Summary

Refactor dish images so PocketBase owns the original image, while the device persistently stores only UI-sized derivatives plus optional on-demand cached originals. Grid/list/detail UI should use a 640px local display image; glass blur should use a smaller blurred local image; full-screen viewing should download/cache the PocketBase original only when requested.

Chosen defaults:

  • Full originals are cached locally after first full-screen view.
  • Backups include thumbnails only, not originals.
  • Existing local originals are deleted once a dish has remoteImageFilename; local-only unsynced originals are kept until upload.

Key Changes

  • Extend Dish with explicit image paths:
    • displayImagePath: local 640px image used by grid/list/detail/Tinder/shopping avatars.
    • blurredImagePath: local ~320px blurred image used for glass effects.
    • cachedOriginalImagePath: optional full original downloaded from PocketBase on demand.
    • pendingUploadImagePath: temporary local original for newly picked/edited images until sync upload succeeds.
    • Keep existing imagePath as legacy migration input only; stop writing new UI logic against it.
  • Regenerate dish.g.dart with flutter pub run build_runner build.
  • Replace ImageService helpers with explicit APIs:
    • createLocalImageSet(sourcePath) creates/copies pendingUploadImagePath, displayImagePath, and blurredImagePath in app documents.
    • createDerivativesFromOriginal(sourcePath) creates display + blur files.
    • deleteDishImageFiles(dish) deletes display, blur, cached original, pending upload, and legacy derivative files.
    • getOrDownloadOriginal(dish, pocketBase) returns cached original if present, otherwise downloads from PocketBase and stores cachedOriginalImagePath.

Implementation

  • Local image creation:
    • On image pick, keep using image_picker with maxWidth: 1920, maxHeight: 1920.
    • On save, copy picked image into app documents as pendingUploadImagePath, generate 640px display image and ~320px blurred image once, then store those paths on the dish.
    • New UI should immediately use displayImagePath and blurredImagePath; it should not render from picker temp paths or original files.
  • Sync:
    • Push uploads pendingUploadImagePath when present; fallback to legacy imagePath only during migration.
    • After successful PocketBase create/update, set remoteImageFilename, clear/delete pendingUploadImagePath, and delete legacy local original if safe.
    • Pull should not download the original by default. It should download the PocketBase file once only to generate display + blur derivatives, then delete the downloaded original unless full-original caching is explicitly happening through full-screen view.
    • If remoteImageFilename changes, regenerate derivatives and delete stale derivative/cached-original files.
  • UI:
    • DishCard, detail header, Tinder card, and shopping avatar use displayImagePath.
    • Glass footer uses blurredImagePath; if missing, trigger derivative repair and fall back to display image or placeholder.
    • Full-screen image viewer shows displayImagePath immediately, then downloads/caches the original from PocketBase and crossfades to it.
    • If no network/original is unavailable, keep showing the 640px display image with a non-blocking error state.
  • Backup/restore:
    • Export includes displayImagePath and blurredImagePath files only.
    • Export excludes cachedOriginalImagePath and pendingUploadImagePath.
    • Restore writes thumbnail files into app documents and rewrites thumbnail paths.
    • Restored dishes can show thumbnails immediately; full-screen originals require PocketBase metadata and network.
  • Migration:
    • For each existing dish with legacy imagePath, generate missing display + blur paths.
    • If remoteImageFilename exists, delete the legacy original and old _thumb/_blur files after successful derivative generation.
    • If no remoteImageFilename exists or dish is dirty, keep/copy the legacy original as pendingUploadImagePath so sync can still upload it.

Test Plan

  • Unit/service tests for image generation:
    • Creates 640px display and ~320px blurred images in app documents.
    • Does not regenerate files when valid derivatives already exist.
    • Deletes all expected image files for a dish.
  • Sync scenarios:
    • New local dish uploads pending original, stores PocketBase filename, deletes pending original, keeps display/blur.
    • Pulled remote dish creates display/blur without retaining original.
    • Changed remoteImageFilename replaces stale local derivatives.
  • UI scenarios:
    • Grid/list/detail/Tinder render from display image, not original.
    • Glass footer renders from blurred image and keeps alignment.
    • Full-screen viewer shows display image first, downloads original, caches it, and reuses cache on next open.
  • Backup/restore:
    • Backup zip contains display + blur images only.
    • Restore shows thumbnails without originals and can fetch full image from PocketBase.
  • Validation:
    • Run dart format.
    • Run flutter pub run build_runner build.
    • Run flutter analyze; existing unrelated warnings may remain.
    • Run available Flutter tests or add focused service tests if no current test suite exists.

Assumptions

  • PocketBase remains the source of truth for full originals.
  • Thumbnail-only backups intentionally do not preserve full originals, including unsynced local-only originals.
  • Existing asset-based default images continue to use asset paths and are handled as a compatibility branch.
  • Local original deletion is allowed only after derivative generation succeeds and either remoteImageFilename exists or the original is only a cached full-screen copy.
## Summary Refactor dish images so PocketBase owns the original image, while the device persistently stores only UI-sized derivatives plus optional on-demand cached originals. Grid/list/detail UI should use a 640px local display image; glass blur should use a smaller blurred local image; full-screen viewing should download/cache the PocketBase original only when requested. Chosen defaults: - Full originals are cached locally after first full-screen view. - Backups include thumbnails only, not originals. - Existing local originals are deleted once a dish has `remoteImageFilename`; local-only unsynced originals are kept until upload. ## Key Changes - Extend `Dish` with explicit image paths: - `displayImagePath`: local 640px image used by grid/list/detail/Tinder/shopping avatars. - `blurredImagePath`: local ~320px blurred image used for glass effects. - `cachedOriginalImagePath`: optional full original downloaded from PocketBase on demand. - `pendingUploadImagePath`: temporary local original for newly picked/edited images until sync upload succeeds. - Keep existing `imagePath` as legacy migration input only; stop writing new UI logic against it. - Regenerate `dish.g.dart` with `flutter pub run build_runner build`. - Replace `ImageService` helpers with explicit APIs: - `createLocalImageSet(sourcePath)` creates/copies `pendingUploadImagePath`, `displayImagePath`, and `blurredImagePath` in app documents. - `createDerivativesFromOriginal(sourcePath)` creates display + blur files. - `deleteDishImageFiles(dish)` deletes display, blur, cached original, pending upload, and legacy derivative files. - `getOrDownloadOriginal(dish, pocketBase)` returns cached original if present, otherwise downloads from PocketBase and stores `cachedOriginalImagePath`. ## Implementation - Local image creation: - On image pick, keep using `image_picker` with `maxWidth: 1920, maxHeight: 1920`. - On save, copy picked image into app documents as `pendingUploadImagePath`, generate 640px display image and ~320px blurred image once, then store those paths on the dish. - New UI should immediately use `displayImagePath` and `blurredImagePath`; it should not render from picker temp paths or original files. - Sync: - Push uploads `pendingUploadImagePath` when present; fallback to legacy `imagePath` only during migration. - After successful PocketBase create/update, set `remoteImageFilename`, clear/delete `pendingUploadImagePath`, and delete legacy local original if safe. - Pull should not download the original by default. It should download the PocketBase file once only to generate display + blur derivatives, then delete the downloaded original unless full-original caching is explicitly happening through full-screen view. - If `remoteImageFilename` changes, regenerate derivatives and delete stale derivative/cached-original files. - UI: - `DishCard`, detail header, Tinder card, and shopping avatar use `displayImagePath`. - Glass footer uses `blurredImagePath`; if missing, trigger derivative repair and fall back to display image or placeholder. - Full-screen image viewer shows `displayImagePath` immediately, then downloads/caches the original from PocketBase and crossfades to it. - If no network/original is unavailable, keep showing the 640px display image with a non-blocking error state. - Backup/restore: - Export includes `displayImagePath` and `blurredImagePath` files only. - Export excludes `cachedOriginalImagePath` and `pendingUploadImagePath`. - Restore writes thumbnail files into app documents and rewrites thumbnail paths. - Restored dishes can show thumbnails immediately; full-screen originals require PocketBase metadata and network. - Migration: - For each existing dish with legacy `imagePath`, generate missing display + blur paths. - If `remoteImageFilename` exists, delete the legacy original and old `_thumb/_blur` files after successful derivative generation. - If no `remoteImageFilename` exists or dish is dirty, keep/copy the legacy original as `pendingUploadImagePath` so sync can still upload it. ## Test Plan - Unit/service tests for image generation: - Creates 640px display and ~320px blurred images in app documents. - Does not regenerate files when valid derivatives already exist. - Deletes all expected image files for a dish. - Sync scenarios: - New local dish uploads pending original, stores PocketBase filename, deletes pending original, keeps display/blur. - Pulled remote dish creates display/blur without retaining original. - Changed `remoteImageFilename` replaces stale local derivatives. - UI scenarios: - Grid/list/detail/Tinder render from display image, not original. - Glass footer renders from blurred image and keeps alignment. - Full-screen viewer shows display image first, downloads original, caches it, and reuses cache on next open. - Backup/restore: - Backup zip contains display + blur images only. - Restore shows thumbnails without originals and can fetch full image from PocketBase. - Validation: - Run `dart format`. - Run `flutter pub run build_runner build`. - Run `flutter analyze`; existing unrelated warnings may remain. - Run available Flutter tests or add focused service tests if no current test suite exists. ## Assumptions - PocketBase remains the source of truth for full originals. - Thumbnail-only backups intentionally do not preserve full originals, including unsynced local-only originals. - Existing asset-based default images continue to use asset paths and are handled as a compatibility branch. - Local original deletion is allowed only after derivative generation succeeds and either `remoteImageFilename` exists or the original is only a cached full-screen copy.
ben closed this issue 2026-05-04 22:02:00 +02:00
ben referenced this issue from a commit 2026-05-09 15:26:36 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ben/LovePlate#15
No description provided.