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

@ -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(),
},
),
],