feat: import/export data

closes #25
This commit is contained in:
Matyáš Caras 2024-01-22 23:32:21 +01:00
parent 7fed91e811
commit 6500c7e5e8
Signed by untrusted user who does not match committer: hernik
GPG key ID: 2A3175F98820C5C6
7 changed files with 370 additions and 155 deletions

View file

@ -1,6 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet.dart';
import 'package:prasule/main.dart'; import 'package:prasule/main.dart';
@ -41,6 +45,126 @@ class WalletManager {
} }
} }
/// Creates a ZIP archive from all wallets
static Future<void> exportAllWallets() async {
if (kIsWeb) {
// TODO
return;
}
final archive = Archive();
for (final w in Directory(
"${(await getApplicationDocumentsDirectory()).path}/wallets",
).listSync()) {
if (w is! File) continue;
logger.i("Zipping ${w.path.split("/").last}");
final wf = w;
archive.addFile(
ArchiveFile.stream(
wf.path.split("/").last,
wf.lengthSync(),
InputFileStream(wf.path),
),
);
}
if (!await FlutterFileDialog.isPickDirectorySupported()) {
File(
"${(await getApplicationDocumentsDirectory()).path}/export_${DateFormat("dd_MM_yyyy").format(DateTime.now())}.zip",
).writeAsBytesSync(ZipEncoder().encode(archive) ?? []);
return;
}
final dir = await FlutterFileDialog.pickDirectory();
if (dir == null) return;
await FlutterFileDialog.saveFileToDirectory(
directory: dir,
data: Uint8List.fromList(ZipEncoder().encode(archive) ?? []),
fileName: "export_${DateFormat("dd_MM_yyyy").format(DateTime.now())}.zip",
mimeType: "application/zip",
);
}
/// Exports a single [Wallet]
static Future<void> exportWallet({Wallet? wallet, String? name}) async {
if (wallet == null && name == null) {
throw Exception("You need to specify either a wallet or a name");
}
final n = name ?? wallet!.name;
if (!await FlutterFileDialog.isPickDirectorySupported()) {
File("${(await getApplicationDocumentsDirectory()).path}/wallets/$n")
.copySync(
"${await getApplicationDocumentsDirectory()}/export_${n.replaceAll(RegExp('[|\\?*<":>+\[\]/\' ]+'), '_')}_${DateFormat("dd_MM_yyyy").format(DateTime.now())}.json",
);
return;
}
final dir = await FlutterFileDialog.pickDirectory();
if (dir == null) return;
await FlutterFileDialog.saveFileToDirectory(
directory: dir,
data:
File("${(await getApplicationDocumentsDirectory()).path}/wallets/$n")
.readAsBytesSync(),
fileName:
"export_${n.replaceAll(RegExp('[|\\?*<":>+\[\]/\' ]+'), '_')}_${DateFormat("dd_MM_yyyy").format(DateTime.now())}.json",
mimeType: "application/json",
);
}
/// Import a single wallet
static Future<void> importWallet({String? data}) async {
var d = data ?? "";
if (data == null) {
final filePath = await FlutterFileDialog.pickFile(
params: const OpenFileDialogParams(
mimeTypesFilter: ["application/json"],
fileExtensionsFilter: ["json"],
),
);
if (filePath == null) return;
d = File(filePath).readAsStringSync();
}
final w = Wallet.fromJson(jsonDecode(d) as Map<String, dynamic>);
if (await WalletManager.exists(w.name)) {
throw Exception("Wallet already exists!");
}
await WalletManager.saveWallet(
w,
);
}
/// Imports wallets from a ZIP archive
static Future<void> importArchive() async {
final filePath = await FlutterFileDialog.pickFile(
params: const OpenFileDialogParams(
mimeTypesFilter: ["application/zip"],
fileExtensionsFilter: ["zip"],
),
);
if (filePath == null) return;
if (kIsWeb) {
// TODO
return;
}
final temp = Directory("${(await getTemporaryDirectory()).path}/data");
if (temp.existsSync()) {
temp.deleteSync();
}
temp.createSync(recursive: true);
final archive = ZipDecoder().decodeBuffer(InputFileStream(filePath));
for (final file in archive.files) {
if (!file.isFile) {
logger.d(file.name);
continue;
}
file.writeContent(OutputFileStream("${temp.path}/${file.name}"));
}
for (final e in temp.listSync()) {
logger.d(e.path);
if (e is! File) continue;
await importWallet(data: e.readAsStringSync());
}
}
/// Loads and returns a single [Wallet] by name /// Loads and returns a single [Wallet] by name
static Future<Wallet> loadWallet(String name) async { static Future<Wallet> loadWallet(String name) async {
final path = final path =
@ -76,4 +200,11 @@ class WalletManager {
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets"); Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
File("${path.path}/${w.name}").deleteSync(); File("${path.path}/${w.name}").deleteSync();
} }
/// Checks if the wallet exists
static Future<bool> exists(String name) async {
return File(
"${(await getApplicationDocumentsDirectory()).path}/wallets/$name",
).existsSync();
}
} }

View file

@ -91,5 +91,18 @@
"evenMoney":"Váš stav je stejný jako minulý měsíc.", "evenMoney":"Váš stav je stejný jako minulý měsíc.",
"balanceStatusA": "Váš stav je ", "balanceStatusA": "Váš stav je ",
"balanceStatusB": " oproti minulému měsíci.", "balanceStatusB": " oproti minulému měsíci.",
"searchLabel":"Prohledat záznamy..." "searchLabel":"Prohledat záznamy...",
"exportSingle":"Exportovat peněženku",
"exportSingleDesc":"Uloží vybranou peněženku do složky Stažené",
"exportArchive":"Exportovat archiv",
"exportArchiveDesc":"Exportuje všechny peněženky do archivu do složky Stažené",
"importSingle":"Importovat peněženku",
"importSingleDesc":"Importuje jednu peněženku ze souboru",
"importArchive":"Importovat archiv",
"importArchiveDesc":"Importuje všechny nepoškozené peněženky z archivu",
"settingsData":"Data",
"selectExportWallet":"Zvolte peněženku k exportování",
"exportError":"Při exportování peněženky nastala chyba",
"exportCompleted":"Export dokončen",
"importCompleted":"Import dokončen"
} }

View file

@ -207,5 +207,18 @@
"evenMoney":"You're on the same balance as last month.", "evenMoney":"You're on the same balance as last month.",
"balanceStatusA": "Your balance is ", "balanceStatusA": "Your balance is ",
"balanceStatusB": " compared to last month.", "balanceStatusB": " compared to last month.",
"searchLabel":"Search entries..." "searchLabel":"Search entries...",
"exportSingle":"Export a wallet",
"exportSingleDesc":"Saves a single wallet into your downloads directory",
"exportArchive":"Export archive",
"exportArchiveDesc":"Exports all wallets into an archive to your downloads directory",
"importSingle":"Import a wallet",
"importSingleDesc":"Imports a single wallet from file",
"importArchive":"Import archive",
"importArchiveDesc":"Imports all valid wallets inside an archive",
"settingsData":"Data",
"selectExportWallet":"Select a wallet to export",
"exportError":"An error occured trying to export wallet",
"exportCompleted":"Export completed",
"importCompleted":"Import completed"
} }

View file

@ -5,12 +5,12 @@ import 'dart:async';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart'; import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';
import 'package:grouped_list/grouped_list.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/date_symbol_data_local.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
@ -125,18 +125,15 @@ class _HomeViewState extends State<HomeView> {
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.camera_alt), child: const Icon(Icons.camera_alt),
label: AppLocalizations.of(context).addCamera, label: AppLocalizations.of(context).addCamera,
onTap: () async { onTap: () {
final picker = ImagePicker(); startOcr(SourceType.camera);
final media =
await picker.pickImage(source: ImageSource.camera);
logger.i(media?.name);
}, },
), ),
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.image), child: const Icon(Icons.image),
label: AppLocalizations.of(context).addGallery, label: AppLocalizations.of(context).addGallery,
onTap: () { onTap: () {
startOcr(ImageSource.gallery); startOcr(SourceType.photoLibrary);
}, },
), ),
], ],
@ -217,6 +214,7 @@ class _HomeViewState extends State<HomeView> {
), ),
) )
.then((value) async { .then((value) async {
wallets = await WalletManager.listWallets();
selectedWallet = selectedWallet =
await WalletManager.loadWallet(selectedWallet!.name); await WalletManager.loadWallet(selectedWallet!.name);
}); });
@ -593,7 +591,7 @@ class _HomeViewState extends State<HomeView> {
); );
} }
Future<void> startOcr(ImageSource imgSrc) async { Future<void> startOcr(SourceType sourceType) async {
final availableLanguages = await TessdataApi.getDownloadedData(); final availableLanguages = await TessdataApi.getDownloadedData();
if (availableLanguages.isEmpty) { if (availableLanguages.isEmpty) {
if (!mounted) return; if (!mounted) return;
@ -637,9 +635,11 @@ class _HomeViewState extends State<HomeView> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () async { onPressed: () async {
final picker = ImagePicker(); final filePath = await FlutterFileDialog.pickFile(
final media = await picker.pickImage(source: imgSrc); params: OpenFileDialogParams(
if (media == null) { dialogType: OpenFileDialogType.image,
sourceType: sourceType));
if (filePath == null) {
if (mounted) Navigator.of(context).pop(); if (mounted) Navigator.of(context).pop();
return; return;
} }
@ -663,7 +663,7 @@ class _HomeViewState extends State<HomeView> {
), ),
); );
final string = await FlutterTesseractOcr.extractText( final string = await FlutterTesseractOcr.extractText(
media.path, filePath,
language: selected, language: selected,
args: { args: {
"psm": "4", "psm": "4",
@ -758,23 +758,4 @@ class _HomeViewState extends State<HomeView> {
), ),
); );
} }
Future<void> getLostData() async {
final picker = ImagePicker();
final response = await picker.retrieveLostData();
if (response.isEmpty) {
return;
}
final files = response.files;
if (files != null) {
logger.i("Found lost files");
_handleLostFiles(files);
} else {
logger.e(response.exception);
}
}
void _handleLostFiles(List<XFile> files) {
// TODO: implement
}
} }

View file

@ -1,9 +1,13 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:prasule/api/wallet_manager.dart';
import 'package:prasule/main.dart'; import 'package:prasule/main.dart';
import 'package:prasule/pw/platformbutton.dart';
import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/pw/platformroute.dart';
import 'package:prasule/util/show_message.dart';
import 'package:prasule/views/settings/edit_categories.dart'; import 'package:prasule/views/settings/edit_categories.dart';
import 'package:prasule/views/settings/graph_type.dart'; import 'package:prasule/views/settings/graph_type.dart';
import 'package:prasule/views/settings/tessdata_list.dart'; import 'package:prasule/views/settings/tessdata_list.dart';
@ -103,6 +107,190 @@ class _SettingsViewState extends State<SettingsView> {
), ),
], ],
), ),
SettingsSection(
title: Text(AppLocalizations.of(context).settingsData),
tiles: [
SettingsTile.navigation(
title: Text(AppLocalizations.of(context).exportSingle),
description:
Text(AppLocalizations.of(context).exportSingleDesc),
onPressed: (ctx) async {
final all = await WalletManager.listWallets();
if (!ctx.mounted) return;
final w = await showAdaptiveDialog<String>(
context: ctx,
builder: (ctx) => AlertDialog.adaptive(
title: Text(
AppLocalizations.of(context).selectExportWallet,
),
actions: [
PlatformButton(
text: AppLocalizations.of(context).cancel,
onPressed: () => Navigator.of(ctx).pop(),
),
],
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.7,
height: MediaQuery.of(context).size.height * 0.3,
child: ListView.builder(
itemBuilder: (con, i) => InkWell(
onTap: () => Navigator.of(ctx).pop(all[i].name),
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
all[i].name,
textAlign: TextAlign.center,
),
),
),
shrinkWrap: true,
itemCount: all.length,
),
),
),
);
if (w == null) return;
try {
await WalletManager.exportWallet(name: w);
} catch (e) {
if (!context.mounted) return;
unawaited(
showAdaptiveDialog(
context: context,
builder: (ctx) => AlertDialog.adaptive(
title: Text(
AppLocalizations.of(context).exportError,
),
content: SingleChildScrollView(
child: Flexible(
child: Text(e.toString()),
),
),
),
),
);
logger.e(e);
return;
}
if (!ctx.mounted) return;
unawaited(
showMessage(
AppLocalizations.of(ctx).exportCompleted,
ctx,
),
);
},
),
SettingsTile.navigation(
title: Text(AppLocalizations.of(context).exportArchive),
description:
Text(AppLocalizations.of(context).exportArchiveDesc),
onPressed: (ctx) async {
try {
await WalletManager.exportAllWallets();
} catch (e) {
if (!ctx.mounted) return;
unawaited(
showAdaptiveDialog(
context: context,
builder: (ctx) => AlertDialog.adaptive(
title: Text(
AppLocalizations.of(context).exportError,
),
content: SingleChildScrollView(
child: Flexible(
child: Text(e.toString()),
),
),
),
),
);
logger.e(e);
return;
}
if (!ctx.mounted) return;
unawaited(
showMessage(
AppLocalizations.of(ctx).exportCompleted,
context,
),
);
},
),
SettingsTile.navigation(
title: Text(AppLocalizations.of(context).importSingle),
description:
Text(AppLocalizations.of(context).importSingleDesc),
onPressed: (ctx) async {
try {
await WalletManager.importWallet();
} catch (e) {
if (!ctx.mounted) return;
unawaited(
showAdaptiveDialog(
context: context,
builder: (ctx) => AlertDialog.adaptive(
title: Text(
AppLocalizations.of(context).exportError,
),
content: SingleChildScrollView(
child: Flexible(
child: Text(e.toString()),
),
),
),
),
);
logger.e(e);
return;
}
if (!ctx.mounted) return;
unawaited(
showMessage(
AppLocalizations.of(ctx).importCompleted,
context,
),
);
},
),
SettingsTile.navigation(
title: Text(AppLocalizations.of(context).importArchive),
description:
Text(AppLocalizations.of(context).importArchiveDesc),
onPressed: (ctx) async {
try {
await WalletManager.importArchive();
} catch (e) {
if (!ctx.mounted) return;
unawaited(
showAdaptiveDialog(
context: context,
builder: (ctx) => AlertDialog.adaptive(
title: Text(
AppLocalizations.of(context).exportError,
),
content: SingleChildScrollView(
child: Flexible(
child: Text(e.toString()),
),
),
),
),
);
logger.e(e);
return;
}
if (!ctx.mounted) return;
unawaited(
showMessage(
AppLocalizations.of(ctx).importCompleted,
context,
),
);
},
),
],
),
], ],
), ),
); );

View file

@ -18,7 +18,7 @@ packages:
source: hosted source: hosted
version: "6.3.0" version: "6.3.0"
archive: archive:
dependency: transitive dependency: "direct main"
description: description:
name: archive name: archive
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
@ -177,14 +177,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.7.2" version: "1.7.2"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
url: "https://pub.dev"
source: hosted
version: "0.3.3+8"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -273,38 +265,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" version: "7.0.0"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492"
url: "https://pub.dev"
source: hosted
version: "0.9.2+1"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6
url: "https://pub.dev"
source: hosted
version: "0.9.3+3"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
source: hosted
version: "2.6.2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0
url: "https://pub.dev"
source: hosted
version: "0.9.3+1"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -347,6 +307,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_file_dialog:
dependency: "direct main"
description:
name: flutter_file_dialog
sha256: "9344b8f07be6a1b6f9854b723fb0cf84a8094ba94761af1d213589d3cb087488"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_iconpicker: flutter_iconpicker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -424,14 +392,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da
url: "https://pub.dev"
source: hosted
version: "2.0.17"
flutter_slidable: flutter_slidable:
dependency: "direct main" dependency: "direct main"
description: description:
@ -519,14 +479,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.2" version: "5.1.2"
http:
dependency: transitive
description:
name: http
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.2.0"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -551,70 +503,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.4" version: "4.1.4"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd"
url: "https://pub.dev"
source: hosted
version: "1.0.7"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1"
url: "https://pub.dev"
source: hosted
version: "0.8.9+3"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3
url: "https://pub.dev"
source: hosted
version: "3.0.2"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3
url: "https://pub.dev"
source: hosted
version: "0.8.9+1"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b
url: "https://pub.dev"
source: hosted
version: "2.9.3"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
integration_test: integration_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter

View file

@ -13,6 +13,7 @@ environment:
# the latest version available on pub.dev. To see which dependencies have newer # the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`. # versions available, run `flutter pub outdated`.
dependencies: dependencies:
archive: ^3.4.10
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
currency_picker: ^2.0.16 currency_picker: ^2.0.16
dio: ^5.3.0 dio: ^5.3.0
@ -21,6 +22,7 @@ dependencies:
flex_color_picker: ^3.3.0 flex_color_picker: ^3.3.0
flutter: flutter:
sdk: flutter sdk: flutter
flutter_file_dialog: ^3.0.2
flutter_iconpicker: ^3.2.4 flutter_iconpicker: ^3.2.4
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
@ -29,7 +31,6 @@ dependencies:
flutter_tesseract_ocr: ^0.4.23 flutter_tesseract_ocr: ^0.4.23
fluttertoast: ^8.2.4 fluttertoast: ^8.2.4
grouped_list: ^5.1.2 grouped_list: ^5.1.2
image_picker: ^1.0.1
intl: any intl: any
introduction_screen: ^3.1.11 introduction_screen: ^3.1.11
json_annotation: ^4.8.1 json_annotation: ^4.8.1