diff --git a/.vscode/settings.json b/.vscode/settings.json index 893f00352a..cae4a39314 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,6 +23,7 @@ "curr", "lerp", "LTRB", + "readwrite", "sublist", "unfocus", "writeln" diff --git a/integration_test/stress/initialization_test.dart b/integration_test/stress/initialization_test.dart index b6457ff7f8..1127c69ac6 100644 --- a/integration_test/stress/initialization_test.dart +++ b/integration_test/stress/initialization_test.dart @@ -45,7 +45,7 @@ void main() { final endTime = DateTime.now(); final duration = endTime.difference(startTime); file.writeAsString( - "${duration.inMilliseconds} ms | ${await TransactionLog.getSize()} | ${TransactionLog.increment}\n", + "${duration.inMilliseconds} ms | ${await TransactionLog.getSize()} | ${TransactionLog.amount}\n", mode: FileMode.append, ); }, timeout: const Timeout(Duration(minutes: 30))); diff --git a/lib/_classes/storage/transaction_log.dart b/lib/_classes/storage/transaction_log.dart index 011a155e3a..c96f7e59e3 100644 --- a/lib/_classes/storage/transaction_log.dart +++ b/lib/_classes/storage/transaction_log.dart @@ -1,92 +1,25 @@ // Copyright 2023 The terCAD team. All rights reserved. // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. -import 'dart:io'; import 'dart:convert'; import 'dart:async'; -import 'dart:math'; import 'package:app_finance/_classes/controller/encryption_handler.dart'; import 'package:app_finance/_classes/storage/app_data.dart'; -import 'package:app_finance/_classes/storage/app_preferences.dart'; +import 'package:app_finance/_classes/storage/transaction_log/abstract_storage_web.dart' + if (dart.library.io) 'package:app_finance/_classes/storage/transaction_log/abstract_storage.dart'; +import 'package:app_finance/_classes/storage/transaction_log/interface_storage.dart'; import 'package:app_finance/_ext/data_ext.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; -class TransactionLog { - static int increment = 0; +class TransactionLog extends AbstractStorage implements InterfaceStorage { + static int amount = 0; - static bool _isLocked = false; + static Future getSize() => AbstractStorage.getSize(); - static File? _logFile; + static void clear() => AbstractStorage.clear(); - static const filePath = '.terCAD/app-finance.log'; + static Stream read() => AbstractStorage.read(); - static Future get logFle async { - if (_logFile != null) { - return Future.value(_logFile); - } - List scope = [ - await getApplicationDocumentsDirectory(), - await getApplicationSupportDirectory(), - Directory.systemTemp, - await getTemporaryDirectory(), - ].map((dir) => File('${dir.absolute.path}/$filePath')).toList(); - File? file = scope.where((f) => f.existsSync()).firstOrNull; - int i = 0; - while (i < scope.length && file == null) { - try { - File tmp = scope[i]; - if (!tmp.existsSync()) { - tmp.createSync(recursive: true); - tmp.writeAsString("\n", mode: FileMode.append); - } - file = tmp; - } catch (e) { - i++; - } - } - if (file == null) { - throw Exception('Write access denied for: $scope.'); - } - return _logFile = file; - } - - static String _formatBytes(int bytes) { - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - if (bytes == 0) return '0 B'; - final i = (log(bytes) / log(1024)).floor(); - return '${(bytes / pow(1024, i)).toStringAsFixed(2)} ${sizes[i]}'; - } - - static Future getSize() async { - int? size; - if (kIsWeb) { - size = increment * 256; - } else { - size = (await logFle).lengthSync(); - } - return _formatBytes(size); - } - - static void saveRaw(String line) { - int retrial = 1; - while (_isLocked && retrial < 1000) { - sleep(Duration(microseconds: retrial * 10)); - retrial++; - } - _isLocked = true; - try { - if (kIsWeb) { - AppPreferences.set('log$increment', line); - } else if (_logFile != null) { - _logFile!.writeAsStringSync("$line\n", mode: FileMode.append); - } - _isLocked = false; - } catch (e) { - _isLocked = false; - rethrow; - } - } + static void saveRaw(String line) => AbstractStorage.saveRaw(line); static void save(dynamic content) { String line = content.toString(); @@ -94,7 +27,7 @@ class TransactionLog { line = EncryptionHandler.encrypt(line); } saveRaw(line); - increment++; + amount++; } static void init(AppData store, String type, Map data) { @@ -104,54 +37,13 @@ class TransactionLog { } } - static Stream _loadWeb() async* { - int attempts = 0; - do { - int i = increment + attempts; - var line = AppPreferences.get('log$i'); - if (line == null) { - attempts++; - } else { - increment += attempts + 1; - attempts = 0; - } - yield line ?? ''; - } while (attempts < 10); - } - - static Stream read() async* { - Stream lines; - increment = 0; - if (kIsWeb) { - lines = _loadWeb(); - } else { - lines = (await logFle).openRead().transform(utf8.decoder).transform(const LineSplitter()); - } - await for (var line in lines) { - if (!kIsWeb) { - increment++; - } - yield line; - } - } - - static clear() { - if (kIsWeb) { - while (increment > 0) { - AppPreferences.clear('log$increment'); - increment--; - } - } else { - _logFile?.deleteSync(); - _logFile?.createSync(); - } - } - static Future load(AppData store) async { bool isEncrypted = EncryptionHandler.doEncrypt(); bool isOK = true; + amount = 0; await for (var line in read()) { isOK &= add(store, line, isEncrypted); + amount++; } return isOK; } diff --git a/lib/_classes/storage/transaction_log/abstract_storage.dart b/lib/_classes/storage/transaction_log/abstract_storage.dart new file mode 100644 index 0000000000..277f692d8e --- /dev/null +++ b/lib/_classes/storage/transaction_log/abstract_storage.dart @@ -0,0 +1,75 @@ +// Copyright 2024 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:app_finance/_classes/storage/transaction_log/interface_storage.dart'; +import 'package:app_finance/_ext/int_ext.dart'; +import 'package:path_provider/path_provider.dart'; + +abstract class AbstractStorage implements InterfaceStorage { + static File? _logFile; + static bool _isLocked = false; + + static const filePath = '.terCAD/app-finance.log'; + + static Future get logFle async { + if (_logFile != null) { + return Future.value(_logFile); + } + List scope = [ + await getApplicationDocumentsDirectory(), + await getApplicationSupportDirectory(), + Directory.systemTemp, + await getTemporaryDirectory(), + ].map((dir) => File('${dir.absolute.path}/$filePath')).toList(); + File? file = scope.where((f) => f.existsSync()).firstOrNull; + int i = 0; + while (i < scope.length && file == null) { + try { + File tmp = scope[i]; + if (!tmp.existsSync()) { + tmp.createSync(recursive: true); + tmp.writeAsString("\n", mode: FileMode.append); + } + file = tmp; + } catch (e) { + i++; + } + } + if (file == null) { + throw Exception('Write access denied for: $scope.'); + } + return _logFile = file; + } + + static Future getSize() async { + int size = (await logFle).lengthSync(); + return size.toByteSize(); + } + + static void saveRaw(String line) { + int retrial = 1; + while (_isLocked && retrial < 1000) { + sleep(Duration(microseconds: retrial * 10)); + retrial++; + } + _isLocked = true; + _logFile!.writeAsStringSync("$line\n", mode: FileMode.append); + _isLocked = false; + } + + static Stream read() async* { + Stream lines = (await logFle).openRead().transform(utf8.decoder).transform(const LineSplitter()); + + await for (var line in lines) { + yield line; + } + } + + static void clear() { + _logFile?.deleteSync(); + _logFile?.createSync(); + } +} diff --git a/lib/_classes/storage/transaction_log/abstract_storage_web.dart b/lib/_classes/storage/transaction_log/abstract_storage_web.dart new file mode 100644 index 0000000000..30c0226ca7 --- /dev/null +++ b/lib/_classes/storage/transaction_log/abstract_storage_web.dart @@ -0,0 +1,77 @@ +// Copyright 2024 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:app_finance/_classes/storage/app_preferences.dart'; +import 'package:app_finance/_classes/storage/transaction_log/interface_storage.dart'; +import 'package:app_finance/_ext/int_ext.dart'; +import 'package:idb_shim/idb_browser.dart'; + +abstract class AbstractStorage implements InterfaceStorage { + static int increment = 0; + static const String storeName = 'records'; + static Database? db; + + static Future _initIndexedDB() async { + IdbFactory? idbFactory = getIdbFactory(); + if (idbFactory == null) { + return; + } + db = await idbFactory.open('fingrom.db', version: 1, onUpgradeNeeded: (VersionChangeEvent event) { + event.database.createObjectStore(storeName, keyPath: 'id'); + }); + } + + static Future getSize() async { + int size = increment * 256; + return size.toByteSize(); + } + + static void saveRaw(String line) { + if (db != null) { + var store = db!.transaction(storeName, 'readwrite').objectStore(storeName); + store.put({'id': 'log$increment', 'line': line}); + } else { + AppPreferences.set('log$increment', line); + } + increment++; + } + + static Stream readRaw(Function callback) async* { + int attempts = 0; + do { + int i = increment + attempts; + String? line = await callback(i); + if (line == null) { + attempts++; + } else { + increment += attempts + 1; + attempts = 0; + } + yield line ?? ''; + } while (attempts < 10); + } + + static Stream read() async* { + increment = 0; + await _initIndexedDB(); + await for (var line in readRaw((i) => AppPreferences.get('log$i'))) { + yield line; + } + if (db != null) { + var store = db!.transaction(storeName, 'readonly').objectStore(storeName); + await for (var line in readRaw((i) async => ((await store.getObject('log$i')) as Map?)?['line'])) { + yield line; + } + } + } + + static void clear() { + while (increment > 0) { + AppPreferences.clear('log$increment'); + if (db != null) { + db!.transaction(storeName, 'readwrite').objectStore(storeName).delete('log$increment'); + } + increment--; + } + } +} diff --git a/lib/_classes/storage/transaction_log/interface_storage.dart b/lib/_classes/storage/transaction_log/interface_storage.dart new file mode 100644 index 0000000000..b736192904 --- /dev/null +++ b/lib/_classes/storage/transaction_log/interface_storage.dart @@ -0,0 +1,12 @@ +// Copyright 2024 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +abstract interface class InterfaceStorage { + static Future getSize() async => Future.value(''); + + static void saveRaw(String line) {} + + static Stream read() async* {} + + static void clear() {} +} diff --git a/lib/_ext/int_ext.dart b/lib/_ext/int_ext.dart index bff2a97ad5..ca87d41374 100644 --- a/lib/_ext/int_ext.dart +++ b/lib/_ext/int_ext.dart @@ -1,6 +1,8 @@ // Copyright 2023 The terCAD team. All rights reserved. // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. +import 'dart:math'; + import 'package:flutter/material.dart'; extension IntExt on int { @@ -14,4 +16,11 @@ extension IntExt on int { return IconData(this, fontFamily: fontFamily); } } + + String toByteSize() { + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + if (this == 0) return '0 B'; + final i = (log(this) / log(1024)).floor(); + return '${(this / pow(1024, i)).toStringAsFixed(2)} ${sizes[i]}'; + } } diff --git a/pubspec.lock b/pubspec.lock index ad74517d4a..b56af3d77a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -599,6 +599,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + idb_shim: + dependency: "direct main" + description: + name: idb_shim + sha256: "9e7ec816139bfafb69ae4b3668ad29dbd43c53428d6eb31f9332d42bd4fa7205" + url: "https://pub.dev" + source: hosted + version: "2.6.1+7" image: dependency: transitive description: @@ -980,6 +988,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + sembast: + dependency: transitive + description: + name: sembast + sha256: "934a7b99297fb4f0b6e69fb1465286737b3b47b1a5149bf8dfc85667fbbdd21d" + url: "https://pub.dev" + source: hosted + version: "3.7.4+3" shared_preferences: dependency: "direct main" description: @@ -1113,6 +1129,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.dev" + source: hosted + version: "3.3.0+3" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b26e5b139c..4a0505346f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: in_app_purchase: ^3.1.11 firebase_crashlytics: ^4.0.0 dart_intl_search: ^1.2.3 + idb_shim: ^2.6.1+7 dev_dependencies: flutter_test: diff --git a/test/pump_main.dart b/test/pump_main.dart index a50d36bcdb..4f993ea8ce 100644 --- a/test/pump_main.dart +++ b/test/pump_main.dart @@ -4,7 +4,7 @@ import 'dart:io' as io; import 'package:app_finance/_classes/herald/app_design.dart'; import 'package:app_finance/_classes/herald/app_purchase.dart'; -import 'package:app_finance/_classes/storage/transaction_log.dart'; +import 'package:app_finance/_classes/storage/transaction_log/abstract_storage.dart'; import 'package:app_finance/_configs/custom_text_theme.dart'; import 'package:dart_class_wrapper/dart_class_wrapper.dart'; import 'package:file/file.dart'; @@ -43,7 +43,7 @@ class PumpMain { final pumpMain = PumpMain(); final tmp = '$path/${UniqueKey()}'; wrapProvider(tester, 'plugins.flutter.io/path_provider', tmp); - io.File('$tmp/${TransactionLog.filePath}').createSync(recursive: true); + io.File('$tmp/${AbstractStorage.filePath}').createSync(recursive: true); await initFonts(); await initPref(isIntegration); await pumpMain.initMain(tester, isIntegration);