From 2944a2b78840999f131e8b4e9bfeb53227262362 Mon Sep 17 00:00:00 2001 From: Alberto Ponces Date: Mon, 5 Sep 2022 13:43:13 +0100 Subject: [PATCH] feat: add App Info View --- assets/i18n/en.json | 20 +- lib/models/patched_application.dart | 2 - lib/services/manager_api.dart | 14 +- lib/services/patcher_api.dart | 2 +- .../app_selector/app_selector_viewmodel.dart | 3 +- lib/ui/views/installer/installer_view.dart | 1 - .../views/installer/installer_viewmodel.dart | 2 +- lib/ui/widgets/appInfoView/app_info_view.dart | 247 ++++++++++++++++++ .../appInfoView/app_info_viewmodel.dart | 132 ++++++++++ .../widgets/homeView/installed_apps_card.dart | 14 +- .../patchesSelectorView/patch_item.dart | 1 - lib/ui/widgets/shared/application_item.dart | 19 +- pubspec.yaml | 1 + 13 files changed, 433 insertions(+), 25 deletions(-) create mode 100644 lib/ui/widgets/appInfoView/app_info_view.dart create mode 100644 lib/ui/widgets/appInfoView/app_info_viewmodel.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 550d8853..e4b8ffeb 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -1,5 +1,8 @@ { "okButton": "OK", + "cancelButton": "Cancel", + "enabledLabel": "Enabled", + "disabledLabel": "Disabled", "main": { "dashboardTab": "Dashboard", "patcherTab": "Patcher", @@ -22,7 +25,7 @@ }, "applicationItem": { "patchButton": "Patch", - "openButton": "Open", + "infoButton": "Info", "changelogLabel": "Changelog" }, "latestCommitCard": { @@ -107,5 +110,20 @@ "grantPermission": "Grant Root Permission", "grantedPermission": "Magisk permission granted: {isRooted}", "nonRootButton": "Nonroot" + }, + "appInfoView": { + "widgetTitle": "App Info", + "openButton": "Open", + "uninstallButton": "Uninstall", + "patchButton": "Patch", + "alertDialogTitle": "Uninstall", + "alertDialogText": "Are you sure you want to uninstall this app?", + "errorDialogText": "App was installed with root mode enabled but currently root mode is disabled.\nPlease enable root mode first.", + "packageNameLabel": "Package Name", + "rootModeLabel": "Root Mode", + "patchedDateLabel": "Patched Date", + "patchedDateHint": "{date} at {time}", + "appliedPatchesLabel": "Applied Patches", + "appliedPatchesHint": "{quantity} applied patches" } } diff --git a/lib/models/patched_application.dart b/lib/models/patched_application.dart index 438d025e..3b97a642 100644 --- a/lib/models/patched_application.dart +++ b/lib/models/patched_application.dart @@ -17,7 +17,6 @@ class PatchedApplication { Uint8List icon; DateTime patchDate; final bool isRooted; - final bool isFromStorage; bool hasUpdates; List appliedPatches; List changelog; @@ -30,7 +29,6 @@ class PatchedApplication { required this.icon, required this.patchDate, this.isRooted = false, - this.isFromStorage = false, this.hasUpdates = false, this.appliedPatches = const [], this.changelog = const [], diff --git a/lib/services/manager_api.dart b/lib/services/manager_api.dart index 8d71d07d..70f7fc1b 100644 --- a/lib/services/manager_api.dart +++ b/lib/services/manager_api.dart @@ -102,12 +102,18 @@ class ManagerAPI { await setPatchedApps(patchedApps); } + Future deletePatchedApp(PatchedApplication app) async { + List patchedApps = getPatchedApps(); + patchedApps.removeWhere((a) => a.packageName == app.packageName); + await setPatchedApps(patchedApps); + } + Future reAssessSavedApps() async { - bool isRoot = isRooted() ?? false; + bool isRooted = this.isRooted() ?? false; List patchedApps = getPatchedApps(); List toRemove = []; for (PatchedApplication app in patchedApps) { - bool isRemove = await isAppUninstalled(app, isRoot); + bool isRemove = await isAppUninstalled(app, isRooted); if (isRemove) { toRemove.add(app); } else { @@ -133,9 +139,9 @@ class ManagerAPI { await setPatchedApps(patchedApps); } - Future isAppUninstalled(PatchedApplication app, bool isRoot) async { + Future isAppUninstalled(PatchedApplication app, bool isRooted) async { bool existsRoot = false; - if (isRoot) { + if (isRooted) { existsRoot = await _rootAPI.isAppInstalled(app.packageName); } bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName); diff --git a/lib/services/patcher_api.dart b/lib/services/patcher_api.dart index 3bf13d89..a70cc2f2 100644 --- a/lib/services/patcher_api.dart +++ b/lib/services/patcher_api.dart @@ -146,7 +146,7 @@ class PatcherAPI { Future installPatchedFile(PatchedApplication patchedApp) async { if (_outFile != null) { try { - if (patchedApp.isRooted && !patchedApp.isFromStorage) { + if (patchedApp.isRooted) { return _rootAPI.installApp( patchedApp.packageName, patchedApp.apkFilePath, diff --git a/lib/ui/views/app_selector/app_selector_viewmodel.dart b/lib/ui/views/app_selector/app_selector_viewmodel.dart index 52e48fc9..329993fc 100644 --- a/lib/ui/views/app_selector/app_selector_viewmodel.dart +++ b/lib/ui/views/app_selector/app_selector_viewmodel.dart @@ -59,8 +59,7 @@ class AppSelectorViewModel extends BaseViewModel { apkFilePath: result.files.single.path!, icon: application.icon, patchDate: DateTime.now(), - isRooted: _isRooted, - isFromStorage: true, + isRooted: false, ); locator().selectedPatches.clear(); locator().notifyListeners(); diff --git a/lib/ui/views/installer/installer_view.dart b/lib/ui/views/installer/installer_view.dart index 27ed7a0e..8729c735 100644 --- a/lib/ui/views/installer/installer_view.dart +++ b/lib/ui/views/installer/installer_view.dart @@ -70,7 +70,6 @@ class InstallerView extends StatelessWidget { label: model.isInstalled ? I18nText('installerView.openButton') : I18nText('installerView.installButton'), - isFilled: true, isExpanded: true, onPressed: () { if (model.isInstalled) { diff --git a/lib/ui/views/installer/installer_viewmodel.dart b/lib/ui/views/installer/installer_viewmodel.dart index 81ea8068..c67fc4a9 100644 --- a/lib/ui/views/installer/installer_viewmodel.dart +++ b/lib/ui/views/installer/installer_viewmodel.dart @@ -99,7 +99,7 @@ class InstallerViewModel extends BaseViewModel { if (_app != null && _patches.isNotEmpty) { String apkFilePath = _app!.apkFilePath; try { - if (_app!.isRooted && !_app!.isFromStorage) { + if (_app!.isRooted) { update(0.0, '', 'Checking if an old patched version exists'); bool oldExists = await _patcherAPI.checkOldPatch(_app!); if (oldExists) { diff --git a/lib/ui/widgets/appInfoView/app_info_view.dart b/lib/ui/widgets/appInfoView/app_info_view.dart new file mode 100644 index 00000000..2e5cb676 --- /dev/null +++ b/lib/ui/widgets/appInfoView/app_info_view.dart @@ -0,0 +1,247 @@ +import 'package:device_apps/device_apps.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:revanced_manager/models/patched_application.dart'; +import 'package:revanced_manager/ui/widgets/appInfoView/app_info_viewmodel.dart'; +import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; +import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; +import 'package:stacked/stacked.dart'; + +class AppInfoView extends StatelessWidget { + final PatchedApplication app; + + const AppInfoView({ + Key? key, + required this.app, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ViewModelBuilder.reactive( + onModelReady: (model) => model.initialize(), + viewModelBuilder: () => AppInfoViewModel(), + builder: (context, model, child) => Scaffold( + body: CustomScrollView( + slivers: [ + CustomSliverAppBar( + title: I18nText( + 'appInfoView.widgetTitle', + child: Text( + '', + style: GoogleFonts.inter( + color: Theme.of(context).textTheme.headline6!.color, + ), + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.all(20.0), + sliver: SliverList( + delegate: SliverChildListDelegate.fixed( + [ + SizedBox( + height: 64.0, + child: CircleAvatar( + child: Image.memory( + app.icon, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(height: 20), + Text( + app.name, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 4), + Text( + app.version, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.subtitle1, + ), + const SizedBox(height: 20), + CustomCard( + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + InkWell( + onTap: () => DeviceApps.openApp( + app.packageName, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.open_in_new_outlined, + color: + Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 10), + I18nText( + 'appInfoView.openButton', + child: Text( + '', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + VerticalDivider( + color: Theme.of(context).canvasColor, + ), + InkWell( + onTap: () => + model.showUninstallAlertDialog(context, app), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.delete_outline, + color: + Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 10), + I18nText( + 'appInfoView.uninstallButton', + child: Text( + '', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + VerticalDivider( + color: Theme.of(context).canvasColor, + ), + InkWell( + onTap: () { + model.navigateToPatcher(app); + Navigator.of(context).pop(); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.build_outlined, + color: + Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 10), + I18nText( + 'appInfoView.patchButton', + child: Text( + '', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + ListTile( + contentPadding: EdgeInsets.zero, + title: I18nText( + 'appInfoView.packageNameLabel', + child: const Text( + '', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ), + subtitle: Text(app.packageName), + ), + const SizedBox(height: 4), + ListTile( + contentPadding: EdgeInsets.zero, + title: I18nText( + 'appInfoView.rootModeLabel', + child: const Text( + '', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ), + subtitle: model.isRooted + ? I18nText('enabledLabel') + : I18nText('disabledLabel'), + ), + const SizedBox(height: 4), + ListTile( + contentPadding: EdgeInsets.zero, + title: I18nText( + 'appInfoView.patchedDateLabel', + child: const Text( + '', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ), + subtitle: I18nText( + 'appInfoView.patchedDateHint', + translationParams: { + 'date': model.getPrettyDate(context, app.patchDate), + 'time': model.getPrettyTime(context, app.patchDate), + }, + ), + ), + const SizedBox(height: 4), + ListTile( + contentPadding: EdgeInsets.zero, + title: I18nText( + 'appInfoView.appliedPatchesLabel', + child: const Text( + '', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ), + subtitle: I18nText( + 'appInfoView.appliedPatchesHint', + translationParams: { + 'quantity': app.appliedPatches.length.toString(), + }, + ), + onTap: () => model.showAppliedPatchesDialog(context, app), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart new file mode 100644 index 00000000..179442d5 --- /dev/null +++ b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart @@ -0,0 +1,132 @@ +import 'package:device_apps/device_apps.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:intl/intl.dart'; +import 'package:revanced_manager/app/app.locator.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/root_api.dart'; +import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; +import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; +import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart'; +import 'package:revanced_manager/utils/string.dart'; +import 'package:stacked/stacked.dart'; + +class AppInfoViewModel extends BaseViewModel { + final ManagerAPI _managerAPI = locator(); + final PatcherAPI _patcherAPI = locator(); + final RootAPI _rootAPI = RootAPI(); + bool isRooted = false; + + void initialize() { + isRooted = _managerAPI.isRooted() ?? false; + } + + void uninstallApp(PatchedApplication app) { + if (app.isRooted) { + _rootAPI.deleteApp(app.packageName, app.apkFilePath); + _managerAPI.deletePatchedApp(app); + } else { + DeviceApps.uninstallApp(app.packageName); + _managerAPI.deletePatchedApp(app); + } + } + + void navigateToPatcher(PatchedApplication app) async { + locator().selectedApp = app; + locator().selectedPatches = + await _patcherAPI.getAppliedPatches(app.appliedPatches); + locator().notifyListeners(); + locator().setIndex(1); + } + + Future showUninstallAlertDialog( + BuildContext context, + PatchedApplication app, + ) async { + if (app.isRooted && !isRooted) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: I18nText('appInfoView.alertDialogTitle'), + content: I18nText('appInfoView.errorDialogText'), + actions: [ + CustomMaterialButton( + label: I18nText('okButton'), + onPressed: () => Navigator.of(context).pop(), + ) + ], + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + ), + ); + } else { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: I18nText('appInfoView.alertDialogTitle'), + content: I18nText('appInfoView.alertDialogText'), + actions: [ + CustomMaterialButton( + isFilled: false, + label: I18nText('cancelButton'), + onPressed: () => Navigator.of(context).pop(), + ), + CustomMaterialButton( + label: I18nText('okButton'), + onPressed: () { + uninstallApp(app); + locator().notifyListeners(); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + ) + ], + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + ), + ); + } + } + + String getPrettyDate(BuildContext context, DateTime dateTime) { + return DateFormat.yMMMMd(Localizations.localeOf(context).languageCode) + .format(dateTime); + } + + String getPrettyTime(BuildContext context, DateTime dateTime) { + return DateFormat.jm(Localizations.localeOf(context).languageCode) + .format(dateTime); + } + + Future showAppliedPatchesDialog( + BuildContext context, + PatchedApplication app, + ) async { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: I18nText('appInfoView.appliedPatchesLabel'), + content: Text(getAppliedPatchesString(app.appliedPatches)), + actions: [ + CustomMaterialButton( + label: I18nText('okButton'), + onPressed: () => Navigator.of(context).pop(), + ) + ], + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + ), + ); + } + + String getAppliedPatchesString(List appliedPatches) { + List names = appliedPatches + .map((p) => p + .replaceAll('-', ' ') + .split('-') + .join(' ') + .toTitleCase() + .replaceFirst('Microg', 'MicroG')) + .toList(); + return '\u2022 ${names.join('\n\u2022 ')}'; + } +} diff --git a/lib/ui/widgets/homeView/installed_apps_card.dart b/lib/ui/widgets/homeView/installed_apps_card.dart index ae0a0fc9..3d82bd17 100644 --- a/lib/ui/widgets/homeView/installed_apps_card.dart +++ b/lib/ui/widgets/homeView/installed_apps_card.dart @@ -1,11 +1,12 @@ -import 'package:device_apps/device_apps.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; +import 'package:revanced_manager/ui/widgets/appInfoView/app_info_view.dart'; import 'package:revanced_manager/ui/widgets/shared/application_item.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; +import 'package:revanced_manager/ui/widgets/shared/open_container_wrapper.dart'; class InstalledAppsCard extends StatelessWidget { InstalledAppsCard({Key? key}) : super(key: key); @@ -39,14 +40,19 @@ class InstalledAppsCard extends StatelessWidget { padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), children: apps - .map((app) => ApplicationItem( + .map( + (app) => OpenContainerWrapper( + openBuilder: (_, __) => AppInfoView(app: app), + closedBuilder: (_, openContainer) => ApplicationItem( icon: app.icon, name: app.name, patchDate: app.patchDate, changelog: app.changelog, isUpdatableApp: false, - onPressed: () => DeviceApps.openApp(app.packageName), - )) + onPressed: openContainer, + ), + ), + ) .toList(), ); } diff --git a/lib/ui/widgets/patchesSelectorView/patch_item.dart b/lib/ui/widgets/patchesSelectorView/patch_item.dart index e7ce7b55..ef86b79b 100644 --- a/lib/ui/widgets/patchesSelectorView/patch_item.dart +++ b/lib/ui/widgets/patchesSelectorView/patch_item.dart @@ -148,7 +148,6 @@ class _PatchItemState extends State { ), actions: [ CustomMaterialButton( - isFilled: true, label: I18nText('okButton'), onPressed: () => Navigator.of(context).pop(), ) diff --git a/lib/ui/widgets/shared/application_item.dart b/lib/ui/widgets/shared/application_item.dart index 86a0018c..0aa741f4 100644 --- a/lib/ui/widgets/shared/application_item.dart +++ b/lib/ui/widgets/shared/application_item.dart @@ -53,14 +53,17 @@ class ApplicationItem extends StatelessWidget { ], ), const Spacer(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: CustomMaterialButton( - label: isUpdatableApp - ? I18nText('applicationItem.patchButton') - : I18nText('applicationItem.openButton'), - onPressed: onPressed, - ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + CustomMaterialButton( + label: isUpdatableApp + ? I18nText('applicationItem.patchButton') + : I18nText('applicationItem.infoButton'), + onPressed: onPressed, + ), + ], ), ], ), diff --git a/pubspec.yaml b/pubspec.yaml index 05e8d400..04d60209 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: google_fonts: ^3.0.1 http: ^0.13.4 injectable: ^1.5.3 + intl: ^0.17.0 json_annotation: ^4.6.0 package_info_plus: ^1.4.3+1 path_provider: ^2.0.11