diet-guard/app/lib/widgets/photo_attach_field.dart
Krzysztof kuhy Rudnicki c43e37b09d Compact LogMealScreen so it fits without scrolling on-screen keyboard
Merge the slot-status bar, slot-selector chips, and "Logging for
HH:00" caption into one selectable, status-colored SlotSelectorRow.
Add an opt-in compact mode to MacroInputRow (single row, abbreviated
labels), AutocompleteSuggestionList (top-3 + "N more" bottom sheet),
and PhotoAttachField (icon-only + badge thumbnail), used only by
LogMealScreen so MealBuilderScreen/EditEntryScreen keep their default
rendering. Verified on-device (BL-9000) that all fields stay visible
with the keyboard open.

Also fixes an unrelated time-bomb in history_screen_test.dart's date
range picker test, which hardcoded an expected "2026-06-01" label
assuming "today" was in June; the picker's displayed month and
selectable range depend on the real current date, so the assertion
now computes its expectation from DateTime.now() instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UorgLvWJ4huH55tmXoUAZ
2026-07-04 05:19:23 +02:00

137 lines
4.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,
this.compact = false,
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;
/// Whether to render an icon-only button and a small thumbnail badge
/// instead of the default text button and 64x64 preview.
final bool compact;
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) {
if (compact) {
return Tooltip(
message: 'Attach photo',
child: IconButton(
onPressed: () => _attach(context),
icon: const Icon(Icons.add_a_photo),
),
);
}
return OutlinedButton.icon(
onPressed: () => _attach(context),
icon: const Icon(Icons.add_a_photo),
label: const Text('Attach photo'),
);
}
final thumbnailSize = compact ? 32.0 : 64.0;
final thumbnail = GestureDetector(
onTap: () => Navigator.of(context).push<void>(
MaterialPageRoute(builder: (_) => PhotoViewerScreen(path: path)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(path),
width: thumbnailSize,
height: thumbnailSize,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => SizedBox(
width: thumbnailSize,
height: thumbnailSize,
child: const Icon(Icons.broken_image),
),
),
),
);
if (compact) {
return Stack(
clipBehavior: Clip.none,
children: [
thumbnail,
Positioned(
top: -6,
right: -6,
child: Tooltip(
message: 'Remove photo',
child: InkWell(
onTap: () => onChanged(null),
customBorder: const CircleBorder(),
child: const CircleAvatar(
radius: 9,
child: Icon(Icons.close, size: 12),
),
),
),
),
],
);
}
return Row(
children: [
thumbnail,
const SizedBox(width: 8),
TextButton(
onPressed: () => onChanged(null),
child: const Text('Remove photo'),
),
],
);
}
}