Add android vpn options Support proxies icon configuration Optimize android immersion display Fix some issues
450 lines
12 KiB
Dart
450 lines
12 KiB
Dart
import 'package:fl_clash/common/app_localizations.dart';
|
|
import 'package:fl_clash/common/constant.dart';
|
|
import 'package:fl_clash/models/common.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:fl_clash/widgets/null_status.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'card.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 AlertDialog(
|
|
title: Text(title),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 16,
|
|
),
|
|
content: SizedBox(
|
|
width: 250,
|
|
child: Wrap(
|
|
children: [
|
|
for (final option in options)
|
|
ListItem.radio(
|
|
delegate: RadioDelegate(
|
|
value: option,
|
|
groupValue: value,
|
|
onChanged: (T? value) {
|
|
Navigator.of(context).pop(value);
|
|
},
|
|
),
|
|
title: Text(
|
|
this.textBuilder(option),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class InputDialog extends StatefulWidget {
|
|
final String title;
|
|
final String value;
|
|
final String? suffixText;
|
|
final String? resetValue;
|
|
|
|
const InputDialog({
|
|
super.key,
|
|
required this.title,
|
|
required this.value,
|
|
this.suffixText,
|
|
this.resetValue,
|
|
});
|
|
|
|
@override
|
|
State<InputDialog> createState() => _InputDialogState();
|
|
}
|
|
|
|
class _InputDialogState extends State<InputDialog> {
|
|
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 {
|
|
final text = textController.value.text;
|
|
if (text.isEmpty) return;
|
|
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 AlertDialog(
|
|
title: Text(title),
|
|
content: SizedBox(
|
|
width: 300,
|
|
child: Wrap(
|
|
runSpacing: 16,
|
|
children: [
|
|
TextField(
|
|
maxLines: 1,
|
|
minLines: 1,
|
|
controller: textController,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
suffixText: suffixText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
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),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class ListPage<T> extends StatelessWidget {
|
|
final String title;
|
|
final Iterable<T> items;
|
|
final Key Function(T item)? keyBuilder;
|
|
final Widget Function(T item) titleBuilder;
|
|
final Widget Function(T item)? subtitleBuilder;
|
|
final Widget Function(T item)? leadingBuilder;
|
|
final String? keyLabel;
|
|
final String? valueLabel;
|
|
final Function(Iterable<T> items) onChange;
|
|
|
|
const ListPage({
|
|
super.key,
|
|
required this.title,
|
|
required this.items,
|
|
this.keyBuilder,
|
|
required this.titleBuilder,
|
|
required this.onChange,
|
|
this.leadingBuilder,
|
|
this.keyLabel,
|
|
this.valueLabel,
|
|
this.subtitleBuilder,
|
|
});
|
|
|
|
bool get isMap => items is Iterable<MapEntry>;
|
|
|
|
_handleAddOrEdit([T? item]) async {
|
|
final value = await globalState.showCommonDialog<T>(
|
|
child: AddDialog(
|
|
keyField: isMap
|
|
? Field(
|
|
label: this.keyLabel ?? appLocalizations.key,
|
|
value:
|
|
item == null ? "" : (item as MapEntry<String, String>).key,
|
|
)
|
|
: null,
|
|
valueField: Field(
|
|
label: this.valueLabel ?? appLocalizations.value,
|
|
value: item == null
|
|
? ""
|
|
: isMap
|
|
? (item as MapEntry<String, String>).value
|
|
: item as String,
|
|
),
|
|
title: title,
|
|
),
|
|
);
|
|
if (value == null) return;
|
|
final entries = List<T>.from(
|
|
items,
|
|
);
|
|
if (item != null) {
|
|
final index = entries.indexWhere(
|
|
(entry) {
|
|
if (isMap) {
|
|
return (entry as MapEntry<String, String>).key ==
|
|
(item as MapEntry<String, String>).key;
|
|
}
|
|
return entry == item;
|
|
},
|
|
);
|
|
if (index != -1) {
|
|
entries[index] = value;
|
|
}
|
|
} else {
|
|
entries.add(value);
|
|
}
|
|
onChange(entries);
|
|
}
|
|
|
|
_handleDelete(T item) {
|
|
final entries = List<T>.from(
|
|
items,
|
|
);
|
|
final index = entries.indexWhere(
|
|
(entry) {
|
|
if (isMap) {
|
|
return (entry as MapEntry<String, String>).key ==
|
|
(item as MapEntry<String, String>).key;
|
|
}
|
|
return entry == item;
|
|
},
|
|
);
|
|
if (index != -1) {
|
|
entries.removeAt(index);
|
|
}
|
|
onChange(entries);
|
|
}
|
|
|
|
Widget _buildList() {
|
|
final items = this.items.toList();
|
|
if (this.keyBuilder != null) {
|
|
return ReorderableListView.builder(
|
|
padding: const EdgeInsets.only(
|
|
bottom: 16 + 64,
|
|
left: 16,
|
|
right: 16,
|
|
),
|
|
buildDefaultDragHandles: false,
|
|
itemCount: items.length,
|
|
itemBuilder: (_, index) {
|
|
final e = items[index];
|
|
return Padding(
|
|
key: keyBuilder!(e),
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: ReorderableDragStartListener(
|
|
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<T>.from(items);
|
|
final item = nextItems.removeAt(oldIndex);
|
|
nextItems.insert(newIndex, item);
|
|
onChange(nextItems);
|
|
},
|
|
);
|
|
} else {
|
|
return ListView.builder(
|
|
padding: const EdgeInsets.only(
|
|
bottom: 16 + 64,
|
|
left: 16,
|
|
right: 16,
|
|
),
|
|
itemCount: items.length,
|
|
itemBuilder: (_, index) {
|
|
final e = items[index];
|
|
return Padding(
|
|
key: ObjectKey(e.toString()),
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
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);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
@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)
|
|
: _buildList(),
|
|
);
|
|
}
|
|
}
|
|
|
|
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 AlertDialog(
|
|
title: Text(widget.title),
|
|
content: Form(
|
|
key: _formKey,
|
|
child: SizedBox(
|
|
width: dialogCommonWidth,
|
|
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) {
|
|
if (keyField!.validator != null) {
|
|
return keyField!.validator!(value);
|
|
}
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.notEmpty;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
TextFormField(
|
|
maxLines: 3,
|
|
minLines: 1,
|
|
controller: valueController,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: valueField.label,
|
|
),
|
|
validator: (String? value) {
|
|
if (valueField.validator != null) {
|
|
return valueField.validator!(value);
|
|
}
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.notEmpty;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: _submit,
|
|
child: Text(appLocalizations.confirm),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|