tane/apps/app_seeds/lib/ui/photo_pick.dart
vjrj 071be44851
Some checks are pending
ci / analyze (push) Waiting to run
ci / test-commons-core (push) Waiting to run
ci / test-app-seeds (push) Waiting to run
site / deploy (push) Successful in 1m8s
perf(android): R8, bitmap downscaling, edge-to-edge; recover device compat; publish to production
Device compatibility (regression fix):
- The CAMERA permission (from zxing_barcode_scanner / image_picker) implicitly
  required android.hardware.camera, excluding camera-less devices — Play showed
  Automotive -96%, Chromebook -86%, TV -25%. Declare camera & location
  uses-feature required="false" to keep those devices supported.
- Detect a real camera at runtime (PackageManager.FEATURE_CAMERA_ANY via the
  existing MethodChannel), cache it at bootstrap, and hide the QR scan button and
  the camera photo-source option when absent, so no broken actions are offered.

Play "app optimization" recommendations:
- Enable R8 (isMinifyEnabled + isShrinkResources) with keep rules for the
  OCR (tesseract4android), SQLCipher and notifications JNI/reflection code.
- Bitmap downscaling: cacheWidth/cacheHeight (and ResizeImage for avatars) on
  list thumbnails and avatars so photos decode to on-screen size, not full res.
- Edge-to-edge: opt in with transparent system bars in main().

Release flow:
- Tagged builds now publish to the production track at 100% (was internal);
  add a manual deploy_internal lane as a QA safety net.
- Document country/region availability as a Play Console setting (not in-repo).
2026-07-20 22:53:48 +02:00

108 lines
3.7 KiB
Dart

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../i18n/strings.g.dart';
import '../services/camera_availability.dart';
/// Asks the user to take a photo or pick one from the gallery, then returns its
/// bytes (or null if cancelled/unavailable). Camera failures — e.g. on desktop,
/// which has no camera — are swallowed and yield null.
Future<Uint8List?> pickPhoto(BuildContext context) async {
final t = context.t;
final source = await showModalBottomSheet<ImageSource>(
context: context,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Only offer the camera when the device actually has one; camera-less
// devices (Chromebooks, Automotive, some TVs) fall back to gallery.
if (deviceHasCamera)
ListTile(
key: const Key('photo.source.camera'),
leading: const Icon(Icons.photo_camera_outlined),
title: Text(t.photo.camera),
onTap: () => Navigator.of(sheetContext).pop(ImageSource.camera),
),
ListTile(
key: const Key('photo.source.gallery'),
leading: const Icon(Icons.photo_library_outlined),
title: Text(t.photo.gallery),
onTap: () => Navigator.of(sheetContext).pop(ImageSource.gallery),
),
],
),
),
);
if (source == null) return null;
try {
final file = await ImagePicker().pickImage(
source: source,
maxWidth: 1280,
imageQuality: 80,
);
return file?.readAsBytes();
} on Object {
return null;
}
}
/// Rapid multi-capture for the "capture now, catalogue later" flow: pick many
/// photos from the gallery at once, or shoot a burst with the camera (it
/// reopens after each shot until the user cancels). Returns every captured
/// image's bytes, or an empty list if cancelled/unavailable.
Future<List<Uint8List>> pickPhotos(BuildContext context) async {
final t = context.t;
final source = await showModalBottomSheet<ImageSource>(
context: context,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Only offer the camera when the device actually has one; camera-less
// devices (Chromebooks, Automotive, some TVs) fall back to gallery.
if (deviceHasCamera)
ListTile(
key: const Key('photos.source.camera'),
leading: const Icon(Icons.photo_camera_outlined),
title: Text(t.photo.camera),
onTap: () => Navigator.of(sheetContext).pop(ImageSource.camera),
),
ListTile(
key: const Key('photos.source.gallery'),
leading: const Icon(Icons.photo_library_outlined),
title: Text(t.photo.gallery),
onTap: () => Navigator.of(sheetContext).pop(ImageSource.gallery),
),
],
),
),
);
if (source == null) return const [];
final picker = ImagePicker();
try {
if (source == ImageSource.gallery) {
final files = await picker.pickMultiImage(
maxWidth: 1280,
imageQuality: 80,
);
return [for (final file in files) await file.readAsBytes()];
}
// Camera burst: keep reopening the camera until the user cancels.
final shots = <Uint8List>[];
while (true) {
final file = await picker.pickImage(
source: ImageSource.camera,
maxWidth: 1280,
imageQuality: 80,
);
if (file == null) break;
shots.add(await file.readAsBytes());
}
return shots;
} on Object {
return const [];
}
}