mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:58:40 +00:00
create backup zip file
This commit is contained in:
parent
4536e82e1b
commit
fcc2c9fb9e
13 changed files with 119 additions and 22 deletions
|
|
@ -1,4 +1,7 @@
|
||||||
class SecureStorageKeys {
|
class SecureStorageKeys {
|
||||||
static const String signalIdentity = "signal_identity";
|
static const String signalIdentity = "signal_identity";
|
||||||
static const String signalSignedPreKey = "signed_pre_key_store";
|
static const String signalSignedPreKey = "signed_pre_key_store";
|
||||||
|
static const String apiAuthToken = "api_auth_token";
|
||||||
|
static const String googleFcm = "google_fcm";
|
||||||
|
static const String userData = "userData";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
|
||||||
// this constructor is required so that the main database can create an instance
|
// this constructor is required so that the main database can create an instance
|
||||||
// of this object.
|
// of this object.
|
||||||
SignalDao(super.db);
|
SignalDao(super.db);
|
||||||
|
|
||||||
Future deleteAllByContactId(int contactId) async {
|
Future deleteAllByContactId(int contactId) async {
|
||||||
await (delete(signalContactPreKeys)
|
await (delete(signalContactPreKeys)
|
||||||
..where((t) => t.contactId.equals(contactId)))
|
..where((t) => t.contactId.equals(contactId)))
|
||||||
|
|
|
||||||
|
|
@ -129,4 +129,13 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
||||||
notifyUpdates({TableUpdate.onTable(messages, kind: UpdateKind.update)});
|
notifyUpdates({TableUpdate.onTable(messages, kind: UpdateKind.update)});
|
||||||
notifyUpdates({TableUpdate.onTable(contacts, kind: UpdateKind.update)});
|
notifyUpdates({TableUpdate.onTable(contacts, kind: UpdateKind.update)});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future deleteDataForTwonlySafe() async {
|
||||||
|
await delete(messages).go();
|
||||||
|
await delete(messageRetransmissions).go();
|
||||||
|
await delete(mediaDownloads).go();
|
||||||
|
await delete(mediaUploads).go();
|
||||||
|
await delete(signalContactPreKeys).go();
|
||||||
|
await delete(signalContactSignedPreKeys).go();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:mutex/mutex.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/app.dart';
|
import 'package:twonly/app.dart';
|
||||||
|
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
|
|
@ -329,7 +330,8 @@ class ApiService {
|
||||||
|
|
||||||
Future<bool> tryAuthenticateWithToken(int userId) async {
|
Future<bool> tryAuthenticateWithToken(int userId) async {
|
||||||
final storage = FlutterSecureStorage();
|
final storage = FlutterSecureStorage();
|
||||||
String? apiAuthToken = await storage.read(key: "api_auth_token");
|
String? apiAuthToken =
|
||||||
|
await storage.read(key: SecureStorageKeys.apiAuthToken);
|
||||||
|
|
||||||
if (apiAuthToken != null) {
|
if (apiAuthToken != null) {
|
||||||
final authenticate = Handshake_Authenticate()
|
final authenticate = Handshake_Authenticate()
|
||||||
|
|
@ -414,7 +416,8 @@ class ApiService {
|
||||||
String apiAuthTokenB64 = base64Encode(apiAuthToken);
|
String apiAuthTokenB64 = base64Encode(apiAuthToken);
|
||||||
|
|
||||||
final storage = FlutterSecureStorage();
|
final storage = FlutterSecureStorage();
|
||||||
await storage.write(key: "api_auth_token", value: apiAuthTokenB64);
|
await storage.write(
|
||||||
|
key: SecureStorageKeys.apiAuthToken, value: apiAuthTokenB64);
|
||||||
|
|
||||||
await tryAuthenticateWithToken(userData.userId);
|
await tryAuthenticateWithToken(userData.userId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import 'package:mutex/mutex.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||||
import 'package:twonly/src/database/tables/media_uploads_table.dart';
|
import 'package:twonly/src/database/tables/media_uploads_table.dart';
|
||||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
|
|
@ -562,8 +563,8 @@ Future<bool> handleMediaUpload(MediaUpload media) async {
|
||||||
|
|
||||||
final uploadRequestBytes = uploadRequest.writeToBuffer();
|
final uploadRequestBytes = uploadRequest.writeToBuffer();
|
||||||
|
|
||||||
final storage = FlutterSecureStorage();
|
String? apiAuthToken =
|
||||||
String? apiAuthToken = await storage.read(key: "api_auth_token");
|
await FlutterSecureStorage().read(key: SecureStorageKeys.apiAuthToken);
|
||||||
if (apiAuthToken == null) {
|
if (apiAuthToken == null) {
|
||||||
Log.error("api auth token not defined.");
|
Log.error("api auth token not defined.");
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,79 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:archive/archive_io.dart';
|
||||||
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:hashlib/hashlib.dart';
|
import 'package:hashlib/hashlib.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||||
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
|
||||||
|
Future performTwonlySafeBackup() async {
|
||||||
|
Log.info("Starting new backup creation.");
|
||||||
|
final baseDir = (await getApplicationSupportDirectory()).path;
|
||||||
|
|
||||||
|
var originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
|
||||||
|
var backupDir = Directory(join(baseDir, "backup_twonly_safe/"));
|
||||||
|
if (backupDir.existsSync()) {
|
||||||
|
await backupDir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
await backupDir.create(recursive: true);
|
||||||
|
|
||||||
|
var backupDatabaseFile =
|
||||||
|
File(join(backupDir.path, "twonly_database.backup.sqlite"));
|
||||||
|
|
||||||
|
// copy database
|
||||||
|
await originalDatabase.copy(backupDatabaseFile.path);
|
||||||
|
|
||||||
|
final backupDB = TwonlyDatabase(
|
||||||
|
driftDatabase(
|
||||||
|
name: "twonly_database.backup",
|
||||||
|
native: DriftNativeOptions(
|
||||||
|
databaseDirectory: () async {
|
||||||
|
return backupDir;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await backupDB.deleteDataForTwonlySafe();
|
||||||
|
|
||||||
|
var secureStorageBackup = {};
|
||||||
|
final storage = FlutterSecureStorage();
|
||||||
|
secureStorageBackup[SecureStorageKeys.signalIdentity] =
|
||||||
|
await storage.read(key: SecureStorageKeys.signalIdentity);
|
||||||
|
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
|
||||||
|
await storage.read(key: SecureStorageKeys.signalSignedPreKey);
|
||||||
|
secureStorageBackup[SecureStorageKeys.userData] =
|
||||||
|
await storage.read(key: SecureStorageKeys.userData);
|
||||||
|
|
||||||
|
var backupSecureStorage = File(join(backupDir.path, "secure_storage.json"));
|
||||||
|
|
||||||
|
await backupSecureStorage.writeAsString(jsonEncode(secureStorageBackup));
|
||||||
|
|
||||||
|
Log.info("Backup files created.");
|
||||||
|
|
||||||
|
var twonlySafeBackupZip = File(join(backupDir.path, "twonly_safe.zip"));
|
||||||
|
|
||||||
|
await createZipArchive(
|
||||||
|
twonlySafeBackupZip.path, [backupSecureStorage, backupDatabaseFile]);
|
||||||
|
|
||||||
|
// await backupDir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createZipArchive(String zipFilePath, List<File> filesToZip) async {
|
||||||
|
final encoder = ZipFileEncoder();
|
||||||
|
encoder.create(zipFilePath);
|
||||||
|
for (var file in filesToZip) {
|
||||||
|
await encoder.addFile(file);
|
||||||
|
}
|
||||||
|
await encoder.close();
|
||||||
|
}
|
||||||
|
|
||||||
Future enableTwonlySafe(String password) async {
|
Future enableTwonlySafe(String password) async {
|
||||||
final user = await getUser();
|
final user = await getUser();
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
|
|
@ -16,6 +87,7 @@ Future enableTwonlySafe(String password) async {
|
||||||
return user;
|
return user;
|
||||||
});
|
});
|
||||||
startTwonlySafeBackup();
|
startTwonlySafeBackup();
|
||||||
|
performTwonlySafeBackup();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future disableTwonlySafe() async {
|
Future disableTwonlySafe() async {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/app.dart';
|
import 'package:twonly/app.dart';
|
||||||
|
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/services/notification.service.dart';
|
import 'package:twonly/src/services/notification.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -16,7 +17,7 @@ Future initFCMAfterAuthenticated() async {
|
||||||
|
|
||||||
final storage = FlutterSecureStorage();
|
final storage = FlutterSecureStorage();
|
||||||
|
|
||||||
String? storedToken = await storage.read(key: "google_fcm");
|
String? storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||||
|
|
@ -27,12 +28,12 @@ Future initFCMAfterAuthenticated() async {
|
||||||
|
|
||||||
if (storedToken == null || fcmToken != storedToken) {
|
if (storedToken == null || fcmToken != storedToken) {
|
||||||
await apiService.updateFCMToken(fcmToken);
|
await apiService.updateFCMToken(fcmToken);
|
||||||
await storage.write(key: "google_fcm", value: fcmToken);
|
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) async {
|
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) async {
|
||||||
await apiService.updateFCMToken(fcmToken);
|
await apiService.updateFCMToken(fcmToken);
|
||||||
await storage.write(key: "google_fcm", value: fcmToken);
|
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
|
||||||
}).onError((err) {
|
}).onError((err) {
|
||||||
Log.error("could not listen on token refresh");
|
Log.error("could not listen on token refresh");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||||
import 'package:twonly/src/model/json/userdata.dart';
|
import 'package:twonly/src/model/json/userdata.dart';
|
||||||
import 'package:twonly/src/providers/connection.provider.dart';
|
import 'package:twonly/src/providers/connection.provider.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -17,8 +18,8 @@ Future<bool> isUserCreated() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UserData?> getUser() async {
|
Future<UserData?> getUser() async {
|
||||||
final storage = FlutterSecureStorage();
|
String? userJson =
|
||||||
String? userJson = await storage.read(key: "userData");
|
await FlutterSecureStorage().read(key: SecureStorageKeys.userData);
|
||||||
if (userJson == null) {
|
if (userJson == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -51,8 +52,8 @@ Future<UserData?> updateUserdata(Function(UserData userData) updateUser) async {
|
||||||
final user = await getUser();
|
final user = await getUser();
|
||||||
if (user == null) return null;
|
if (user == null) return null;
|
||||||
UserData updated = updateUser(user);
|
UserData updated = updateUser(user);
|
||||||
final storage = FlutterSecureStorage();
|
FlutterSecureStorage()
|
||||||
storage.write(key: "userData", value: jsonEncode(updated));
|
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated));
|
||||||
return user;
|
return user;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +63,6 @@ Future<bool> deleteLocalUserData() async {
|
||||||
if (appDir.existsSync()) {
|
if (appDir.existsSync()) {
|
||||||
appDir.deleteSync(recursive: true);
|
appDir.deleteSync(recursive: true);
|
||||||
}
|
}
|
||||||
final storage = FlutterSecureStorage();
|
await FlutterSecureStorage().deleteAll();
|
||||||
await storage.deleteAll();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||||
|
|
@ -32,8 +33,6 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
_isTryingToRegister = true;
|
_isTryingToRegister = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
final storage = FlutterSecureStorage();
|
|
||||||
|
|
||||||
await createIfNotExistsSignalIdentity();
|
await createIfNotExistsSignalIdentity();
|
||||||
|
|
||||||
int userId = 0;
|
int userId = 0;
|
||||||
|
|
@ -66,7 +65,10 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
subscriptionPlan: "Preview",
|
subscriptionPlan: "Preview",
|
||||||
isDemoUser: isDemoAccount,
|
isDemoUser: isDemoAccount,
|
||||||
);
|
);
|
||||||
storage.write(key: "userData", value: jsonEncode(userData));
|
|
||||||
|
FlutterSecureStorage()
|
||||||
|
.write(key: SecureStorageKeys.userData, value: jsonEncode(userData));
|
||||||
|
|
||||||
if (!isDemoAccount) {
|
if (!isDemoAccount) {
|
||||||
await apiService.authenticate();
|
await apiService.authenticate();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/src/services/backup.identitiy.service.dart';
|
import 'package:twonly/src/services/backup.identitiy.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||||
import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart';
|
import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart';
|
||||||
|
|
||||||
class BackupView extends StatefulWidget {
|
class BackupView extends StatefulWidget {
|
||||||
|
|
@ -49,7 +50,11 @@ class _BackupViewState extends State<BackupView> {
|
||||||
autoBackupEnabled: _twonlyIdBackupEnabled,
|
autoBackupEnabled: _twonlyIdBackupEnabled,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (_twonlyIdBackupEnabled) {
|
if (_twonlyIdBackupEnabled) {
|
||||||
|
bool disable = await showAlertDialog(context, "Are you sure?",
|
||||||
|
"Without an backup, you can not restore your user account.");
|
||||||
|
if (disable) {
|
||||||
await disableTwonlySafe();
|
await disableTwonlySafe();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await Navigator.push(context,
|
await Navigator.push(context,
|
||||||
MaterialPageRoute(builder: (context) {
|
MaterialPageRoute(builder: (context) {
|
||||||
|
|
@ -110,7 +115,7 @@ class BackupOption extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: (autoBackupEnabled) ? null : onTap,
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.all(8.0),
|
margin: EdgeInsets.all(8.0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||||
import 'package:twonly/src/services/fcm.service.dart';
|
import 'package:twonly/src/services/fcm.service.dart';
|
||||||
import 'package:twonly/src/services/notification.service.dart';
|
import 'package:twonly/src/services/notification.service.dart';
|
||||||
|
|
@ -26,8 +27,8 @@ class NotificationView extends StatelessWidget {
|
||||||
subtitle: Text(context.lang.settingsNotifyTroubleshootingDesc),
|
subtitle: Text(context.lang.settingsNotifyTroubleshootingDesc),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await initFCMAfterAuthenticated();
|
await initFCMAfterAuthenticated();
|
||||||
final storage = FlutterSecureStorage();
|
String? storedToken = await FlutterSecureStorage()
|
||||||
String? storedToken = await storage.read(key: "google_fcm");
|
.read(key: SecureStorageKeys.googleFcm);
|
||||||
await setupNotificationWithUsers(force: true);
|
await setupNotificationWithUsers(force: true);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.4.5"
|
version: "7.4.5"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ dependencies:
|
||||||
tutorial_coach_mark: ^1.3.0
|
tutorial_coach_mark: ^1.3.0
|
||||||
background_downloader: ^9.2.2
|
background_downloader: ^9.2.2
|
||||||
hashlib: ^2.0.0
|
hashlib: ^2.0.0
|
||||||
|
archive: ^4.0.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue