diff --git a/lib/components/number_field.dart b/lib/components/number_field.dart new file mode 100644 index 0000000..b650937 --- /dev/null +++ b/lib/components/number_field.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class NumberFormField extends StatelessWidget { + const NumberFormField( + {Key? key, + this.min, + this.max, + this.initialValue, + this.errorMsg, + this.decoration, + this.onChanged}) + : super(key: key); + + final int? min; + final int? max; + final int? initialValue; + final String? errorMsg; + final InputDecoration? decoration; + final Function(int?)? onChanged; + + @override + Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context)!; + return TextFormField( + initialValue: initialValue?.toString(), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r"[\-0-9]")), + ], + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (String? value) { + if (value == null || value.isEmpty) { + return null; + } + try { + int v = int.parse(value); + if (min != null && v < min!) { + return errorMsg ?? i18n.numberOutOfRange; + } + if (max != null && v > max!) { + return errorMsg ?? i18n.numberOutOfRange; + } + } catch (e) { + return errorMsg ?? i18n.invalidNumber; + } + return null; + }, + decoration: decoration, + onChanged: (String? value) { + if (value == null || value.isEmpty) { + onChanged?.call(null); + } else { + try { + int v = int.parse(value); + if (max != null && v > max!) { + onChanged?.call(null); + } else if (min != null && v < min!) { + onChanged?.call(null); + } else { + onChanged?.call(v); + } + } catch (e) { + onChanged?.call(null); + } + } + }); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2dcb247..1b09cb2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -97,5 +97,15 @@ "mpv": "Fetch page data from Multi-Page Viewer.", "downloadOriginalImg": "Download original images.", "exportZipJpnTitle": "Use japanese title first when exporting zip.", - "removePreviousGallery": "Remove old galleries which replaced by new ones." + "removePreviousGallery": "Remove old galleries which replaced by new ones.", + "serverDbPath": "The folder where the database is stored", + "serverDbPathHelp": "If not set, the download location is used.", + "userAgent": "User Agent", + "useBrowserUA": "Use current browser's user agent", + "downloadLocation": "Download location", + "enterCookies": "Enter cookies here", + "enterNewCookies": "Enter new cookies here", + "maxTaskCount": "Maximum number of parallel tasks", + "invalidNumber": "Invalid number.", + "numberOutOfRange": "The number is out of range." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f166335..2d244de 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -97,5 +97,15 @@ "mpv": "从 Multi-Page Viewer 获取页面数据。", "downloadOriginalImg": "下载原始画质的图片。", "exportZipJpnTitle": "导出Zip时优先使用日语标题。", - "removePreviousGallery": "移除被新画廊替代的旧画廊。" + "removePreviousGallery": "移除被新画廊替代的旧画廊。", + "serverDbPath": "存放数据库的文件夹位置", + "serverDbPathHelp": "如果未设置,将使用下载位置。", + "userAgent": "用户代理(User Agent)", + "useBrowserUA": "使用当前浏览器的用户代理", + "downloadLocation": "下载位置", + "enterCookies": "在此输入 Cookies", + "enterNewCookies": "在此输入新的 Cookies", + "maxTaskCount": "最大并行任务数", + "invalidNumber": "非法的数字。", + "numberOutOfRange": "数字超出了范围。" } diff --git a/lib/platform/ua_none.dart b/lib/platform/ua_none.dart index 5796a91..505eb1a 100644 --- a/lib/platform/ua_none.dart +++ b/lib/platform/ua_none.dart @@ -1,2 +1,3 @@ +String get oUA => ""; bool get isSafari => false; bool get isMobile => false; diff --git a/lib/platform/ua_web.dart b/lib/platform/ua_web.dart index 384a424..6295db6 100644 --- a/lib/platform/ua_web.dart +++ b/lib/platform/ua_web.dart @@ -5,5 +5,6 @@ import 'package:user_agent_analyzer/user_agent_analyzer.dart'; UserAgent? _ua; UserAgent get ua => _ua ??= UserAgent(window.navigator.userAgent); +String get oUA => window.navigator.userAgent; bool get isSafari => ua.isSafari; bool get isMobile => ua.isMobile; diff --git a/lib/server_settings.dart b/lib/server_settings.dart index 1dd2061..eae7c42 100644 --- a/lib/server_settings.dart +++ b/lib/server_settings.dart @@ -1,10 +1,13 @@ import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'api/config.dart'; +import 'components/number_field.dart'; import 'globals.dart'; +import 'platform/ua.dart'; final _log = Logger("ServerSettingsPage"); @@ -28,6 +31,7 @@ class _ServerSettingsPage extends State Object? _error; CancelToken? _cancel; CancelToken? _saveCancel; + late TextEditingController _uaController; Future _fetchData() async { _cancel = CancelToken(); @@ -83,6 +87,9 @@ class _ServerSettingsPage extends State _changed = false; _controller = ScrollController(); _now = ConfigOptional(); + if (kIsWeb) { + _uaController = TextEditingController(); + } } @override @@ -91,6 +98,9 @@ class _ServerSettingsPage extends State _formKey.currentState?.dispose(); _controller.dispose(); _saveCancel?.cancel(); + if (kIsWeb) { + _uaController.dispose(); + } super.dispose(); } @@ -164,6 +174,7 @@ class _ServerSettingsPage extends State SliverList( delegate: SliverChildListDelegate([ _buildCheckBox(context), + _buildTextBox(context), _buildBottomBar(context), ])), ], @@ -174,7 +185,7 @@ class _ServerSettingsPage extends State return Container( padding: MediaQuery.of(context).size.width > 810 ? const EdgeInsets.symmetric(horizontal: 100) - : null, + : const EdgeInsets.symmetric(horizontal: 16), child: child, ); } @@ -250,6 +261,95 @@ class _ServerSettingsPage extends State ])); } + Widget _buildTextBox(BuildContext context) { + final i18n = AppLocalizations.of(context)!; + if (kIsWeb) { + _uaController.text = _now.ua ?? _config!.ua ?? ""; + } + return _buildWithHorizontalPadding( + context, + Column(mainAxisSize: MainAxisSize.min, children: [ + _buildWithVecticalPadding(TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: + _config!.cookies ? i18n.enterNewCookies : i18n.enterCookies, + ), + onChanged: (s) { + setState(() { + _now.cookies = s; + _changed = true; + }); + }, + )), + _buildWithVecticalPadding(TextFormField( + initialValue: _now.dbPath ?? _config!.dbPath, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: i18n.serverDbPath, + helperText: i18n.serverDbPathHelp, + ), + onChanged: (s) { + setState(() { + _now.dbPath = s; + _changed = true; + }); + })), + _buildWithVecticalPadding(TextFormField( + initialValue: kIsWeb ? null : _now.ua ?? _config!.ua, + controller: kIsWeb ? _uaController : null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: i18n.userAgent, + counter: kIsWeb + ? TextButton( + onPressed: () { + setState(() { + _now.ua = oUA; + _changed = true; + }); + }, + child: Text(i18n.useBrowserUA)) + : null, + ), + onChanged: (s) { + setState(() { + _now.ua = s; + _changed = true; + }); + })), + _buildWithVecticalPadding(TextFormField( + initialValue: _now.base ?? _config!.base, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: i18n.downloadLocation, + ), + onChanged: (s) { + setState(() { + _now.base = s; + _changed = true; + }); + }, + )), + _buildWithVecticalPadding(NumberFormField( + min: 1, + initialValue: _now.maxTaskCount ?? _config!.maxTaskCount, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: i18n.maxTaskCount, + ), + onChanged: (s) { + if (s != null) { + setState(() { + _now.maxTaskCount = s; + _changed = true; + }); + } + }, + )), + ])); + } + Widget _buildBottomBar(BuildContext context) { final i18n = AppLocalizations.of(context)!; return _buildWithHorizontalPadding(