mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-07-04 15:43:25 +02:00
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
100 lines
3.1 KiB
Dart
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'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|