feat: Add ability to set null in patch options (#1947)

This commit is contained in:
aAbed 2024-06-24 22:22:16 +05:45 committed by GitHub
parent fc52560244
commit 5c68d513a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 155 additions and 278 deletions

View file

@ -118,16 +118,18 @@
},
"patchOptionsView": {
"customValue": "Custom value",
"setToNull": "Set to null",
"nullValue": "This option value is currently null",
"resetOptionsTooltip": "Reset patch options",
"viewTitle": "Patch options",
"saveOptions": "Save",
"addOptions": "Add options",
"deselectPatch": "Deselect patch",
"unselectPatch": "Unselect patch",
"tooltip": "More input options",
"selectFilePath": "Select file path",
"selectFolder": "Select folder",
"selectOption": "Select option",
"requiredOption": "This option is required",
"requiredOption": "Setting this option is required",
"unsupportedOption": "This option is not supported",
"requiredOptionNull": "The following options have to be set:\n\n${options}"
},

View file

@ -31,7 +31,6 @@ class ManagerAPI {
final String cliRepo = 'revanced-cli';
late SharedPreferences _prefs;
List<Patch> patches = [];
List<Option> modifiedOptions = [];
List<Option> options = [];
Patch? selectedPatch;
BuildContext? ctx;

View file

@ -37,79 +37,35 @@ class PatchOptionsView extends StatelessWidget {
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
actions: [
IconButton(
onPressed: () {
model.resetOptions();
},
icon: const Icon(
Icons.history,
),
tooltip: t.patchOptionsView.resetOptionsTooltip,
),
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
for (final Option option in model.visibleOptions)
for (final Option option in model.modifiedOptions)
if (option.valueType == 'String' ||
option.valueType == 'Int')
IntAndStringPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
model: model,
)
else if (option.valueType == 'Boolean')
BooleanPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
model: model,
)
else if (option.valueType == 'StringArray' ||
option.valueType == 'IntArray' ||
option.valueType == 'LongArray')
IntStringLongListPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
model: model,
)
else
UnsupportedPatchOption(
patchOption: option,
),
if (model.visibleOptions.length !=
model.options.length) ...[
const SizedBox(
height: 8,
),
FilledButton(
onPressed: () {
model.showAddOptionDialog(context);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add),
Text(t.patchOptionsView.addOptions),
],
),
),
],
const SizedBox(
height: 80,
),

View file

@ -5,7 +5,6 @@ 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:stacked/stacked.dart';
class PatchOptionsViewModel extends BaseViewModel {
@ -14,7 +13,7 @@ class PatchOptionsViewModel extends BaseViewModel {
locator<PatcherViewModel>().selectedApp!.packageName;
List<Option> options = [];
List<Option> savedOptions = [];
List<Option> visibleOptions = [];
List<Option> modifiedOptions = [];
Future<void> initialize() async {
options = getDefaultOptions();
@ -28,36 +27,18 @@ class PatchOptionsViewModel extends BaseViewModel {
savedOptions.add(savedOption);
}
}
if (savedOptions.isNotEmpty) {
visibleOptions = [
...savedOptions,
...options.where(
(option) =>
option.required &&
!savedOptions.any((sOption) => sOption.key == option.key),
),
];
} else {
visibleOptions = [
...options.where((option) => option.required),
];
}
}
void addOption(Option option) {
visibleOptions.add(option);
notifyListeners();
}
void removeOption(Option option) {
visibleOptions.removeWhere((vOption) => vOption.key == option.key);
notifyListeners();
modifiedOptions = [
...savedOptions,
...options.where(
(option) => !savedOptions.any((sOption) => sOption.key == option.key),
),
];
}
bool saveOptions(BuildContext context) {
final List<Option> requiredNullOptions = [];
for (final Option option in options) {
if (!visibleOptions.any((vOption) => vOption.key == option.key)) {
if (modifiedOptions.any((mOption) => mOption.key == option.key)) {
_managerAPI.clearPatchOption(
selectedApp,
_managerAPI.selectedPatch!.name,
@ -65,7 +46,7 @@ class PatchOptionsViewModel extends BaseViewModel {
);
}
}
for (final Option option in visibleOptions) {
for (final Option option in modifiedOptions) {
if (option.required && option.value == null) {
requiredNullOptions.add(option);
} else {
@ -98,11 +79,8 @@ class PatchOptionsViewModel extends BaseViewModel {
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);
modifiedOptions.removeWhere((mOption) => mOption.key == option.key);
modifiedOptions.add(modifiedOption);
}
List<Option> getDefaultOptions() {
@ -122,93 +100,11 @@ class PatchOptionsViewModel extends BaseViewModel {
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(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
t.patchOptionsView.addOptions,
),
Text(
'',
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
],
),
actions: [
FilledButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(t.cancelButton),
),
],
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.surface,
onTap: () {
addOption(e);
Navigator.pop(context);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
e.title,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
e.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
],
),
),
);
}).toList(),
),
),
);
}
dynamic getDefaultValue(Option patchOption) => _managerAPI.options
.firstWhere(
(option) => option.key == patchOption.key,
)
.value;
}
Future<void> showRequiredOptionNullDialog(
@ -248,7 +144,7 @@ Future<void> showRequiredOptionNullDialog(
PatchesSelectorViewModel().showPatchesChangeDialog(context);
}
},
child: Text(t.patchOptionsView.deselectPatch),
child: Text(t.patchOptionsView.unselectPatch),
),
FilledButton(
onPressed: () {

View file

@ -61,7 +61,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
void navigateToPatchOptions(List<Option> setOptions, Patch patch) {
_managerAPI.options = setOptions;
_managerAPI.selectedPatch = patch;
_managerAPI.modifiedOptions.clear();
_navigationService.navigateToPatchOptionsView();
}

View file

@ -3,19 +3,18 @@ import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:revanced_manager/gen/strings.g.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/shared/custom_card.dart';
class BooleanPatchOption extends StatelessWidget {
const BooleanPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
required this.model,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
final PatchOptionsViewModel model;
@override
Widget build(BuildContext context) {
@ -30,88 +29,94 @@ class BooleanPatchOption extends StatelessWidget {
value: value ?? false,
onChanged: (bool value) {
patchOptionValue.value = value;
onChanged(value, patchOption);
model.modifyOptions(value, patchOption);
},
);
},
),
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
patchOptionValue: patchOptionValue,
model: model,
);
}
}
class IntAndStringPatchOption extends StatelessWidget {
class IntAndStringPatchOption extends StatefulWidget {
const IntAndStringPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
required this.model,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
final PatchOptionsViewModel model;
@override
State<IntAndStringPatchOption> createState() =>
_IntAndStringPatchOptionState();
}
class _IntAndStringPatchOptionState extends State<IntAndStringPatchOption> {
ValueNotifier? patchOptionValue;
String getKey() {
if (patchOptionValue!.value != null && widget.patchOption.values != null) {
final List values = widget.patchOption.values!.entries
.where((e) => e.value == patchOptionValue!.value)
.toList();
if (values.isNotEmpty) {
return values.first.key;
}
}
return '';
}
@override
Widget build(BuildContext context) {
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
String getKey() {
if (patchOption.value != null && patchOption.values != null) {
final List values = patchOption.values!.entries
.where((e) => e.value == patchOption.value)
.toList();
if (values.isNotEmpty) {
return values.first.key;
}
}
return '';
}
patchOptionValue ??= ValueNotifier(widget.patchOption.value);
return PatchOption(
widget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFieldForPatchOption(
value: patchOption.value,
values: patchOption.values,
optionType: patchOption.valueType,
selectedKey: getKey(),
onChanged: (value) {
patchOptionValue.value = value;
onChanged(value, patchOption);
},
),
ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
if (patchOption.required && value == null) {
return Column(
widget: ValueListenableBuilder(
valueListenable: patchOptionValue!,
builder: (context, value, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFieldForPatchOption(
value: value,
patchOption: widget.patchOption,
selectedKey: getKey(),
onChanged: (value) {
patchOptionValue!.value = value;
widget.model.modifyOptions(value, widget.patchOption);
},
),
if (value == null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text(
t.patchOptionsView.requiredOption,
widget.patchOption.required
? t.patchOptionsView.requiredOption
: t.patchOptionsView.nullValue,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
color: widget.patchOption.required
? Theme.of(context).colorScheme.error
: Theme.of(context)
.colorScheme
.onSecondaryContainer
.withOpacity(0.6),
),
),
],
);
} else {
return const SizedBox();
}
},
),
],
),
],
);
},
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
patchOption: widget.patchOption,
patchOptionValue: patchOptionValue!,
model: widget.model,
);
}
}
@ -120,13 +125,11 @@ class IntStringLongListPatchOption extends StatelessWidget {
const IntStringLongListPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
required this.model,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
final PatchOptionsViewModel model;
@override
Widget build(BuildContext context) {
@ -172,8 +175,7 @@ class IntStringLongListPatchOption extends StatelessWidget {
final e = values[index];
return TextFieldForPatchOption(
value: e.toString(),
values: patchOption.values,
optionType: type,
patchOption: patchOption,
selectedKey: value.length > 1 ? '' : getKey(e),
showDropdown: index == 0,
onChanged: (newValue) {
@ -205,13 +207,13 @@ class IntStringLongListPatchOption extends StatelessWidget {
}
}
patchOptionValue.value = List.from(values);
onChanged(values, patchOption);
model.modifyOptions(values, patchOption);
},
removeValue: () {
patchOptionValue.value = List.from(patchOptionValue.value)
..removeAt(index);
values.removeAt(index);
onChanged(values, patchOption);
model.modifyOptions(values, patchOption);
},
);
},
@ -231,7 +233,7 @@ class IntStringLongListPatchOption extends StatelessWidget {
List.from(patchOptionValue.value)..add(0);
values.add(0);
}
onChanged(values, patchOption);
model.modifyOptions(values, patchOption);
},
child: Row(
mainAxisSize: MainAxisSize.min,
@ -254,9 +256,8 @@ class IntStringLongListPatchOption extends StatelessWidget {
},
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
patchOptionValue: patchOptionValue,
model: model,
);
}
}
@ -282,7 +283,8 @@ class UnsupportedPatchOption extends StatelessWidget {
),
),
patchOption: patchOption,
removeOption: (_) {},
patchOptionValue: ValueNotifier(null),
model: PatchOptionsViewModel(),
);
}
}
@ -292,15 +294,18 @@ class PatchOption extends StatelessWidget {
super.key,
required this.widget,
required this.patchOption,
required this.removeOption,
required this.patchOptionValue,
required this.model,
});
final Widget widget;
final Option patchOption;
final void Function(Option option) removeOption;
final ValueNotifier patchOptionValue;
final PatchOptionsViewModel model;
@override
Widget build(BuildContext context) {
final defaultValue = model.getDefaultValue(patchOption);
return Padding(
padding: const EdgeInsets.all(8.0),
child: CustomCard(
@ -337,11 +342,24 @@ class PatchOption extends StatelessWidget {
],
),
),
if (!patchOption.required)
IconButton(
onPressed: () => removeOption(patchOption),
icon: const Icon(Icons.delete),
),
ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
if (defaultValue != patchOptionValue.value) {
return IconButton(
onPressed: () {
patchOptionValue.value = defaultValue;
model.modifyOptions(
defaultValue,
patchOption,
);
},
icon: const Icon(Icons.history),
);
}
return const SizedBox();
},
),
],
),
const SizedBox(height: 4),
@ -360,17 +378,15 @@ class TextFieldForPatchOption extends StatefulWidget {
const TextFieldForPatchOption({
super.key,
required this.value,
required this.values,
required this.patchOption,
this.removeValue,
required this.onChanged,
required this.optionType,
required this.selectedKey,
this.showDropdown = true,
});
final String? value;
final Map<String, dynamic>? values;
final String optionType;
final Option patchOption;
final String selectedKey;
final bool showDropdown;
final void Function()? removeValue;
@ -388,20 +404,19 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
@override
Widget build(BuildContext context) {
final bool isStringOption = widget.optionType.contains('String');
final bool isArrayOption = widget.optionType.contains('Array');
selectedKey ??= widget.selectedKey;
controller.text = !isStringOption &&
isArrayOption &&
selectedKey == '' &&
(widget.value != null && widget.value.toString().startsWith('['))
? ''
: widget.value ?? '';
final bool isStringOption = widget.patchOption.valueType.contains('String');
final bool isArrayOption = widget.patchOption.valueType.contains('Array');
selectedKey = selectedKey == '' ? selectedKey : widget.selectedKey;
final bool isValueArray = widget.value?.startsWith('[') ?? false;
final bool shouldResetValue =
!isStringOption && isArrayOption && selectedKey == '' && isValueArray;
controller.text = shouldResetValue ? '' : widget.value ?? '';
defaultValue ??= controller.text;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.showDropdown && (widget.values?.isNotEmpty ?? false))
if (widget.showDropdown &&
(widget.patchOption.values?.isNotEmpty ?? false))
DropdownButton<String>(
style: const TextStyle(
fontSize: 16,
@ -410,11 +425,12 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
dropdownColor: Theme.of(context).colorScheme.secondaryContainer,
isExpanded: true,
value: selectedKey,
items: widget.values!.entries
items: widget.patchOption.values!.entries
.map(
(e) => DropdownMenuItem(
value: e.key,
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
text: e.key,
style: TextStyle(
@ -427,7 +443,7 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
TextSpan(
text: ' ${e.value}',
style: TextStyle(
fontSize: 14,
fontSize: 16,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer
@ -447,9 +463,7 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
t.patchOptionsView.customValue,
style: TextStyle(
fontSize: 16,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
),
@ -459,9 +473,11 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
controller.text = defaultValue!;
widget.onChanged(controller.text);
} else {
controller.text = widget.values![value].toString();
controller.text = widget.patchOption.values![value].toString();
widget.onChanged(
isArrayOption ? widget.values![value] : controller.text,
isArrayOption
? widget.patchOption.values![value]
: controller.text,
);
}
setState(() {
@ -472,9 +488,9 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
if (selectedKey == '')
TextFormField(
inputFormatters: [
if (widget.optionType.contains('Int'))
if (widget.patchOption.valueType.contains('Int'))
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
if (widget.optionType.contains('Long'))
if (widget.patchOption.valueType.contains('Long'))
FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*\.?[0-9]*')),
],
controller: controller,
@ -487,31 +503,36 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
return [
if (isArrayOption)
PopupMenuItem(
value: t.remove,
value: 'remove',
child: Text(t.remove),
),
if (isStringOption) ...[
PopupMenuItem(
value: t.patchOptionsView.selectFilePath,
value: 'file',
child: Text(t.patchOptionsView.selectFilePath),
),
PopupMenuItem(
value: t.patchOptionsView.selectFolder,
value: 'folder',
child: Text(t.patchOptionsView.selectFolder),
),
],
if (!widget.patchOption.required)
PopupMenuItem(
value: 'null',
child: Text(t.patchOptionsView.setToNull),
),
];
},
onSelected: (String selection) async {
switch (selection) {
case 'patchOptionsView.selectFilePath':
case 'file':
final String? result = await FlutterFileDialog.pickFile();
if (result != null) {
controller.text = result;
widget.onChanged(controller.text);
}
break;
case 'patchOptionsView.selectFolder':
case 'folder':
final DirectoryLocation? result =
await FlutterFileDialog.pickDirectory();
if (result != null) {
@ -522,6 +543,10 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
case 'remove':
widget.removeValue!();
break;
case 'null':
controller.text = '';
widget.onChanged(null);
break;
}
},
),