diff --git a/CHANGELOG.md b/CHANGELOG.md index f9035a9..316244f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,18 @@ -## Next +## Beta 0.5.2 * Fixed transaction page date selector's time was always set to **zero** (12AM) * Now uses [pie_menu](https://pub.dev/packages/pie_menu) from pub.dev since fork's additional features have are in the new release * Uses serializer from [`moment_dart`](https://github.com/sadespresso/moment_dart) -* Saves automated backups in app data, fixes [#131](https://github.com/flow-mn/flow/issues/131) for now +* Saves all backups in app data, fixes [#131](https://github.com/flow-mn/flow/issues/131) +* Backups are now deletable * Minor improvements +## Beta 0.5.1 + +* [FEAT] Customize order of new transaction buttons by @sadespresso in https://github.com/flow-mn/flow/pull/148 +* Reform account edit page by @sadespresso in https://github.com/flow-mn/flow/pull/149 + ## Beta 0.5.0 * Added calculator by @sadespresso in diff --git a/assets/l10n/en_US.json b/assets/l10n/en_US.json index 321ec8a..b938a2c 100644 --- a/assets/l10n/en_US.json +++ b/assets/l10n/en_US.json @@ -287,5 +287,6 @@ "error.sync.invalidBackupFile": "Invalid backup file", "error.sync.safetyBackupFailed": "Unable to start import", "error.sync.exportFailed": "Unable to export, please contact developer.", + "error.sync.fileDeleteFailed": "An error occured during backup deletion", "error.transaction.missingAccount": "Please select an account" } diff --git a/assets/l10n/it_IT.json b/assets/l10n/it_IT.json index 606dcc1..0948273 100644 --- a/assets/l10n/it_IT.json +++ b/assets/l10n/it_IT.json @@ -287,5 +287,6 @@ "error.sync.invalidBackupFile": "File di backup non valido", "error.sync.safetyBackupFailed": "Impossibile avviare l'importazione", "error.sync.exportFailed": "Impossibile esportare, si prega di contattare lo sviluppatore.", + "error.sync.fileDeleteFailed": "Impossibile eliminare il file", "error.transaction.missingAccount": "Selezionare un account" } diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index f6ed187..06c2f87 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -287,5 +287,6 @@ "error.sync.invalidBackupFile": "Нөөц файл алдаатай байна", "error.sync.safetyBackupFailed": "Сэргээх үйлдэл эхлэх боломжгүй", "error.sync.exportFailed": "Нөөцлөх явцад алдаа гарлаа, хөгжүүлэгчид хандана уу.", + "error.sync.fileDeleteFailed": "Нөөц устгах үед алдаа гарлаа", "error.transaction.missingAccount": "Гүйлгээ хийх данс сонгоно уу" } diff --git a/lib/entity/backup_entry.dart b/lib/entity/backup_entry.dart index e09f936..eb1706a 100644 --- a/lib/entity/backup_entry.dart +++ b/lib/entity/backup_entry.dart @@ -44,6 +44,14 @@ class BackupEntry { Future exists() => File(filePath).exists(); bool existsSync() => File(filePath).existsSync(); + Future getFileSize() => File(filePath).length(); + int? getFileSizeSync() { + try { + return File(filePath).lengthSync(); + } catch (e) { + return null; + } + } BackupEntry({ this.id = 0, diff --git a/lib/objectbox/actions.dart b/lib/objectbox/actions.dart index 0f8d50b..0f49bc9 100644 --- a/lib/objectbox/actions.dart +++ b/lib/objectbox/actions.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'dart:io'; import 'dart:math' as math; import 'package:flow/data/flow_analytics.dart'; @@ -6,6 +7,7 @@ import 'package:flow/data/memo.dart'; import 'package:flow/data/money_flow.dart'; import 'package:flow/data/prefs/frecency_group.dart'; import 'package:flow/entity/account.dart'; +import 'package:flow/entity/backup_entry.dart'; import 'package:flow/entity/category.dart'; import 'package:flow/entity/transaction.dart'; import 'package:flow/entity/transaction/extensions/base.dart'; @@ -551,3 +553,19 @@ extension AccountActions on Account { return id; } } + +extension BackupEntryActions on BackupEntry { + Future delete() async { + try { + final File file = File(filePath); + + if (await file.exists()) { + await file.delete(); + } + + return ObjectBox().box().remove(id); + } catch (e) { + return false; + } + } +} diff --git a/lib/routes/export/export_history_page.dart b/lib/routes/export/export_history_page.dart index 4e406f0..9c79fc1 100644 --- a/lib/routes/export/export_history_page.dart +++ b/lib/routes/export/export_history_page.dart @@ -36,9 +36,10 @@ class _ExportHistoryPageState extends State { return switch ((backupEntires?.length ?? 0, snapshot.hasData)) { (0, true) => const Text("empty"), (_, true) => ListView.separated( - padding: const EdgeInsets.all(16.0), - itemBuilder: (context, index) => - BackupEntryCard(entry: backupEntires[index]), + itemBuilder: (context, index) => BackupEntryCard( + entry: backupEntires[index], + dismissibleKey: ValueKey(backupEntires[index].id), + ), separatorBuilder: (context, index) => separator, itemCount: backupEntires!.length, ), diff --git a/lib/sync/export.dart b/lib/sync/export.dart index ca42760..e08ba2a 100644 --- a/lib/sync/export.dart +++ b/lib/sync/export.dart @@ -9,7 +9,6 @@ import 'package:flow/sync/export/mode.dart'; import 'package:flow/sync/sync.dart'; import 'package:flow/utils/utils.dart'; import 'package:moment_dart/moment_dart.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:share_plus/share_plus.dart'; @@ -76,14 +75,8 @@ Future saveBackupFile( // Save to cache if it's possible to share later. // Otherwise, save to documents directory, and reveal the file on system. - final Directory saveDir = switch (type) { - BackupEntryType.automated || - BackupEntryType.preAccountDeletion || - BackupEntryType.preImport => - Directory(ObjectBox.appDataDirectory), - _ when isShareSupported => await getApplicationCacheDirectory(), - _ => await getApplicationDocumentsDirectory() - }; + final Directory saveDir = + Directory(path.join(ObjectBox.appDataDirectory, 'backups')); final String dateTime = Moment.now().lll.replaceAll(RegExp("\\s"), "_"); final String randomValue = math.Random().nextInt(536870912).toRadixString(36); diff --git a/lib/sync/export/history/backup_entry_card.dart b/lib/sync/export/history/backup_entry_card.dart index 92eee47..7b65b0d 100644 --- a/lib/sync/export/history/backup_entry_card.dart +++ b/lib/sync/export/history/backup_entry_card.dart @@ -3,11 +3,13 @@ import 'dart:io'; import 'package:flow/entity/backup_entry.dart'; import 'package:flow/l10n/extensions.dart'; import 'package:flow/l10n/named_enum.dart'; +import 'package:flow/objectbox/actions.dart'; import 'package:flow/theme/theme.dart'; import 'package:flow/utils/toast.dart'; +import 'package:flow/utils/utils.dart'; import 'package:flow/widgets/general/flow_icon.dart'; -import 'package:flow/widgets/general/surface.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:moment_dart/moment_dart.dart'; import 'package:share_plus/share_plus.dart'; @@ -16,21 +18,29 @@ class BackupEntryCard extends StatelessWidget { final BackupEntry entry; final BorderRadius borderRadius; + final EdgeInsets padding; + + final Key? dismissibleKey; const BackupEntryCard({ super.key, required this.entry, this.borderRadius = const BorderRadius.all(Radius.circular(16.0)), + this.padding = const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 4.0, + ), + this.dismissibleKey, }); @override Widget build(BuildContext context) { - final bool fileExists = entry.existsSync(); + final int? fileSize = entry.getFileSizeSync(); - return Surface( - shape: RoundedRectangleBorder(borderRadius: borderRadius), - builder: (context) => InkWell( - borderRadius: borderRadius, + final Widget listTile = InkWell( + borderRadius: borderRadius, + child: Padding( + padding: padding, child: Row( children: [ FlowIcon( @@ -48,9 +58,14 @@ class BackupEntryCard extends StatelessWidget { entry.backupEntryType.localizedNameContext(context), maxLines: 2, overflow: TextOverflow.ellipsis, + style: context.textTheme.labelLarge, ), Text( - "${entry.fileExt} • ${entry.createdDate.toMoment().calendar()}", + [ + entry.createdDate.toMoment().calendar(), + entry.fileExt, + fileSize?.binarySize + ].nonNulls.join(" • "), style: context.textTheme.bodyMedium?.semi(context), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -60,8 +75,8 @@ class BackupEntryCard extends StatelessWidget { ), const SizedBox(width: 8.0), IconButton( - onPressed: () => showShareSheet(context, fileExists), - icon: fileExists + onPressed: () => showShareSheet(context, fileSize != null), + icon: fileSize != null ? const Icon(Symbols.save_alt_rounded) : Icon( Symbols.error_circle_rounded_error, @@ -73,6 +88,21 @@ class BackupEntryCard extends StatelessWidget { ), ), ); + + return Slidable( + key: dismissibleKey, + endActionPane: ActionPane( + motion: const DrawerMotion(), + children: [ + SlidableAction( + onPressed: (context) => delete(context), + icon: Symbols.delete_forever_rounded, + backgroundColor: context.flowColors.expense, + ) + ], + ), + child: listTile, + ); } Future showShareSheet(BuildContext context, bool exists) async { @@ -99,4 +129,23 @@ class BackupEntryCard extends StatelessWidget { "date": entry.createdDate.toMoment().lll, })); } + + Future delete(BuildContext context) async { + final String title = entry.backupEntryType.localizedNameContext(context); + + final confirmation = await context.showConfirmDialog( + isDeletionConfirmation: true, + title: "general.delete.confirmName".t(context, title), + ); + + if (confirmation == true) { + final bool deleted = await entry.delete(); + + if (!context.mounted) return; + + if (!deleted) { + context.showErrorToast(error: "error.sync.fileDeleteFailed".t(context)); + } + } + } } diff --git a/lib/utils/extensions/num.dart b/lib/utils/extensions/num.dart index 967a023..c585b56 100644 --- a/lib/utils/extensions/num.dart +++ b/lib/utils/extensions/num.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + extension NumberFormatter on num { /// Returns string with [decimalPlaces] decimal places. /// @@ -24,4 +26,13 @@ extension NumberFormatter on num { /// Example: /// `0.42.percent2 => "42.00%"` String get percent2 => percent(2); + + String get binarySize { + const log1024 = 6.931471805599453; + const formats = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; + + final int unitIndex = (log(toDouble()) / log1024).floor(); + + return "${(this / pow(1024, unitIndex)).round()} ${formats[unitIndex]}"; + } } diff --git a/lib/widgets/transaction_list_tile.dart b/lib/widgets/transaction_list_tile.dart index 95f9724..e6d964e 100644 --- a/lib/widgets/transaction_list_tile.dart +++ b/lib/widgets/transaction_list_tile.dart @@ -1,6 +1,3 @@ -import 'dart:io'; - -import 'package:flow/constants.dart'; import 'package:flow/data/flow_icon.dart'; import 'package:flow/entity/transaction.dart'; import 'package:flow/entity/transaction/extensions/default/transfer.dart'; @@ -8,7 +5,6 @@ import 'package:flow/l10n/extensions.dart'; import 'package:flow/objectbox/actions.dart'; import 'package:flow/theme/theme.dart'; import 'package:flow/widgets/general/flow_icon.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; @@ -98,14 +94,13 @@ class TransactionListTile extends StatelessWidget { return Slidable( key: dismissibleKey, - enabled: flowDebugMode || Platform.isIOS, endActionPane: ActionPane( motion: const DrawerMotion(), children: [ SlidableAction( onPressed: (context) => deleteFn(), - icon: CupertinoIcons.delete, - backgroundColor: CupertinoColors.destructiveRed, + icon: Symbols.delete_forever_rounded, + backgroundColor: context.flowColors.expense, ) ], ), diff --git a/pubspec.yaml b/pubspec.yaml index a523e9a..ae866fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A personal finance managing app publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: "0.5.2+45" +version: "0.5.2+46" environment: sdk: ">=3.1.3 <4.0.0"