From e1884d422766fe25c25edef25ba36074bdc57a92 Mon Sep 17 00:00:00 2001 From: nemoryoliver Date: Thu, 14 Mar 2024 11:08:40 +0800 Subject: [PATCH] latest from macbook --- lib/core/form_fields/richtext.field.dart | 39 ++- lib/core/liso/liso.manager.dart | 1 + lib/core/services/cipher.service.dart | 14 +- lib/core/utils/minio.util.dart | 62 ++-- .../custom_provider_screen.controller.dart | 286 ++++++++--------- .../provider/custom_provider_screen.dart | 290 +++++++++--------- lib/features/files/sync.service.dart | 1 + pubspec.yaml | 12 +- 8 files changed, 356 insertions(+), 349 deletions(-) diff --git a/lib/core/form_fields/richtext.field.dart b/lib/core/form_fields/richtext.field.dart index c9ee3799..7b42404f 100644 --- a/lib/core/form_fields/richtext.field.dart +++ b/lib/core/form_fields/richtext.field.dart @@ -47,25 +47,32 @@ class RichTextFormField extends StatelessWidget with ConsoleMixin { } } - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (!readOnly) ...[ - QuillToolbar.basic( - controller: _fieldController!, - multiRowsDisplay: false, + return QuillProvider( + configurations: QuillConfigurations(controller: _fieldController!), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!readOnly) ...[ + // QuillToolbar.basic( + // controller: _fieldController!, + // multiRowsDisplay: false, + // ), + const QuillToolbar( + configurations: QuillToolbarConfigurations( + multiRowsDisplay: false, + ), + ), + const Divider(), + ], + SizedBox( + height: 300, + child: QuillEditor.basic( + configurations: QuillEditorConfigurations(readOnly: readOnly), + ), ), const Divider(), ], - SizedBox( - height: 300, - child: QuillEditor.basic( - controller: _fieldController!, - readOnly: readOnly, // true for view only mode - ), - ), - const Divider(), - ], + ), ); } } diff --git a/lib/core/liso/liso.manager.dart b/lib/core/liso/liso.manager.dart index e4df0ac0..7fd09f81 100644 --- a/lib/core/liso/liso.manager.dart +++ b/lib/core/liso/liso.manager.dart @@ -95,6 +95,7 @@ class LisoManager { Uint8List bytes, { Uint8List? cipherKey, }) async { + console.wtf('decrypt parseVaultBytes()'); final decryptedBytes = CipherService.to.decrypt( bytes, cipherKey: cipherKey, diff --git a/lib/core/services/cipher.service.dart b/lib/core/services/cipher.service.dart index 4a4675a7..a4a3bfd6 100644 --- a/lib/core/services/cipher.service.dart +++ b/lib/core/services/cipher.service.dart @@ -20,18 +20,16 @@ class CipherService extends GetxService with ConsoleMixin { // FUNCTIONS Uint8List encrypt(List bytes, {Uint8List? cipherKey}) { - final encrypter = Encrypter( - AES(Key(cipherKey ?? SecretPersistence.to.cipherKey)), - ); - + final key = Key(cipherKey ?? SecretPersistence.to.cipherKey); + console.wtf('encrypt key: ${key.length}'); + final encrypter = Encrypter(AES(key)); return encrypter.encryptBytes(bytes, iv: iv).bytes; } List decrypt(Uint8List bytes, {Uint8List? cipherKey}) { - final encrypter = Encrypter( - AES(Key(cipherKey ?? SecretPersistence.to.cipherKey)), - ); - + final key = Key(cipherKey ?? SecretPersistence.to.cipherKey); + console.wtf('decrypt key: ${key.length}'); + final encrypter = Encrypter(AES(key)); return encrypter.decryptBytes(Encrypted(bytes), iv: iv); } diff --git a/lib/core/utils/minio.util.dart b/lib/core/utils/minio.util.dart index 9c00e9d0..a29680a8 100644 --- a/lib/core/utils/minio.util.dart +++ b/lib/core/utils/minio.util.dart @@ -1,35 +1,35 @@ -import 'package:minio/models.dart'; +// import 'package:minio/models.dart'; -class MinioUtil { - static Object objectFromJson(Map json) => Object( - json['e_tag'], - json['key'], - DateTime.parse(json['last_modified']), - ownerFromJson(json['owner']), - json['size'], - json['storage_class'], - ); +// class MinioUtil { +// static Object objectFromJson(Map json) => Object( +// json['e_tag'], +// json['key'], +// DateTime.parse(json['last_modified']), +// ownerFromJson(json['owner']), +// json['size'], +// json['storage_class'], +// ); - static Map objectToJson(Object? object) { - return { - "e_tag": object?.eTag?.replaceAll('"', ''), - "path": object?.key, - "last_modified": object?.lastModified?.toIso8601String(), - "owner": ownerToJson(object?.owner), - "size": object?.size, - "storage_class": object?.storageClass, - }; - } +// static Map objectToJson(Object? object) { +// return { +// "e_tag": object?.eTag?.replaceAll('"', ''), +// "path": object?.key, +// "last_modified": object?.lastModified?.toIso8601String(), +// "owner": ownerToJson(object?.owner), +// "size": object?.size, +// "storage_class": object?.storageClass, +// }; +// } - static Owner ownerFromJson(Map json) => Owner( - json['display_name'], - json['id'], - ); +// static Owner ownerFromJson(Map json) => Owner( +// json['display_name'], +// json['id'], +// ); - static Map ownerToJson(Owner? owner) { - return { - "display_name": owner?.displayName, - "id": owner?.iD, - }; - } -} +// static Map ownerToJson(Owner? owner) { +// return { +// "display_name": owner?.displayName, +// "id": owner?.iD, +// }; +// } +// } diff --git a/lib/features/files/provider/custom_provider_screen.controller.dart b/lib/features/files/provider/custom_provider_screen.controller.dart index 1f2ec3ae..29914170 100644 --- a/lib/features/files/provider/custom_provider_screen.controller.dart +++ b/lib/features/files/provider/custom_provider_screen.controller.dart @@ -1,143 +1,143 @@ -import 'dart:async'; - -import 'package:app_core/globals.dart'; -import 'package:app_core/utils/ui_utils.dart'; -import 'package:console_mixin/console_mixin.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:liso/core/persistence/persistence.dart'; -import 'package:minio/minio.dart'; - -import '../../../core/persistence/persistence.secret.dart'; -import '../../../core/utils/globals.dart'; - -class CustomSyncProviderScreenController extends GetxController - with StateMixin, ConsoleMixin { - // VARIABLES - final formKey = GlobalKey(); - final persistence = Get.find(); - - final endpointController = TextEditingController(); - final accessKeyController = TextEditingController(); - final secretKeyController = TextEditingController(); - final bucketController = TextEditingController(); - final portController = TextEditingController(); - final regionController = TextEditingController(); - final sessionTokenController = TextEditingController(); - - // PROPERTIES - final busy = false.obs; - - // PROPERTIES - - // GETTERS - - // INIT - @override - void onInit() { - change(null, status: RxStatus.success()); - - // Populate from persistence - endpointController.text = persistence.s3Endpoint.val; - accessKeyController.text = persistence.s3AccessKey.val; - secretKeyController.text = persistence.s3SecretKey.val; - bucketController.text = persistence.s3Bucket.val; - portController.text = persistence.s3Port.val; - regionController.text = persistence.s3Region.val; - sessionTokenController.text = persistence.s3SessionToken.val; - - super.onInit(); - } - - @override - void change(newState, {RxStatus? status}) { - busy.value = status?.isLoading ?? false; - super.change(newState, status: status); - } - - // FUNCTIONS - void save() { - AppPersistence.to.syncProvider.val = LisoSyncProvider.custom.name; - persistence.s3Endpoint.val = endpointController.text; - persistence.s3AccessKey.val = accessKeyController.text; - persistence.s3SecretKey.val = secretKeyController.text; - persistence.s3Bucket.val = bucketController.text; - persistence.s3Port.val = portController.text; - persistence.s3Region.val = regionController.text; - persistence.s3SessionToken.val = sessionTokenController.text; - - // TODO: self-hosting - // SyncService.to.init(); - Get.close(2); - } - - void testConnection() async { - if (!formKey.currentState!.validate()) return; - if (busy.value) return console.error('still busy'); - change(null, status: RxStatus.loading()); - - final client = Minio( - endPoint: endpointController.text, - accessKey: accessKeyController.text, - secretKey: secretKeyController.text, - port: int.tryParse(portController.text), - region: regionController.text.isEmpty ? null : regionController.text, - sessionToken: sessionTokenController.text.isEmpty - ? null - : sessionTokenController.text, - enableTrace: persistence.s3EnableTrace.val, - useSSL: persistence.s3UseSsl.val, - ); - - bool bucketExists = false; - - try { - bucketExists = await client - .bucketExists( - bucketController.text, - ) - .timeout(10.seconds); - change(null, status: RxStatus.success()); - } on TimeoutException { - change(null, status: RxStatus.success()); - return UIUtils.showSimpleDialog( - 'Connection Timed Out', - 'Please check your configuration and your network', - ); - } catch (e) { - change(null, status: RxStatus.success()); - console.error(e.toString()); - - return UIUtils.showSimpleDialog( - 'Connection Error', - e.toString(), - ); - } - - if (bucketExists) { - const dialogContent = Text('Configuration is ready to be used'); - - Get.dialog(AlertDialog( - title: const Text('Connection Success'), - content: isSmallScreen - ? dialogContent - : const SizedBox(width: 450, child: dialogContent), - actions: [ - TextButton( - onPressed: Get.back, - child: Text('cancel'.tr), - ), - TextButton( - onPressed: save, - child: const Text('Use Configuration'), - ), - ], - )); - } else { - UIUtils.showSimpleDialog( - 'Connection Failed', - 'Bucket: ${bucketController.text} is not found', - ); - } - } -} +// import 'dart:async'; + +// import 'package:app_core/globals.dart'; +// import 'package:app_core/utils/ui_utils.dart'; +// import 'package:console_mixin/console_mixin.dart'; +// import 'package:flutter/material.dart'; +// import 'package:get/get.dart'; +// import 'package:liso/core/persistence/persistence.dart'; +// import 'package:minio/minio.dart'; + +// import '../../../core/persistence/persistence.secret.dart'; +// import '../../../core/utils/globals.dart'; + +// class CustomSyncProviderScreenController extends GetxController +// with StateMixin, ConsoleMixin { +// // VARIABLES +// final formKey = GlobalKey(); +// final persistence = Get.find(); + +// final endpointController = TextEditingController(); +// final accessKeyController = TextEditingController(); +// final secretKeyController = TextEditingController(); +// final bucketController = TextEditingController(); +// final portController = TextEditingController(); +// final regionController = TextEditingController(); +// final sessionTokenController = TextEditingController(); + +// // PROPERTIES +// final busy = false.obs; + +// // PROPERTIES + +// // GETTERS + +// // INIT +// @override +// void onInit() { +// change(null, status: RxStatus.success()); + +// // Populate from persistence +// endpointController.text = persistence.s3Endpoint.val; +// accessKeyController.text = persistence.s3AccessKey.val; +// secretKeyController.text = persistence.s3SecretKey.val; +// bucketController.text = persistence.s3Bucket.val; +// portController.text = persistence.s3Port.val; +// regionController.text = persistence.s3Region.val; +// sessionTokenController.text = persistence.s3SessionToken.val; + +// super.onInit(); +// } + +// @override +// void change(newState, {RxStatus? status}) { +// busy.value = status?.isLoading ?? false; +// super.change(newState, status: status); +// } + +// // FUNCTIONS +// void save() { +// AppPersistence.to.syncProvider.val = LisoSyncProvider.custom.name; +// persistence.s3Endpoint.val = endpointController.text; +// persistence.s3AccessKey.val = accessKeyController.text; +// persistence.s3SecretKey.val = secretKeyController.text; +// persistence.s3Bucket.val = bucketController.text; +// persistence.s3Port.val = portController.text; +// persistence.s3Region.val = regionController.text; +// persistence.s3SessionToken.val = sessionTokenController.text; + +// // TODO: self-hosting +// // SyncService.to.init(); +// Get.close(2); +// } + +// void testConnection() async { +// if (!formKey.currentState!.validate()) return; +// if (busy.value) return console.error('still busy'); +// change(null, status: RxStatus.loading()); + +// final client = Minio( +// endPoint: endpointController.text, +// accessKey: accessKeyController.text, +// secretKey: secretKeyController.text, +// port: int.tryParse(portController.text), +// region: regionController.text.isEmpty ? null : regionController.text, +// sessionToken: sessionTokenController.text.isEmpty +// ? null +// : sessionTokenController.text, +// enableTrace: persistence.s3EnableTrace.val, +// useSSL: persistence.s3UseSsl.val, +// ); + +// bool bucketExists = false; + +// try { +// bucketExists = await client +// .bucketExists( +// bucketController.text, +// ) +// .timeout(10.seconds); +// change(null, status: RxStatus.success()); +// } on TimeoutException { +// change(null, status: RxStatus.success()); +// return UIUtils.showSimpleDialog( +// 'Connection Timed Out', +// 'Please check your configuration and your network', +// ); +// } catch (e) { +// change(null, status: RxStatus.success()); +// console.error(e.toString()); + +// return UIUtils.showSimpleDialog( +// 'Connection Error', +// e.toString(), +// ); +// } + +// if (bucketExists) { +// const dialogContent = Text('Configuration is ready to be used'); + +// Get.dialog(AlertDialog( +// title: const Text('Connection Success'), +// content: isSmallScreen +// ? dialogContent +// : const SizedBox(width: 450, child: dialogContent), +// actions: [ +// TextButton( +// onPressed: Get.back, +// child: Text('cancel'.tr), +// ), +// TextButton( +// onPressed: save, +// child: const Text('Use Configuration'), +// ), +// ], +// )); +// } else { +// UIUtils.showSimpleDialog( +// 'Connection Failed', +// 'Bucket: ${bucketController.text} is not found', +// ); +// } +// } +// } diff --git a/lib/features/files/provider/custom_provider_screen.dart b/lib/features/files/provider/custom_provider_screen.dart index e50aae40..9c767679 100644 --- a/lib/features/files/provider/custom_provider_screen.dart +++ b/lib/features/files/provider/custom_provider_screen.dart @@ -1,152 +1,152 @@ -import 'package:app_core/pages/routes.dart'; -import 'package:app_core/utils/utils.dart'; -import 'package:app_core/widgets/appbar_leading.widget.dart'; -import 'package:console_mixin/console_mixin.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:liso/features/files/provider/custom_provider_screen.controller.dart'; +// import 'package:app_core/pages/routes.dart'; +// import 'package:app_core/utils/utils.dart'; +// import 'package:app_core/widgets/appbar_leading.widget.dart'; +// import 'package:console_mixin/console_mixin.dart'; +// import 'package:flutter/material.dart'; +// import 'package:get/get.dart'; +// import 'package:liso/features/files/provider/custom_provider_screen.controller.dart'; -import '../../../core/persistence/secret_persistence.builder.dart'; -import '../../../core/utils/globals.dart'; +// import '../../../core/persistence/secret_persistence.builder.dart'; +// import '../../../core/utils/globals.dart'; -class CustomSyncProviderScreen extends StatelessWidget with ConsoleMixin { - const CustomSyncProviderScreen({Key? key}) : super(key: key); +// class CustomSyncProviderScreen extends StatelessWidget with ConsoleMixin { +// const CustomSyncProviderScreen({Key? key}) : super(key: key); - @override - Widget build(BuildContext context) { - final controller = Get.put(CustomSyncProviderScreenController()); +// @override +// Widget build(BuildContext context) { +// final controller = Get.put(CustomSyncProviderScreenController()); - final content = SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Form( - key: controller.formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: controller.endpointController, - validator: (data) => data!.isNotEmpty ? null : 'required'.tr, - autovalidateMode: AutovalidateMode.onUserInteraction, - decoration: const InputDecoration( - labelText: 'Endpoint', - hintText: 's3.filebase.com', - ), - ), - const SizedBox(height: 15), - TextFormField( - controller: controller.accessKeyController, - validator: (data) => data!.isNotEmpty ? null : 'required'.tr, - autovalidateMode: AutovalidateMode.onUserInteraction, - decoration: const InputDecoration( - labelText: 'Access Key', - ), - ), - const SizedBox(height: 15), - TextFormField( - controller: controller.secretKeyController, - validator: (data) => data!.isNotEmpty ? null : 'required'.tr, - autovalidateMode: AutovalidateMode.onUserInteraction, - decoration: const InputDecoration( - labelText: 'Secret Key', - ), - ), - const SizedBox(height: 15), - TextFormField( - controller: controller.bucketController, - validator: (data) => data!.isNotEmpty ? null : 'required'.tr, - autovalidateMode: AutovalidateMode.onUserInteraction, - decoration: const InputDecoration( - labelText: 'Bucket', - ), - ), - const SizedBox(height: 15), - TextFormField( - controller: controller.portController, - // TODO: validator - inputFormatters: [ - inputFormatterRestrictSpaces, - inputFormatterNumericOnly, - ], - keyboardType: TextInputType.number, - decoration: const InputDecoration( - labelText: 'Port', - hintText: '8080', - ), - ), - const SizedBox(height: 15), - TextFormField( - controller: controller.regionController, - // TODO: validator - decoration: const InputDecoration( - labelText: 'Region', - ), - ), - const SizedBox(height: 15), - TextFormField( - controller: controller.sessionTokenController, - // TODO: validator - decoration: const InputDecoration( - labelText: 'Session Token', - ), - ), - const Divider(), - SecretPersistenceBuilder( - builder: (p, context) => SwitchListTile( - title: const Text('Enable Trace'), - value: p.s3EnableTrace.val, - onChanged: (value) => p.s3EnableTrace.val = value, - // contentPadding: EdgeInsets.zero, - ), - ), - const Divider(), - SecretPersistenceBuilder( - builder: (p, context) => SwitchListTile( - title: const Text('Use SSL'), - value: p.s3UseSsl.val, - onChanged: (value) => p.s3UseSsl.val = value, - // contentPadding: EdgeInsets.zero, - ), - ), - const Divider(), - ], - ), - ), - ); +// final content = SingleChildScrollView( +// padding: const EdgeInsets.all(20), +// child: Form( +// key: controller.formKey, +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// TextFormField( +// controller: controller.endpointController, +// validator: (data) => data!.isNotEmpty ? null : 'required'.tr, +// autovalidateMode: AutovalidateMode.onUserInteraction, +// decoration: const InputDecoration( +// labelText: 'Endpoint', +// hintText: 's3.filebase.com', +// ), +// ), +// const SizedBox(height: 15), +// TextFormField( +// controller: controller.accessKeyController, +// validator: (data) => data!.isNotEmpty ? null : 'required'.tr, +// autovalidateMode: AutovalidateMode.onUserInteraction, +// decoration: const InputDecoration( +// labelText: 'Access Key', +// ), +// ), +// const SizedBox(height: 15), +// TextFormField( +// controller: controller.secretKeyController, +// validator: (data) => data!.isNotEmpty ? null : 'required'.tr, +// autovalidateMode: AutovalidateMode.onUserInteraction, +// decoration: const InputDecoration( +// labelText: 'Secret Key', +// ), +// ), +// const SizedBox(height: 15), +// TextFormField( +// controller: controller.bucketController, +// validator: (data) => data!.isNotEmpty ? null : 'required'.tr, +// autovalidateMode: AutovalidateMode.onUserInteraction, +// decoration: const InputDecoration( +// labelText: 'Bucket', +// ), +// ), +// const SizedBox(height: 15), +// TextFormField( +// controller: controller.portController, +// // TODO: validator +// inputFormatters: [ +// inputFormatterRestrictSpaces, +// inputFormatterNumericOnly, +// ], +// keyboardType: TextInputType.number, +// decoration: const InputDecoration( +// labelText: 'Port', +// hintText: '8080', +// ), +// ), +// const SizedBox(height: 15), +// TextFormField( +// controller: controller.regionController, +// // TODO: validator +// decoration: const InputDecoration( +// labelText: 'Region', +// ), +// ), +// const SizedBox(height: 15), +// TextFormField( +// controller: controller.sessionTokenController, +// // TODO: validator +// decoration: const InputDecoration( +// labelText: 'Session Token', +// ), +// ), +// const Divider(), +// SecretPersistenceBuilder( +// builder: (p, context) => SwitchListTile( +// title: const Text('Enable Trace'), +// value: p.s3EnableTrace.val, +// onChanged: (value) => p.s3EnableTrace.val = value, +// // contentPadding: EdgeInsets.zero, +// ), +// ), +// const Divider(), +// SecretPersistenceBuilder( +// builder: (p, context) => SwitchListTile( +// title: const Text('Use SSL'), +// value: p.s3UseSsl.val, +// onChanged: (value) => p.s3UseSsl.val = value, +// // contentPadding: EdgeInsets.zero, +// ), +// ), +// const Divider(), +// ], +// ), +// ), +// ); - final actions = [ - TextButton( - onPressed: () => Utils.adaptiveRouteOpen(name: Routes.feedback), - child: const Text('Help ?'), - ), - Obx( - () => controller.busy.value - ? const Padding( - padding: EdgeInsets.all(10), - child: Center( - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ), - ), - ) - : IconButton( - onPressed: controller.testConnection, - icon: const Icon(Icons.check), - ), - ), - const SizedBox(width: 10), - ]; +// final actions = [ +// TextButton( +// onPressed: () => Utils.adaptiveRouteOpen(name: Routes.feedback), +// child: const Text('Help ?'), +// ), +// Obx( +// () => controller.busy.value +// ? const Padding( +// padding: EdgeInsets.all(10), +// child: Center( +// child: SizedBox( +// height: 20, +// width: 20, +// child: CircularProgressIndicator(), +// ), +// ), +// ) +// : IconButton( +// onPressed: controller.testConnection, +// icon: const Icon(Icons.check), +// ), +// ), +// const SizedBox(width: 10), +// ]; - final appBar = AppBar( - title: const Text('S3 Configuration'), - leading: const AppBarLeadingButton(), - actions: actions, - ); +// final appBar = AppBar( +// title: const Text('S3 Configuration'), +// leading: const AppBarLeadingButton(), +// actions: actions, +// ); - return Scaffold( - appBar: appBar, - body: content, - ); - } -} +// return Scaffold( +// appBar: appBar, +// body: content, +// ); +// } +// } diff --git a/lib/features/files/sync.service.dart b/lib/features/files/sync.service.dart index 991a1507..d36d60af 100644 --- a/lib/features/files/sync.service.dart +++ b/lib/features/files/sync.service.dart @@ -126,6 +126,7 @@ class SyncService extends GetxService with ConsoleMixin { ); if (result.isLeft) return Left(result.left); + console.wtf('decrypt sync(): ${result.right.lengthInBytes}'); final decryptedBytes = CipherService.to.decrypt(result.right); final jsonMap = jsonDecode(utf8.decode(decryptedBytes)); // TODO: isolate final vault = LisoVault.fromJson(jsonMap); diff --git a/pubspec.yaml b/pubspec.yaml index f56a78c7..154d245b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,7 @@ dependencies: bip39: ^1.0.6 # Mnemonic Seed Phrases bip32: ^2.0.0 # Derive Path hex: ^0.2.0 # Hex encoding & decoding - encrypt: ^5.0.1 # File Encryption + encrypt: ^5.0.1 otp: ^3.1.4 # IO @@ -38,10 +38,10 @@ dependencies: # API # minio: ^3.5.0 # S3 API - minio: - git: - url: https://github.com/oliverbytes/minio-dart.git - ref: 076c9f33729713c06f885448d86e79092c09d11c + # minio: + # git: + # url: https://github.com/oliverbytes/minio-dart.git + # ref: 076c9f33729713c06f885448d86e79092c09d11c # git: https://github.com/oliverbytes/minio-dart.git # coingecko_api: ^1.1.1+1 # Coingecko API coingecko_api: @@ -57,7 +57,7 @@ dependencies: blur: ^3.1.0 # blurring of seed phrases flutter_json_viewer: ^1.0.1 #json viewer flutter_swipe_action_cell: ^3.1.0 - flutter_quill: ^7.2.1 + flutter_quill: ^8.1.10 badges: ^3.1.1 # pending changes indicator qr_flutter: ^4.1.0 # generate QR Codes skeletons: ^0.0.3 # shimmer loading effects