import 'dart:io'; import 'dart:typed_data'; import 'package:alert_dialog/alert_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_pdfview/flutter_pdfview.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:moneymgr_mobile/services/storage/expenses.dart'; import 'package:moneymgr_mobile/services/storage/prefs.dart'; import 'package:moneymgr_mobile/utils/time_utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:scanbot_sdk/scanbot_sdk.dart'; import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart' hide IconButton, EdgeInsets; part 'scan_screen.g.dart'; @riverpod Future _scanDocument(Ref ref) async { var configuration = DocumentScanningFlow( appearance: DocumentFlowAppearanceConfiguration( statusBarMode: StatusBarMode.DARK, ), cleanScanningSession: true, outputSettings: DocumentScannerOutputSettings(pagesScanLimit: 1), screens: DocumentScannerScreens( review: ReviewScreenConfiguration(enabled: false), ), ); var documentResult = await ScanbotSdkUiV2.startDocumentScanner(configuration); if (documentResult.status != OperationStatus.OK) { throw Exception("Scanner failed with status ${documentResult.status}"); } // Convert result to PDF var result = await ScanbotSdk.document.createPDFForDocument( PDFFromDocumentParams( documentID: documentResult.data!.uuid, pdfConfiguration: PdfConfiguration(), ), ); final pdfPath = result.pdfFileUri.replaceFirst("file://", ""); return File(pdfPath).readAsBytes(); } class ScanScreen extends HookConsumerWidget { const ScanScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final boredSuggestion = ref.watch(_scanDocumentProvider); restartScan() async { try { final val = ref.refresh(_scanDocumentProvider); Logger.root.info("Load again startup result: $val"); } catch (e, s) { Logger.root.shout("Failed to try again startup loading! $e $s"); } } // Perform a switch-case on the result to handle loading/error states return Padding( padding: const EdgeInsets.all(8.0), child: switch (boredSuggestion) { AsyncData(:final value) when value != null => ExpenseEditor( file: value, onFinished: (e) {}, ), // No data AsyncData(:final value) when value == null => ScanErrorScreen( message: "No document scanned!", onTryAgain: restartScan, ), // Error AsyncError(:final error) => ScanErrorScreen( message: error.toString(), onTryAgain: restartScan, ), _ => const Center(child: CircularProgressIndicator()), }, ); } } class ScanErrorScreen extends StatelessWidget { final String message; final Function() onTryAgain; const ScanErrorScreen({ super.key, required this.message, required this.onTryAgain, }); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Spacer(flex: 5), Text("An error occurred while scanning"), Spacer(flex: 1), Text(message), Spacer(flex: 1), MaterialButton( onPressed: onTryAgain, child: Text("Try again".toUpperCase()), ), Spacer(flex: 5), ], ), ); } } class ExpenseEditor extends HookConsumerWidget { final Uint8List file; final Function(Expense) onFinished; const ExpenseEditor({ super.key, required this.file, required this.onFinished, }); @override Widget build(BuildContext context, WidgetRef ref) { final serverConfig = ref.watch(prefsProvider).requireValue.serverConfig()!; final labelController = useTextEditingController(); final costController = useTextEditingController(); final timeController = useState(DateTime.now()); // Pick a new date handlePickDate() async { final date = await showDatePicker( context: context, firstDate: DateTime(2000), lastDate: DateTime(2099), initialDate: timeController.value, ); if (date != null) timeController.value = date; } return ListView( children: [ // Expense preview SizedBox( height: 200, child: PDFView( pdfData: file, onError: (e) { Logger.root.warning("Failed to render PDF $e"); alert(context, content: Text("Failed to render PDF $e")); }, fitPolicy: FitPolicy.BOTH, ), ), SizedBox(height: 10), // Cost TextField( controller: costController, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Cost'), textInputAction: TextInputAction.done, ), SizedBox(height: 10), // Label TextField( controller: labelController, keyboardType: TextInputType.text, decoration: const InputDecoration(labelText: 'Label'), textInputAction: TextInputAction.done, maxLength: serverConfig.constraints.inbox_entry_label.max, ), SizedBox(height: 10), // Date TextField( enabled: true, readOnly: true, controller: TextEditingController( text: timeController.value.simpleDate, ), keyboardType: TextInputType.datetime, decoration: InputDecoration( labelText: 'Date', suffixIcon: IconButton( onPressed: handlePickDate, icon: const Icon(Icons.date_range), ), ), ), ], ); } }