mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-04-30 05:54:26 +02:00
feat: ability to store and load selected patches (#469)
* feat: ability to store and load selected patches * fix: I18n * fix: do not append but truncate file * fix: use json file, minor fixes * fix: better ui * WIP * feat: load patches selection after app selection * feat: import/export json file * fix: reformat code * fix: rare bug on import feature fixed * fix: move export/ipmort to settings page & import full json * fix: minor improvements * fix: minor code quality improvements * fix: export filename fix * fix: select list element istead of removing it
This commit is contained in:
parent
fd5d71e24d
commit
c571cf2c53
@ -77,6 +77,8 @@
|
|||||||
"viewTitle": "Select patches",
|
"viewTitle": "Select patches",
|
||||||
"searchBarHint": "Search patches",
|
"searchBarHint": "Search patches",
|
||||||
"doneButton": "Done",
|
"doneButton": "Done",
|
||||||
|
"loadPatchesSelection": "Load patches selection",
|
||||||
|
"noSavedPatches": "No saved patches for the selected app\nPress Done to save current selection",
|
||||||
"noPatchesFound": "No patches found for the selected app",
|
"noPatchesFound": "No patches found for the selected app",
|
||||||
"selectAllPatchesWarningTitle": "Warning",
|
"selectAllPatchesWarningTitle": "Warning",
|
||||||
"selectAllPatchesWarningContent": "You are about to select all patches, that includes unrecommended patches and can cause unwanted behavior."
|
"selectAllPatchesWarningContent": "You are about to select all patches, that includes unrecommended patches and can cause unwanted behavior."
|
||||||
@ -148,6 +150,17 @@
|
|||||||
"deleteTempDirLabel": "Delete temp directory",
|
"deleteTempDirLabel": "Delete temp directory",
|
||||||
"deleteTempDirHint": "Delete the temporary directory used to store temporary files",
|
"deleteTempDirHint": "Delete the temporary directory used to store temporary files",
|
||||||
"deletedTempDir": "Temp directory deleted",
|
"deletedTempDir": "Temp directory deleted",
|
||||||
|
"exportPatchesLabel": "Export patches",
|
||||||
|
"exportPatchesHint": "Export patches to json file",
|
||||||
|
"exportedPatches": "Patches exported",
|
||||||
|
"noExportFileFound": "No patches to export",
|
||||||
|
"importPatchesLabel": "Import patches",
|
||||||
|
"importPatchesHint": "Import patches from json file",
|
||||||
|
"importedPatches": "Patches imported",
|
||||||
|
"resetStoredPatchesLabel": "Reset patches",
|
||||||
|
"resetStoredPatchesHint": "Reset the stored patches selection",
|
||||||
|
"resetStoredPatches": "Patches selection has been reset",
|
||||||
|
"jsonSelectorErrorMessage": "Unable to use selected json file",
|
||||||
"deleteLogsLabel": "Delete logs",
|
"deleteLogsLabel": "Delete logs",
|
||||||
"deleteLogsHint": "Delete collected manager logs",
|
"deleteLogsHint": "Delete collected manager logs",
|
||||||
"deletedLogs": "Logs deleted"
|
"deletedLogs": "Logs deleted"
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:device_apps/device_apps.dart';
|
import 'package:device_apps/device_apps.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
@ -19,6 +20,7 @@ class ManagerAPI {
|
|||||||
final RootAPI _rootAPI = RootAPI();
|
final RootAPI _rootAPI = RootAPI();
|
||||||
final String patcherRepo = 'revanced-patcher';
|
final String patcherRepo = 'revanced-patcher';
|
||||||
final String cliRepo = 'revanced-cli';
|
final String cliRepo = 'revanced-cli';
|
||||||
|
late String storedPatchesFile = '/selected-patches.json';
|
||||||
late SharedPreferences _prefs;
|
late SharedPreferences _prefs;
|
||||||
String defaultApiUrl = 'https://releases.revanced.app/';
|
String defaultApiUrl = 'https://releases.revanced.app/';
|
||||||
String defaultPatcherRepo = 'revanced/revanced-patcher';
|
String defaultPatcherRepo = 'revanced/revanced-patcher';
|
||||||
@ -29,6 +31,8 @@ class ManagerAPI {
|
|||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
|
storedPatchesFile =
|
||||||
|
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getApiUrl() {
|
String getApiUrl() {
|
||||||
@ -391,4 +395,43 @@ class ManagerAPI {
|
|||||||
}
|
}
|
||||||
return app != null && app.isSplit;
|
return app != null && app.isSplit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setSelectedPatches(String app, List<String> patches) async {
|
||||||
|
final File selectedPatchesFile = File(storedPatchesFile);
|
||||||
|
Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
|
||||||
|
if (patches.isEmpty) {
|
||||||
|
patchesMap.remove(app);
|
||||||
|
} else {
|
||||||
|
patchesMap[app] = patches;
|
||||||
|
}
|
||||||
|
if (selectedPatchesFile.existsSync()) {
|
||||||
|
selectedPatchesFile.createSync(recursive: true);
|
||||||
|
}
|
||||||
|
selectedPatchesFile.writeAsString(jsonEncode(patchesMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getSelectedPatches(String app) async {
|
||||||
|
Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
|
||||||
|
if (patchesMap.isNotEmpty) {
|
||||||
|
final List<String> patches =
|
||||||
|
List.from(patchesMap.putIfAbsent(app, () => List.empty()));
|
||||||
|
return patches;
|
||||||
|
}
|
||||||
|
return List.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> readSelectedPatchesFile() async {
|
||||||
|
final File selectedPatchesFile = File(storedPatchesFile);
|
||||||
|
if (selectedPatchesFile.existsSync()) {
|
||||||
|
String string = selectedPatchesFile.readAsStringSync();
|
||||||
|
if (string.trim().isEmpty) return {};
|
||||||
|
return json.decode(string);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resetLastSelectedPatches() async {
|
||||||
|
final File selectedPatchesFile = File(storedPatchesFile);
|
||||||
|
selectedPatchesFile.deleteSync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ class AppSelectorViewModel extends BaseViewModel {
|
|||||||
icon: application.icon,
|
icon: application.icon,
|
||||||
patchDate: DateTime.now(),
|
patchDate: DateTime.now(),
|
||||||
);
|
);
|
||||||
locator<PatcherViewModel>().selectedPatches.clear();
|
locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||||
locator<PatcherViewModel>().notifyListeners();
|
locator<PatcherViewModel>().notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ class AppSelectorViewModel extends BaseViewModel {
|
|||||||
patchDate: DateTime.now(),
|
patchDate: DateTime.now(),
|
||||||
isFromStorage: true,
|
isFromStorage: true,
|
||||||
);
|
);
|
||||||
locator<PatcherViewModel>().selectedPatches.clear();
|
locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||||
locator<PatcherViewModel>().notifyListeners();
|
locator<PatcherViewModel>().notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,4 +107,15 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
'appSelectorCard.recommendedVersion',
|
'appSelectorCard.recommendedVersion',
|
||||||
)}: $recommendedVersion';
|
)}: $recommendedVersion';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadLastSelectedPatches() async {
|
||||||
|
this.selectedPatches.clear();
|
||||||
|
List<String> selectedPatches =
|
||||||
|
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName);
|
||||||
|
List<Patch> patches =
|
||||||
|
await _patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
|
||||||
|
this.selectedPatches
|
||||||
|
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
|
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
actions: [
|
actions: [
|
||||||
Container(
|
Container(
|
||||||
height: 2,
|
height: 2,
|
||||||
margin: const EdgeInsets.only(right: 16, top: 12, bottom: 12),
|
margin: const EdgeInsets.only(top: 12, bottom: 12),
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -78,6 +79,22 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
CustomPopupMenu(
|
||||||
|
onSelected: (value) => {
|
||||||
|
model.onMenuSelection(value)
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
0: I18nText(
|
||||||
|
'patchesSelectorView.loadPatchesSelection',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(64.0),
|
preferredSize: const Size.fromHeight(64.0),
|
||||||
|
@ -5,6 +5,7 @@ import 'package:revanced_manager/models/patch.dart';
|
|||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/patcher_api.dart';
|
import 'package:revanced_manager/services/patcher_api.dart';
|
||||||
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
@ -76,6 +77,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
|
|
||||||
void selectPatches() {
|
void selectPatches() {
|
||||||
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
||||||
|
saveSelectedPatches();
|
||||||
locator<PatcherViewModel>().notifyListeners();
|
locator<PatcherViewModel>().notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,4 +119,33 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
pack.name == app.packageName &&
|
pack.name == app.packageName &&
|
||||||
(pack.versions.isEmpty || pack.versions.contains(app.version)));
|
(pack.versions.isEmpty || pack.versions.contains(app.version)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onMenuSelection(value) {
|
||||||
|
switch (value) {
|
||||||
|
case 0:
|
||||||
|
loadSelectedPatches();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveSelectedPatches() async {
|
||||||
|
List<String> selectedPatches =
|
||||||
|
this.selectedPatches.map((patch) => patch.name).toList();
|
||||||
|
await _managerAPI.setSelectedPatches(
|
||||||
|
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
||||||
|
selectedPatches);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadSelectedPatches() async {
|
||||||
|
List<String> selectedPatches = await _managerAPI.getSelectedPatches(
|
||||||
|
locator<PatcherViewModel>().selectedApp!.originalPackageName);
|
||||||
|
if (selectedPatches.isNotEmpty) {
|
||||||
|
this.selectedPatches.clear();
|
||||||
|
this.selectedPatches.addAll(
|
||||||
|
patches.where((patch) => selectedPatches.contains(patch.name)));
|
||||||
|
} else {
|
||||||
|
locator<Toast>().showBottom('patchesSelectorView.noSavedPatches');
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,6 +185,54 @@ class SettingsView extends StatelessWidget {
|
|||||||
subtitle: I18nText('settingsView.deleteTempDirHint'),
|
subtitle: I18nText('settingsView.deleteTempDirHint'),
|
||||||
onTap: () => model.deleteTempDir(),
|
onTap: () => model.deleteTempDir(),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
title: I18nText(
|
||||||
|
'settingsView.exportPatchesLabel',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: I18nText('settingsView.exportPatchesHint'),
|
||||||
|
onTap: () => model.exportPatches(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
title: I18nText(
|
||||||
|
'settingsView.importPatchesLabel',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: I18nText('settingsView.importPatchesHint'),
|
||||||
|
onTap: () => model.importPatches(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
title: I18nText(
|
||||||
|
'settingsView.resetStoredPatchesLabel',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
|
||||||
|
onTap: () => model.resetSelectedPatches(),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:cr_file_saver/file_saver.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
@ -11,8 +13,10 @@ import 'package:revanced_manager/app/app.locator.dart';
|
|||||||
import 'package:revanced_manager/app/app.router.dart';
|
import 'package:revanced_manager/app/app.router.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/toast.dart';
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
|
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
import 'package:share_extend/share_extend.dart';
|
import 'package:share_extend/share_extend.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
@ -347,6 +351,60 @@ class SettingsViewModel extends BaseViewModel {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> exportPatches() async {
|
||||||
|
try {
|
||||||
|
File outFile = File(_managerAPI.storedPatchesFile);
|
||||||
|
if (outFile.existsSync()) {
|
||||||
|
String dateTime = DateTime.now()
|
||||||
|
.toString()
|
||||||
|
.replaceAll(' ', '_')
|
||||||
|
.split('.').first;
|
||||||
|
String tempFilePath = '${outFile.path.substring(0, outFile.path.lastIndexOf('/') + 1)}selected_patches_$dateTime.json';
|
||||||
|
outFile.copySync(tempFilePath);
|
||||||
|
await CRFileSaver.saveFileWithDialog(SaveFileDialogParams(
|
||||||
|
sourceFilePath: tempFilePath,
|
||||||
|
destinationFileName: ''
|
||||||
|
));
|
||||||
|
File(tempFilePath).delete();
|
||||||
|
locator<Toast>().showBottom('settingsView.exportedPatches');
|
||||||
|
} else {
|
||||||
|
locator<Toast>().showBottom('settingsView.noExportFileFound');
|
||||||
|
}
|
||||||
|
} on Exception catch (e, s) {
|
||||||
|
Sentry.captureException(e, stackTrace: s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> importPatches() async {
|
||||||
|
try {
|
||||||
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ['json'],
|
||||||
|
);
|
||||||
|
if (result != null && result.files.single.path != null) {
|
||||||
|
File inFile = File(result.files.single.path!);
|
||||||
|
final File storedPatchesFile = File(_managerAPI.storedPatchesFile);
|
||||||
|
if (!storedPatchesFile.existsSync()) {
|
||||||
|
storedPatchesFile.createSync(recursive: true);
|
||||||
|
}
|
||||||
|
inFile.copySync(storedPatchesFile.path);
|
||||||
|
inFile.delete();
|
||||||
|
if (locator<PatcherViewModel>().selectedApp != null) {
|
||||||
|
locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||||
|
}
|
||||||
|
locator<Toast>().showBottom('settingsView.importedPatches');
|
||||||
|
}
|
||||||
|
} on Exception catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
locator<Toast>().showBottom('settingsView.jsonSelectorErrorMessage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetSelectedPatches() {
|
||||||
|
_managerAPI.resetLastSelectedPatches();
|
||||||
|
_toast.showBottom('settingsView.resetStoredPatches');
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> getSdkVersion() async {
|
Future<int> getSdkVersion() async {
|
||||||
AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
||||||
return info.version.sdkInt ?? -1;
|
return info.version.sdkInt ?? -1;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user