mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-06-20 02:44:19 +08:00
Add new settings to server settings page
This commit is contained in:
307
lib/components/string_map_field.dart
Normal file
307
lib/components/string_map_field.dart
Normal file
@@ -0,0 +1,307 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class StringMapFormField extends StatefulWidget {
|
||||
const StringMapFormField(
|
||||
{Key? key,
|
||||
this.initialValue,
|
||||
this.onChanged,
|
||||
this.keyDecoration,
|
||||
this.valueDecoration,
|
||||
this.padding,
|
||||
this.keyPadding,
|
||||
this.valuePadding,
|
||||
this.keyValidator,
|
||||
this.valueValidator,
|
||||
this.keyAutovalidateMode,
|
||||
this.valueAutovalidateMode,
|
||||
this.label,
|
||||
this.helper,
|
||||
this.constraints})
|
||||
: super(key: key);
|
||||
final Map<String, String>? initialValue;
|
||||
final ValueChanged<Map<String, String>>? onChanged;
|
||||
final InputDecoration? keyDecoration;
|
||||
final InputDecoration? valueDecoration;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? keyPadding;
|
||||
final EdgeInsetsGeometry? valuePadding;
|
||||
final FormFieldValidator<String>? keyValidator;
|
||||
final FormFieldValidator<String>? valueValidator;
|
||||
final AutovalidateMode? keyAutovalidateMode;
|
||||
final AutovalidateMode? valueAutovalidateMode;
|
||||
final Widget? label;
|
||||
final Widget? helper;
|
||||
final BoxConstraints? constraints;
|
||||
|
||||
@override
|
||||
State<StringMapFormField> createState() => _StringMapFormField();
|
||||
}
|
||||
|
||||
class _StringMapFormField extends State<StringMapFormField> {
|
||||
late Map<String, String> value;
|
||||
late String parentKey;
|
||||
late Key lastKey;
|
||||
late List<Key> keys;
|
||||
late int rebuildKeys;
|
||||
late List<String> keyList;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
value = widget.initialValue ?? {};
|
||||
parentKey = widget.key?.toString() ?? "";
|
||||
lastKey = ValueKey("${parentKey}_new");
|
||||
final len = value.length;
|
||||
keys = List.generate(len, (index) => ValueKey("${parentKey}_$index"));
|
||||
keyList = value.keys.toList();
|
||||
rebuildKeys = 0;
|
||||
}
|
||||
|
||||
void onReorder(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final String item = keyList.removeAt(oldIndex);
|
||||
keyList.insert(newIndex, item);
|
||||
final key = keys.removeAt(oldIndex);
|
||||
keys.insert(newIndex, key);
|
||||
});
|
||||
widget.onChanged?.call(value);
|
||||
}
|
||||
|
||||
Widget _buildKeyItem(BuildContext context, int index, bool expanded) {
|
||||
final i18n = AppLocalizations.of(context)!;
|
||||
Widget item = TextFormField(
|
||||
initialValue: keyList[index],
|
||||
decoration: widget.keyDecoration,
|
||||
onChanged: (String? value) {
|
||||
if (widget.keyValidator != null) {
|
||||
final re = widget.keyValidator?.call(value);
|
||||
if (re != null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
final v = value ?? "";
|
||||
if (v.isEmpty || this.value.containsKey(v)) {
|
||||
return;
|
||||
}
|
||||
final old = keyList[index];
|
||||
keyList[index] = v;
|
||||
final va = this.value.remove(old);
|
||||
this.value[v] = va ?? "";
|
||||
widget.onChanged?.call(this.value);
|
||||
},
|
||||
validator: (s) {
|
||||
final re = widget.keyValidator?.call(s);
|
||||
if (re != null) {
|
||||
return re;
|
||||
}
|
||||
final v = s ?? "";
|
||||
if (v.isEmpty) {
|
||||
return i18n.keyIsEmpty;
|
||||
}
|
||||
if (value.containsKey(v)) {
|
||||
return i18n.keyIsExists;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
autovalidateMode: widget.keyAutovalidateMode,
|
||||
);
|
||||
if (expanded) {
|
||||
if (widget.keyPadding != null) {
|
||||
item = Padding(
|
||||
padding: widget.keyPadding!,
|
||||
child: item,
|
||||
);
|
||||
}
|
||||
item = Expanded(child: item);
|
||||
} else if (widget.padding != null) {
|
||||
item = Padding(
|
||||
padding: widget.padding!,
|
||||
child: item,
|
||||
);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
Widget _buildValueItem(BuildContext context, int index, bool expanded) {
|
||||
Widget item = TextFormField(
|
||||
initialValue: value[keyList[index]],
|
||||
decoration: widget.keyDecoration,
|
||||
onChanged: (String? value) {
|
||||
if (widget.valueValidator != null) {
|
||||
final re = widget.valueValidator?.call(value);
|
||||
if (re != null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.value[keyList[index]] = value ?? "";
|
||||
widget.onChanged?.call(this.value);
|
||||
},
|
||||
validator: widget.valueValidator,
|
||||
autovalidateMode: widget.valueAutovalidateMode,
|
||||
);
|
||||
if (expanded) {
|
||||
if (widget.valuePadding != null) {
|
||||
item = Padding(
|
||||
padding: widget.valuePadding!,
|
||||
child: item,
|
||||
);
|
||||
}
|
||||
item = Expanded(child: item);
|
||||
} else if (widget.padding != null) {
|
||||
item = Padding(
|
||||
padding: widget.padding!,
|
||||
child: item,
|
||||
);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
Widget _buildItem(BuildContext context, int index) {
|
||||
final useMobile = MediaQuery.of(context).size.width <= 810;
|
||||
Widget item = Row(
|
||||
key: keys[index],
|
||||
children: [
|
||||
useMobile
|
||||
? Expanded(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
_buildKeyItem(context, index, false),
|
||||
_buildValueItem(context, index, false),
|
||||
]))
|
||||
: _buildKeyItem(context, index, true),
|
||||
useMobile ? Container() : _buildValueItem(context, index, true),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
final key = keyList.removeAt(index);
|
||||
value.remove(key);
|
||||
});
|
||||
widget.onChanged?.call(value);
|
||||
},
|
||||
),
|
||||
ReorderableDragStartListener(
|
||||
index: index, child: const Icon(Icons.reorder)),
|
||||
],
|
||||
);
|
||||
if (!useMobile && widget.padding != null) {
|
||||
item = Padding(
|
||||
key: item.key,
|
||||
padding: widget.padding!,
|
||||
child: item,
|
||||
);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
Widget _buildList(BuildContext context) {
|
||||
Widget list = ReorderableList(
|
||||
itemBuilder: _buildItem,
|
||||
itemCount: value.length,
|
||||
onReorder: onReorder,
|
||||
proxyDecorator: proxyDecorator,
|
||||
shrinkWrap: true);
|
||||
if (widget.constraints != null) {
|
||||
list = ConstrainedBox(
|
||||
constraints: widget.constraints!,
|
||||
child: list,
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
Widget proxyDecorator(Widget child, int index, Animation<double> animation) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
final double animValue = Curves.easeInOut.transform(animation.value);
|
||||
final double elevation = lerpDouble(0, 6, animValue)!;
|
||||
return Material(
|
||||
elevation: elevation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLabel(BuildContext context) {
|
||||
if (widget.label == null) {
|
||||
return Container();
|
||||
}
|
||||
if (widget.padding == null) {
|
||||
return widget.label!;
|
||||
}
|
||||
return Padding(
|
||||
padding: widget.padding!,
|
||||
child: widget.label!,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHelper(BuildContext context) {
|
||||
if (widget.helper == null) {
|
||||
return Container();
|
||||
}
|
||||
if (widget.padding == null) {
|
||||
return widget.helper!;
|
||||
}
|
||||
return Padding(
|
||||
padding: widget.padding!,
|
||||
child: widget.helper!,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddButton(BuildContext context) {
|
||||
Widget button = LayoutBuilder(builder: (context, box) {
|
||||
return SizedBox(
|
||||
width: box.maxWidth,
|
||||
child: IconButton(
|
||||
key: lastKey,
|
||||
onPressed: value.containsKey("")
|
||||
? null
|
||||
: () {
|
||||
if (value.containsKey("")) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
value[""] = "";
|
||||
keyList.add("");
|
||||
keys.add(ValueKey("${parentKey}_${value.length}"));
|
||||
});
|
||||
widget.onChanged?.call(value);
|
||||
},
|
||||
icon: const Icon(Icons.add)));
|
||||
});
|
||||
if (widget.padding != null) {
|
||||
button = Padding(
|
||||
padding: widget.padding!,
|
||||
child: button,
|
||||
);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (value.length != keys.length) {
|
||||
final len = value.length;
|
||||
keys = List.generate(
|
||||
len, (index) => ValueKey("${parentKey}_${rebuildKeys}_$index"));
|
||||
keyList = value.keys.toList();
|
||||
rebuildKeys++;
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildLabel(context),
|
||||
_buildList(context),
|
||||
_buildAddButton(context),
|
||||
_buildHelper(context),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -135,5 +135,9 @@
|
||||
"corsCredentialsHostsHelp": "Add websites to this list can lead to security risks, make sure you trust these websites.",
|
||||
"galleryDetails": "Gallery Details",
|
||||
"title2": "Title",
|
||||
"titleJpn": "Japanese Title"
|
||||
"titleJpn": "Japanese Title",
|
||||
"meiliHosts": "Meilisearch server hostname for specific domains",
|
||||
"meiliHostsHelp": "Requests from these domains will receive the corresponding Meilisearch server hostname.",
|
||||
"keyIsEmpty": "Key is empty.",
|
||||
"keyIsExists": "Key is exists."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,9 @@
|
||||
"corsCredentialsHostsHelp": "将网站加入这个列表会导致安全风险,请确保你信任这些网站。",
|
||||
"galleryDetails": "画廊详情",
|
||||
"title2": "标题",
|
||||
"titleJpn": "日语标题"
|
||||
"titleJpn": "日语标题",
|
||||
"meiliHosts": "特定域名的 Meilisearch 服务器主机名",
|
||||
"meiliHostsHelp": "来自这些域的请求将收到相应的 Meilisearch 服务器主机名。",
|
||||
"keyIsEmpty": "键不能为空。",
|
||||
"keyIsExists": "键已存在。"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'api/config.dart';
|
||||
import 'components/labeled_checkbox.dart';
|
||||
import 'components/number_field.dart';
|
||||
import 'components/string_list_field.dart';
|
||||
import 'components//string_map_field.dart';
|
||||
import 'globals.dart';
|
||||
import 'platform/ua.dart';
|
||||
|
||||
@@ -34,6 +35,7 @@ class _ServerSettingsPage extends State<ServerSettingsPage>
|
||||
CancelToken? _cancel;
|
||||
CancelToken? _saveCancel;
|
||||
late TextEditingController _uaController;
|
||||
late AppLocalizations i18n;
|
||||
|
||||
Future<void> _fetchData() async {
|
||||
_cancel = CancelToken();
|
||||
@@ -59,6 +61,7 @@ class _ServerSettingsPage extends State<ServerSettingsPage>
|
||||
if (_isSaving) return;
|
||||
try {
|
||||
_now.corsCredentialsHosts?.removeWhere((e) => e.isEmpty);
|
||||
_now.meiliHosts?.removeWhere((k, v) => k.isEmpty || v.isEmpty);
|
||||
_saveCancel = CancelToken();
|
||||
setState(() {
|
||||
_isSaving = true;
|
||||
@@ -112,6 +115,7 @@ class _ServerSettingsPage extends State<ServerSettingsPage>
|
||||
if (!tryInitApi(context)) {
|
||||
return Container();
|
||||
}
|
||||
this.i18n = AppLocalizations.of(context)!;
|
||||
final isLoading = _config == null && _error == null;
|
||||
if (isLoading && !_isLoading) _fetchData();
|
||||
final i18n = AppLocalizations.of(context)!;
|
||||
@@ -155,6 +159,24 @@ class _ServerSettingsPage extends State<ServerSettingsPage>
|
||||
: _buildForm(context));
|
||||
}
|
||||
|
||||
String? urlOriginValidator(String? s) {
|
||||
if (s == null || s.isEmpty) return null;
|
||||
try {
|
||||
final u = Uri.parse(s);
|
||||
if (u.hasQuery ||
|
||||
u.userInfo.isNotEmpty ||
|
||||
u.hasFragment ||
|
||||
!u.hasEmptyPath ||
|
||||
!u.hasScheme) return i18n.invalidURLOrigin;
|
||||
if (u.scheme != "http" && u.scheme != "https") {
|
||||
return i18n.httpHttpsNeeded;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return i18n.invalidURL;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildForm(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context)!;
|
||||
return Form(
|
||||
@@ -522,6 +544,35 @@ class _ServerSettingsPage extends State<ServerSettingsPage>
|
||||
});
|
||||
},
|
||||
)),
|
||||
StringMapFormField(
|
||||
key: const ValueKey("meiliHosts"),
|
||||
initialValue: _now.meiliHosts ?? _config!.meiliHosts,
|
||||
keyDecoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
valueDecoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
keyPadding: const EdgeInsets.only(right: 4),
|
||||
valuePadding: const EdgeInsets.only(left: 4),
|
||||
onChanged: (s) {
|
||||
setState(() {
|
||||
_now.meiliHosts = s;
|
||||
_changed = true;
|
||||
});
|
||||
},
|
||||
keyValidator: urlOriginValidator,
|
||||
valueValidator: urlOriginValidator,
|
||||
keyAutovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
valueAutovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
label: Text(i18n.meiliHosts),
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 300,
|
||||
),
|
||||
helper: Text(i18n.meiliHostsHelp,
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
),
|
||||
StringListFormField(
|
||||
key: const ValueKey("corsCredentialsHosts"),
|
||||
initialValue:
|
||||
@@ -537,23 +588,7 @@ class _ServerSettingsPage extends State<ServerSettingsPage>
|
||||
_changed = true;
|
||||
});
|
||||
},
|
||||
validator: (s) {
|
||||
if (s == null || s.isEmpty) return null;
|
||||
try {
|
||||
final u = Uri.parse(s);
|
||||
if (u.hasQuery ||
|
||||
u.userInfo.isNotEmpty ||
|
||||
u.hasFragment ||
|
||||
!u.hasEmptyPath ||
|
||||
!u.hasScheme) return i18n.invalidURLOrigin;
|
||||
if (u.scheme != "http" && u.scheme != "https") {
|
||||
return i18n.httpHttpsNeeded;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return i18n.invalidURL;
|
||||
}
|
||||
},
|
||||
validator: urlOriginValidator,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
label: Text(i18n.corsCredentialsHosts),
|
||||
constraints: const BoxConstraints(
|
||||
|
||||
Reference in New Issue
Block a user