feat: Save last patched app (#1414)

Co-authored-by: aAbed <39409020+TheAabedKhan@users.noreply.github.com>
Co-authored-by: Ushie <ushiekane@gmail.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: Mr. X <79870712+n30mrx@users.noreply.github.com>
Co-authored-by: festry0 <153519925+festry0@users.noreply.github.com>
This commit is contained in:
Benjamin 2024-06-29 05:38:00 -07:00 committed by GitHub
parent b26760b216
commit 77204087bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 448 additions and 46 deletions

View file

@ -27,10 +27,12 @@
"refreshSuccess": "Refreshed successfully", "refreshSuccess": "Refreshed successfully",
"widgetTitle": "Dashboard", "widgetTitle": "Dashboard",
"updatesSubtitle": "Updates", "updatesSubtitle": "Updates",
"patchedSubtitle": "Patched apps", "lastPatchedAppSubtitle": "Last patched app",
"patchedSubtitle": "Installed apps",
"changeLaterSubtitle": "You can change this in the settings at a later time.", "changeLaterSubtitle": "You can change this in the settings at a later time.",
"noUpdates": "No updates available", "noUpdates": "No updates available",
"WIP": "Work in progress...", "WIP": "Work in progress...",
"noSavedAppFound": "No app found",
"noInstallations": "No patched apps installed", "noInstallations": "No patched apps installed",
"installUpdate": "Continue to install the update?", "installUpdate": "Continue to install the update?",
"updateSheetTitle": "Update ReVanced Manager", "updateSheetTitle": "Update ReVanced Manager",
@ -205,6 +207,8 @@
"showUpdateDialogHint": "Show a dialog when a new update is available", "showUpdateDialogHint": "Show a dialog when a new update is available",
"universalPatchesLabel": "Show universal patches", "universalPatchesLabel": "Show universal patches",
"universalPatchesHint": "Display all apps and universal patches (may slow down the app list)", "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", "versionCompatibilityCheckLabel": "Version compatibility check",
"versionCompatibilityCheckHint": "Prevent selecting patches that are not compatible with the selected app version", "versionCompatibilityCheckHint": "Prevent selecting patches that are not compatible with the selected app version",
"requireSuggestedAppVersionLabel": "Require suggested app version", "requireSuggestedAppVersionLabel": "Require suggested app version",
@ -256,18 +260,25 @@
"appInfoView": { "appInfoView": {
"widgetTitle": "App info", "widgetTitle": "App info",
"openButton": "Open", "openButton": "Open",
"installButton": "Install",
"uninstallButton": "Uninstall", "uninstallButton": "Uninstall",
"unmountButton": "Unmount", "unmountButton": "Unmount",
"exportButton": "Export",
"deleteButton": "Delete",
"rootDialogTitle": "Error", "rootDialogTitle": "Error",
"lastPatchedAppDescription": "This is a backup of the app that was last patched.",
"unmountDialogText": "Are you sure you want to unmount this app?", "unmountDialogText": "Are you sure you want to unmount this app?",
"uninstallDialogText": "Are you sure you want to uninstall 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.", "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", "packageNameLabel": "Package name",
"installTypeLabel": "Installation type", "installTypeLabel": "Installation type",
"mountTypeLabel": "Mount", "mountTypeLabel": "Mount",
"regularTypeLabel": "Regular", "regularTypeLabel": "Regular",
"patchedDateLabel": "Patched date", "patchedDateLabel": "Patched date",
"appliedPatchesLabel": "Applied patches", "appliedPatchesLabel": "Applied patches",
"sizeLabel": "File size",
"patchedDateHint": "${date} at ${time}", "patchedDateHint": "${date} at ${time}",
"appliedPatchesHint": "${quantity} applied patches", "appliedPatchesHint": "${quantity} applied patches",
"updateNotImplemented": "This feature has not been implemented yet" "updateNotImplemented": "This feature has not been implemented yet"

View file

@ -16,6 +16,8 @@ class PatchedApplication {
this.isRooted = false, this.isRooted = false,
this.isFromStorage = false, this.isFromStorage = false,
this.appliedPatches = const [], this.appliedPatches = const [],
this.patchedFilePath = '',
this.fileSize = 0,
}); });
factory PatchedApplication.fromJson(Map<String, dynamic> json) => factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
@ -33,6 +35,8 @@ class PatchedApplication {
bool isRooted; bool isRooted;
bool isFromStorage; bool isFromStorage;
List<String> appliedPatches; List<String> appliedPatches;
String patchedFilePath;
int fileSize;
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this); Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);

View file

@ -302,6 +302,14 @@ class ManagerAPI {
await _prefs.setBool('requireSuggestedAppVersionEnabled', value); await _prefs.setBool('requireSuggestedAppVersionEnabled', value);
} }
bool isLastPatchedAppEnabled() {
return _prefs.getBool('lastPatchedAppEnabled') ?? true;
}
Future<void> enableLastPatchedAppStatus(bool value) async {
await _prefs.setBool('lastPatchedAppEnabled', value);
}
Future<void> setKeystorePassword(String password) async { Future<void> setKeystorePassword(String password) async {
await _prefs.setString('keystorePassword', password); 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<void> deleteLastPatchedApp() async {
final PatchedApplication? app = getLastPatchedApp();
if (app != null) {
final File file = File(app.patchedFilePath);
await file.delete();
await _prefs.remove('lastPatchedApp');
}
}
Future<void> 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<PatchedApplication> getPatchedApps() { List<PatchedApplication> getPatchedApps() {
final List<String> apps = _prefs.getStringList('patchedApps') ?? []; final List<String> apps = _prefs.getStringList('patchedApps') ?? [];
return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList(); return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList();
@ -692,6 +728,16 @@ class ManagerAPI {
patchedApps.addAll(mountedApps); patchedApps.addAll(mountedApps);
await setPatchedApps(patchedApps); 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<bool> isAppUninstalled(PatchedApplication app) async { Future<bool> isAppUninstalled(PatchedApplication app) async {
@ -786,4 +832,82 @@ class ManagerAPI {
selectedPatchesFile.deleteSync(); selectedPatchesFile.deleteSync();
} }
} }
Future<bool> installTypeDialog(BuildContext context) async {
final ValueNotifier<int> 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;
}
} }

View file

@ -217,7 +217,7 @@ class PatcherAPI {
BuildContext context, BuildContext context,
PatchedApplication patchedApp, PatchedApplication patchedApp,
) async { ) async {
if (outFile != null) { if (patchedApp.patchedFilePath != '') {
_managerAPI.ctx = context; _managerAPI.ctx = context;
try { try {
if (patchedApp.isRooted) { if (patchedApp.isRooted) {
@ -232,7 +232,7 @@ class PatcherAPI {
return await _rootAPI.install( return await _rootAPI.install(
patchedApp.packageName, patchedApp.packageName,
patchedApp.apkFilePath, patchedApp.apkFilePath,
outFile!.path, patchedApp.patchedFilePath,
) )
? 0 ? 0
: 1; : 1;
@ -246,7 +246,7 @@ class PatcherAPI {
if (context.mounted) { if (context.mounted) {
return await installApk( return await installApk(
context, context,
outFile!.path, patchedApp.patchedFilePath,
); );
} }
} }
@ -368,13 +368,13 @@ class PatcherAPI {
return cleanInstall ? 10 : 1; return cleanInstall ? 10 : 1;
} }
void exportPatchedFile(String appName, String version) { void exportPatchedFile(PatchedApplication app) {
try { try {
if (outFile != null) { if (outFile != null) {
final String newName = _getFileName(appName, version); final String newName = _getFileName(app.name, app.version);
FlutterFileDialog.saveFile( FlutterFileDialog.saveFile(
params: SaveFileDialogParams( params: SaveFileDialogParams(
sourceFilePath: outFile!.path, sourceFilePath: app.patchedFilePath,
fileName: newName, fileName: newName,
mimeTypesFilter: ['application/vnd.android.package-archive'], mimeTypesFilter: ['application/vnd.android.package-archive'],
), ),
@ -387,14 +387,14 @@ class PatcherAPI {
} }
} }
void sharePatchedFile(String appName, String version) { void sharePatchedFile(PatchedApplication app) {
try { try {
if (outFile != null) { if (outFile != null) {
final String newName = _getFileName(appName, version); final String newName = _getFileName(app.name, app.version);
final int lastSeparator = outFile!.path.lastIndexOf('/'); final int lastSeparator = app.patchedFilePath.lastIndexOf('/');
final String newPath = final String newPath =
outFile!.path.substring(0, lastSeparator + 1) + newName; app.patchedFilePath.substring(0, lastSeparator + 1) + newName;
final File shareFile = outFile!.copySync(newPath); final File shareFile = File(app.patchedFilePath).copySync(newPath);
Share.shareXFiles([XFile(shareFile.path)]); Share.shareXFiles([XFile(shareFile.path)]);
} }
} on Exception catch (e) { } on Exception catch (e) {

View file

@ -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/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/homeView/installed_apps_card.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/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:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
@ -44,6 +45,22 @@ class HomeView extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
LatestCommitCard(model: model, parentContext: context), LatestCommitCard(model: model, parentContext: context),
const SizedBox(height: 23), 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( Text(
t.homeView.patchedSubtitle, t.homeView.patchedSubtitle,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,

View file

@ -35,6 +35,7 @@ class HomeViewModel extends BaseViewModel {
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
bool showUpdatableApps = false; bool showUpdatableApps = false;
PatchedApplication? lastPatchedApp;
bool releaseBuild = false; bool releaseBuild = false;
List<PatchedApplication> patchedInstalledApps = []; List<PatchedApplication> patchedInstalledApps = [];
String _currentManagerVersion = ''; String _currentManagerVersion = '';
@ -102,10 +103,10 @@ class HomeViewModel extends BaseViewModel {
} }
} }
void navigateToAppInfo(PatchedApplication app) { void navigateToAppInfo(PatchedApplication app, bool isLastPatchedApp) {
_navigationService.navigateTo( _navigationService.navigateTo(
Routes.appInfoView, Routes.appInfoView,
arguments: AppInfoViewArguments(app: app), arguments: AppInfoViewArguments(app: app, isLastPatchedApp: isLastPatchedApp),
); );
} }
@ -123,10 +124,15 @@ class HomeViewModel extends BaseViewModel {
} }
void getPatchedApps() { void getPatchedApps() {
lastPatchedApp = _managerAPI.getLastPatchedApp();
patchedInstalledApps = _managerAPI.getPatchedApps().toList(); patchedInstalledApps = _managerAPI.getPatchedApps().toList();
notifyListeners(); notifyListeners();
} }
bool isLastPatchedAppEnabled() {
return _managerAPI.isLastPatchedAppEnabled();
}
Future<bool> hasManagerUpdates() async { Future<bool> hasManagerUpdates() async {
if (!_managerAPI.releaseBuild) { if (!_managerAPI.releaseBuild) {
return false; return false;

View file

@ -123,7 +123,7 @@ class InstallerViewModel extends BaseViewModel {
}); });
await WakelockPlus.enable(); await WakelockPlus.enable();
await handlePlatformChannelMethods(); await handlePlatformChannelMethods();
await runPatcher(); await runPatcher(context);
} }
Future<dynamic> handlePlatformChannelMethods() async { Future<dynamic> handlePlatformChannelMethods() async {
@ -182,13 +182,20 @@ class InstallerViewModel extends BaseViewModel {
notifyListeners(); notifyListeners();
} }
Future<void> runPatcher() async { Future<void> runPatcher(BuildContext context) async {
try { try {
await _patcherAPI.runPatcher( await _patcherAPI.runPatcher(
_app.packageName, _app.packageName,
_app.apkFilePath, _app.apkFilePath,
_patches, _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<HomeViewModel>().initialize(context);
} on Exception catch (e) { } on Exception catch (e) {
update( update(
-100.0, -100.0,
@ -488,7 +495,7 @@ class InstallerViewModel extends BaseViewModel {
Future<void> installResult(BuildContext context, bool installAsRoot) async { Future<void> installResult(BuildContext context, bool installAsRoot) async {
isInstalling = true; isInstalling = true;
try { try {
_app.isRooted = installAsRoot; _app.isRooted = await _managerAPI.installTypeDialog(context);
if (headerLogs != 'Installing...') { if (headerLogs != 'Installing...') {
update( update(
.85, .85,
@ -501,17 +508,15 @@ class InstallerViewModel extends BaseViewModel {
isInstalled = true; isInstalled = true;
_app.isFromStorage = false; _app.isFromStorage = false;
_app.patchDate = DateTime.now(); _app.patchDate = DateTime.now();
_app.appliedPatches = _patches.map((p) => p.name).toList();
// In case a patch changed the app name or package name, // In case a patch changed the app name or package name,
// update the app info. // update the app info.
final app = final app =
await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path); await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path);
if (app != null) { if (app != null) {
_app.name = app.appName; _app.name = app.appName;
_app.packageName = app.packageName; _app.packageName = app.packageName;
} }
await _managerAPI.savePatchedApp(_app); await _managerAPI.savePatchedApp(_app);
_managerAPI _managerAPI
@ -544,7 +549,7 @@ class InstallerViewModel extends BaseViewModel {
void exportResult() { void exportResult() {
try { try {
_patcherAPI.exportPatchedFile(_app.name, _app.version); _patcherAPI.exportPatchedFile(_app);
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e); print(e);

View file

@ -141,6 +141,18 @@ class SettingsViewModel extends BaseViewModel {
notifyListeners(); notifyListeners();
} }
bool isLastPatchedAppEnabled() {
return _managerAPI.isLastPatchedAppEnabled();
}
void useLastPatchedApp(bool value) {
_managerAPI.enableLastPatchedAppStatus(value);
if (!value) {
_managerAPI.deleteLastPatchedApp();
}
notifyListeners();
}
bool isVersionCompatibilityCheckEnabled() { bool isVersionCompatibilityCheckEnabled() {
return _managerAPI.isVersionCompatibilityCheckEnabled(); return _managerAPI.isVersionCompatibilityCheckEnabled();
} }

View file

@ -11,8 +11,10 @@ class AppInfoView extends StatelessWidget {
const AppInfoView({ const AppInfoView({
super.key, super.key,
required this.app, required this.app,
required this.isLastPatchedApp,
}); });
final PatchedApplication app; final PatchedApplication app;
final bool isLastPatchedApp;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -57,6 +59,14 @@ class AppInfoView extends StatelessWidget {
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
const SizedBox(height: 20), 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(
padding: const EdgeInsets.symmetric(horizontal: 20.0), padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: CustomCard( child: CustomCard(
@ -71,20 +81,26 @@ class AppInfoView extends StatelessWidget {
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(16.0), borderRadius: BorderRadius.circular(16.0),
onTap: () => model.openApp(app), onTap: () => isLastPatchedApp
? model.installApp(context, app)
: model.openApp(app),
child: Column( child: Column(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.center, MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Icon( Icon(
Icons.open_in_new_outlined, isLastPatchedApp
? Icons.download_outlined
: Icons.open_in_new_outlined,
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.primary, .primary,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
t.appInfoView.openButton, isLastPatchedApp
? t.appInfoView.installButton
: t.appInfoView.openButton,
style: TextStyle( style: TextStyle(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
@ -108,24 +124,30 @@ class AppInfoView extends StatelessWidget {
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(16.0), borderRadius: BorderRadius.circular(16.0),
onTap: () => model.showUninstallDialog( onTap: () => isLastPatchedApp
context, ? model.exportApp(app)
app, : model.showUninstallDialog(
false, context,
), app,
false,
),
child: Column( child: Column(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.center, MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Icon( Icon(
Icons.delete_outline, isLastPatchedApp
? Icons.save
: Icons.delete_outline,
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.primary, .primary,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
t.appInfoView.uninstallButton, isLastPatchedApp
? t.appInfoView.exportButton
: t.appInfoView.uninstallButton,
style: TextStyle( style: TextStyle(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
@ -144,14 +166,57 @@ class AppInfoView extends StatelessWidget {
endIndent: 12.0, endIndent: 12.0,
width: 1.0, width: 1.0,
), ),
if (app.isRooted) if (isLastPatchedApp)
VerticalDivider( VerticalDivider(
color: Theme.of(context).canvasColor, color: Theme.of(context).canvasColor,
indent: 12.0, indent: 12.0,
endIndent: 12.0, endIndent: 12.0,
width: 1.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: <Widget>[
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( Expanded(
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
@ -240,6 +305,23 @@ class AppInfoView extends StatelessWidget {
), ),
), ),
const SizedBox(height: 4), 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( ListTile(
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0), const EdgeInsets.symmetric(horizontal: 20.0),

View file

@ -1,4 +1,5 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:math';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -20,6 +21,23 @@ class AppInfoViewModel extends BaseViewModel {
final RootAPI _rootAPI = RootAPI(); final RootAPI _rootAPI = RootAPI();
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
Future<void> installApp(
BuildContext context,
PatchedApplication app,
) async {
app.isRooted = await _managerAPI.installTypeDialog(context);
final int statusCode = await _patcherAPI.installPatchedFile(context, app);
if (statusCode == 0) {
locator<HomeViewModel>().initialize(context);
}
}
Future<void> exportApp(
PatchedApplication app,
) async {
_patcherAPI.exportPatchedFile(app);
}
Future<void> uninstallApp( Future<void> uninstallApp(
BuildContext context, BuildContext context,
PatchedApplication app, PatchedApplication app,
@ -123,6 +141,34 @@ class AppInfoViewModel extends BaseViewModel {
} }
} }
Future<void> 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: <Widget>[
OutlinedButton(
child: Text(t.cancelButton),
onPressed: () => Navigator.of(context).pop(),
),
FilledButton(
child: Text(t.okButton),
onPressed: () => {
_managerAPI.deleteLastPatchedApp(),
Navigator.of(context)..pop()..pop(),
locator<HomeViewModel>().initialize(context),
},
),
],
),
);
}
String getPrettyDate(BuildContext context, DateTime dateTime) { String getPrettyDate(BuildContext context, DateTime dateTime) {
return DateFormat.yMMMMd(Localizations.localeOf(context).languageCode) return DateFormat.yMMMMd(Localizations.localeOf(context).languageCode)
.format(dateTime); .format(dateTime);
@ -133,6 +179,12 @@ class AppInfoViewModel extends BaseViewModel {
.format(dateTime); .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<void> showAppliedPatchesDialog( Future<void> showAppliedPatchesDialog(
BuildContext context, BuildContext context,
PatchedApplication app, PatchedApplication app,

View file

@ -76,7 +76,7 @@ class InstalledAppsCard extends StatelessWidget {
name: app.name, name: app.name,
patchDate: app.patchDate, patchDate: app.patchDate,
onPressed: () => onPressed: () =>
locator<HomeViewModel>().navigateToAppInfo(app), locator<HomeViewModel>().navigateToAppInfo(app, false),
), ),
) )
.toList(), .toList(),

View file

@ -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<HomeViewModel>().lastPatchedApp;
@override
Widget build(BuildContext context) {
return app == null
? CustomCard(
child: Center(
child: Column(
children: <Widget>[
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<HomeViewModel>().navigateToAppInfo(app!, true),
);
}
}

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:revanced_manager/gen/strings.g.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_auto_update_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.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_require_suggested_app_version.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_show_update_dialog.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_show_update_dialog.dart';
@ -24,6 +25,7 @@ class SAdvancedSection extends StatelessWidget {
SRequireSuggestedAppVersion(), SRequireSuggestedAppVersion(),
SVersionCompatibilityCheck(), SVersionCompatibilityCheck(),
SUniversalPatches(), SUniversalPatches(),
SLastPatchedApp(),
], ],
); );
} }

View file

@ -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<SLastPatchedApp> createState() =>
_SLastPatchedAppState();
}
final _settingsViewModel = SettingsViewModel();
class _SLastPatchedAppState
extends State<SLastPatchedApp> {
@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);
});
},
);
}
}

View file

@ -33,6 +33,7 @@ class _ApplicationItemState extends State<ApplicationItem> {
return Container( return Container(
margin: const EdgeInsets.only(bottom: 16.0), margin: const EdgeInsets.only(bottom: 16.0),
child: CustomCard( child: CustomCard(
onTap: widget.onPressed,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View file

@ -571,18 +571,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.5" version: "10.0.4"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.3"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
@ -644,18 +644,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.8.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.14.0" version: "1.12.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -1148,10 +1148,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.1" version: "0.7.0"
timeago: timeago:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1276,10 +1276,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.2" version: "14.2.1"
wakelock_plus: wakelock_plus:
dependency: "direct main" dependency: "direct main"
description: description: