Update server settings page

This commit is contained in:
2024-01-20 15:02:04 +08:00
parent aa176f64c6
commit a2386edc9c
4 changed files with 257 additions and 3 deletions

View File

@@ -0,0 +1,196 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class StringListFormField extends StatefulWidget {
const StringListFormField(
{Key? key,
this.initialValue,
this.onChanged,
this.decoration,
this.padding,
this.validator,
this.autovalidateMode,
this.label,
this.helper,
this.constraints})
: super(key: key);
final List<String>? initialValue;
final ValueChanged<List<String>>? onChanged;
final InputDecoration? decoration;
final EdgeInsetsGeometry? padding;
final FormFieldValidator<String>? validator;
final AutovalidateMode? autovalidateMode;
final Widget? label;
final Widget? helper;
final BoxConstraints? constraints;
@override
State<StringListFormField> createState() => _StringListFormField();
}
class _StringListFormField extends State<StringListFormField> {
late List<String> value;
late String parentKey;
late Key lastKey;
late List<Key> keys;
late int rebuildKeys;
@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"));
rebuildKeys = 0;
}
Widget _buildItem(BuildContext context, int index) {
final isLast = index == value.length;
if (isLast) {
return IconButton(
key: lastKey,
onPressed: () {
setState(() {
value.add("");
keys.add(ValueKey("${parentKey}_${value.length}"));
});
widget.onChanged?.call(value);
},
icon: const Icon(Icons.add));
}
return Row(
key: keys[index],
children: [
Expanded(
child: TextFormField(
initialValue: value[index],
decoration: widget.decoration,
onChanged: (String? value) {
if (widget.validator != null) {
final re = widget.validator?.call(value);
if (re != null) {
return;
}
}
this.value[index] = value ?? "";
widget.onChanged?.call(this.value);
},
validator: widget.validator,
autovalidateMode: widget.autovalidateMode,
),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
value.removeAt(index);
});
widget.onChanged?.call(value);
},
),
ReorderableDragStartListener(
index: index, child: const Icon(Icons.reorder)),
],
);
}
void onReorder(int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final String item = value.removeAt(oldIndex);
value.insert(newIndex, item);
final key = keys.removeAt(oldIndex);
keys.insert(newIndex, key);
});
widget.onChanged?.call(value);
}
Widget _buildList(BuildContext context) {
Widget list = ReorderableList(
itemBuilder: (context, index) {
final item = _buildItem(context, index);
if (widget.padding == null) {
return item;
}
return Padding(
key: item.key,
padding: widget.padding!,
child: item,
);
},
itemCount: value.length + 1,
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!,
);
}
@override
Widget build(BuildContext context) {
if (value.length != keys.length) {
final len = value.length;
keys = List.generate(
len, (index) => ValueKey("${parentKey}_${rebuildKeys}_$index"));
rebuildKeys++;
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel(context),
_buildList(context),
_buildHelper(context),
]);
}
}

View File

@@ -126,5 +126,11 @@
"downloadTimeout": "Download timeout",
"downloadTimeoutHelp": "The download will be terminated when nothing is received within the specified time period.",
"millisecond": "millisecond",
"ffprobePath": "The path to the ffprobe binary"
"ffprobePath": "The path to the ffprobe binary",
"corsCredentialsHostsHint": "eg. https://ehf.lifegpc.com",
"invalidURL": "Invalid URL.",
"invalidURLOrigin": "Invalid URL origin.",
"httpHttpsNeeded": "Unsupported scheme. Only https or http are supported.",
"corsCredentialsHosts": "The URL origins which are allowed to send CORS requests with credentials",
"corsCredentialsHostsHelp": "Add websites to this list can lead to security risks, make sure you trust these websites."
}

View File

@@ -126,5 +126,11 @@
"downloadTimeout": "下载超时时间",
"downloadTimeoutHelp": "当指定时间段内没有任何内容收到时,将会停止下载。",
"millisecond": "毫秒",
"ffprobePath": "FFPROBE 二进制的位置"
"ffprobePath": "FFPROBE 二进制的位置",
"corsCredentialsHostsHint": "例如: https://ehf.lifegpc.com",
"invalidURL": "非法的 URL。",
"invalidURLOrigin": "非法的 URL 源。",
"httpHttpsNeeded": "不支持的协议。仅支持 http 或 https。",
"corsCredentialsHosts": "被允许发送带有凭证的 CORS 请求的 URL 源",
"corsCredentialsHostsHelp": "将网站加入这个列表会导致安全风险,请确保你信任这些网站。"
}

View File

@@ -7,6 +7,7 @@ import 'package:logging/logging.dart';
import 'api/config.dart';
import 'components/labeled_checkbox.dart';
import 'components/number_field.dart';
import 'components/string_list_field.dart';
import 'globals.dart';
import 'platform/ua.dart';
@@ -57,6 +58,7 @@ class _ServerSettingsPage extends State<ServerSettingsPage>
Future<void> _saveConfig() async {
if (_isSaving) return;
try {
_now.corsCredentialsHosts?.removeWhere((e) => e.isEmpty);
_saveCancel = CancelToken();
setState(() {
_isSaving = true;
@@ -171,7 +173,8 @@ class _ServerSettingsPage extends State<ServerSettingsPage>
actions: [
buildThemeModeIcon(context),
buildMoreVertSettingsButon(context),
]),
],
floating: true),
SliverList(
delegate: SliverChildListDelegate([
_buildCheckBox(context),
@@ -514,6 +517,49 @@ class _ServerSettingsPage extends State<ServerSettingsPage>
});
},
)),
StringListFormField(
key: const ValueKey("corsCredentialsHosts"),
initialValue:
_now.corsCredentialsHosts ?? _config!.corsCredentialsHosts,
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: i18n.corsCredentialsHostsHint,
),
padding: const EdgeInsets.symmetric(vertical: 8),
onChanged: (s) {
setState(() {
_now.corsCredentialsHosts = s;
_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;
}
},
autovalidateMode: AutovalidateMode.onUserInteraction,
label: Text(i18n.corsCredentialsHosts),
constraints: const BoxConstraints(
maxHeight: 300,
),
helper: Text(i18n.corsCredentialsHostsHelp,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Colors.red)),
),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.flutterFrontend ?? _config!.flutterFrontend,
decoration: InputDecoration(