mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-09 16:52:14 +01:00
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:
parent
b26760b216
commit
77204087bb
16 changed files with 448 additions and 46 deletions
|
@ -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"
|
||||
|
|
|
@ -16,6 +16,8 @@ class PatchedApplication {
|
|||
this.isRooted = false,
|
||||
this.isFromStorage = false,
|
||||
this.appliedPatches = const [],
|
||||
this.patchedFilePath = '',
|
||||
this.fileSize = 0,
|
||||
});
|
||||
|
||||
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
||||
|
@ -33,6 +35,8 @@ class PatchedApplication {
|
|||
bool isRooted;
|
||||
bool isFromStorage;
|
||||
List<String> appliedPatches;
|
||||
String patchedFilePath;
|
||||
int fileSize;
|
||||
|
||||
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);
|
||||
|
||||
|
|
|
@ -302,6 +302,14 @@ class ManagerAPI {
|
|||
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 {
|
||||
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() {
|
||||
final List<String> 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<bool> isAppUninstalled(PatchedApplication app) async {
|
||||
|
@ -786,4 +832,82 @@ class ManagerAPI {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -35,6 +35,7 @@ class HomeViewModel extends BaseViewModel {
|
|||
final Toast _toast = locator<Toast>();
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
bool showUpdatableApps = false;
|
||||
PatchedApplication? lastPatchedApp;
|
||||
bool releaseBuild = false;
|
||||
List<PatchedApplication> 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<bool> hasManagerUpdates() async {
|
||||
if (!_managerAPI.releaseBuild) {
|
||||
return false;
|
||||
|
|
|
@ -123,7 +123,7 @@ class InstallerViewModel extends BaseViewModel {
|
|||
});
|
||||
await WakelockPlus.enable();
|
||||
await handlePlatformChannelMethods();
|
||||
await runPatcher();
|
||||
await runPatcher(context);
|
||||
}
|
||||
|
||||
Future<dynamic> handlePlatformChannelMethods() async {
|
||||
|
@ -182,13 +182,20 @@ class InstallerViewModel extends BaseViewModel {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> runPatcher() async {
|
||||
Future<void> 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<HomeViewModel>().initialize(context);
|
||||
} on Exception catch (e) {
|
||||
update(
|
||||
-100.0,
|
||||
|
@ -488,7 +495,7 @@ class InstallerViewModel extends BaseViewModel {
|
|||
Future<void> 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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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: <Widget>[
|
||||
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: <Widget>[
|
||||
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: <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(
|
||||
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),
|
||||
|
|
|
@ -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<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(
|
||||
BuildContext context,
|
||||
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) {
|
||||
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<void> showAppliedPatchesDialog(
|
||||
BuildContext context,
|
||||
PatchedApplication app,
|
||||
|
|
|
@ -76,7 +76,7 @@ class InstalledAppsCard extends StatelessWidget {
|
|||
name: app.name,
|
||||
patchDate: app.patchDate,
|
||||
onPressed: () =>
|
||||
locator<HomeViewModel>().navigateToAppInfo(app),
|
||||
locator<HomeViewModel>().navigateToAppInfo(app, false),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
49
lib/ui/widgets/homeView/last_patched_app_card.dart
Normal file
49
lib/ui/widgets/homeView/last_patched_app_card.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
37
lib/ui/widgets/settingsView/settings_last_patched_app.dart
Normal file
37
lib/ui/widgets/settingsView/settings_last_patched_app.dart
Normal 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);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@ class _ApplicationItemState extends State<ApplicationItem> {
|
|||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
child: CustomCard(
|
||||
onTap: widget.onPressed,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
|
24
pubspec.lock
24
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:
|
||||
|
|
Loading…
Reference in a new issue