feat(photos): camera-or-gallery picker + swipeable full-screen viewer

- Adding a photo (quick-add and detail) now offers a Camera / Gallery chooser
  (shared pickPhoto helper); camera errors on desktop degrade gracefully.
- Full-screen viewer: pinch-to-zoom pans only while zoomed, so a plain
  horizontal swipe pages between photos again.

54 tests green; Linux runs.
This commit is contained in:
vjrj 2026-07-08 14:15:13 +02:00
parent 4b209d6752
commit c789cd1027
8 changed files with 136 additions and 40 deletions

View file

@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:url_launcher/url_launcher.dart';
@ -10,6 +11,7 @@ import '../db/enums.dart';
import '../i18n/strings.g.dart';
import '../state/variety_detail_cubit.dart';
import 'harvest_date_picker.dart';
import 'photo_pick.dart';
import 'quantity_kind_l10n.dart';
import 'quantity_picker.dart';
import 'seed_glyph.dart';
@ -854,12 +856,7 @@ class _PhotoGalleryState extends State<_PhotoGallery> {
}
Future<void> _addPhoto(VarietyDetailCubit cubit) async {
final file = await ImagePicker().pickImage(
source: ImageSource.gallery,
maxWidth: 1280,
imageQuality: 80,
);
final bytes = await file?.readAsBytes();
final bytes = await pickPhoto(context);
if (bytes != null) await cubit.addPhoto(bytes);
}
@ -1042,16 +1039,49 @@ class _PhotoViewer extends StatelessWidget {
body: PageView.builder(
controller: PageController(initialPage: initialIndex),
itemCount: photos.length,
itemBuilder: (_, i) => InteractiveViewer(
minScale: 1,
maxScale: 5,
child: Center(child: Image.memory(photos[i].bytes)),
),
itemBuilder: (_, i) => _ZoomablePhoto(bytes: photos[i].bytes),
),
);
}
}
/// Pinch-to-zoom photo that only pans while zoomed, so a plain horizontal
/// swipe still pages the surrounding [PageView].
class _ZoomablePhoto extends StatefulWidget {
const _ZoomablePhoto({required this.bytes});
final Uint8List bytes;
@override
State<_ZoomablePhoto> createState() => _ZoomablePhotoState();
}
class _ZoomablePhotoState extends State<_ZoomablePhoto> {
final _controller = TransformationController();
bool _zoomed = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return InteractiveViewer(
transformationController: _controller,
minScale: 1,
maxScale: 5,
panEnabled: _zoomed,
onInteractionEnd: (_) {
final zoomed = _controller.value.getMaxScaleOnAxis() > 1.01;
if (zoomed != _zoomed) setState(() => _zoomed = zoomed);
},
child: Center(child: Image.memory(widget.bytes)),
);
}
}
class _SectionTitle extends StatelessWidget {
const _SectionTitle(this.text);