Temporary setup of alternative to advertisements

This commit is contained in:
Courtney Arnold 2026-09-19 20:45:59 -05:00
parent ad2d932842
commit 57a9793a40
14 changed files with 653 additions and 56 deletions

View file

@ -28,6 +28,34 @@ Future<void> captureReceiptForVehicle(BuildContext context, String vehicleId) as
final keepMaxQuality = context.read<AppState>().keepMaxQualityReceiptPhotos;
final picker = ImagePicker();
// Raised *before* the camera/gallery opens rather than after it returns.
// [ImagePicker.pickImage] doesn't complete when the camera closes — it
// also downscales the captured photo (several MB straight off the
// sensor), which takes seconds on a busy device. With the progress
// shown only afterward, that whole stretch left the user staring at an
// unchanged Receipts list with no sign anything was happening, which
// reads as "the capture did nothing" — and tapping away during it
// genuinely does abandon the capture.
var progressShowing = false;
void showProgress() {
if (progressShowing || !context.mounted) return;
progressShowing = true;
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const _ReceiptProgressDialog(),
);
}
void hideProgress() {
if (!progressShowing || !context.mounted) return;
progressShowing = false;
Navigator.of(context).pop();
}
showProgress();
XFile? photo;
try {
photo = await picker.pickImage(
@ -37,6 +65,7 @@ Future<void> captureReceiptForVehicle(BuildContext context, String vehicleId) as
maxHeight: keepMaxQuality ? null : receiptImageMaxDimension,
);
} catch (e) {
hideProgress();
if (context.mounted) {
final sourceLabel = source == ImageSource.camera ? 'camera' : 'photo library';
ScaffoldMessenger.of(context).showSnackBar(
@ -45,16 +74,16 @@ Future<void> captureReceiptForVehicle(BuildContext context, String vehicleId) as
}
return;
}
if (photo == null) return;
// Cancelled (backed out of the camera, or didn't confirm the shot) —
// nothing to say about that, it was deliberate.
if (photo == null) {
hideProgress();
return;
}
final imageFile = File(photo.path);
if (!context.mounted) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
final ocrService = OcrService();
String recognizedText = '';
@ -69,7 +98,7 @@ Future<void> captureReceiptForVehicle(BuildContext context, String vehicleId) as
final parsed = ReceiptParser.parse(recognizedText);
if (!context.mounted) return;
Navigator.of(context).pop(); // close the OCR loading spinner
hideProgress();
final state = parsed.state;
if (state != null && state != 'MO') {
@ -90,6 +119,45 @@ Future<void> captureReceiptForVehicle(BuildContext context, String vehicleId) as
}
}
/// Shown from the moment the camera/gallery is opened until the parsed
/// receipt is ready to review. Deliberately says what's happening rather
/// than showing a bare spinner: the wait here is mostly downscaling and
/// reading the photo, which is long enough on a slow device that an
/// unexplained spinner invites tapping away mid-capture.
class _ReceiptProgressDialog extends StatelessWidget {
const _ReceiptProgressDialog();
@override
Widget build(BuildContext context) {
return PopScope(
// Backing out here wouldn't cancel the underlying capture, it would
// just hide it and strand the user.
canPop: false,
child: Dialog(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 20),
Text('Reading your receipt…', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 6),
Text(
'This can take a few seconds.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
),
);
}
}
/// Missouri's fuel tax refund only applies to fuel bought in Missouri —
/// warn if the receipt's address is somewhere else, and let the user
/// decide whether to log it anyway (e.g. it might still be worth tracking