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:
Boris M 2022-11-09 08:36:04 +01:00 committed by GitHub
parent fd5d71e24d
commit c571cf2c53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 224 additions and 3 deletions

View file

@ -77,6 +77,8 @@
"viewTitle": "Select patches",
"searchBarHint": "Search patches",
"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",
"selectAllPatchesWarningTitle": "Warning",
"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",
"deleteTempDirHint": "Delete the temporary directory used to store temporary files",
"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",
"deleteLogsHint": "Delete collected manager logs",
"deletedLogs": "Logs deleted"

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:injectable/injectable.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/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
@ -19,6 +20,7 @@ class ManagerAPI {
final RootAPI _rootAPI = RootAPI();
final String patcherRepo = 'revanced-patcher';
final String cliRepo = 'revanced-cli';
late String storedPatchesFile = '/selected-patches.json';
late SharedPreferences _prefs;
String defaultApiUrl = 'https://releases.revanced.app/';
String defaultPatcherRepo = 'revanced/revanced-patcher';
@ -29,6 +31,8 @@ class ManagerAPI {
Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
storedPatchesFile =
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
}
String getApiUrl() {
@ -391,4 +395,43 @@ class ManagerAPI {
}
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();
}
}

View file

@ -34,7 +34,7 @@ class AppSelectorViewModel extends BaseViewModel {
icon: application.icon,
patchDate: DateTime.now(),
);
locator<PatcherViewModel>().selectedPatches.clear();
locator<PatcherViewModel>().loadLastSelectedPatches();
locator<PatcherViewModel>().notifyListeners();
}
@ -66,7 +66,7 @@ class AppSelectorViewModel extends BaseViewModel {
patchDate: DateTime.now(),
isFromStorage: true,
);
locator<PatcherViewModel>().selectedPatches.clear();
locator<PatcherViewModel>().loadLastSelectedPatches();
locator<PatcherViewModel>().notifyListeners();
}
}

View file

@ -107,4 +107,15 @@ class PatcherViewModel extends BaseViewModel {
'appSelectorCard.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();
}
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.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/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:stacked/stacked.dart';
@ -63,7 +64,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
actions: [
Container(
height: 2,
margin: const EdgeInsets.only(right: 16, top: 12, bottom: 12),
margin: const EdgeInsets.only(top: 12, bottom: 12),
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
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(
preferredSize: const Size.fromHeight(64.0),

View file

@ -5,6 +5,7 @@ import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/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:stacked/stacked.dart';
@ -76,6 +77,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectPatches() {
locator<PatcherViewModel>().selectedPatches = selectedPatches;
saveSelectedPatches();
locator<PatcherViewModel>().notifyListeners();
}
@ -117,4 +119,33 @@ class PatchesSelectorViewModel extends BaseViewModel {
pack.name == app.packageName &&
(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();
}
}

View file

@ -185,6 +185,54 @@ class SettingsView extends StatelessWidget {
subtitle: I18nText('settingsView.deleteTempDirHint'),
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(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),

View file

@ -1,7 +1,9 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:cr_file_saver/file_saver.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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/services/manager_api.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/settingsView/custom_text_field.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:share_extend/share_extend.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
@ -347,6 +351,60 @@ class SettingsViewModel extends BaseViewModel {
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 {
AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt ?? -1;