feat: Add patch options (#1354)

This commit is contained in:
aAbed
2023-10-12 00:00:39 +00:00
committed by GitHub
parent 2abadc73e4
commit ac636670c3
31 changed files with 1889 additions and 397 deletions

View File

@ -23,7 +23,6 @@ class _AppSelectorViewState extends State<AppSelectorView> {
onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => AppSelectorViewModel(),
builder: (context, model, child) => Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended(
label: I18nText('appSelectorView.storageButton'),
icon: const Icon(Icons.sd_storage),

View File

@ -13,6 +13,7 @@ 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:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
class AppSelectorViewModel extends BaseViewModel {
@ -78,7 +79,7 @@ class AppSelectorViewModel extends BaseViewModel {
icon: application.icon,
patchDate: DateTime.now(),
);
locator<PatcherViewModel>().loadLastSelectedPatches();
await locator<PatcherViewModel>().loadLastSelectedPatches();
}
Future<void> canSelectInstalled(
@ -93,10 +94,14 @@ class AppSelectorViewModel extends BaseViewModel {
return showSelectFromStorageDialog(context);
}
} else if (!await checkSplitApk(packageName) || isRooted) {
selectApp(app);
await selectApp(app);
if (context.mounted) {
Navigator.pop(context);
}
final List<Option> requiredNullOptions = getNullRequiredOptions(locator<PatcherViewModel>().selectedPatches, packageName);
if(requiredNullOptions.isNotEmpty){
locator<PatcherViewModel>().showRequiredOptionDialog();
}
}
}
}

View File

@ -34,7 +34,6 @@ class HomeViewModel extends BaseViewModel {
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
final Toast _toast = locator<Toast>();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
DateTime? _lastUpdate;
bool showUpdatableApps = false;
List<PatchedApplication> patchedInstalledApps = [];
String? _latestManagerVersion = '';

View File

@ -139,6 +139,7 @@ class InstallerViewModel extends BaseViewModel {
}
Future<void> runPatcher() async {
try {
await _patcherAPI.runPatcher(
_app.packageName,
@ -156,9 +157,9 @@ class InstallerViewModel extends BaseViewModel {
}
}
// Necessary to reset the state of patches by reloading them
// in a later patching process.
_managerAPI.patches.clear();
// Necessary to reset the state of patches so that they
// can be reloaded again.
_managerAPI.patches.clear();
await _patcherAPI.loadPatches();
try {

View File

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_options_fields.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
class PatchOptionsView extends StatelessWidget {
const PatchOptionsView({super.key});
@override
Widget build(BuildContext context) {
return ViewModelBuilder<PatchOptionsViewModel>.reactive(
onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => PatchOptionsViewModel(),
builder: (context, model, child) => GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
final bool saved = model.saveOptions(context);
if (saved && context.mounted) {
Navigator.pop(context);
}
},
label: I18nText('patchOptionsView.saveOptions'),
icon: const Icon(Icons.save),
),
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: I18nText(
'patchOptionsView.viewTitle',
child: Text(
'',
style: GoogleFonts.inter(
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
actions: [
IconButton(
onPressed: () {
model.resetOptions();
},
icon: const Icon(
Icons.history,
),
),
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
for (final Option option in model.visibleOptions)
if (option.optionClassType == 'StringPatchOption' ||
option.optionClassType == 'IntPatchOption')
IntAndStringPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
)
else if (option.optionClassType == 'BooleanPatchOption')
BooleanPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
)
else if (option.optionClassType ==
'StringListPatchOption' ||
option.optionClassType == 'IntListPatchOption' ||
option.optionClassType == 'LongListPatchOption')
IntStringLongListPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
)
else
UnsupportedPatchOption(
patchOption: option,
),
if (model.visibleOptions.length !=
model.options.length) ...[
const SizedBox(
height: 8,
),
CustomMaterialButton(
onPressed: () {
model.showAddOptionDialog(context);
},
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add),
I18nText('patchOptionsView.addOptions'),
],
),
),
],
const SizedBox(
height: 80,
),
],
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,258 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
class PatchOptionsViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final String selectedApp =
locator<PatcherViewModel>().selectedApp!.packageName;
List<Option> options = [];
List<Option> savedOptions = [];
List<Option> visibleOptions = [];
Future<void> initialize() async {
options = getDefaultOptions();
for (final Option option in options) {
final Option? savedOption = _managerAPI.getPatchOption(
selectedApp,
_managerAPI.selectedPatch!.name,
option.key,
);
if (savedOption != null) {
savedOptions.add(savedOption);
}
}
if (savedOptions.isNotEmpty) {
visibleOptions = [
...savedOptions,
...options
.where(
(option) =>
option.required &&
!savedOptions.any((sOption) => sOption.key == option.key),
)
.toList(),
];
} else {
visibleOptions = [
...options.where((option) => option.required).toList(),
];
}
}
void addOption(Option option) {
visibleOptions.add(option);
notifyListeners();
}
void removeOption(Option option) {
visibleOptions.removeWhere((vOption) => vOption.key == option.key);
notifyListeners();
}
bool saveOptions(BuildContext context) {
final List<Option> requiredNullOptions = [];
for (final Option option in options) {
if (!visibleOptions.any((vOption) => vOption.key == option.key)) {
_managerAPI.clearPatchOption(
selectedApp, _managerAPI.selectedPatch!.name, option.key);
}
}
for (final Option option in visibleOptions) {
if (option.required && option.value == null) {
requiredNullOptions.add(option);
} else {
_managerAPI.setPatchOption(
option, _managerAPI.selectedPatch!.name, selectedApp);
}
}
if (requiredNullOptions.isNotEmpty) {
showRequiredOptionNullDialog(
context,
requiredNullOptions,
_managerAPI,
selectedApp,
);
return false;
}
return true;
}
void modifyOptions(dynamic value, Option option) {
final Option modifiedOption = Option(
title: option.title,
description: option.description,
optionClassType: option.optionClassType,
value: value,
required: option.required,
key: option.key,
);
visibleOptions[visibleOptions
.indexWhere((vOption) => vOption.key == option.key)] = modifiedOption;
_managerAPI.modifiedOptions
.removeWhere((mOption) => mOption.key == option.key);
_managerAPI.modifiedOptions.add(modifiedOption);
}
List<Option> getDefaultOptions() {
final List<Option> defaultOptions = [];
for (final option in _managerAPI.options) {
final Option defaultOption = Option(
title: option.title,
description: option.description,
optionClassType: option.optionClassType,
value: option.value is List ? option.value.toList() : option.value,
required: option.required,
key: option.key,
);
defaultOptions.add(defaultOption);
}
return defaultOptions;
}
void resetOptions() {
_managerAPI.modifiedOptions.clear();
visibleOptions =
getDefaultOptions().where((option) => option.required).toList();
notifyListeners();
}
Future<void> showAddOptionDialog(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
I18nText(
'patchOptionsView.addOptions',
),
Text(
'',
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
],
),
actions: [
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
contentPadding: const EdgeInsets.all(8),
content: Wrap(
spacing: 14,
runSpacing: 14,
children: options
.where(
(option) =>
!visibleOptions.any((vOption) => vOption.key == option.key),
)
.map((e) {
return CustomCard(
padding: const EdgeInsets.all(4),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
onTap: () {
addOption(e);
Navigator.pop(context);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
e.title,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
e.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
)
],
),
),
);
}).toList(),
),
),
);
}
}
Future<void> showRequiredOptionNullDialog(
BuildContext context,
List<Option> options,
ManagerAPI managerAPI,
String selectedApp,
) async {
final List<String> optionsTitles = [];
for (final option in options) {
optionsTitles.add('${option.title}');
}
await showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('notice'),
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText(
'patchOptionsView.deselectPatch',
),
onPressed: () async {
if (managerAPI.isPatchesChangeEnabled()) {
locator<PatcherViewModel>()
.selectedPatches
.remove(managerAPI.selectedPatch);
locator<PatcherViewModel>().notifyListeners();
for (final option in options) {
managerAPI.clearPatchOption(
selectedApp, managerAPI.selectedPatch!.name, option.key);
}
Navigator.of(context)
..pop()
..pop()
..pop();
} else {
PatchesSelectorViewModel().showPatchesChangeDialog(context);
}
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: I18nText(
'patchOptionsView.requiredOptionNull',
translationParams: {
'options': optionsTitles.join('\n'),
},
),
),
);
}

View File

@ -22,7 +22,14 @@ class PatcherView extends StatelessWidget {
child: FloatingActionButton.extended(
label: I18nText('patcherView.patchButton'),
icon: const Icon(Icons.build),
onPressed: () => model.showRemovedPatchesDialog(context),
onPressed: () async{
if (model.checkRequiredPatchOption(context)) {
final bool proceed = model.showRemovedPatchesDialog(context);
if (proceed && context.mounted) {
model.showArmv7WarningDialog(context);
}
}
},
),
),
body: CustomScrollView(
@ -45,7 +52,10 @@ class PatcherView extends StatelessWidget {
delegate: SliverChildListDelegate.fixed(
<Widget>[
AppSelectorCard(
onPressed: () => model.navigateToAppSelector(),
onPressed: () => {
model.navigateToAppSelector(),
model.ctx = context,
},
),
const SizedBox(height: 16),
Opacity(

View File

@ -21,6 +21,7 @@ class PatcherViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
PatchedApplication? selectedApp;
BuildContext? ctx;
List<Patch> selectedPatches = [];
List<String> removedPatches = [];
@ -44,9 +45,9 @@ class PatcherViewModel extends BaseViewModel {
return selectedApp == null;
}
Future<void> showRemovedPatchesDialog(BuildContext context) async {
bool showRemovedPatchesDialog(BuildContext context) {
if (removedPatches.isNotEmpty) {
return showDialog(
showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('notice'),
@ -59,21 +60,58 @@ class PatcherViewModel extends BaseViewModel {
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
Navigator.of(context).pop();
navigateToInstaller();
showArmv7WarningDialog(context);
},
),
],
),
);
} else {
showArmv7WarningDialog(context); // TODO(aabed): Find out why this is here
return false;
}
return true;
}
bool checkRequiredPatchOption(BuildContext context) {
if (getNullRequiredOptions(selectedPatches, selectedApp!.packageName).isNotEmpty) {
showRequiredOptionDialog(context);
return false;
}
return true;
}
void showRequiredOptionDialog([context]) {
showDialog(
context: context ?? ctx,
builder: (context) => AlertDialog(
title: I18nText('notice'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.requiredOptionDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () => {
Navigator.of(context).pop(),
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => {
Navigator.pop(context),
navigateToPatchesSelector(),
},
),
],
),
);
}
Future<void> showArmv7WarningDialog(BuildContext context) async {
@ -163,7 +201,10 @@ class PatcherViewModel extends BaseViewModel {
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
for (final patch in usedPatches){
if (!patches.any((p) => p.name == patch.name)){
removedPatches.add('\u2022 ${patch.name}');
removedPatches.add(' ${patch.name}');
for (final option in patch.options) {
_managerAPI.clearPatchOption(selectedApp!.packageName, patch.name, option.key);
}
}
}
notifyListeners();

View File

@ -37,7 +37,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => PatchesSelectorViewModel(),
builder: (context, model, child) => Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: Visibility(
visible: model.patches.isNotEmpty,
child: FloatingActionButton.extended(
@ -49,8 +48,10 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
),
icon: const Icon(Icons.check),
onPressed: () {
model.selectPatches();
Navigator.of(context).pop();
if (!model.areRequiredOptionsNull(context)) {
model.selectPatches();
Navigator.of(context).pop();
}
},
),
),
@ -73,7 +74,10 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
Icons.arrow_back,
color: Theme.of(context).textTheme.titleLarge!.color,
),
onPressed: () => Navigator.of(context).pop(),
onPressed: () {
model.resetSelection();
Navigator.of(context).pop();
},
),
actions: [
FittedBox(
@ -81,7 +85,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 12),
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
@ -99,7 +103,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
),
CustomPopupMenu(
onSelected: (value) =>
{model.onMenuSelection(value, context)},
{model.onMenuSelection(value, context)},
children: {
0: I18nText(
'patchesSelectorView.loadPatchesSelection',
@ -188,6 +192,93 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
),
],
),
if (model.newPatchExists())
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.newPatches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
...model.getQueriedPatches(_query).map((patch) {
if (model.isPatchNew(patch)) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion:
model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: model.isSelected(patch),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
);
} else {
return Container();
}
}),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.patches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
),
),
],
),
...model.getQueriedPatches(_query).map(
(patch) {
if (patch.compatiblePackages.isNotEmpty) {
@ -201,13 +292,21 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
isNew: model.isPatchNew(
patch,
model.getAppInfo().packageName,
),
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options, patch),
options: patch.options,
isSelected: model.isSelected(patch),
onChanged: (value) =>
model.selectPatch(patch, value, context),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
);
} else {
return Container();
@ -254,8 +353,18 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
isNew: false,
hasUnsupportedPatchOption:
hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: model.isSelected(patch),
navigateToOptions: (options) =>
model.navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => model.selectPatch(
patch,
value,

View File

@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
@ -11,11 +12,14 @@ 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/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
class PatchesSelectorViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final NavigationService _navigationService = locator<NavigationService>();
final List<Patch> patches = [];
final List<Patch> currentSelection = [];
final List<Patch> selectedPatches =
locator<PatcherViewModel>().selectedPatches;
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
@ -31,14 +35,18 @@ class PatchesSelectorViewModel extends BaseViewModel {
selectedApp!.packageName,
),
);
final List<Option> requiredNullOptions =
getNullRequiredOptions(patches, selectedApp!.packageName);
patches.sort((a, b) {
if (isPatchNew(a, selectedApp!.packageName) ==
isPatchNew(b, selectedApp!.packageName)) {
return a.name.compareTo(b.name);
if (b.options.any((option) => requiredNullOptions.contains(option)) &&
a.options.isEmpty) {
return 1;
} else {
return isPatchNew(b, selectedApp!.packageName) ? 1 : -1;
return a.name.compareTo(b.name);
}
});
currentSelection.clear();
currentSelection.addAll(selectedPatches);
notifyListeners();
}
@ -48,6 +56,60 @@ class PatchesSelectorViewModel extends BaseViewModel {
);
}
void navigateToPatchOptions(List<Option> setOptions, Patch patch) {
_managerAPI.options = setOptions;
_managerAPI.selectedPatch = patch;
_managerAPI.modifiedOptions.clear();
_navigationService.navigateToPatchOptionsView();
}
bool areRequiredOptionsNull(BuildContext context) {
final List<String> patchesWithNullRequiredOptions = [];
final List<Option> requiredNullOptions =
getNullRequiredOptions(selectedPatches, selectedApp!.packageName);
if (requiredNullOptions.isNotEmpty) {
for (final patch in selectedPatches) {
for (final patchOption in patch.options) {
if (requiredNullOptions.contains(patchOption)) {
patchesWithNullRequiredOptions.add(patch.name);
break;
}
}
}
showSetRequiredOption(context, patchesWithNullRequiredOptions);
return true;
}
return false;
}
Future<void> showSetRequiredOption(
BuildContext context,
List<String> patches,
) async {
return showDialog(
barrierDismissible: false,
context: context,
builder: (context) => AlertDialog(
title: I18nText('notice'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
'patchesSelectorView.setRequiredOption',
translationParams: {
'patches': patches.map((patch) => '$patch').join('\n'),
},
),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => {
Navigator.of(context).pop(),
},
),
],
),
);
}
void selectPatch(Patch patch, bool isSelected, BuildContext context) {
if (_managerAPI.isPatchesChangeEnabled()) {
if (isSelected && !selectedPatches.contains(patch)) {
@ -123,9 +185,19 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectPatches() {
locator<PatcherViewModel>().selectedPatches = selectedPatches;
saveSelectedPatches();
if (_managerAPI.ctx != null) {
Navigator.pop(_managerAPI.ctx!);
_managerAPI.ctx = null;
}
locator<PatcherViewModel>().notifyListeners();
}
void resetSelection() {
selectedPatches.clear();
selectedPatches.addAll(currentSelection);
notifyListeners();
}
Future<void> getPatchesVersion() async {
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
}
@ -153,8 +225,9 @@ class PatchesSelectorViewModel extends BaseViewModel {
return locator<PatcherViewModel>().selectedApp!;
}
bool isPatchNew(Patch patch, String packageName) {
final List<Patch> savedPatches = _managerAPI.getSavedPatches(packageName);
bool isPatchNew(Patch patch) {
final List<Patch> savedPatches =
_managerAPI.getSavedPatches(selectedApp!.packageName);
if (savedPatches.isEmpty) {
return false;
} else {
@ -163,6 +236,12 @@ class PatchesSelectorViewModel extends BaseViewModel {
}
}
bool newPatchExists() {
return patches.any(
(patch) => isPatchNew(patch),
);
}
List<String> getSupportedVersions(Patch patch) {
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
final Package? package = patch.compatiblePackages.firstWhereOrNull(

View File

@ -246,6 +246,11 @@ class SettingsViewModel extends BaseViewModel {
}
}
void resetAllOptions() {
_managerAPI.resetAllOptions();
_toast.showBottom('settingsView.resetStoredOptions');
}
void resetSelectedPatches() {
_managerAPI.resetLastSelectedPatches();
_toast.showBottom('settingsView.resetStoredPatches');

View File

@ -146,7 +146,7 @@ class AppInfoViewModel extends BaseViewModel {
}
String getAppliedPatchesString(List<String> appliedPatches) {
return '\u2022 ${appliedPatches.join('\n\u2022 ')}';
return ' ${appliedPatches.join('\n ')}';
}
void openApp(PatchedApplication app) {

View File

@ -61,7 +61,7 @@ class PatchSelectorCard extends StatelessWidget {
final List<Patch> selectedPatches = locator<PatcherViewModel>().selectedPatches;
selectedPatches.sort((a, b) => a.name.compareTo(b.name));
for (final Patch p in selectedPatches) {
text += '\u2022 ${p.getSimpleName()}\n';
text += ' ${p.getSimpleName()}\n';
}
return text.substring(0, text.length - 1);
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
@ -16,11 +17,12 @@ class PatchItem extends StatefulWidget {
required this.packageVersion,
required this.supportedPackageVersions,
required this.isUnsupported,
required this.isNew,
required this.hasUnsupportedPatchOption,
required this.options,
required this.isSelected,
required this.onChanged,
required this.navigateToOptions,
required this.isChangeEnabled,
this.child,
}) : super(key: key);
final String name;
final String simpleName;
@ -28,11 +30,12 @@ class PatchItem extends StatefulWidget {
final String packageVersion;
final List<String> supportedPackageVersions;
final bool isUnsupported;
final bool isNew;
final bool hasUnsupportedPatchOption;
final List<Option> options;
bool isSelected;
final Function(bool) onChanged;
final void Function(List<Option>) navigateToOptions;
final bool isChangeEnabled;
final Widget? child;
final toast = locator<Toast>();
final _managerAPI = locator<ManagerAPI>();
@ -45,7 +48,8 @@ class _PatchItemState extends State<PatchItem> {
Widget build(BuildContext context) {
widget.isSelected = widget.isSelected &&
(!widget.isUnsupported ||
widget._managerAPI.areExperimentalPatchesEnabled());
widget._managerAPI.areExperimentalPatchesEnabled()) &&
!widget.hasUnsupportedPatchOption;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Opacity(
@ -54,148 +58,150 @@ class _PatchItemState extends State<PatchItem> {
? 0.5
: 1,
child: CustomCard(
padding: EdgeInsets.only(
top: 12,
bottom: 16,
left: 8.0,
right: widget.options.isNotEmpty ? 4.0 : 8.0,
),
onTap: () {
setState(() {
if (widget.isUnsupported &&
!widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.isSelected = false;
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
} else if (widget.isChangeEnabled) {
widget.isSelected = !widget.isSelected;
if (widget.isUnsupported &&
!widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.isSelected = false;
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
} else if (widget.isChangeEnabled) {
if (!widget.isSelected) {
if (widget.hasUnsupportedPatchOption) {
_showUnsupportedRequiredOptionDialog();
return;
}
}
});
if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.isSelected = !widget.isSelected;
setState(() {});
}
if (!widget.isUnsupported ||
widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.onChanged(widget.isSelected);
}
},
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Expanded(
child: Text(
widget.simpleName,
maxLines: 2,
overflow: TextOverflow.visible,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 4),
Text(
widget.description,
softWrap: true,
overflow: TextOverflow.visible,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Transform.scale(
scale: 1.2,
child: Checkbox(
value: widget.isSelected,
activeColor: Theme.of(context).colorScheme.primary,
checkColor: Theme.of(context).colorScheme.secondaryContainer,
side: BorderSide(
width: 2.0,
color: Theme.of(context).colorScheme.primary,
),
Transform.scale(
scale: 1.2,
child: Checkbox(
value: widget.isSelected,
activeColor: Theme.of(context).colorScheme.primary,
checkColor:
Theme.of(context).colorScheme.secondaryContainer,
side: BorderSide(
width: 2.0,
color: Theme.of(context).colorScheme.primary,
),
onChanged: (newValue) {
setState(() {
if (widget.isUnsupported &&
!widget._managerAPI
.areExperimentalPatchesEnabled()) {
widget.isSelected = false;
widget.toast.showBottom(
'patchItem.unsupportedPatchVersion',
);
} else if (widget.isChangeEnabled) {
widget.isSelected = newValue!;
}
});
if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.onChanged(widget.isSelected);
onChanged: (newValue) {
if (widget.isUnsupported &&
!widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.isSelected = false;
widget.toast.showBottom(
'patchItem.unsupportedPatchVersion',
);
} else if (widget.isChangeEnabled) {
if (!widget.isSelected) {
if (widget.hasUnsupportedPatchOption) {
_showUnsupportedRequiredOptionDialog();
return;
}
},
),
}
widget.isSelected = newValue!;
setState(() {});
}
if (!widget.isUnsupported ||
widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.onChanged(widget.isSelected);
}
},
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.simpleName,
maxLines: 2,
overflow: TextOverflow.visible,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Text(
widget.description,
softWrap: true,
overflow: TextOverflow.visible,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
if (widget.description.isNotEmpty)
Align(
alignment: Alignment.topLeft,
child: Wrap(
spacing: 8,
runSpacing: 4,
children: [
if (widget.isUnsupported &&
widget._managerAPI
.areExperimentalPatchesEnabled())
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextButton.icon(
label: I18nText('warning'),
icon: const Icon(
Icons.warning_amber_outlined,
size: 20.0,
),
onPressed: () =>
_showUnsupportedWarningDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context)
.colorScheme
.secondary,
),
),
),
backgroundColor:
MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor:
MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
),
),
),
),
],
),
),
],
),
],
),
),
Row(
children: [
if (widget.isUnsupported &&
widget._managerAPI.areExperimentalPatchesEnabled())
Padding(
padding: const EdgeInsets.only(top: 8, right: 8),
child: TextButton.icon(
label: I18nText('warning'),
icon: const Icon(Icons.warning, size: 20.0),
onPressed: () => _showUnsupportedWarningDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context).colorScheme.secondary,
),
),
),
backgroundColor: MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
),
),
),
),
if (widget.isNew)
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextButton.icon(
label: I18nText('new'),
icon: const Icon(Icons.star, size: 20.0),
onPressed: () => _showNewPatchDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context).colorScheme.secondary,
),
),
),
backgroundColor: MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
),
),
),
),
],
),
widget.child ?? const SizedBox(),
if (widget.options.isNotEmpty)
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () => widget.navigateToOptions(widget.options),
),
],
),
),
@ -214,7 +220,7 @@ class _PatchItemState extends State<PatchItem> {
translationParams: {
'packageVersion': widget.packageVersion,
'supportedVersions':
'\u2022 ${widget.supportedPackageVersions.reversed.join('\n\u2022 ')}',
' ${widget.supportedPackageVersions.reversed.join('\n ')}',
},
),
actions: <Widget>[
@ -227,14 +233,14 @@ class _PatchItemState extends State<PatchItem> {
);
}
Future<void> _showNewPatchDialog() {
Future<void> _showUnsupportedRequiredOptionDialog() {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('patchItem.newPatch'),
title: I18nText('notice'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
'patchItem.newPatchDialogText',
'patchItem.unsupportedRequiredOption',
),
actions: <Widget>[
CustomMaterialButton(

View File

@ -1,73 +1,387 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class OptionsTextField extends StatelessWidget {
const OptionsTextField({Key? key, required this.hint}) : super(key: key);
final String hint;
class BooleanPatchOption extends StatelessWidget {
const BooleanPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final sHeight = size.height;
final sWidth = size.width;
return Container(
margin: const EdgeInsets.only(top: 12, bottom: 6),
padding: EdgeInsets.zero,
child: TextField(
decoration: InputDecoration(
constraints: BoxConstraints(
maxHeight: sHeight * 0.05,
maxWidth: sWidth * 1,
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
return PatchOption(
widget: Align(
alignment: Alignment.bottomLeft,
child: ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
return Switch(
value: value ?? false,
onChanged: (bool value) {
patchOptionValue.value = value;
onChanged(value, patchOption);
},
);
},
),
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
);
}
}
class IntAndStringPatchOption extends StatelessWidget {
const IntAndStringPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
@override
Widget build(BuildContext context) {
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
return PatchOption(
widget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFieldForPatchOption(
value: patchOption.value,
optionType: patchOption.optionClassType,
onChanged: (value) {
patchOptionValue.value = value;
onChanged(value, patchOption);
},
),
border: const OutlineInputBorder(),
labelText: hint,
ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
if (patchOption.required && value == null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:[
const SizedBox(height: 8),
I18nText(
'patchOptionsView.requiredOption',
child: Text(
'',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
);
} else {
return const SizedBox();
}
},
),
],
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
);
}
}
class IntStringLongListPatchOption extends StatelessWidget {
const IntStringLongListPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
@override
Widget build(BuildContext context) {
final String type = patchOption.optionClassType;
final List<dynamic> values = patchOption.value ?? [];
final ValueNotifier patchOptionValue = ValueNotifier(values);
return PatchOption(
widget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
return ListView.builder(
shrinkWrap: true,
itemCount: value.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
final e = values[index];
return TextFieldForPatchOption(
value: e.toString(),
optionType: type,
onChanged: (newValue) {
values[index] = type == 'StringListPatchOption' ? newValue : type == 'IntListPatchOption' ? int.parse(newValue) : num.parse(newValue);
onChanged(values, patchOption);
},
removeValue: (value) {
patchOptionValue.value = List.from(patchOptionValue.value)..removeAt(index);
values.removeAt(index);
onChanged(values, patchOption);
},
);
},
);
},
),
const SizedBox(height: 4),
Align(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: () {
if (type == 'StringListPatchOption') {
patchOptionValue.value = List.from(patchOptionValue.value)..add('');
values.add('');
} else {
patchOptionValue.value = List.from(patchOptionValue.value)..add(0);
values.add(0);
}
onChanged(values, patchOption);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add, size: 20),
I18nText(
'add',
child: const Text(
'',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
],
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
);
}
}
class UnsupportedPatchOption extends StatelessWidget {
const UnsupportedPatchOption({super.key, required this.patchOption});
final Option patchOption;
@override
Widget build(BuildContext context) {
return PatchOption(
widget: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: I18nText(
'patchOptionsView.unsupportedOption',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
),
),
),
),
),
patchOption: patchOption,
removeOption: (_) {},
);
}
}
class PatchOption extends StatelessWidget {
const PatchOption({
super.key,
required this.widget,
required this.patchOption,
required this.removeOption,
});
final Widget widget;
final Option patchOption;
final void Function(Option option) removeOption;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: CustomCard(
onTap: () {},
child: Row(
children: [
Expanded(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
patchOption.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
patchOption.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
],
),
),
if (!patchOption.required)
IconButton(
onPressed: () => removeOption(patchOption),
icon: const Icon(Icons.delete),
),
],
),
const SizedBox(height: 4),
widget,
],
),
),
],
),
),
);
}
}
class OptionsFilePicker extends StatelessWidget {
const OptionsFilePicker({Key? key, required this.optionName})
: super(key: key);
final String optionName;
class TextFieldForPatchOption extends StatefulWidget {
const TextFieldForPatchOption({
super.key,
required this.value,
this.removeValue,
required this.onChanged,
required this.optionType,
});
final String? value;
final String optionType;
final void Function(dynamic value)? removeValue;
final void Function(dynamic value) onChanged;
@override
State<TextFieldForPatchOption> createState() =>
_TextFieldForPatchOptionState();
}
class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
final TextEditingController controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
I18nText(
optionName,
child: Text(
'',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
final bool isStringOption = widget.optionType.contains('String');
final bool isListOption = widget.optionType.contains('List');
controller.text = widget.value ?? '';
return TextFormField(
inputFormatters: [
if (widget.optionType.contains('Int'))
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
if (widget.optionType.contains('Long'))
FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*\.?[0-9]*')),
],
controller: controller,
keyboardType: isStringOption ? TextInputType.text : TextInputType.number,
decoration: InputDecoration(
suffixIcon: PopupMenuButton(
tooltip: FlutterI18n.translate(
context,
'patchOptionsView.tooltip',
),
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.primary,
),
),
onPressed: () {
// pick files
},
child: Text(
'Select File',
style: TextStyle(
color: Theme.of(context).textTheme.bodyLarge?.color,
),
),
),
],
itemBuilder: (BuildContext context) {
return [
if (isListOption)
PopupMenuItem(
value: 'remove',
child: I18nText('remove'),
),
if (isStringOption && !isListOption) ...[
PopupMenuItem(
value: 'patchOptionsView.selectFilePath',
child: I18nText('patchOptionsView.selectFilePath'),
),
PopupMenuItem(
value: 'patchOptionsView.selectFolder',
child: I18nText('patchOptionsView.selectFolder'),
),
],
];
},
onSelected: (String selection) async {
switch (selection) {
case 'patchOptionsView.selectFilePath':
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.single.path != null) {
controller.text = result.files.single.path.toString();
widget.onChanged(controller.text);
}
break;
case 'patchOptionsView.selectFolder':
final result = await FilePicker.platform.getDirectoryPath();
if (result != null) {
controller.text = result;
widget.onChanged(controller.text);
}
break;
case 'remove':
widget.removeValue!(widget.value);
break;
}
},
),
hintStyle: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
onChanged: (String value) {
widget.onChanged(value);
},
);
}
}

View File

@ -94,21 +94,49 @@ class SExportSection extends StatelessWidget {
),
),
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
onTap: () => _showResetStoredPatchesDialog(context),
onTap: () => _showResetDialog(
context,
'settingsView.resetStoredPatchesDialogTitle',
'settingsView.resetStoredPatchesDialogText',
_settingsViewModel.resetSelectedPatches,
),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.resetStoredOptionsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.resetStoredOptionsHint'),
onTap: () => _showResetDialog(
context,
'settingsView.resetStoredOptionsDialogTitle',
'settingsView.resetStoredOptionsDialogText',
_settingsViewModel.resetAllOptions,
),
),
],
);
}
Future<void> _showResetStoredPatchesDialog(context) {
Future<void> _showResetDialog(
context,
dialogTitle,
dialogText,
dialogAction,
) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.resetStoredPatchesDialogTitle'),
title: I18nText(dialogTitle),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
'settingsView.resetStoredPatchesDialogText',
),
content: I18nText(dialogText),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
@ -119,7 +147,7 @@ class SExportSection extends StatelessWidget {
label: I18nText('yesButton'),
onPressed: () => {
Navigator.of(context).pop(),
_settingsViewModel.resetSelectedPatches(),
dialogAction(),
},
),
],