diff --git a/README.md b/README.md index 852907e..1128e98 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ This is an unofficial application. The AdGuard Home team and the development of - [flutter svg](https://pub.dev/packages/flutter_svg) - [bottom sheet](https://pub.dev/packages/bottom_sheet) - [percent indicator](https://pub.dev/packages/percent_indicator) +- [store checker](https://pub.dev/packages/store_checker) +- [fl downloader](https://pub.dev/packages/fl_downloader) +- [install plugin v2](https://pub.dev/packages/install_plugin_v2) +- [permission handler](https://pub.dev/packages/permission_handler)
diff --git a/android/app/build.gradle b/android/app/build.gradle index 814311b..d93673c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 33 ndkVersion flutter.ndkVersion compileOptions { diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index bf4e526..5b76055 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -5,4 +5,9 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index aada37e..6c51c3b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,9 +1,15 @@ + + + + + + + + diff --git a/lib/base.dart b/lib/base.dart index ed649a0..4eb984e 100644 --- a/lib/base.dart +++ b/lib/base.dart @@ -1,25 +1,137 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:animations/animations.dart'; +import 'package:install_plugin_v2/install_plugin_v2.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; +import 'package:store_checker/store_checker.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/bottom_nav_bar.dart'; +import 'package:adguard_home_manager/widgets/update_modal.dart'; +import 'package:adguard_home_manager/widgets/download_modal.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/models/github_release.dart'; +import 'package:adguard_home_manager/constants/package_name.dart'; +import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/models/app_screen.dart'; import 'package:adguard_home_manager/config/app_screens.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; class Base extends StatefulWidget { - const Base({Key? key}) : super(key: key); + final AppConfigProvider appConfigProvider; + + const Base({ + Key? key, + required this.appConfigProvider, + }) : super(key: key); @override State createState() => _BaseState(); } -class _BaseState extends State { +class _BaseState extends State with WidgetsBindingObserver { int selectedScreen = 0; + Future checkInstallationSource() async { + Source installationSource = await StoreChecker.getSource; + if (installationSource != Source.IS_INSTALLED_FROM_PLAY_STORE) { + final result = await checkAppUpdatesGitHub(); + if (result['result'] == 'success') { + if (result['body'].tagName != widget.appConfigProvider.getAppInfo!.version) { + return result['body']; + } + } + } + return null; + } + + Future managePermission() async { + try { + if (await Permission.storage.isGranted) { + return true; + } + else { + final PermissionStatus status = await Permission.storage.request(); + if (status.isGranted == false) { + return false; + } + else { + return true; + } + } + } catch (e) { + return false; + } + } + + void installApk(String path) async { + final granted = await managePermission(); + + if (granted == true) { + path = path.replaceFirst(r'file://', ''); + + final file = File(path); + final exists = await file.exists(); + + if (exists) { + InstallPlugin.installApk( + path, PackageName.packageName + ); + } + } + } + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + void download(String link, String version) async { + final granted = await managePermission(); + if (granted == true) { + showDialog( + context: context, + builder: (context) => DownloadModal( + url: link, + version: version, + onFinish: installApk, + ), + barrierDismissible: false + ); + } + else { + showSnacbkar( + context: context, + appConfigProvider: widget.appConfigProvider, + label: AppLocalizations.of(context)!.permissionNotGranted, + color: Colors.red + ); + } + } + + final result = await checkInstallationSource(); + + if (result != null) { + await showDialog( + context: context, + builder: (context) => UpdateModal( + gitHubRelease: result, + onDownload: download, + ), + ); + } + }); + } + @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); diff --git a/lib/constants/package_name.dart b/lib/constants/package_name.dart new file mode 100644 index 0000000..e693d89 --- /dev/null +++ b/lib/constants/package_name.dart @@ -0,0 +1,3 @@ +class PackageName { + static const String packageName = "com.jgeek00.adguard_home_manager"; +} \ No newline at end of file diff --git a/lib/constants/urls.dart b/lib/constants/urls.dart index 894856f..5b60855 100644 --- a/lib/constants/urls.dart +++ b/lib/constants/urls.dart @@ -2,4 +2,5 @@ class Urls { static const String playStore = "https://play.google.com/store/apps/details?id=com.jgeek00.adguard_home_manager"; static const String gitHub = "https://github.com/JGeek00/adguard-home-manager"; static const String customRuleDocs = "https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters"; + static const String checkLatestReleaseUrl = "https://api.github.com/repos/JGeek00/adguard-home-manager/releases/latest"; } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a7109d9..c0622ba 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -559,5 +559,15 @@ "nameTimeLogs": "Name and time on logs", "nameTimeLogsDescription": "Show client name and processing time on logs list", "hostNames": "Host names", - "keyType": "Key type" + "keyType": "Key type", + "updateAvailable": "Update available", + "installedVersion": "Installed version", + "newVersion": "New version", + "source": "Source", + "downloadUpdate": "Download update", + "download": "Download", + "doNotRememberAgainUpdate": "Do not remember again for this version", + "downloadingUpdate": "Downloading", + "completed": "completed", + "permissionNotGranted": "Permission not granted" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index d8976fe..0b558bf 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -559,5 +559,15 @@ "nameTimeLogs": "Nombre y tiempo en logs", "nameTimeLogsDescription": "Mostrar el nombre del cliente y el tiempo de procesamiento en la lista de logs", "hostNames": "Nombres de host", - "keyType": "Tipo de clave" + "keyType": "Tipo de clave", + "updateAvailable": "Actualización disponible", + "installedVersion": "Versión instalada", + "newVersion": "Nueva versión", + "source": "Fuente", + "downloadUpdate": "Descargar actualización", + "download": "Descargar", + "doNotRememberAgainUpdate": "No recordar de nuevo para esta actualización", + "downloadingUpdate": "Descargando", + "completed": "completado", + "permissionNotGranted": "Permiso no concedido" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index cc10c9b..5ef8610 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -137,7 +137,7 @@ class _MainState extends State
{ child: child!, ); }, - home: const Base(), + home: Base(appConfigProvider: appConfigProvider), ), ); } diff --git a/lib/models/github_release.dart b/lib/models/github_release.dart new file mode 100644 index 0000000..80746d5 --- /dev/null +++ b/lib/models/github_release.dart @@ -0,0 +1,232 @@ +class GitHubRelease { + final String url; + final String assetsUrl; + final String uploadUrl; + final String htmlUrl; + final int id; + final Author author; + final String nodeId; + final String tagName; + final String targetCommitish; + final String name; + final bool draft; + final bool prerelease; + final DateTime createdAt; + final DateTime publishedAt; + final List assets; + final String tarballUrl; + final String zipballUrl; + final String body; + + GitHubRelease({ + required this.url, + required this.assetsUrl, + required this.uploadUrl, + required this.htmlUrl, + required this.id, + required this.author, + required this.nodeId, + required this.tagName, + required this.targetCommitish, + required this.name, + required this.draft, + required this.prerelease, + required this.createdAt, + required this.publishedAt, + required this.assets, + required this.tarballUrl, + required this.zipballUrl, + required this.body, + }); + + + factory GitHubRelease.fromJson(Map json) => GitHubRelease( + url: json["url"], + assetsUrl: json["assets_url"], + uploadUrl: json["upload_url"], + htmlUrl: json["html_url"], + id: json["id"], + author: Author.fromJson(json["author"]), + nodeId: json["node_id"], + tagName: json["tag_name"], + targetCommitish: json["target_commitish"], + name: json["name"], + draft: json["draft"], + prerelease: json["prerelease"], + createdAt: DateTime.parse(json["created_at"]), + publishedAt: DateTime.parse(json["published_at"]), + assets: List.from(json["assets"].map((x) => Asset.fromJson(x))), + tarballUrl: json["tarball_url"], + zipballUrl: json["zipball_url"], + body: json["body"], + ); + + Map toJson() => { + "url": url, + "assets_url": assetsUrl, + "upload_url": uploadUrl, + "html_url": htmlUrl, + "id": id, + "author": author.toJson(), + "node_id": nodeId, + "tag_name": tagName, + "target_commitish": targetCommitish, + "name": name, + "draft": draft, + "prerelease": prerelease, + "created_at": createdAt.toIso8601String(), + "published_at": publishedAt.toIso8601String(), + "assets": List.from(assets.map((x) => x.toJson())), + "tarball_url": tarballUrl, + "zipball_url": zipballUrl, + "body": body, + }; +} + +class Asset { + final String url; + final int id; + final String nodeId; + final String name; + final dynamic label; + final Author uploader; + final String contentType; + final String state; + final int size; + final int downloadCount; + final DateTime createdAt; + final DateTime updatedAt; + final String browserDownloadUrl; + + Asset({ + required this.url, + required this.id, + required this.nodeId, + required this.name, + required this.label, + required this.uploader, + required this.contentType, + required this.state, + required this.size, + required this.downloadCount, + required this.createdAt, + required this.updatedAt, + required this.browserDownloadUrl, + }); + + factory Asset.fromJson(Map json) => Asset( + url: json["url"], + id: json["id"], + nodeId: json["node_id"], + name: json["name"], + label: json["label"], + uploader: Author.fromJson(json["uploader"]), + contentType: json["content_type"], + state: json["state"], + size: json["size"], + downloadCount: json["download_count"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + browserDownloadUrl: json["browser_download_url"], + ); + + Map toJson() => { + "url": url, + "id": id, + "node_id": nodeId, + "name": name, + "label": label, + "uploader": uploader.toJson(), + "content_type": contentType, + "state": state, + "size": size, + "download_count": downloadCount, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "browser_download_url": browserDownloadUrl, + }; +} + +class Author { + final String login; + final int id; + final String nodeId; + final String avatarUrl; + final String gravatarId; + final String url; + final String htmlUrl; + final String followersUrl; + final String followingUrl; + final String gistsUrl; + final String starredUrl; + final String subscriptionsUrl; + final String organizationsUrl; + final String reposUrl; + final String eventsUrl; + final String receivedEventsUrl; + final String type; + final bool siteAdmin; + + Author({ + required this.login, + required this.id, + required this.nodeId, + required this.avatarUrl, + required this.gravatarId, + required this.url, + required this.htmlUrl, + required this.followersUrl, + required this.followingUrl, + required this.gistsUrl, + required this.starredUrl, + required this.subscriptionsUrl, + required this.organizationsUrl, + required this.reposUrl, + required this.eventsUrl, + required this.receivedEventsUrl, + required this.type, + required this.siteAdmin, + }); + + factory Author.fromJson(Map json) => Author( + login: json["login"], + id: json["id"], + nodeId: json["node_id"], + avatarUrl: json["avatar_url"], + gravatarId: json["gravatar_id"], + url: json["url"], + htmlUrl: json["html_url"], + followersUrl: json["followers_url"], + followingUrl: json["following_url"], + gistsUrl: json["gists_url"], + starredUrl: json["starred_url"], + subscriptionsUrl: json["subscriptions_url"], + organizationsUrl: json["organizations_url"], + reposUrl: json["repos_url"], + eventsUrl: json["events_url"], + receivedEventsUrl: json["received_events_url"], + type: json["type"], + siteAdmin: json["site_admin"], + ); + + Map toJson() => { + "login": login, + "id": id, + "node_id": nodeId, + "avatar_url": avatarUrl, + "gravatar_id": gravatarId, + "url": url, + "html_url": htmlUrl, + "followers_url": followersUrl, + "following_url": followingUrl, + "gists_url": gistsUrl, + "starred_url": starredUrl, + "subscriptions_url": subscriptionsUrl, + "organizations_url": organizationsUrl, + "repos_url": reposUrl, + "events_url": eventsUrl, + "received_events_url": receivedEventsUrl, + "type": type, + "site_admin": siteAdmin, + }; +} diff --git a/lib/services/http_requests.dart b/lib/services/http_requests.dart index 7a90887..686c989 100644 --- a/lib/services/http_requests.dart +++ b/lib/services/http_requests.dart @@ -8,6 +8,7 @@ import 'package:adguard_home_manager/models/dhcp.dart'; import 'package:adguard_home_manager/models/dns_info.dart'; import 'package:adguard_home_manager/models/encryption.dart'; import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/models/github_release.dart'; import 'package:adguard_home_manager/models/logs.dart'; import 'package:adguard_home_manager/models/filtering_status.dart'; import 'package:adguard_home_manager/models/app_log.dart'; @@ -17,6 +18,7 @@ import 'package:adguard_home_manager/models/server_status.dart'; import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; import 'package:adguard_home_manager/models/server.dart'; +import 'package:adguard_home_manager/constants/urls.dart'; Future> apiRequest({ @@ -1811,4 +1813,75 @@ Future saveEncryptionSettings({ else { return result; } +} + +Future checkAppUpdatesGitHub() async { + try { + HttpClient httpClient = HttpClient(); + HttpClientRequest request = await httpClient.getUrl(Uri.parse(Urls.checkLatestReleaseUrl)); + HttpClientResponse response = await request.close(); + String reply = await response.transform(utf8.decoder).join(); + httpClient.close(); + if (response.statusCode == 200) { + return { + 'result': 'success', + 'hasResponse': true, + 'error': false, + 'statusCode': response.statusCode, + 'body': GitHubRelease.fromJson(jsonDecode(reply)) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_encryption_settings', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: response.statusCode.toString(), + resBody: reply, + ) + }; + } + } on SocketException { + return { + 'result': 'no_connection', + 'message': 'SocketException', + 'log': AppLog( + type: 'check_latest_release_github', + dateTime: DateTime.now(), + message: 'SocketException' + ) + }; + } on TimeoutException { + return { + 'result': 'no_connection', + 'message': 'TimeoutException', + 'log': AppLog( + type: 'check_latest_release_github', + dateTime: DateTime.now(), + message: 'TimeoutException' + ) + }; + } on HandshakeException { + return { + 'result': 'ssl_error', + 'message': 'HandshakeException', + 'log': AppLog( + type: 'check_latest_release_github', + dateTime: DateTime.now(), + message: 'HandshakeException' + ) + }; + } catch (e) { + return { + 'result': 'error', + 'message': e.toString(), + 'log': AppLog( + type: 'check_latest_release_github', + dateTime: DateTime.now(), + message: e.toString() + ) + }; + } } \ No newline at end of file diff --git a/lib/widgets/download_modal.dart b/lib/widgets/download_modal.dart new file mode 100644 index 0000000..1241fe5 --- /dev/null +++ b/lib/widgets/download_modal.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:percent_indicator/percent_indicator.dart'; +import 'package:fl_downloader/fl_downloader.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class DownloadModal extends StatefulWidget { + final String url; + final String version; + final void Function(String) onFinish; + + const DownloadModal({ + Key? key, + required this.url, + required this.version, + required this.onFinish, + }) : super(key: key); + + @override + State createState() => _DownloadModalState(); +} + +class _DownloadModalState extends State { + int progress = 0; + late StreamSubscription progressStream; + + void download() async { + final downloads = Directory('/storage/emulated/0/Download').listSync(); + final installers = downloads.where((file) => file.path.contains('adguard-home-manager_v')); + + try { + for (FileSystemEntity installer in installers) { + if (await installer.exists()) { + await installer.delete(); + } + } + + FlDownloader.initialize(); + progressStream = FlDownloader.progressStream.listen((event) { + if (event.status == DownloadStatus.successful) { + setState(() => progress = event.progress); + + Navigator.pop(context); + widget.onFinish(event.filePath!); + } + else if (event.status == DownloadStatus.running) { + setState(() => progress = event.progress); + } + }); + + FlDownloader.download(widget.url, fileName: 'adguard-home-manager_v${widget.version}.apk'); + } catch (_) { + + } + } + + @override + void initState() { + super.initState(); + + download(); + } + + @override + void dispose() { + super.dispose(); + progressStream.cancel(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("${AppLocalizations.of(context)!.downloadingUpdate}..."), + titlePadding: const EdgeInsets.only( + left: 24, right: 24, top: 24, bottom: 10, + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + LinearPercentIndicator( + animation: true, + lineHeight: 4, + animationDuration: 500, + curve: Curves.easeOut, + percent: progress/100, + animateFromLastPercent: true, + barRadius: const Radius.circular(5), + progressColor: Theme.of(context).primaryColor, + backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: Text("$progress% ${AppLocalizations.of(context)!.completed}"), + ) + ], + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/update_modal.dart b/lib/widgets/update_modal.dart new file mode 100644 index 0000000..cbaa2c0 --- /dev/null +++ b/lib/widgets/update_modal.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/github_release.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class UpdateModal extends StatefulWidget { + final GitHubRelease gitHubRelease; + final void Function(String, String) onDownload; + + const UpdateModal({ + Key? key, + required this.gitHubRelease, + required this.onDownload, + }) : super(key: key); + + @override + State createState() => _UpdateModalState(); +} + +class _UpdateModalState extends State { + bool doNotRemember = false; + + String getDownloadLink() { + return widget.gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('apk')).browserDownloadUrl; + } + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + + return AlertDialog( + scrollable: true, + title: Column( + children: [ + const Icon( + Icons.system_update_rounded, + size: 26, + ), + const SizedBox(height: 20), + Text( + AppLocalizations.of(context)!.updateAvailable, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 26 + ), + ) + ], + ), + content: Column( + children: [ + const SizedBox(height: 10), + Text("${AppLocalizations.of(context)!.installedVersion}: ${appConfigProvider.getAppInfo!.version}"), + const SizedBox(height: 10), + Text("${AppLocalizations.of(context)!.newVersion}: ${widget.gitHubRelease.tagName}"), + const SizedBox(height: 10), + Text("${AppLocalizations.of(context)!.source}: GitHub"), + const SizedBox(height: 20), + GestureDetector( + onTap: () => setState(() => doNotRemember = !doNotRemember), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Checkbox( + value: doNotRemember, + onChanged: (value) => setState(() => doNotRemember = value!), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5) + ), + ), + const SizedBox(width: 10), + Flexible(child: Text(AppLocalizations.of(context)!.doNotRememberAgainUpdate)) + ], + ), + ) + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + widget.onDownload(getDownloadLink(), widget.gitHubRelease.tagName); + }, + child: Text(AppLocalizations.of(context)!.download) + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close) + ), + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index a42a339..b17be0e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -197,6 +197,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.55.2" + fl_downloader: + dependency: "direct main" + description: + name: fl_downloader + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" flutter: dependency: "direct main" description: flutter @@ -287,6 +294,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.0" + install_plugin_v2: + dependency: "direct main" + description: + name: install_plugin_v2 + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" intl: dependency: "direct main" description: @@ -413,6 +427,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.2" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + url: "https://pub.dartlang.org" + source: hosted + version: "10.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + url: "https://pub.dartlang.org" + source: hosted + version: "10.2.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + url: "https://pub.dartlang.org" + source: hosted + version: "9.0.7" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.9.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" petitparser: dependency: transitive description: @@ -467,6 +516,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.10.0" + store_checker: + dependency: "direct main" + description: + name: store_checker + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 83ccd89..65f1f53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,10 @@ dependencies: flutter_svg: ^1.1.5 bottom_sheet: ^3.1.2 percent_indicator: ^4.2.2 + store_checker: ^1.1.0 + fl_downloader: ^1.0.3 + install_plugin_v2: ^1.0.0 + permission_handler: ^10.2.0 dev_dependencies: flutter_test: