diff --git a/assets/i18n/strings.i18n.json b/assets/i18n/strings.i18n.json index 516ed5c6..0726c018 100755 --- a/assets/i18n/strings.i18n.json +++ b/assets/i18n/strings.i18n.json @@ -27,10 +27,12 @@ "refreshSuccess": "Refreshed successfully", "widgetTitle": "Dashboard", "updatesSubtitle": "Updates", - "patchedSubtitle": "Patched apps", + "lastPatchedAppSubtitle": "Last patched app", + "patchedSubtitle": "Installed apps", "changeLaterSubtitle": "You can change this in the settings at a later time.", "noUpdates": "No updates available", "WIP": "Work in progress...", + "noSavedAppFound": "No app found", "noInstallations": "No patched apps installed", "installUpdate": "Continue to install the update?", "updateSheetTitle": "Update ReVanced Manager", @@ -205,6 +207,8 @@ "showUpdateDialogHint": "Show a dialog when a new update is available", "universalPatchesLabel": "Show universal patches", "universalPatchesHint": "Display all apps and universal patches (may slow down the app list)", + "lastPatchedAppLabel": "Save patched app", + "lastPatchedAppHint": "Save the last patch to install or export later", "versionCompatibilityCheckLabel": "Version compatibility check", "versionCompatibilityCheckHint": "Prevent selecting patches that are not compatible with the selected app version", "requireSuggestedAppVersionLabel": "Require suggested app version", @@ -256,18 +260,25 @@ "appInfoView": { "widgetTitle": "App info", "openButton": "Open", + "installButton": "Install", "uninstallButton": "Uninstall", "unmountButton": "Unmount", + "exportButton": "Export", + "deleteButton": "Delete", "rootDialogTitle": "Error", + "lastPatchedAppDescription": "This is a backup of the app that was last patched.", "unmountDialogText": "Are you sure you want to unmount this app?", "uninstallDialogText": "Are you sure you want to uninstall this app?", "rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.", + "removeAppDialogTitle": "Delete app?", + "removeAppDialogText": "Are you sure you want to delete this backup?", "packageNameLabel": "Package name", "installTypeLabel": "Installation type", "mountTypeLabel": "Mount", "regularTypeLabel": "Regular", "patchedDateLabel": "Patched date", "appliedPatchesLabel": "Applied patches", + "sizeLabel": "File size", "patchedDateHint": "${date} at ${time}", "appliedPatchesHint": "${quantity} applied patches", "updateNotImplemented": "This feature has not been implemented yet" diff --git a/lib/models/patched_application.dart b/lib/models/patched_application.dart index 23517595..93b5a821 100644 --- a/lib/models/patched_application.dart +++ b/lib/models/patched_application.dart @@ -16,6 +16,8 @@ class PatchedApplication { this.isRooted = false, this.isFromStorage = false, this.appliedPatches = const [], + this.patchedFilePath = '', + this.fileSize = 0, }); factory PatchedApplication.fromJson(Map json) => @@ -33,6 +35,8 @@ class PatchedApplication { bool isRooted; bool isFromStorage; List appliedPatches; + String patchedFilePath; + int fileSize; Map toJson() => _$PatchedApplicationToJson(this); diff --git a/lib/services/manager_api.dart b/lib/services/manager_api.dart index a950c3d1..4f2f8974 100644 --- a/lib/services/manager_api.dart +++ b/lib/services/manager_api.dart @@ -302,6 +302,14 @@ class ManagerAPI { await _prefs.setBool('requireSuggestedAppVersionEnabled', value); } + bool isLastPatchedAppEnabled() { + return _prefs.getBool('lastPatchedAppEnabled') ?? true; + } + + Future enableLastPatchedAppStatus(bool value) async { + await _prefs.setBool('lastPatchedAppEnabled', value); + } + Future setKeystorePassword(String password) async { await _prefs.setString('keystorePassword', password); } @@ -334,6 +342,34 @@ class ManagerAPI { } } + PatchedApplication? getLastPatchedApp() { + final String? app = _prefs.getString('lastPatchedApp'); + return app != null ? PatchedApplication.fromJson(jsonDecode(app)) : null; + } + + Future deleteLastPatchedApp() async { + final PatchedApplication? app = getLastPatchedApp(); + if (app != null) { + final File file = File(app.patchedFilePath); + await file.delete(); + await _prefs.remove('lastPatchedApp'); + } + } + + Future setLastPatchedApp( + PatchedApplication app, + File outFile, + ) async { + deleteLastPatchedApp(); + final Directory appCache = await getApplicationSupportDirectory(); + app.patchedFilePath = outFile.copySync('${appCache.path}/lastPatchedApp.apk').path; + app.fileSize = outFile.lengthSync(); + await _prefs.setString( + 'lastPatchedApp', + json.encode(app.toJson()), + ); + } + List getPatchedApps() { final List apps = _prefs.getStringList('patchedApps') ?? []; return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList(); @@ -692,6 +728,16 @@ class ManagerAPI { patchedApps.addAll(mountedApps); await setPatchedApps(patchedApps); + + // Delete the saved app if the file is not found. + final PatchedApplication? lastPatchedApp = getLastPatchedApp(); + if (lastPatchedApp != null) { + final File file = File(lastPatchedApp.patchedFilePath); + if (!file.existsSync()) { + deleteLastPatchedApp(); + _prefs.remove('lastPatchedApp'); + } + } } Future isAppUninstalled(PatchedApplication app) async { @@ -786,4 +832,82 @@ class ManagerAPI { selectedPatchesFile.deleteSync(); } } + + Future installTypeDialog(BuildContext context) async { + final ValueNotifier installType = ValueNotifier(0); + if (isRooted) { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: Text(t.installerView.installType), + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + icon: const Icon(Icons.file_download_outlined), + contentPadding: const EdgeInsets.symmetric(vertical: 16), + content: SingleChildScrollView( + child: ValueListenableBuilder( + valueListenable: installType, + builder: (context, value, child) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + child: Text( + t.installerView.installTypeDescription, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + RadioListTile( + title: Text(t.installerView.installNonRootType), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16), + value: 0, + groupValue: value, + onChanged: (selected) { + installType.value = selected!; + }, + ), + RadioListTile( + title: Text(t.installerView.installRootType), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16), + value: 1, + groupValue: value, + onChanged: (selected) { + installType.value = selected!; + }, + ), + ], + ); + }, + ), + ), + actions: [ + OutlinedButton( + child: Text(t.cancelButton), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + FilledButton( + child: Text(t.installerView.installButton), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + } + return false; + } } diff --git a/lib/services/patcher_api.dart b/lib/services/patcher_api.dart index 294d6fef..df9d2154 100644 --- a/lib/services/patcher_api.dart +++ b/lib/services/patcher_api.dart @@ -217,7 +217,7 @@ class PatcherAPI { BuildContext context, PatchedApplication patchedApp, ) async { - if (outFile != null) { + if (patchedApp.patchedFilePath != '') { _managerAPI.ctx = context; try { if (patchedApp.isRooted) { @@ -232,7 +232,7 @@ class PatcherAPI { return await _rootAPI.install( patchedApp.packageName, patchedApp.apkFilePath, - outFile!.path, + patchedApp.patchedFilePath, ) ? 0 : 1; @@ -246,7 +246,7 @@ class PatcherAPI { if (context.mounted) { return await installApk( context, - outFile!.path, + patchedApp.patchedFilePath, ); } } @@ -368,13 +368,13 @@ class PatcherAPI { return cleanInstall ? 10 : 1; } - void exportPatchedFile(String appName, String version) { + void exportPatchedFile(PatchedApplication app) { try { if (outFile != null) { - final String newName = _getFileName(appName, version); + final String newName = _getFileName(app.name, app.version); FlutterFileDialog.saveFile( params: SaveFileDialogParams( - sourceFilePath: outFile!.path, + sourceFilePath: app.patchedFilePath, fileName: newName, mimeTypesFilter: ['application/vnd.android.package-archive'], ), @@ -387,14 +387,14 @@ class PatcherAPI { } } - void sharePatchedFile(String appName, String version) { + void sharePatchedFile(PatchedApplication app) { try { if (outFile != null) { - final String newName = _getFileName(appName, version); - final int lastSeparator = outFile!.path.lastIndexOf('/'); + final String newName = _getFileName(app.name, app.version); + final int lastSeparator = app.patchedFilePath.lastIndexOf('/'); final String newPath = - outFile!.path.substring(0, lastSeparator + 1) + newName; - final File shareFile = outFile!.copySync(newPath); + app.patchedFilePath.substring(0, lastSeparator + 1) + newName; + final File shareFile = File(app.patchedFilePath).copySync(newPath); Share.shareXFiles([XFile(shareFile.path)]); } } on Exception catch (e) { diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart index c5deea5e..6e76ea3e 100644 --- a/lib/ui/views/home/home_view.dart +++ b/lib/ui/views/home/home_view.dart @@ -5,6 +5,7 @@ import 'package:revanced_manager/gen/strings.g.dart'; import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; import 'package:revanced_manager/ui/widgets/homeView/installed_apps_card.dart'; import 'package:revanced_manager/ui/widgets/homeView/latest_commit_card.dart'; +import 'package:revanced_manager/ui/widgets/homeView/last_patched_app_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; import 'package:stacked/stacked.dart'; @@ -44,6 +45,22 @@ class HomeView extends StatelessWidget { const SizedBox(height: 10), LatestCommitCard(model: model, parentContext: context), const SizedBox(height: 23), + Visibility( + visible: model.isLastPatchedAppEnabled(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.homeView.lastPatchedAppSubtitle, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 10), + LastPatchedAppCard(), + const SizedBox(height: 10), + + ], + ), + ), Text( t.homeView.patchedSubtitle, style: Theme.of(context).textTheme.titleLarge, diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index 8b7c1816..65d47871 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -35,6 +35,7 @@ class HomeViewModel extends BaseViewModel { final Toast _toast = locator(); final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); bool showUpdatableApps = false; + PatchedApplication? lastPatchedApp; bool releaseBuild = false; List patchedInstalledApps = []; String _currentManagerVersion = ''; @@ -102,10 +103,10 @@ class HomeViewModel extends BaseViewModel { } } - void navigateToAppInfo(PatchedApplication app) { + void navigateToAppInfo(PatchedApplication app, bool isLastPatchedApp) { _navigationService.navigateTo( Routes.appInfoView, - arguments: AppInfoViewArguments(app: app), + arguments: AppInfoViewArguments(app: app, isLastPatchedApp: isLastPatchedApp), ); } @@ -123,10 +124,15 @@ class HomeViewModel extends BaseViewModel { } void getPatchedApps() { + lastPatchedApp = _managerAPI.getLastPatchedApp(); patchedInstalledApps = _managerAPI.getPatchedApps().toList(); notifyListeners(); } + bool isLastPatchedAppEnabled() { + return _managerAPI.isLastPatchedAppEnabled(); + } + Future hasManagerUpdates() async { if (!_managerAPI.releaseBuild) { return false; diff --git a/lib/ui/views/installer/installer_viewmodel.dart b/lib/ui/views/installer/installer_viewmodel.dart index c806a839..9ec2e52b 100644 --- a/lib/ui/views/installer/installer_viewmodel.dart +++ b/lib/ui/views/installer/installer_viewmodel.dart @@ -123,7 +123,7 @@ class InstallerViewModel extends BaseViewModel { }); await WakelockPlus.enable(); await handlePlatformChannelMethods(); - await runPatcher(); + await runPatcher(context); } Future handlePlatformChannelMethods() async { @@ -182,13 +182,20 @@ class InstallerViewModel extends BaseViewModel { notifyListeners(); } - Future runPatcher() async { + Future runPatcher(BuildContext context) async { try { await _patcherAPI.runPatcher( _app.packageName, _app.apkFilePath, _patches, ); + _app.appliedPatches = _patches.map((p) => p.name).toList(); + if (_managerAPI.isLastPatchedAppEnabled()) { + await _managerAPI.setLastPatchedApp(_app, _patcherAPI.outFile!); + } else { + _app.patchedFilePath = _patcherAPI.outFile!.path; + } + locator().initialize(context); } on Exception catch (e) { update( -100.0, @@ -488,7 +495,7 @@ class InstallerViewModel extends BaseViewModel { Future installResult(BuildContext context, bool installAsRoot) async { isInstalling = true; try { - _app.isRooted = installAsRoot; + _app.isRooted = await _managerAPI.installTypeDialog(context); if (headerLogs != 'Installing...') { update( .85, @@ -501,17 +508,15 @@ class InstallerViewModel extends BaseViewModel { isInstalled = true; _app.isFromStorage = false; _app.patchDate = DateTime.now(); - _app.appliedPatches = _patches.map((p) => p.name).toList(); // In case a patch changed the app name or package name, // update the app info. final app = - await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path); + await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path); if (app != null) { _app.name = app.appName; _app.packageName = app.packageName; } - await _managerAPI.savePatchedApp(_app); _managerAPI @@ -544,7 +549,7 @@ class InstallerViewModel extends BaseViewModel { void exportResult() { try { - _patcherAPI.exportPatchedFile(_app.name, _app.version); + _patcherAPI.exportPatchedFile(_app); } on Exception catch (e) { if (kDebugMode) { print(e); diff --git a/lib/ui/views/settings/settings_viewmodel.dart b/lib/ui/views/settings/settings_viewmodel.dart index 8dbfefc4..1323aaac 100644 --- a/lib/ui/views/settings/settings_viewmodel.dart +++ b/lib/ui/views/settings/settings_viewmodel.dart @@ -141,6 +141,18 @@ class SettingsViewModel extends BaseViewModel { notifyListeners(); } + bool isLastPatchedAppEnabled() { + return _managerAPI.isLastPatchedAppEnabled(); + } + + void useLastPatchedApp(bool value) { + _managerAPI.enableLastPatchedAppStatus(value); + if (!value) { + _managerAPI.deleteLastPatchedApp(); + } + notifyListeners(); + } + bool isVersionCompatibilityCheckEnabled() { return _managerAPI.isVersionCompatibilityCheckEnabled(); } diff --git a/lib/ui/widgets/appInfoView/app_info_view.dart b/lib/ui/widgets/appInfoView/app_info_view.dart index 4de2ed46..135c9316 100644 --- a/lib/ui/widgets/appInfoView/app_info_view.dart +++ b/lib/ui/widgets/appInfoView/app_info_view.dart @@ -11,8 +11,10 @@ class AppInfoView extends StatelessWidget { const AppInfoView({ super.key, required this.app, + required this.isLastPatchedApp, }); final PatchedApplication app; + final bool isLastPatchedApp; @override Widget build(BuildContext context) { @@ -57,6 +59,14 @@ class AppInfoView extends StatelessWidget { style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 20), + if (isLastPatchedApp) ...[ + ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 20.0), + subtitle: Text(t.appInfoView.lastPatchedAppDescription), + ), + const SizedBox(height: 4), + ], Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: CustomCard( @@ -71,20 +81,26 @@ class AppInfoView extends StatelessWidget { type: MaterialType.transparency, child: InkWell( borderRadius: BorderRadius.circular(16.0), - onTap: () => model.openApp(app), + onTap: () => isLastPatchedApp + ? model.installApp(context, app) + : model.openApp(app), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.open_in_new_outlined, + isLastPatchedApp + ? Icons.download_outlined + : Icons.open_in_new_outlined, color: Theme.of(context) .colorScheme .primary, ), const SizedBox(height: 10), Text( - t.appInfoView.openButton, + isLastPatchedApp + ? t.appInfoView.installButton + : t.appInfoView.openButton, style: TextStyle( color: Theme.of(context) .colorScheme @@ -108,24 +124,30 @@ class AppInfoView extends StatelessWidget { type: MaterialType.transparency, child: InkWell( borderRadius: BorderRadius.circular(16.0), - onTap: () => model.showUninstallDialog( - context, - app, - false, - ), + onTap: () => isLastPatchedApp + ? model.exportApp(app) + : model.showUninstallDialog( + context, + app, + false, + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.delete_outline, + isLastPatchedApp + ? Icons.save + : Icons.delete_outline, color: Theme.of(context) .colorScheme .primary, ), const SizedBox(height: 10), Text( - t.appInfoView.uninstallButton, + isLastPatchedApp + ? t.appInfoView.exportButton + : t.appInfoView.uninstallButton, style: TextStyle( color: Theme.of(context) .colorScheme @@ -144,14 +166,57 @@ class AppInfoView extends StatelessWidget { endIndent: 12.0, width: 1.0, ), - if (app.isRooted) + if (isLastPatchedApp) VerticalDivider( color: Theme.of(context).canvasColor, indent: 12.0, endIndent: 12.0, width: 1.0, ), - if (app.isRooted) + if (isLastPatchedApp) + Expanded( + child: Material( + type: MaterialType.transparency, + child: InkWell( + borderRadius: BorderRadius.circular(16.0), + onTap: () => model.showDeleteDialog( + context, + app, + ), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons + .delete_forever_outlined, + color: Theme.of(context) + .colorScheme + .primary, + ), + const SizedBox(height: 10), + Text( + t.appInfoView.deleteButton, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + if (!isLastPatchedApp && app.isRooted) + VerticalDivider( + color: Theme.of(context).canvasColor, + indent: 12.0, + endIndent: 12.0, + width: 1.0, + ), + if (!isLastPatchedApp && app.isRooted) Expanded( child: Material( type: MaterialType.transparency, @@ -240,6 +305,23 @@ class AppInfoView extends StatelessWidget { ), ), const SizedBox(height: 4), + if (isLastPatchedApp) ...[ + ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 20.0), + title: Text( + t.appInfoView.sizeLabel, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + model.getFileSizeString(app.fileSize), + ), + ), + const SizedBox(height: 4), + ], ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), diff --git a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart index 1c9ec660..b5ec82ca 100644 --- a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart +++ b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart @@ -1,4 +1,5 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:math'; import 'package:device_apps/device_apps.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -20,6 +21,23 @@ class AppInfoViewModel extends BaseViewModel { final RootAPI _rootAPI = RootAPI(); final Toast _toast = locator(); + Future installApp( + BuildContext context, + PatchedApplication app, + ) async { + app.isRooted = await _managerAPI.installTypeDialog(context); + final int statusCode = await _patcherAPI.installPatchedFile(context, app); + if (statusCode == 0) { + locator().initialize(context); + } + } + + Future exportApp( + PatchedApplication app, + ) async { + _patcherAPI.exportPatchedFile(app); + } + Future uninstallApp( BuildContext context, PatchedApplication app, @@ -123,6 +141,34 @@ class AppInfoViewModel extends BaseViewModel { } } + Future showDeleteDialog( + BuildContext context, + PatchedApplication app, + ) async { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(t.appInfoView.removeAppDialogTitle), + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + content: Text(t.appInfoView.removeAppDialogText), + actions: [ + OutlinedButton( + child: Text(t.cancelButton), + onPressed: () => Navigator.of(context).pop(), + ), + FilledButton( + child: Text(t.okButton), + onPressed: () => { + _managerAPI.deleteLastPatchedApp(), + Navigator.of(context)..pop()..pop(), + locator().initialize(context), + }, + ), + ], + ), + ); + } + String getPrettyDate(BuildContext context, DateTime dateTime) { return DateFormat.yMMMMd(Localizations.localeOf(context).languageCode) .format(dateTime); @@ -133,6 +179,12 @@ class AppInfoViewModel extends BaseViewModel { .format(dateTime); } + String getFileSizeString(int bytes) { + const suffixes = ['B', 'KB', 'MB', 'GB', 'TB']; + final i = (log(bytes) / log(1024)).floor(); + return '${(bytes / pow(1024, i)).toStringAsFixed(2)} ${suffixes[i]}'; + } + Future showAppliedPatchesDialog( BuildContext context, PatchedApplication app, diff --git a/lib/ui/widgets/homeView/installed_apps_card.dart b/lib/ui/widgets/homeView/installed_apps_card.dart index 8de0fae9..f91d70f3 100644 --- a/lib/ui/widgets/homeView/installed_apps_card.dart +++ b/lib/ui/widgets/homeView/installed_apps_card.dart @@ -76,7 +76,7 @@ class InstalledAppsCard extends StatelessWidget { name: app.name, patchDate: app.patchDate, onPressed: () => - locator().navigateToAppInfo(app), + locator().navigateToAppInfo(app, false), ), ) .toList(), diff --git a/lib/ui/widgets/homeView/last_patched_app_card.dart b/lib/ui/widgets/homeView/last_patched_app_card.dart new file mode 100644 index 00000000..dcfc5ceb --- /dev/null +++ b/lib/ui/widgets/homeView/last_patched_app_card.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:revanced_manager/app/app.locator.dart'; +import 'package:revanced_manager/gen/strings.g.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/shared/application_item.dart'; +import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; + +//ignore: must_be_immutable +class LastPatchedAppCard extends StatelessWidget { + LastPatchedAppCard({super.key}); + PatchedApplication? app = locator().lastPatchedApp; + + @override + Widget build(BuildContext context) { + return app == null + ? CustomCard( + child: Center( + child: Column( + children: [ + Icon( + size: 40, + Icons.update_disabled, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(height: 16), + Text( + t.homeView.noSavedAppFound, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + color: + Theme.of(context).colorScheme.secondary, + ), + ), + ], + ), + ), + ) + : ApplicationItem( + icon: app!.icon, + name: app!.name, + patchDate: app!.patchDate, + onPressed: () => + locator().navigateToAppInfo(app!, true), + ); + } +} diff --git a/lib/ui/widgets/settingsView/settings_advanced_section.dart b/lib/ui/widgets/settingsView/settings_advanced_section.dart index 3287f61b..87dade01 100644 --- a/lib/ui/widgets/settingsView/settings_advanced_section.dart +++ b/lib/ui/widgets/settingsView/settings_advanced_section.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:revanced_manager/gen/strings.g.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart'; +import 'package:revanced_manager/ui/widgets/settingsView/settings_last_patched_app.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_require_suggested_app_version.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_show_update_dialog.dart'; @@ -24,6 +25,7 @@ class SAdvancedSection extends StatelessWidget { SRequireSuggestedAppVersion(), SVersionCompatibilityCheck(), SUniversalPatches(), + SLastPatchedApp(), ], ); } diff --git a/lib/ui/widgets/settingsView/settings_last_patched_app.dart b/lib/ui/widgets/settingsView/settings_last_patched_app.dart new file mode 100644 index 00000000..087182b2 --- /dev/null +++ b/lib/ui/widgets/settingsView/settings_last_patched_app.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:revanced_manager/gen/strings.g.dart'; +import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; + +class SLastPatchedApp extends StatefulWidget { + const SLastPatchedApp({super.key}); + + @override + State createState() => + _SLastPatchedAppState(); +} + +final _settingsViewModel = SettingsViewModel(); + +class _SLastPatchedAppState + extends State { + @override + Widget build(BuildContext context) { + return SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), + title: Text( + t.settingsView.lastPatchedAppLabel, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text(t.settingsView.lastPatchedAppHint), + value: _settingsViewModel.isLastPatchedAppEnabled(), + onChanged: (value) { + setState(() { + _settingsViewModel.useLastPatchedApp(value); + }); + }, + ); + } +} diff --git a/lib/ui/widgets/shared/application_item.dart b/lib/ui/widgets/shared/application_item.dart index 3980dff8..2977879d 100644 --- a/lib/ui/widgets/shared/application_item.dart +++ b/lib/ui/widgets/shared/application_item.dart @@ -33,6 +33,7 @@ class _ApplicationItemState extends State { return Container( margin: const EdgeInsets.only(bottom: 16.0), child: CustomCard( + onTap: widget.onPressed, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/pubspec.lock b/pubspec.lock index 3f7dd5ea..b320bba5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -571,18 +571,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: @@ -644,18 +644,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.14.0" + version: "1.12.0" mime: dependency: transitive description: @@ -1148,10 +1148,10 @@ packages: dependency: transitive description: name: test_api - sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.0" timeago: dependency: "direct main" description: @@ -1276,10 +1276,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "14.2.2" + version: "14.2.1" wakelock_plus: dependency: "direct main" description: