diet-guard/app/lib/services/photo_attach_service.dart
Krzysztof kuhy Rudnicki feef5984f8 Add photo attach, full-size viewer, and a minimal history screen
Milestone 2 of the diet-app-as-wise-balloon plan, plus feedback from
manually testing it on-device:

- PhotoAttachService wraps image_picker and copies the picked photo into
  <app documents>/images/<uuid>.<ext>, so the file survives after the
  picker's own (possibly cache-cleared) temp copy is gone. Phone-local
  only, per the sync plan: imagePath is never synced.
- PhotoAttachField is a shared attach/preview/remove widget, used
  identically by both the single-item and composite-meal logging
  screens, so logging a multi-item meal can now carry a photo too.
- PhotoViewerScreen gives a full-screen, pinch-to-zoom view of an
  attached photo -- the 64x64 inline thumbnail was too small to
  actually check the photo.
- HistoryScreen lists every logged entry across all days, newest first,
  with a thumbnail when one is attached. There was previously no way to
  confirm what got logged (or whether a photo actually attached) short
  of inspecting food_log.json directly.

Verified on a physical device (BL9000): built, installed, and the user
confirmed the photo-attach flow logs a real entry with a real photo,
visible afterward in the new history list. 88 Flutter tests passing,
`flutter analyze` clean against very_good_analysis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU3f5KQ1GHXsbbSecfVEyF
2026-06-22 18:57:58 +02:00

53 lines
1.9 KiB
Dart

/// Picks a photo and copies it into permanent phone-local storage.
library;
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
/// Wraps [ImagePicker] and persists the result under the app's documents
/// directory, so the returned path survives after the picker's own
/// (possibly cache-cleared) temp file is gone.
///
/// Photos are phone-local only: per the sync plan (Milestone 3), a logged
/// entry's `imagePath` is stripped before push and never read from a pulled
/// remote copy, so no storage here needs to be sync-aware.
class PhotoAttachService {
PhotoAttachService._(this._testDir);
static PhotoAttachService _instance = PhotoAttachService._(null);
/// The singleton instance.
static PhotoAttachService get instance => _instance;
final Directory? _testDir;
/// Redirects where picked photos are copied to, so a test never touches
/// the real documents directory. Pass null to restore default behavior.
@visibleForTesting
static void resetForTesting({Directory? testDir}) {
_instance = PhotoAttachService._(testDir);
}
/// Opens [source] (camera or gallery), and on a successful pick, copies
/// the image into `<app documents>/images/<uuid>.<ext>`.
///
/// Returns the new permanent path, or null if the user cancelled the
/// picker.
Future<String?> pickAndStore(ImageSource source) async {
final picked = await ImagePicker().pickImage(source: source);
if (picked == null) return null;
final docsDir = _testDir ?? await getApplicationDocumentsDirectory();
final imagesDir = Directory(p.join(docsDir.path, 'images'));
await imagesDir.create(recursive: true);
final ext = p.extension(picked.path);
final dest = p.join(imagesDir.path, '${const Uuid().v4()}$ext');
await File(picked.path).copy(dest);
return dest;
}
}