diet-guard/app/lib/widgets/photo_attach_field.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

100 lines
3.1 KiB
Dart

/// Shared attach/preview/remove control for a meal entry's optional photo.
library;
import 'dart:io';
import 'package:diet_guard_app/screens/photo_viewer_screen.dart';
import 'package:diet_guard_app/services/photo_attach_service.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
/// Shows an "Attach photo" button when [imagePath] is null, or a tappable
/// thumbnail (opens [PhotoViewerScreen] full-screen) plus a "Remove photo"
/// action once one is set.
///
/// Used identically by the single-item and composite-meal logging screens,
/// so the attach/preview/remove behavior only needs to be implemented once.
class PhotoAttachField extends StatelessWidget {
/// Creates a [PhotoAttachField].
const PhotoAttachField({
required this.imagePath,
required this.onChanged,
super.key,
});
/// The currently attached photo's local path, or null if none.
final String? imagePath;
/// Called with the new path after a successful pick, or null after the
/// user removes the current photo.
final ValueChanged<String?> onChanged;
Future<void> _attach(BuildContext context) async {
final source = await showModalBottomSheet<ImageSource>(
context: context,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.photo_camera),
title: const Text('Take a photo'),
onTap: () =>
Navigator.of(sheetContext).pop(ImageSource.camera),
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Choose from gallery'),
onTap: () =>
Navigator.of(sheetContext).pop(ImageSource.gallery),
),
],
),
),
);
if (source == null) return;
final path = await PhotoAttachService.instance.pickAndStore(source);
if (path != null) onChanged(path);
}
@override
Widget build(BuildContext context) {
final path = imagePath;
if (path == null) {
return OutlinedButton.icon(
onPressed: () => _attach(context),
icon: const Icon(Icons.add_a_photo),
label: const Text('Attach photo'),
);
}
return Row(
children: [
GestureDetector(
onTap: () => Navigator.of(context).push<void>(
MaterialPageRoute(builder: (_) => PhotoViewerScreen(path: path)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(path),
width: 64,
height: 64,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => const SizedBox(
width: 64,
height: 64,
child: Icon(Icons.broken_image),
),
),
),
),
const SizedBox(width: 8),
TextButton(
onPressed: () => onChanged(null),
child: const Text('Remove photo'),
),
],
);
}
}