Files
MWClash/lib/widgets/input.dart
chen08209 e92900dbbd Fix tab delay view issues
Fix tray action issues

Fix get profile redirect client ua issues

Fix proxy card delay view issues

Add Russian, Japanese adaptation

Fix some issues
2025-03-07 23:49:27 +08:00

454 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;
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,
),
onSubmitted: (_) {
_handleUpdate();
},
),
],
),
),
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),
)
],
);
}
}