import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart'; import 'package:grouped_list/grouped_list.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:prasule/api/entry_data.dart'; import 'package:prasule/api/walletentry.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/walletmanager.dart'; import 'package:prasule/main.dart'; import 'package:prasule/network/tessdata.dart'; import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/views/create_entry.dart'; import 'package:prasule/views/multientry_creator.dart'; import 'package:prasule/views/settings/settings.dart'; import 'package:prasule/views/settings/tessdata_list.dart'; import 'package:prasule/views/setup.dart'; class HomeView extends StatefulWidget { const HomeView({super.key}); @override State createState() => _HomeViewState(); } class _HomeViewState extends State { Wallet? selectedWallet; DateTime? prevDate; late String locale; @override void didChangeDependencies() { super.didChangeDependencies(); locale = Localizations.localeOf(context).languageCode; initializeDateFormatting(Localizations.localeOf(context).languageCode); } @override void initState() { super.initState(); loadWallet(); } void loadWallet() async { var wallets = await WalletManager.listWallets(); if (wallets.isEmpty && mounted) { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (c) => const SetupView())); return; } selectedWallet = await WalletManager.loadWallet(wallets.first); setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: SpeedDial( icon: Icons.add, activeIcon: Icons.close, children: [ SpeedDialChild( child: const Icon(Icons.edit), label: "Add new", onTap: () async { var sw = await Navigator.of(context).push( MaterialPageRoute( builder: (c) => CreateEntryView(w: selectedWallet!), ), ); if (sw != null) { selectedWallet = sw; } setState(() {}); }), SpeedDialChild( child: const Icon(Icons.camera_alt), label: "Add through camera", onTap: () async { final ImagePicker picker = ImagePicker(); final XFile? media = await picker.pickImage(source: ImageSource.camera); logger.i(media?.name); }, ), SpeedDialChild( child: const Icon(Icons.image), label: "Add through saved image", onTap: () { startOcr(ImageSource.gallery); }, ), ], ), appBar: AppBar( title: const Text("Home"), actions: [ PopupMenuButton( itemBuilder: (context) => ["Settings", "About"] .map((e) => PopupMenuItem(value: e, child: Text(e))) .toList(), onSelected: (value) { if (value == "Settings") { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const SettingsView(), ), ); } else if (value == "About") { showAboutDialog( context: context, applicationLegalese: "©️ 2023 Matyáš Caras\nReleased under the GNU AGPL license version 3", applicationName: "Prašule"); } }, ) ], ), body: Center( child: SizedBox( width: MediaQuery.of(context).size.width * 0.9, height: MediaQuery.of(context).size.height, child: (selectedWallet == null) ? const Column( children: [ SizedBox( width: 40, height: 40, child: CircularProgressIndicator(), ) ], ) : (selectedWallet!.entries.isEmpty) ? const Column( children: [ Text( "No entries :(", style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), Text( "Add one using the floating action button.", ) ], ) : GroupedListView( groupHeaderBuilder: (element) => Text( DateFormat.yMMMM(locale).format(element.date), style: TextStyle( color: Theme.of(context).colorScheme.primary), ), elements: selectedWallet!.entries ..sort((a, b) => a.date.compareTo(b.date)), groupBy: (e) => DateFormat.yMMMM(locale).format(e.date), itemBuilder: (context, element) => Slidable( endActionPane: ActionPane(motion: const ScrollMotion(), children: [ SlidableAction( onPressed: (c) { Navigator.of(context) .push( MaterialPageRoute( builder: (c) => CreateEntryView( w: selectedWallet!, editEntry: element, ), ), ) .then( (editedEntry) { if (editedEntry == null) return; selectedWallet!.entries.remove(element); selectedWallet!.entries.add(editedEntry); WalletManager.saveWallet(selectedWallet!); setState(() {}); }, ); }, backgroundColor: Theme.of(context).colorScheme.secondary, foregroundColor: Theme.of(context).colorScheme.onSecondary, icon: Icons.edit, ), SlidableAction( backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, icon: Icons.delete, onPressed: (c) { showDialog( context: context, builder: (cx) => PlatformDialog( title: "Are you sure", content: const Text( "Do you really want to delete this entry?"), actions: [ PlatformButton( text: "Yes", onPressed: () { selectedWallet?.entries.removeWhere( (e) => e.id == element.id); WalletManager.saveWallet( selectedWallet!); Navigator.of(cx).pop(); setState(() {}); }, ), PlatformButton( text: "No", onPressed: () { Navigator.of(cx).pop(); }, ), ], ), ); }, ), ]), child: ListTile( leading: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: Theme.of(context).colorScheme.secondary), child: Padding( padding: const EdgeInsets.all(8.0), child: Icon( element.category.icon, color: Theme.of(context).colorScheme.onSecondary, ), ), ), title: Text(element.data.name), subtitle: Text( "${element.data.amount} ${selectedWallet!.currency.symbol}"), ), ), ), ), ), ); } Future startOcr(ImageSource imgSrc) async { var availableLanguages = await TessdataApi.getDownloadedData(); if (availableLanguages.isEmpty) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text("You do not have any OCR language data downloaded"), action: SnackBarAction( label: "Download", onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (c) => const TessdataListView(), ), ); }, ), ), ); return; } if (!mounted) return; var selectedLanguages = List.filled(availableLanguages.length, false); selectedLanguages[0] = true; showDialog( context: context, builder: (c) => StatefulBuilder( builder: (ctx, setState) => PlatformDialog( actions: [ TextButton( onPressed: () async { final ImagePicker picker = ImagePicker(); final XFile? media = await picker.pickImage(source: imgSrc); if (media == null) { if (mounted) Navigator.of(context).pop(); return; } // get selected languages var selected = availableLanguages .where((element) => selectedLanguages[ availableLanguages.indexOf(element)]) .join("+") .replaceAll(".traineddata", ""); logger.i(selected); if (!mounted) return; showDialog( context: context, builder: (c) => const PlatformDialog( title: "Loading text from image, please wait a moment..."), barrierDismissible: false); var string = await FlutterTesseractOcr.extractText(media.path, language: selected, args: { "psm": "4", "preserve_interword_spaces": "1", }); if (!mounted) return; Navigator.of(context).pop(); logger.i(string); if (!mounted) return; var lines = string.split("\n") ..removeWhere((element) { element.trim(); return element.isEmpty; }); var data = []; for (var line in lines) { var regex = RegExp(r''); } Navigator.of(context).pop(); Navigator.of(context).push( MaterialPageRoute( builder: (c) => MultientryCreateView( linesToAdd: data, ), ), ); }, child: const Text("Ok")), TextButton( onPressed: () { Navigator.of(c).pop(); }, child: const Text("Cancel")), ], title: "Select languages for OCR", content: Column( children: [ ...List.generate( availableLanguages.length, (index) => Row( children: [ Checkbox( value: selectedLanguages[index], onChanged: (value) { if (value == null || (selectedLanguages .where((element) => element) .length <= 1 && !value)) return; selectedLanguages[index] = value; setState(() {}); }, ), const SizedBox( width: 10, ), Text(availableLanguages[index].split(".").first) ], ), ) ], ), ), ), ); } Future getLostData() async { final ImagePicker picker = ImagePicker(); final LostDataResponse response = await picker.retrieveLostData(); if (response.isEmpty) { return; } final List? files = response.files; if (files != null) { logger.i("Found lost files"); _handleLostFiles(files); } else { logger.e(response.exception); } } void _handleLostFiles(List files) { // TODO: implement } }