diff --git a/assets/i18n/en_US.json b/assets/i18n/en_US.json index 49a95bf5..b6e02309 100644 --- a/assets/i18n/en_US.json +++ b/assets/i18n/en_US.json @@ -72,7 +72,8 @@ "viewTitle": "Select an application", "searchBarHint": "Search applications", "storageButton": "Storage", - "errorMessage": "Unable to use selected application" + "errorMessage": "Unable to use selected application", + "downloadToast": "Download function is not available yet" }, "patchesSelectorView": { "viewTitle": "Select patches", diff --git a/lib/ui/views/app_selector/app_selector_view.dart b/lib/ui/views/app_selector/app_selector_view.dart index ea2c9264..33845f91 100644 --- a/lib/ui/views/app_selector/app_selector_view.dart +++ b/lib/ui/views/app_selector/app_selector_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart'; import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart'; import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart'; +import 'package:revanced_manager/ui/widgets/appSelectorView/not_installed_app_item.dart'; import 'package:revanced_manager/ui/widgets/shared/search_bar.dart'; import 'package:stacked/stacked.dart' hide SkeletonLoader; @@ -75,8 +76,8 @@ class _AppSelectorViewState extends State { ), SliverToBoxAdapter( child: model.noApps - ? Center( - child: I18nText('appSelectorCard.noAppsLabel'), + ? const Center( + child: Text('No apps found.'), ) : model.apps.isEmpty ? const AppSkeletonLoader() @@ -84,22 +85,42 @@ class _AppSelectorViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 12.0) .copyWith(bottom: 80), child: Column( - children: model - .getFilteredApps(_query) - .map( - (app) => InstalledAppItem( - name: app.appName, - pkgName: app.packageName, - icon: app.icon, - patchesCount: - model.patchesCount(app.packageName), - onTap: () { - model.selectApp(app); - Navigator.of(context).pop(); - }, - ), - ) - .toList(), + children: [ + ...model + .getFilteredApps(_query) + .map( + (app) => InstalledAppItem( + name: app.appName, + pkgName: app.packageName, + icon: app.icon, + patchesCount: + model.patchesCount(app.packageName), + recommendedVersion: + model.getRecommendedVersion( + app.packageName, + ), + onTap: () { + model.selectApp(app); + Navigator.of(context).pop(); + }, + ), + ) + .toList(), + ...model + .getFilteredAppsNames(_query) + .map( + (app) => NotInstalledAppItem( + name: app, + patchesCount: model.patchesCount(app), + recommendedVersion: + model.getRecommendedVersion(app), + onTap: () { + model.showDownloadToast(); + }, + ), + ) + .toList(), + ], ), ), ), diff --git a/lib/ui/views/app_selector/app_selector_viewmodel.dart b/lib/ui/views/app_selector/app_selector_viewmodel.dart index 21d21d96..bcc58099 100644 --- a/lib/ui/views/app_selector/app_selector_viewmodel.dart +++ b/lib/ui/views/app_selector/app_selector_viewmodel.dart @@ -5,9 +5,11 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:revanced_manager/app/app.locator.dart'; +import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/patcher_api.dart'; +import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:stacked/stacked.dart'; @@ -15,14 +17,20 @@ import 'package:stacked/stacked.dart'; class AppSelectorViewModel extends BaseViewModel { final PatcherAPI _patcherAPI = locator(); final ManagerAPI _managerAPI = locator(); + final RevancedAPI _revancedAPI = locator(); final Toast _toast = locator(); final List apps = []; + List allApps = []; bool noApps = false; int patchesCount(String packageName) { return _patcherAPI.getFilteredPatches(packageName).length; } + List patches = []; + Future initialize() async { + patches = await _revancedAPI.getPatches(); + apps.addAll( await _patcherAPI .getFilteredInstalledApps(_managerAPI.areUniversalPatchesEnabled()), @@ -34,9 +42,25 @@ class AppSelectorViewModel extends BaseViewModel { .compareTo(_patcherAPI.getFilteredPatches(a.packageName).length), ); noApps = apps.isEmpty; + getAllApps(); + notifyListeners(); } + List getAllApps() { + allApps = patches + .expand((e) => e.compatiblePackages.map((p) => p.name)) + .toSet() + .where((name) => !apps.any((app) => app.packageName == name)) + .toList(); + + return allApps; + } + + String getRecommendedVersion(String packageName) { + return _patcherAPI.getRecommendedVersion(packageName); + } + Future selectApp(ApplicationWithIcon application) async { locator().selectedApp = PatchedApplication( name: application.appName, @@ -105,4 +129,18 @@ class AppSelectorViewModel extends BaseViewModel { ) .toList(); } + + List getFilteredAppsNames(String query) { + return allApps + .where( + (app) => + query.isEmpty || + query.length < 2 || + app.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + } + + void showDownloadToast() => + _toast.showBottom('appSelectorView.downloadToast'); } diff --git a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart index 490aea60..9d16fb0c 100644 --- a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart +++ b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart @@ -1,6 +1,4 @@ import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patched_application.dart'; @@ -9,7 +7,6 @@ import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:stacked/stacked.dart'; class PatchesSelectorViewModel extends BaseViewModel { diff --git a/lib/ui/widgets/appSelectorView/installed_app_item.dart b/lib/ui/widgets/appSelectorView/installed_app_item.dart index 839960c5..5d15d92e 100644 --- a/lib/ui/widgets/appSelectorView/installed_app_item.dart +++ b/lib/ui/widgets/appSelectorView/installed_app_item.dart @@ -9,12 +9,14 @@ class InstalledAppItem extends StatefulWidget { required this.pkgName, required this.icon, required this.patchesCount, + required this.recommendedVersion, this.onTap, }) : super(key: key); final String name; final String pkgName; final Uint8List icon; final int patchesCount; + final String recommendedVersion; final Function()? onTap; @override @@ -46,31 +48,35 @@ class _InstalledAppItemState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + widget.name, + maxLines: 2, + overflow: TextOverflow.visible, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text(widget.pkgName), Row( - children: [ + children: [ Text( - widget.name, - maxLines: 2, - overflow: TextOverflow.visible, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), + widget.recommendedVersion.isEmpty + ? 'All versions' + : widget.recommendedVersion, ), - const SizedBox(width: 6), + const SizedBox(width: 4), Text( widget.patchesCount == 1 - ? '${widget.patchesCount} patch' - : '${widget.patchesCount} patches', + ? '• ${widget.patchesCount} patch' + : '• ${widget.patchesCount} patches', style: TextStyle( - fontSize: 8, color: Theme.of(context).colorScheme.secondary, ), ), ], ), - const SizedBox(height: 4), - Text(widget.pkgName), ], ), ), diff --git a/lib/ui/widgets/appSelectorView/not_installed_app_item.dart b/lib/ui/widgets/appSelectorView/not_installed_app_item.dart new file mode 100644 index 00000000..807a31a1 --- /dev/null +++ b/lib/ui/widgets/appSelectorView/not_installed_app_item.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; + +class NotInstalledAppItem extends StatefulWidget { + const NotInstalledAppItem({ + Key? key, + required this.name, + required this.patchesCount, + required this.recommendedVersion, + this.onTap, + }) : super(key: key); + final String name; + final int patchesCount; + final String recommendedVersion; + final Function()? onTap; + + @override + State createState() => _NotInstalledAppItem(); +} + +class _NotInstalledAppItem extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: CustomCard( + onTap: widget.onTap, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 48, + padding: const EdgeInsets.symmetric(vertical: 4.0), + alignment: Alignment.center, + child: const CircleAvatar( + backgroundColor: Colors.transparent, + child: Icon( + Icons.square_rounded, + color: Colors.grey, + size: 44, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + const Text('App not installed.'), + const SizedBox(height: 4), + Row( + children: [ + Text( + widget.recommendedVersion.isEmpty + ? 'All versions' + : widget.recommendedVersion, + ), + const SizedBox(width: 4), + Text( + widget.patchesCount == 1 + ? '• ${widget.patchesCount} patch' + : '• ${widget.patchesCount} patches', + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +}