Support proxies search Support svg display Optimize config persistence Add some scenes auto close connections Update core Optimize more details
622 lines
16 KiB
Dart
622 lines
16 KiB
Dart
import 'package:fl_clash/common/app_localizations.dart';
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/models/common.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:fl_clash/widgets/dialog.dart';
|
|
import 'package:fl_clash/widgets/null_status.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'card.dart';
|
|
import 'effect.dart';
|
|
import 'float_layout.dart';
|
|
import 'list.dart';
|
|
|
|
class OptionsDialog<T> extends StatelessWidget {
|
|
final String title;
|
|
final List<T> options;
|
|
final T value;
|
|
final String Function(T value) textBuilder;
|
|
|
|
const OptionsDialog({
|
|
super.key,
|
|
required this.title,
|
|
required this.options,
|
|
required this.textBuilder,
|
|
required this.value,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CommonDialog(
|
|
title: title,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 16,
|
|
),
|
|
child: Wrap(
|
|
children: [
|
|
for (final option in options)
|
|
Builder(
|
|
builder: (context) {
|
|
if (value == option) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
Scrollable.ensureVisible(context);
|
|
});
|
|
}
|
|
return ListItem.radio(
|
|
delegate: RadioDelegate(
|
|
value: option,
|
|
groupValue: value,
|
|
onChanged: (T? value) {
|
|
Navigator.of(context).pop(value);
|
|
},
|
|
),
|
|
title: Text(textBuilder(option)),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class CommonCheckBox extends StatelessWidget {
|
|
final bool? value;
|
|
final ValueChanged<bool?>? onChanged;
|
|
final bool isCircle;
|
|
|
|
const CommonCheckBox({
|
|
required this.value,
|
|
required this.onChanged,
|
|
this.isCircle = false,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Checkbox(
|
|
shape: isCircle ? const CircleBorder() : null,
|
|
value: value,
|
|
onChanged: onChanged,
|
|
);
|
|
}
|
|
}
|
|
|
|
class InputDialog extends StatefulWidget {
|
|
final String title;
|
|
final String value;
|
|
final String? suffixText;
|
|
final String? labelText;
|
|
final String? resetValue;
|
|
final String? hintText;
|
|
final FormFieldValidator<String>? validator;
|
|
final AutovalidateMode? autovalidateMode;
|
|
|
|
const InputDialog({
|
|
super.key,
|
|
required this.title,
|
|
required this.value,
|
|
this.suffixText,
|
|
this.resetValue,
|
|
this.hintText,
|
|
this.validator,
|
|
this.labelText,
|
|
this.autovalidateMode = AutovalidateMode.onUserInteraction,
|
|
});
|
|
|
|
@override
|
|
State<InputDialog> createState() => _InputDialogState();
|
|
}
|
|
|
|
class _InputDialogState extends State<InputDialog> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
late TextEditingController textController;
|
|
|
|
String get value => widget.value;
|
|
|
|
String get title => widget.title;
|
|
|
|
String? get suffixText => widget.suffixText;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
textController = TextEditingController(
|
|
text: value,
|
|
);
|
|
}
|
|
|
|
_handleUpdate() async {
|
|
if (_formKey.currentState?.validate() == false) return;
|
|
final text = textController.value.text;
|
|
Navigator.of(context).pop<String>(text);
|
|
}
|
|
|
|
_handleReset() async {
|
|
if (widget.resetValue == null) {
|
|
return;
|
|
}
|
|
Navigator.of(context).pop<String>(widget.resetValue);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CommonDialog(
|
|
title: title,
|
|
actions: [
|
|
if (widget.resetValue != null &&
|
|
textController.value.text != widget.resetValue) ...[
|
|
TextButton(
|
|
onPressed: _handleReset,
|
|
child: Text(appLocalizations.reset),
|
|
),
|
|
const SizedBox(
|
|
width: 4,
|
|
),
|
|
],
|
|
TextButton(
|
|
onPressed: _handleUpdate,
|
|
child: Text(appLocalizations.submit),
|
|
)
|
|
],
|
|
child: Form(
|
|
autovalidateMode: widget.autovalidateMode,
|
|
key: _formKey,
|
|
child: Wrap(
|
|
runSpacing: 16,
|
|
children: [
|
|
TextFormField(
|
|
keyboardType: TextInputType.url,
|
|
maxLines: 5,
|
|
minLines: 1,
|
|
controller: textController,
|
|
onFieldSubmitted: (_) {
|
|
_handleUpdate();
|
|
},
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
suffixText: suffixText,
|
|
hintText: widget.hintText,
|
|
labelText: widget.labelText,
|
|
),
|
|
validator: widget.validator,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ListInputPage extends StatelessWidget {
|
|
final String title;
|
|
final List<String> items;
|
|
final Widget Function(String item) titleBuilder;
|
|
final Widget Function(String item)? subtitleBuilder;
|
|
final Widget Function(String item)? leadingBuilder;
|
|
final String? valueLabel;
|
|
final Function(List<String> items) onChange;
|
|
|
|
const ListInputPage({
|
|
super.key,
|
|
required this.title,
|
|
required this.items,
|
|
required this.titleBuilder,
|
|
required this.onChange,
|
|
this.leadingBuilder,
|
|
this.valueLabel,
|
|
this.subtitleBuilder,
|
|
});
|
|
|
|
_handleAddOrEdit([String? item]) async {
|
|
uniqueValidator(String? value) {
|
|
final index = items.indexWhere(
|
|
(entry) {
|
|
return entry == value;
|
|
},
|
|
);
|
|
final current = item == value;
|
|
if (index != -1 && !current) {
|
|
return appLocalizations.existsTip(appLocalizations.value);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
final valueField = Field(
|
|
label: valueLabel ?? appLocalizations.value,
|
|
value: item ?? "",
|
|
validator: uniqueValidator,
|
|
);
|
|
final value = await globalState.showCommonDialog<String>(
|
|
child: AddDialog(
|
|
valueField: valueField,
|
|
title: title,
|
|
),
|
|
);
|
|
if (value == null) return;
|
|
final index = items.indexWhere(
|
|
(entry) {
|
|
return entry == item;
|
|
},
|
|
);
|
|
final nextItems = List<String>.from(items);
|
|
if (item != null) {
|
|
nextItems[index] = value;
|
|
} else {
|
|
nextItems.add(value);
|
|
}
|
|
onChange(nextItems);
|
|
}
|
|
|
|
_handleDelete(String? item) {
|
|
final entries = List<String>.from(
|
|
items,
|
|
);
|
|
final index = entries.indexWhere(
|
|
(entry) {
|
|
return entry == item;
|
|
},
|
|
);
|
|
if (index != -1) {
|
|
entries.removeAt(index);
|
|
}
|
|
onChange(entries);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FloatLayout(
|
|
floatingWidget: FloatWrapper(
|
|
child: FloatingActionButton(
|
|
onPressed: () async {
|
|
_handleAddOrEdit();
|
|
},
|
|
child: const Icon(Icons.add),
|
|
),
|
|
),
|
|
child: items.isEmpty
|
|
? NullStatus(label: appLocalizations.noData)
|
|
: ReorderableListView.builder(
|
|
padding: const EdgeInsets.only(
|
|
bottom: 16 + 64,
|
|
),
|
|
buildDefaultDragHandles: false,
|
|
itemCount: items.length,
|
|
itemBuilder: (context, index) {
|
|
final e = items[index];
|
|
return _InputItem(
|
|
key: ValueKey(e),
|
|
ReorderableDelayedDragStartListener(
|
|
index: index,
|
|
child: CommonCard(
|
|
child: ListItem(
|
|
leading:
|
|
leadingBuilder != null ? leadingBuilder!(e) : null,
|
|
title: titleBuilder(e),
|
|
subtitle: subtitleBuilder != null
|
|
? subtitleBuilder!(e)
|
|
: null,
|
|
trailing: IconButton(
|
|
icon: const Icon(Icons.delete_outline),
|
|
onPressed: () {
|
|
_handleDelete(e);
|
|
},
|
|
),
|
|
),
|
|
onPressed: () {
|
|
_handleAddOrEdit(e);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
onReorder: (oldIndex, newIndex) {
|
|
if (oldIndex < newIndex) {
|
|
newIndex -= 1;
|
|
}
|
|
final nextItems = List<String>.from(items);
|
|
final item = nextItems.removeAt(oldIndex);
|
|
nextItems.insert(newIndex, item);
|
|
onChange(nextItems);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MapInputPage extends StatelessWidget {
|
|
final String title;
|
|
final Map<String, String> map;
|
|
final Widget Function(MapEntry<String, String> item) titleBuilder;
|
|
final Widget Function(MapEntry<String, String> item)? subtitleBuilder;
|
|
final Widget Function(MapEntry<String, String> item)? leadingBuilder;
|
|
final String? keyLabel;
|
|
final String? valueLabel;
|
|
final Function(Map<String, String> items) onChange;
|
|
|
|
const MapInputPage({
|
|
super.key,
|
|
required this.title,
|
|
required this.map,
|
|
required this.titleBuilder,
|
|
required this.onChange,
|
|
this.leadingBuilder,
|
|
this.keyLabel,
|
|
this.valueLabel,
|
|
this.subtitleBuilder,
|
|
});
|
|
|
|
List<MapEntry<String, String>> get items =>
|
|
List<MapEntry<String, String>>.from(
|
|
map.entries,
|
|
);
|
|
|
|
_handleAddOrEdit([MapEntry<String, String>? item]) async {
|
|
uniqueValidator(String? value) {
|
|
final index = items.indexWhere(
|
|
(entry) {
|
|
return entry.key == value;
|
|
},
|
|
);
|
|
final current = item?.key == value;
|
|
if (index != -1 && !current) {
|
|
return appLocalizations.existsTip(appLocalizations.key);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
final keyField = Field(
|
|
label: keyLabel ?? appLocalizations.key,
|
|
value: item == null ? "" : item.key,
|
|
validator: uniqueValidator,
|
|
);
|
|
|
|
final valueField = Field(
|
|
label: valueLabel ?? appLocalizations.value,
|
|
value: item == null ? "" : item.value,
|
|
);
|
|
|
|
final value = await globalState.showCommonDialog<MapEntry<String, String>>(
|
|
child: AddDialog(
|
|
keyField: keyField,
|
|
valueField: valueField,
|
|
title: title,
|
|
),
|
|
);
|
|
if (value == null) return;
|
|
final index = items.indexWhere(
|
|
(entry) {
|
|
return entry.key == item?.key;
|
|
},
|
|
);
|
|
|
|
final nextItems = List<MapEntry<String, String>>.from(items);
|
|
if (item != null) {
|
|
nextItems[index] = value;
|
|
} else {
|
|
nextItems.add(value);
|
|
}
|
|
onChange(Map.fromEntries(nextItems));
|
|
}
|
|
|
|
_handleDelete(MapEntry<String, String> item) {
|
|
final entries = List<MapEntry<String, String>>.from(
|
|
items,
|
|
);
|
|
final index = entries.indexWhere(
|
|
(entry) {
|
|
return entry.key == item.key && item.value == entry.value;
|
|
},
|
|
);
|
|
if (index != -1) {
|
|
entries.removeAt(index);
|
|
}
|
|
onChange(Map.fromEntries(entries));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FloatLayout(
|
|
floatingWidget: FloatWrapper(
|
|
child: FloatingActionButton(
|
|
onPressed: () async {
|
|
_handleAddOrEdit();
|
|
},
|
|
child: const Icon(Icons.add),
|
|
),
|
|
),
|
|
child: items.isEmpty
|
|
? NullStatus(label: appLocalizations.noData)
|
|
: ReorderableListView.builder(
|
|
padding: const EdgeInsets.only(
|
|
bottom: 16 + 64,
|
|
),
|
|
proxyDecorator: proxyDecorator,
|
|
buildDefaultDragHandles: false,
|
|
itemCount: items.length,
|
|
itemBuilder: (_, index) {
|
|
final e = items[index];
|
|
return _InputItem(
|
|
key: ValueKey(e.key),
|
|
ReorderableDelayedDragStartListener(
|
|
index: index,
|
|
child: CommonCard(
|
|
child: ListItem(
|
|
leading:
|
|
leadingBuilder != null ? leadingBuilder!(e) : null,
|
|
title: titleBuilder(e),
|
|
subtitle: subtitleBuilder != null
|
|
? subtitleBuilder!(e)
|
|
: null,
|
|
trailing: IconButton(
|
|
icon: const Icon(Icons.delete_outline),
|
|
onPressed: () {
|
|
_handleDelete(e);
|
|
},
|
|
),
|
|
),
|
|
onPressed: () {
|
|
_handleAddOrEdit(e);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
onReorder: (oldIndex, newIndex) {
|
|
if (oldIndex < newIndex) {
|
|
newIndex -= 1;
|
|
}
|
|
final nextItems = List<MapEntry<String, String>>.from(items);
|
|
final item = nextItems.removeAt(oldIndex);
|
|
nextItems.insert(newIndex, item);
|
|
onChange(Map.fromEntries(nextItems));
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AddDialog extends StatefulWidget {
|
|
final String title;
|
|
final Field? keyField;
|
|
final Field valueField;
|
|
|
|
const AddDialog({
|
|
super.key,
|
|
required this.title,
|
|
this.keyField,
|
|
required this.valueField,
|
|
});
|
|
|
|
@override
|
|
State<AddDialog> createState() => _AddDialogState();
|
|
}
|
|
|
|
class _AddDialogState extends State<AddDialog> {
|
|
TextEditingController? keyController;
|
|
late TextEditingController valueController;
|
|
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
|
|
|
Field? get keyField => widget.keyField;
|
|
|
|
Field get valueField => widget.valueField;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (keyField != null) {
|
|
keyController = TextEditingController(
|
|
text: keyField!.value,
|
|
);
|
|
}
|
|
valueController = TextEditingController(
|
|
text: valueField.value,
|
|
);
|
|
}
|
|
|
|
_submit() {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
if (keyField != null) {
|
|
Navigator.of(context).pop<MapEntry<String, String>>(
|
|
MapEntry(
|
|
keyController!.text,
|
|
valueController.text,
|
|
),
|
|
);
|
|
} else {
|
|
Navigator.of(context).pop<String>(
|
|
valueController.text,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CommonDialog(
|
|
title: widget.title,
|
|
actions: [
|
|
TextButton(
|
|
onPressed: _submit,
|
|
child: Text(appLocalizations.confirm),
|
|
)
|
|
],
|
|
child: Form(
|
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
key: _formKey,
|
|
child: Wrap(
|
|
runSpacing: 16,
|
|
children: [
|
|
if (keyField != null)
|
|
TextFormField(
|
|
maxLines: 2,
|
|
minLines: 1,
|
|
controller: keyController,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: keyField!.label,
|
|
),
|
|
validator: (String? value) {
|
|
String? res;
|
|
if (keyField!.validator != null) {
|
|
res = keyField!.validator!(value);
|
|
}
|
|
if (res != null) {
|
|
return res;
|
|
}
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.emptyTip(appLocalizations.key);
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
TextFormField(
|
|
maxLines: 3,
|
|
minLines: 1,
|
|
controller: valueController,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: valueField.label,
|
|
),
|
|
validator: (String? value) {
|
|
String? res;
|
|
if (valueField.validator != null) {
|
|
res = valueField.validator!(value);
|
|
}
|
|
if (res != null) {
|
|
return res;
|
|
}
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.emptyTip(appLocalizations.value);
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _InputItem extends StatelessWidget {
|
|
final Widget child;
|
|
|
|
const _InputItem(this.child, {super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
elevation: 0,
|
|
key: key,
|
|
color: Colors.transparent,
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
margin: EdgeInsets.symmetric(vertical: 8),
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
}
|