mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
opt.: webdav sync logic
This commit is contained in:
@@ -3,9 +3,9 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:icloud_storage/icloud_storage.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/data/model/app/backup.dart';
|
||||
import 'package:toolbox/data/model/app/sync.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
|
||||
import '../../../data/model/app/error.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
@@ -13,6 +13,8 @@ import '../../../data/res/path.dart';
|
||||
abstract final class ICloud {
|
||||
static const _containerId = 'iCloud.tech.lolli.serverbox';
|
||||
|
||||
static final _logger = Logger('iCloud');
|
||||
|
||||
/// Upload file to iCloud
|
||||
///
|
||||
/// - [relativePath] is the path relative to [docDir],
|
||||
@@ -171,12 +173,12 @@ abstract final class ICloud {
|
||||
|
||||
return SyncResult(up: uploadFiles, down: downloadFiles, err: errs);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('iCloud sync: $relativePaths failed', e, s);
|
||||
_logger.warning('Sync: $relativePaths failed', e, s);
|
||||
return SyncResult(up: uploadFiles, down: downloadFiles, err: {
|
||||
'Generic': ICloudErr(type: ICloudErrType.generic, message: '$e')
|
||||
});
|
||||
} finally {
|
||||
Loggers.app.info('iCloud sync, up: $uploadFiles, down: $downloadFiles');
|
||||
_logger.info('Sync, up: $uploadFiles, down: $downloadFiles');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,30 +186,30 @@ abstract final class ICloud {
|
||||
try {
|
||||
final result = await download(relativePath: Paths.bakName);
|
||||
if (result != null) {
|
||||
Loggers.app.warning('Download backup failed: $result');
|
||||
_logger.warning('Download backup failed: $result');
|
||||
return;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Download backup failed', e, s);
|
||||
_logger.warning('Download backup failed', e, s);
|
||||
}
|
||||
final dlFile = await File(await Paths.bak).readAsString();
|
||||
final dlBak = await compute(Backup.fromJsonString, dlFile);
|
||||
final restore = await dlBak.restore();
|
||||
switch (restore) {
|
||||
case true:
|
||||
Loggers.app.info('Restore from iCloud (${dlBak.lastModTime}) success');
|
||||
_logger.info('Restore from ${dlBak.lastModTime} success');
|
||||
break;
|
||||
case false:
|
||||
await Backup.backup();
|
||||
final uploadResult = await upload(relativePath: Paths.bakName);
|
||||
if (uploadResult != null) {
|
||||
Loggers.app.warning('Upload iCloud backup failed: $uploadResult');
|
||||
_logger.warning('Upload backup failed: $uploadResult');
|
||||
} else {
|
||||
Loggers.app.info('Upload iCloud backup success');
|
||||
_logger.info('Upload backup success');
|
||||
}
|
||||
break;
|
||||
case null:
|
||||
Loggers.app.info('Skip iCloud sync');
|
||||
_logger.info('Skip sync');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/data/model/app/backup.dart';
|
||||
import 'package:toolbox/data/model/app/error.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/path.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
// ignore: implementation_imports
|
||||
@@ -16,13 +16,15 @@ abstract final class Webdav {
|
||||
pwd: Stores.setting.webdavPwd.fetch(),
|
||||
);
|
||||
|
||||
static final _logger = Logger('Webdav');
|
||||
|
||||
static Future<String?> test(String url, String user, String pwd) async {
|
||||
final client = WebdavClient(url: url, user: user, pwd: pwd);
|
||||
try {
|
||||
await client.ping();
|
||||
return null;
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Webdav test failed', e, s);
|
||||
_logger.warning('Test failed', e, s);
|
||||
return e.toString();
|
||||
}
|
||||
}
|
||||
@@ -37,7 +39,7 @@ abstract final class Webdav {
|
||||
relativePath,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Webdav upload failed', e, s);
|
||||
_logger.warning('Upload $relativePath failed', e, s);
|
||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
||||
}
|
||||
return null;
|
||||
@@ -47,7 +49,7 @@ abstract final class Webdav {
|
||||
try {
|
||||
await _client.remove(relativePath);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Webdav delete failed', e, s);
|
||||
_logger.warning('Delete $relativePath failed', e, s);
|
||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
||||
}
|
||||
return null;
|
||||
@@ -62,8 +64,8 @@ abstract final class Webdav {
|
||||
relativePath,
|
||||
localPath ?? '${await Paths.doc}/$relativePath',
|
||||
);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Webdav download failed', e, s);
|
||||
} catch (e) {
|
||||
_logger.warning('Download $relativePath failed');
|
||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
||||
}
|
||||
return null;
|
||||
@@ -74,34 +76,49 @@ abstract final class Webdav {
|
||||
}
|
||||
|
||||
static Future<void> sync() async {
|
||||
try {
|
||||
final result = await download(relativePath: Paths.bakName);
|
||||
if (result != null) {
|
||||
Loggers.app.warning('Download backup failed: $result');
|
||||
return;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Download backup failed', e, s);
|
||||
final result = await download(relativePath: Paths.bakName);
|
||||
if (result != null) {
|
||||
await backup();
|
||||
return;
|
||||
}
|
||||
final dlFile = await File(await Paths.bak).readAsString();
|
||||
final dlBak = await compute(Backup.fromJsonString, dlFile);
|
||||
final restore = await dlBak.restore();
|
||||
final dlFile = await compute(
|
||||
(message) async {
|
||||
try {
|
||||
final file = await File(message).readAsString();
|
||||
final bak = Backup.fromJsonString(file);
|
||||
return bak;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
await Paths.bak,
|
||||
);
|
||||
if (dlFile == null) {
|
||||
await backup();
|
||||
return;
|
||||
}
|
||||
final restore = await dlFile.restore();
|
||||
switch (restore) {
|
||||
case true:
|
||||
Loggers.app.info('Restore from iCloud (${dlBak.lastModTime}) success');
|
||||
_logger.info('Restore from ${dlFile.lastModTime} success');
|
||||
break;
|
||||
case false:
|
||||
await Backup.backup();
|
||||
final uploadResult = await upload(relativePath: Paths.bakName);
|
||||
if (uploadResult != null) {
|
||||
Loggers.app.warning('Upload iCloud backup failed: $uploadResult');
|
||||
} else {
|
||||
Loggers.app.info('Upload iCloud backup success');
|
||||
}
|
||||
await backup();
|
||||
break;
|
||||
case null:
|
||||
Loggers.app.info('Skip iCloud sync');
|
||||
_logger.info('Skip sync');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a local backup and upload it to WebDAV
|
||||
static Future<void> backup() async {
|
||||
await Backup.backup();
|
||||
final uploadResult = await upload(relativePath: Paths.bakName);
|
||||
if (uploadResult != null) {
|
||||
_logger.warning('Upload failed: $uploadResult');
|
||||
} else {
|
||||
_logger.info('Upload success');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@ class Backup {
|
||||
return path;
|
||||
}
|
||||
|
||||
/// - Return null if same time
|
||||
/// - Return false if local is newer
|
||||
/// - Return true if restore success
|
||||
Future<bool?> restore({bool force = false}) async {
|
||||
final curTime = Stores.lastModTime ?? 0;
|
||||
final thisTime = lastModTime ?? 0;
|
||||
@@ -108,7 +111,7 @@ class Backup {
|
||||
|
||||
Pros.reload();
|
||||
RebuildNodes.app.rebuild();
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:toolbox/core/channel/bg_run.dart';
|
||||
import 'package:toolbox/core/utils/sync/icloud.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/sync/webdav.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
@@ -95,6 +96,7 @@ Future<void> initApp() async {
|
||||
if (isIOS || isMacOS) {
|
||||
if (Stores.setting.icloudSync.fetch()) ICloud.sync();
|
||||
}
|
||||
if (Stores.setting.webdavSync.fetch()) Webdav.sync();
|
||||
}
|
||||
|
||||
void _setupProviders() {
|
||||
|
||||
@@ -54,7 +54,7 @@ class BackupPage extends StatelessWidget {
|
||||
Widget _buildFile(BuildContext context) {
|
||||
return CardX(
|
||||
ExpandTile(
|
||||
leading: const Icon(Icons.file_open, size: 19),
|
||||
leading: const Icon(Icons.file_open),
|
||||
title: Text(l10n.files),
|
||||
initiallyExpanded: true,
|
||||
children: [
|
||||
@@ -139,7 +139,7 @@ class BackupPage extends StatelessWidget {
|
||||
Widget _buildIcloud(BuildContext context) {
|
||||
return CardX(
|
||||
ExpandTile(
|
||||
leading: const Icon(Icons.cloud, size: 19),
|
||||
leading: const Icon(Icons.cloud),
|
||||
title: const Text('iCloud'),
|
||||
initiallyExpanded: true,
|
||||
children: [
|
||||
@@ -231,7 +231,7 @@ class BackupPage extends StatelessWidget {
|
||||
Widget _buildWebdav(BuildContext context) {
|
||||
return CardX(
|
||||
ExpandTile(
|
||||
leading: const Icon(Icons.storage, size: 19),
|
||||
leading: const Icon(Icons.storage),
|
||||
title: const Text('WebDAV'),
|
||||
initiallyExpanded: !(isIOS || isMacOS),
|
||||
children: [
|
||||
|
||||
90
pubspec.lock
90
pubspec.lock
@@ -25,14 +25,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansicolor
|
||||
sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
archive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: archive
|
||||
sha256: "7e0d52067d05f2e0324268097ba723b71cb41ac8a6a2b24d1edf9c536b987b03"
|
||||
sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.6"
|
||||
version: "3.4.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -93,10 +101,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65"
|
||||
sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "4.0.1"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -109,10 +117,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b"
|
||||
sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.6"
|
||||
version: "2.4.7"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -133,10 +141,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "723b4021e903217dfc445ec4cf5b42e27975aece1fc4ebbc1ca6329c2d9fb54e"
|
||||
sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.7.0"
|
||||
version: "8.8.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -182,10 +190,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677"
|
||||
sha256: b2151ce26a06171005b379ecff6e08d34c470180ffe16b8e14b6d52be292b55f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.0"
|
||||
version: "4.8.0"
|
||||
code_text_field:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -214,18 +222,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: countly_flutter
|
||||
sha256: "8e774b37b926523458dc7f36e4e4975671b4b2108cdfc284371c08b1bc2b475b"
|
||||
sha256: "06260f1a15c2abfcb0b5c96dc986de640fae27607c6b0cfdba60d5c50cf2173e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "23.8.3"
|
||||
version: "23.8.4"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c"
|
||||
sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+6"
|
||||
version: "0.3.3+8"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -262,10 +270,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334
|
||||
sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "2.3.4"
|
||||
dartssh2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -278,10 +286,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
|
||||
sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.3"
|
||||
version: "5.4.0"
|
||||
dynamic_color:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -376,10 +384,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_native_splash
|
||||
sha256: d93394f22f73e810bda59e11ebe83329c5511d6460b6b7509c4e1f3c92d6d625
|
||||
sha256: c4d899312b36e7454bedfd0a4740275837b99e532d81c8477579d8183db1de6c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.3.6"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -754,10 +762,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6
|
||||
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
version: "6.0.2"
|
||||
pinenacl:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -786,10 +794,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
|
||||
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
version: "2.1.7"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -810,10 +818,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
|
||||
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.5"
|
||||
version: "6.1.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -906,10 +914,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
|
||||
sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.2"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1063,10 +1071,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a"
|
||||
sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
version: "6.2.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1095,10 +1103,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2"
|
||||
sha256: "138bd45b3a456dcfafc46d1a146787424f8d2edfbf2809c9324361e58f851cf7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1111,10 +1119,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7
|
||||
sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "4.2.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1169,7 +1177,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: "233b3ebaa01b7bb35a414c9cfd5e2933b3a008ba"
|
||||
resolved-ref: "88de97ad783c624d81b0dbcc09927b10aabd5580"
|
||||
url: "https://github.com/lollipopkit/webdav_client"
|
||||
source: git
|
||||
version: "1.2.1"
|
||||
@@ -1177,10 +1185,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
|
||||
sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.9"
|
||||
version: "5.1.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1193,10 +1201,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xml
|
||||
sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.2"
|
||||
version: "6.5.0"
|
||||
xterm:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1223,5 +1231,5 @@ packages:
|
||||
source: hosted
|
||||
version: "0.0.6"
|
||||
sdks:
|
||||
dart: ">=3.2.0-194.0.dev <4.0.0"
|
||||
flutter: ">=3.13.0"
|
||||
dart: ">=3.2.0 <4.0.0"
|
||||
flutter: ">=3.16.0"
|
||||
|
||||
Reference in New Issue
Block a user