mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-06-06 05:49:03 +08:00
846 lines
28 KiB
Dart
846 lines
28 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:eh_downloader_flutter/l10n_gen/app_localizations.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
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 '../../components/string_map_field.dart';
|
|
import '../../globals.dart';
|
|
import '../../platform/ua.dart';
|
|
|
|
final _log = Logger("ServerSettingsPage");
|
|
|
|
class ServerSettingsPage extends StatefulWidget {
|
|
const ServerSettingsPage({super.key});
|
|
static get routeName => "/settings/server";
|
|
|
|
@override
|
|
State<ServerSettingsPage> createState() => _ServerSettingsPage();
|
|
}
|
|
|
|
class _ServerSettingsPage extends State<ServerSettingsPage>
|
|
with IsTopWidget2, ThemeModeWidget {
|
|
final _formKey = GlobalKey<FormState>();
|
|
late bool _isLoading;
|
|
late bool _isSaving;
|
|
late bool _changed;
|
|
late ScrollController _controller;
|
|
late ConfigOptional _now;
|
|
Config? _config;
|
|
Object? _error;
|
|
CancelToken? _cancel;
|
|
CancelToken? _saveCancel;
|
|
late TextEditingController _uaController;
|
|
late AppLocalizations i18n;
|
|
|
|
Future<void> _fetchData() async {
|
|
_cancel = CancelToken();
|
|
try {
|
|
final config = await api.getConfig(cancel: _cancel);
|
|
if (!_cancel!.isCancelled) {
|
|
setState(() {
|
|
_config = config;
|
|
_error = null;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (!_cancel!.isCancelled) {
|
|
_log.warning("Error when fetching config:", e);
|
|
setState(() {
|
|
_error = e;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _saveConfig() async {
|
|
if (_isSaving) return;
|
|
try {
|
|
_now.corsCredentialsHosts?.removeWhere((e) => e.isEmpty);
|
|
_now.meiliHosts?.removeWhere((k, v) => k.isEmpty || v.isEmpty);
|
|
_saveCancel = CancelToken();
|
|
setState(() {
|
|
_isSaving = true;
|
|
});
|
|
await api.updateConfig(_now, cancel: _saveCancel);
|
|
if (!_saveCancel!.isCancelled) {
|
|
setState(() {
|
|
_isSaving = false;
|
|
_now = ConfigOptional();
|
|
_changed = false;
|
|
_config = null;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (!_saveCancel!.isCancelled) {
|
|
_log.warning("Error when saving config:", e);
|
|
setState(() {
|
|
_isSaving = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_isLoading = false;
|
|
_isSaving = false;
|
|
_changed = false;
|
|
_controller = ScrollController();
|
|
_now = ConfigOptional();
|
|
if (kIsWeb) {
|
|
_uaController = TextEditingController();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_cancel?.cancel();
|
|
_formKey.currentState?.dispose();
|
|
_controller.dispose();
|
|
_saveCancel?.cancel();
|
|
if (kIsWeb) {
|
|
_uaController.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
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)!;
|
|
if (isTop(context)) {
|
|
setCurrentTitle("${i18n.settings} - ${i18n.server}");
|
|
}
|
|
return Scaffold(
|
|
appBar: isLoading
|
|
? AppBar(
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () {
|
|
context.canPop() ? context.pop() : context.go("/settings");
|
|
},
|
|
),
|
|
title: Text(i18n.server),
|
|
actions: [
|
|
buildThemeModeIcon(context),
|
|
buildMoreVertSettingsButon(context),
|
|
])
|
|
: null,
|
|
body: isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _error != null
|
|
? Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
SelectableText("Error $_error"),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
_fetchData();
|
|
setState(() {
|
|
_error = null;
|
|
});
|
|
},
|
|
icon: const Icon(Icons.refresh),
|
|
label: Text(AppLocalizations.of(context)!.retry))
|
|
]))
|
|
: _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(
|
|
key: _formKey,
|
|
child: CustomScrollView(
|
|
controller: _controller,
|
|
slivers: [
|
|
SliverAppBar(
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () {
|
|
context.canPop() ? context.pop() : context.go("/");
|
|
},
|
|
),
|
|
title: Text(i18n.server),
|
|
actions: [
|
|
buildThemeModeIcon(context),
|
|
buildMoreVertSettingsButon(context),
|
|
],
|
|
floating: true),
|
|
SliverList(
|
|
delegate: SliverChildListDelegate([
|
|
_buildCheckBox(context),
|
|
_buildTextBox(context),
|
|
_buildBottomBar(context),
|
|
])),
|
|
],
|
|
));
|
|
}
|
|
|
|
Widget _buildWithHorizontalPadding(BuildContext context, Widget child) {
|
|
return Container(
|
|
padding: MediaQuery.of(context).size.width > 810
|
|
? const EdgeInsets.symmetric(horizontal: 100)
|
|
: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
Widget _buildWithVecticalPadding(Widget child) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
Widget _buildCheckBox(BuildContext context) {
|
|
final i18n = AppLocalizations.of(context)!;
|
|
return _buildWithHorizontalPadding(
|
|
context,
|
|
Column(mainAxisSize: MainAxisSize.min, children: [
|
|
_buildWithVecticalPadding(LabeledCheckbox(
|
|
value: _now.ex ?? _config!.ex,
|
|
onChanged: (b) {
|
|
if (b != null) {
|
|
setState(() {
|
|
_now.ex = b;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
label: Text(i18n.useEx))),
|
|
_buildWithVecticalPadding(LabeledCheckbox(
|
|
value: _now.mpv ?? _config!.mpv,
|
|
onChanged: (b) {
|
|
if (b != null) {
|
|
setState(() {
|
|
_now.mpv = b;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
label: Text(i18n.mpv))),
|
|
_buildWithVecticalPadding(LabeledCheckbox(
|
|
value: _now.downloadOriginalImg ?? _config!.downloadOriginalImg,
|
|
onChanged: (b) {
|
|
if (b != null) {
|
|
setState(() {
|
|
_now.downloadOriginalImg = b;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
label: Text(i18n.downloadOriginalImg))),
|
|
_buildWithVecticalPadding(LabeledCheckbox(
|
|
value: _now.exportZipJpnTitle ?? _config!.exportZipJpnTitle,
|
|
onChanged: (b) {
|
|
if (b != null) {
|
|
setState(() {
|
|
_now.exportZipJpnTitle = b;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
label: Text(i18n.exportZipJpnTitle))),
|
|
_buildWithVecticalPadding(LabeledCheckbox(
|
|
value:
|
|
_now.removePreviousGallery ?? _config!.removePreviousGallery,
|
|
onChanged: (b) {
|
|
if (b != null) {
|
|
setState(() {
|
|
_now.removePreviousGallery = b;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
label: Text(i18n.removePreviousGallery))),
|
|
_buildWithVecticalPadding(LabeledCheckbox(
|
|
value: _now.redirectToFlutter ?? _config!.redirectToFlutter,
|
|
onChanged: (b) {
|
|
if (b != null) {
|
|
setState(() {
|
|
_now.redirectToFlutter = b;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
label: Text(i18n.redirectToFlutter))),
|
|
_buildWithVecticalPadding(LabeledCheckbox(
|
|
value: _now.usePathBasedImgUrl ?? _config!.usePathBasedImgUrl,
|
|
onChanged: (b) {
|
|
if (b != null) {
|
|
setState(() {
|
|
_now.usePathBasedImgUrl = b;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
label: Text(i18n.usePathBasedImgUrl))),
|
|
_buildWithVecticalPadding(LabeledCheckbox(
|
|
value: _now.checkFileHash ?? _config!.checkFileHash,
|
|
onChanged: (b) {
|
|
if (b != null) {
|
|
setState(() {
|
|
_now.checkFileHash = b;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
label: Text(i18n.checkFileHash))),
|
|
_buildWithVecticalPadding(LabeledCheckbox(
|
|
value: _now.enableServerTiming ?? _config!.enableServerTiming,
|
|
onChanged: (b) {
|
|
if (b != null) {
|
|
setState(() {
|
|
_now.enableServerTiming = b;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
label: Text(i18n.enableServerTiming),
|
|
)),
|
|
_buildWithVecticalPadding(LabeledCheckbox(
|
|
value: _now.loggingStack ?? _config!.loggingStack,
|
|
onChanged: (b) {
|
|
if (b != null) {
|
|
setState(() {
|
|
_now.loggingStack = b;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
label: Text(i18n.loggingStack),
|
|
)),
|
|
]));
|
|
}
|
|
|
|
Widget _buildTextBox(BuildContext context) {
|
|
final i18n = AppLocalizations.of(context)!;
|
|
if (kIsWeb) {
|
|
final t = _now.ua ?? _config!.ua ?? "";
|
|
if (_uaController.text != t) {
|
|
_uaController.text = t;
|
|
}
|
|
}
|
|
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: auth.isDocker == true
|
|
? i18n.dockerHelper
|
|
: 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,
|
|
helperText: auth.isDocker == true ? i18n.dockerHelper : null,
|
|
),
|
|
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;
|
|
});
|
|
}
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(NumberFormField(
|
|
min: 1,
|
|
initialValue: _now.maxRetryCount ?? _config!.maxRetryCount,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.maxRetryCount,
|
|
),
|
|
onChanged: (s) {
|
|
if (s != null) {
|
|
setState(() {
|
|
_now.maxRetryCount = s;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(NumberFormField(
|
|
min: 1,
|
|
initialValue:
|
|
_now.maxDownloadImgCount ?? _config!.maxDownloadImgCount,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.maxDownloadImgCount,
|
|
),
|
|
onChanged: (s) {
|
|
if (s != null) {
|
|
setState(() {
|
|
_now.maxDownloadImgCount = s;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
)),
|
|
auth.isDocker == true
|
|
? Container()
|
|
: _buildWithVecticalPadding(NumberFormField(
|
|
min: 0,
|
|
max: 65535,
|
|
initialValue: _now.port ?? _config!.port,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.listeningPort,
|
|
),
|
|
onChanged: (s) {
|
|
if (s != null) {
|
|
setState(() {
|
|
_now.port = s;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
)),
|
|
auth.isDocker == true
|
|
? Container()
|
|
: _buildWithVecticalPadding(TextFormField(
|
|
initialValue: _now.hostname ?? _config!.hostname,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.listeningHostname,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.hostname = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(TextFormField(
|
|
initialValue: _now.meiliHost ?? _config!.meiliHost,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.meiliHost,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.meiliHost = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(TextFormField(
|
|
initialValue: _now.meiliSearchApiKey ?? _config!.meiliSearchApiKey,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.meiliSearchApiKey,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.meiliSearchApiKey = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(TextFormField(
|
|
initialValue: _now.meiliUpdateApiKey ?? _config!.meiliUpdateApiKey,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.meiliUpdateApiKey,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.meiliUpdateApiKey = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(TextFormField(
|
|
initialValue: _now.ffmpegPath ?? _config!.ffmpegPath,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.ffmpegPath,
|
|
helperText: auth.isDocker == true ? i18n.dockerHelper : null,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.ffmpegPath = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(DropdownButtonFormField<ThumbnailMethod>(
|
|
items: [
|
|
DropdownMenuItem(
|
|
value: ThumbnailMethod.ffmpegBinary,
|
|
child: Text(i18n.thumbnailMethod0)),
|
|
DropdownMenuItem(
|
|
value: ThumbnailMethod.ffmpegApi,
|
|
child: Text(i18n.thumbnailMethod1)),
|
|
],
|
|
onChanged: (v) {
|
|
if (v != null) {
|
|
setState(() {
|
|
_now.thumbnailMethod = v;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
value: _now.thumbnailMethod ?? _config!.thumbnailMethod,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.thumbnailMethod,
|
|
))),
|
|
_buildWithVecticalPadding(TextFormField(
|
|
initialValue: _now.thumbnailDir ?? _config!.thumbnailDir,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.thumbnailDir,
|
|
helperText: auth.isDocker == true ? i18n.dockerHelper : null,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.thumbnailDir = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(TextFormField(
|
|
initialValue: _now.imgVerifySecret ?? _config!.imgVerifySecret,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.imgVerifySecret,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.imgVerifySecret = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
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:
|
|
_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: urlOriginValidator,
|
|
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(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.flutterFrontend,
|
|
helperText: auth.isDocker == true ? i18n.dockerHelper : null,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.flutterFrontend = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(NumberFormField(
|
|
min: 1,
|
|
initialValue: _now.fetchTimeout ?? _config!.fetchTimeout,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.fetchTimeout,
|
|
suffixText: i18n.millisecond,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.fetchTimeout = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(NumberFormField(
|
|
min: 1,
|
|
initialValue: _now.downloadTimeout ?? _config!.downloadTimeout,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.downloadTimeout,
|
|
suffixText: i18n.millisecond,
|
|
helperText: i18n.downloadTimeoutHelp,
|
|
helperMaxLines: 3,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.downloadTimeout = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(TextFormField(
|
|
initialValue: _now.ffprobePath ?? _config!.ffprobePath,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.ffprobePath,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.ffprobePath = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(NumberFormField(
|
|
min: 1,
|
|
initialValue: _now.downloadTimeoutCheckInterval ??
|
|
_config!.downloadTimeoutCheckInterval,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.downloadTimeoutCheckInterval,
|
|
suffixText: i18n.millisecond,
|
|
helperText: i18n.downloadTimeoutCheckIntervalHelp,
|
|
helperMaxLines: 3,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.downloadTimeoutCheckInterval = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(NumberFormField(
|
|
min: 1,
|
|
initialValue:
|
|
_now.ehMetadataCacheTime ?? _config!.ehMetadataCacheTime,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.ehMetadataCacheTime,
|
|
suffixText: i18n.hour,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.ehMetadataCacheTime = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(TextFormField(
|
|
initialValue: _now.randomFileSecret ?? _config!.randomFileSecret,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.randomFileSecret,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.randomFileSecret = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(DropdownButtonFormField<ImportMethod>(
|
|
items: ImportMethod.values
|
|
.map((e) => DropdownMenuItem(
|
|
value: e, child: Text(e.localText(context))))
|
|
.toList(),
|
|
onChanged: (v) {
|
|
if (v != null) {
|
|
setState(() {
|
|
_now.importMethod = v;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
value: _now.importMethod ?? _config!.importMethod,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.importMethod,
|
|
))),
|
|
_buildWithVecticalPadding(NumberFormField(
|
|
min: 1,
|
|
initialValue: _now.maxImportImgCount ?? _config!.maxImportImgCount,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.maxImportImgCount,
|
|
),
|
|
onChanged: (s) {
|
|
setState(() {
|
|
_now.maxImportImgCount = s;
|
|
_changed = true;
|
|
});
|
|
},
|
|
)),
|
|
_buildWithVecticalPadding(DropdownButtonFormField<ThumbnailFormat>(
|
|
items: ThumbnailFormat.values
|
|
.map((e) => DropdownMenuItem(
|
|
value: e,
|
|
child: Text(
|
|
e.toString().replaceFirst("ThumbnailFormat.", ""))))
|
|
.toList(),
|
|
onChanged: (v) {
|
|
if (v != null) {
|
|
setState(() {
|
|
_now.thumbnailFormat = v;
|
|
_changed = true;
|
|
});
|
|
}
|
|
},
|
|
value: _now.thumbnailFormat ?? _config!.thumbnailFormat,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: i18n.thumbnailFormat,
|
|
helperText: i18n.thumbnailFormatHelp,
|
|
))),
|
|
]));
|
|
}
|
|
|
|
Widget _buildBottomBar(BuildContext context) {
|
|
final i18n = AppLocalizations.of(context)!;
|
|
return _buildWithHorizontalPadding(
|
|
context,
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
_buildWithVecticalPadding(ElevatedButton.icon(
|
|
icon: const Icon(Icons.save),
|
|
label: Text(i18n.save),
|
|
onPressed: _changed
|
|
? () {
|
|
_saveConfig();
|
|
}
|
|
: null)),
|
|
],
|
|
));
|
|
}
|
|
}
|