diff --git a/ui/lib/activity.dart b/ui/lib/activity.dart index e4fff27..8e7b47d 100644 --- a/ui/lib/activity.dart +++ b/ui/lib/activity.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; import 'package:ui/providers/activity.dart'; -import 'package:ui/utils.dart'; +import 'package:ui/widgets/utils.dart'; import 'package:ui/widgets/progress_indicator.dart'; class ActivityPage extends ConsumerStatefulWidget { diff --git a/ui/lib/movie_watchlist.dart b/ui/lib/movie_watchlist.dart index b3b50d0..dad44e4 100644 --- a/ui/lib/movie_watchlist.dart +++ b/ui/lib/movie_watchlist.dart @@ -5,7 +5,7 @@ import 'package:ui/providers/APIs.dart'; import 'package:ui/providers/activity.dart'; import 'package:ui/providers/series_details.dart'; import 'package:ui/providers/settings.dart'; -import 'package:ui/utils.dart'; +import 'package:ui/widgets/utils.dart'; import 'package:ui/welcome_page.dart'; import 'package:ui/widgets/progress_indicator.dart'; diff --git a/ui/lib/search.dart b/ui/lib/search.dart index dd1e971..d3827dc 100644 --- a/ui/lib/search.dart +++ b/ui/lib/search.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:ui/providers/APIs.dart'; import 'package:ui/providers/settings.dart'; import 'package:ui/providers/welcome_data.dart'; -import 'package:ui/utils.dart'; +import 'package:ui/widgets/utils.dart'; import 'package:ui/widgets/progress_indicator.dart'; class SearchPage extends ConsumerStatefulWidget { diff --git a/ui/lib/settings.dart b/ui/lib/settings.dart index ae4144e..889e73f 100644 --- a/ui/lib/settings.dart +++ b/ui/lib/settings.dart @@ -1,14 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:quiver/strings.dart'; -import 'package:ui/providers/login.dart'; -import 'package:ui/providers/notifier.dart'; -import 'package:ui/providers/settings.dart'; -import 'package:ui/utils.dart'; -import 'package:ui/widgets/progress_indicator.dart'; -import 'package:ui/widgets/widgets.dart'; -import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:ui/settings/auth.dart'; +import 'package:ui/settings/downloader.dart'; +import 'package:ui/settings/general.dart'; +import 'package:ui/settings/indexer.dart'; +import 'package:ui/settings/notifier.dart'; +import 'package:ui/settings/storage.dart'; class SystemSettingsPage extends ConsumerStatefulWidget { static const route = "/settings"; @@ -21,718 +18,52 @@ class SystemSettingsPage extends ConsumerStatefulWidget { } class _SystemSettingsPageState extends ConsumerState { - final _formKey = GlobalKey(); - final _formKey2 = GlobalKey(); - bool? _enableAuth; - @override Widget build(BuildContext context) { - var settings = ref.watch(settingProvider); - - var tmdbSetting = settings.when( - data: (v) { - return FormBuilder( - key: _formKey, //设置globalKey,用于后面获取FormState - autovalidateMode: AutovalidateMode.onUserInteraction, - initialValue: { - "tmdb_api": v.tmdbApiKey, - "download_dir": v.downloadDIr, - "log_level": v.logLevel, - "proxy": v.proxy, - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FormBuilderTextField( - name: "tmdb_api", - decoration: Commons.requiredTextFieldStyle( - text: "TMDB Api Key", icon: const Icon(Icons.key)), - // - validator: FormBuilderValidators.required(), - ), - FormBuilderTextField( - name: "download_dir", - decoration: Commons.requiredTextFieldStyle( - text: "下载路径", - icon: const Icon(Icons.folder), - helperText: "媒体文件临时下载路径,非最终存储路径"), - // - validator: FormBuilderValidators.required(), - ), - FormBuilderTextField( - name: "proxy", - decoration: const InputDecoration( - labelText: "代理地址", - icon: Icon(Icons.folder), - helperText: "后台联网代理地址,留空表示不启用代理"), - ), - SizedBox( - width: 300, - child: FormBuilderDropdown( - name: "log_level", - decoration: const InputDecoration( - labelText: "日志级别", - icon: Icon(Icons.file_present_rounded), - ), - items: const [ - DropdownMenuItem(value: "debug", child: Text("DEBUG")), - DropdownMenuItem(value: "info", child: Text("INFO")), - DropdownMenuItem(value: "warn", child: Text("WARN")), - DropdownMenuItem(value: "error", child: Text("ERROR")), - ], - validator: FormBuilderValidators.required(), - ), - ), - Center( - child: Padding( - padding: const EdgeInsets.only(top: 28.0), - child: ElevatedButton( - child: const Padding( - padding: EdgeInsets.all(16.0), - child: Text("保存"), - ), - onPressed: () { - if (_formKey.currentState!.saveAndValidate()) { - var values = _formKey.currentState!.value; - var f = ref - .read(settingProvider.notifier) - .updateSettings(GeneralSetting( - tmdbApiKey: values["tmdb_api"], - downloadDIr: values["download_dir"], - logLevel: values["log_level"], - proxy: values["proxy"])); - f.then((v) { - Utils.showSnakeBar("更新成功"); - }).onError((e, s) { - Utils.showSnakeBar("更新失败:$e"); - }); - } - }), - ), - ) - ], - ), - ); - }, - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()); - - var indexers = ref.watch(indexersProvider); - var indexerSetting = indexers.when( - data: (value) => Wrap( - children: List.generate(value.length + 1, (i) { - if (i < value.length) { - var indexer = value[i]; - return SettingsCard( - onTap: () => showIndexerDetails(indexer), - child: Text(indexer.name ?? "")); - } - return SettingsCard( - onTap: () => showIndexerDetails(Indexer()), - child: const Icon(Icons.add)); - }), - ), - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()); - - var downloadClients = ref.watch(dwonloadClientsProvider); - var downloadSetting = downloadClients.when( - data: (value) => Wrap( - children: List.generate(value.length + 1, (i) { - if (i < value.length) { - var client = value[i]; - return SettingsCard( - onTap: () => showDownloadClientDetails(client), - child: Text(client.name ?? "")); - } - return SettingsCard( - onTap: () => showDownloadClientDetails(DownloadClient()), - child: const Icon(Icons.add)); - })), - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()); - - var storageSettingData = ref.watch(storageSettingProvider); - var storageSetting = storageSettingData.when( - data: (value) => Wrap( - children: List.generate(value.length + 1, (i) { - if (i < value.length) { - var storage = value[i]; - return SettingsCard( - onTap: () => showStorageDetails(storage), - child: Text(storage.name ?? "")); - } - return SettingsCard( - onTap: () => showStorageDetails(Storage()), - child: const Icon(Icons.add)); - }), - ), - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()); - - var authData = ref.watch(authSettingProvider); - var authSetting = authData.when( - data: (data) { - if (_enableAuth == null) { - setState(() { - _enableAuth = data.enable; - }); - } - return FormBuilder( - key: _formKey2, - initialValue: { - "user": data.user, - "password": data.password, - "enable": data.enable - }, - child: Column( - children: [ - FormBuilderSwitch( - name: "enable", - title: const Text("开启认证"), - onChanged: (v) { - setState(() { - _enableAuth = v; - }); - }), - _enableAuth! - ? Column( - children: [ - FormBuilderTextField( - name: "user", - autovalidateMode: - AutovalidateMode.onUserInteraction, - validator: FormBuilderValidators.required(), - decoration: Commons.requiredTextFieldStyle( - text: "用户名", - icon: const Icon(Icons.account_box), - )), - FormBuilderTextField( - name: "password", - obscureText: true, - enableSuggestions: false, - autocorrect: false, - autovalidateMode: - AutovalidateMode.onUserInteraction, - validator: FormBuilderValidators.required(), - decoration: Commons.requiredTextFieldStyle( - text: "密码", - icon: const Icon(Icons.password), - )) - ], - ) - : const Column(), - Center( - child: ElevatedButton( - child: const Text("保存"), - onPressed: () { - if (_formKey2.currentState!.saveAndValidate()) { - var values = _formKey2.currentState!.value; - var f = ref - .read(authSettingProvider.notifier) - .updateAuthSetting(_enableAuth!, - values["user"], values["password"]); - f.then((v) { - Utils.showSnakeBar("更新成功"); - }).onError((e, s) { - Utils.showSnakeBar("更新失败:$e"); - }); - } - })) - ], - )); - }, - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()); - return ListView( - children: [ + children: const [ ExpansionTile( expandedAlignment: Alignment.centerLeft, - childrenPadding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + childrenPadding: EdgeInsets.fromLTRB(20, 0, 20, 0), initiallyExpanded: true, - title: const Text("常规"), - children: [tmdbSetting], + title: Text("常规"), + children: [GeneralSettings()], ), ExpansionTile( expandedAlignment: Alignment.centerLeft, - childrenPadding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + childrenPadding: EdgeInsets.fromLTRB(20, 0, 20, 0), initiallyExpanded: false, - title: const Text("索引器"), - children: [indexerSetting], + title: Text("索引器"), + children: [IndexerSettings()], ), ExpansionTile( expandedAlignment: Alignment.centerLeft, - childrenPadding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + childrenPadding: EdgeInsets.fromLTRB(20, 0, 20, 0), initiallyExpanded: false, - title: const Text("下载器"), - children: [downloadSetting], + title: Text("下载器"), + children: [DownloaderSettings()], ), ExpansionTile( expandedAlignment: Alignment.centerLeft, - childrenPadding: const EdgeInsets.fromLTRB(20, 0, 50, 0), + childrenPadding: EdgeInsets.fromLTRB(20, 0, 50, 0), initiallyExpanded: false, - title: const Text("存储"), - children: [storageSetting], + title: Text("存储"), + children: [StorageSettings()], ), ExpansionTile( expandedAlignment: Alignment.centerLeft, - childrenPadding: const EdgeInsets.fromLTRB(20, 0, 50, 0), + childrenPadding: EdgeInsets.fromLTRB(20, 0, 50, 0), initiallyExpanded: false, - title: const Text("通知客户端"), - children: [notifers()], + title: Text("通知客户端"), + children: [NotifierSettings()], ), ExpansionTile( - childrenPadding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + childrenPadding: EdgeInsets.fromLTRB(20, 0, 20, 0), initiallyExpanded: false, - title: const Text("认证"), - children: [authSetting], + title: Text("认证"), + children: [AuthSettings()], ), ], ); } - - Widget notifers() { - final notifierData = ref.watch(notifiersDataProvider); - return notifierData.when( - data: (v) => Wrap( - children: List.generate(v.length + 1, (i) { - if (i < v.length) { - final client = v[i]; - return SettingsCard( - child: Text("${client.name!} (${client.service})"), - onTap: () => showNotifierDetails(client), - ); - } - return SettingsCard( - onTap: () => showNotifierDetails(NotifierData()), - child: const Icon(Icons.add)); - }), - ), - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()); - } - - Future showNotifierDetails(NotifierData notifier) { - final _formKey = GlobalKey(); - - var body = FormBuilder( - key: _formKey, - initialValue: { - "name": notifier.name, - "service": notifier.service, - "enabled": notifier.enabled ?? true, - "app_token": - notifier.settings != null ? notifier.settings!["app_token"] : "", - "user_key": - notifier.settings != null ? notifier.settings!["user_key"] : "", - }, - child: Column( - children: [ - FormBuilderDropdown( - name: "service", - decoration: const InputDecoration(labelText: "类型"), - items: const [ - DropdownMenuItem(value: "pushover", child: Text("Pushover")), - ], - ), - FormBuilderTextField( - name: "name", - decoration: Commons.requiredTextFieldStyle(text: "名称"), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: FormBuilderValidators.required(), - ), - FormBuilderTextField( - name: "app_token", - decoration: Commons.requiredTextFieldStyle(text: "APP密钥"), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: FormBuilderValidators.required(), - ), - FormBuilderTextField( - name: "user_key", - decoration: Commons.requiredTextFieldStyle(text: "用户密钥"), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: FormBuilderValidators.required(), - ), - FormBuilderSwitch(name: "enabled", title: const Text("启用")) - ], - ), - ); - onDelete() async { - return ref.read(notifiersDataProvider.notifier).delete(notifier.id!); - } - - onSubmit() async { - if (_formKey.currentState!.saveAndValidate()) { - var values = _formKey.currentState!.value; - return ref.read(notifiersDataProvider.notifier).add(NotifierData( - name: values["name"], - service: values["service"], - enabled: values["enabled"], - settings: { - "app_token": values["app_token"], - "user_key": values["user_key"] - })); - } else { - throw "validation_error"; - } - } - - return showSettingDialog( - "通知客户端", notifier.id != null, body, onSubmit, onDelete); - } - - Future showIndexerDetails(Indexer indexer) { - final _formKey = GlobalKey(); - - var body = FormBuilder( - key: _formKey, - initialValue: { - "name": indexer.name, - "url": indexer.url, - "api_key": indexer.apiKey, - "impl": "torznab" - }, - child: Column( - children: [ - FormBuilderDropdown( - name: "impl", - decoration: const InputDecoration(labelText: "类型"), - items: const [ - DropdownMenuItem(value: "torznab", child: Text("Torznab")), - ], - ), - FormBuilderTextField( - name: "name", - decoration: Commons.requiredTextFieldStyle(text: "名称"), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: FormBuilderValidators.required(), - ), - FormBuilderTextField( - name: "url", - decoration: Commons.requiredTextFieldStyle(text: "地址"), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: FormBuilderValidators.required(), - ), - FormBuilderTextField( - name: "api_key", - decoration: Commons.requiredTextFieldStyle(text: "API Key"), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: FormBuilderValidators.required(), - ), - ], - ), - ); - onDelete() async { - return ref.read(indexersProvider.notifier).deleteIndexer(indexer.id!); - } - - onSubmit() async { - if (_formKey.currentState!.saveAndValidate()) { - var values = _formKey.currentState!.value; - return ref.read(indexersProvider.notifier).addIndexer(Indexer( - name: values["name"], - url: values["url"], - apiKey: values["api_key"])); - } else { - throw "validation_error"; - } - } - - return showSettingDialog( - "索引器", indexer.id != null, body, onSubmit, onDelete); - } - - Future showDownloadClientDetails(DownloadClient client) { - final _formKey = GlobalKey(); - var _enableAuth = isNotBlank(client.user); - String selectImpl = "transmission"; - - final body = - StatefulBuilder(builder: (BuildContext context, StateSetter setState) { - return FormBuilder( - key: _formKey, - initialValue: { - "name": client.name, - "url": client.url, - "user": client.user, - "password": client.password, - "impl": "transmission" - }, - child: Column( - children: [ - FormBuilderDropdown( - name: "impl", - decoration: const InputDecoration(labelText: "类型"), - onChanged: (value) { - setState(() { - selectImpl = value!; - }); - }, - items: const [ - DropdownMenuItem( - value: "transmission", child: Text("Transmission")), - ], - ), - FormBuilderTextField( - name: "name", - decoration: const InputDecoration(labelText: "名称"), - validator: FormBuilderValidators.required(), - autovalidateMode: AutovalidateMode.onUserInteraction), - FormBuilderTextField( - name: "url", - decoration: const InputDecoration( - labelText: "地址", hintText: "http://127.0.0.1:9091"), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: FormBuilderValidators.required(), - ), - StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return Column( - children: [ - FormBuilderSwitch( - name: "auth", - title: const Text("需要认证"), - initialValue: _enableAuth, - onChanged: (v) { - setState(() { - _enableAuth = v!; - }); - }), - _enableAuth - ? Column( - children: [ - FormBuilderTextField( - name: "user", - decoration: Commons.requiredTextFieldStyle( - text: "用户"), - validator: FormBuilderValidators.required(), - autovalidateMode: - AutovalidateMode.onUserInteraction), - FormBuilderTextField( - name: "password", - decoration: Commons.requiredTextFieldStyle( - text: "密码"), - validator: FormBuilderValidators.required(), - obscureText: true, - autovalidateMode: - AutovalidateMode.onUserInteraction), - ], - ) - : Container() - ], - ); - }) - ], - )); - }); - onDelete() async { - return ref - .read(dwonloadClientsProvider.notifier) - .deleteDownloadClients(client.id!); - } - - onSubmit() async { - if (_formKey.currentState!.saveAndValidate()) { - var values = _formKey.currentState!.value; - return ref.read(dwonloadClientsProvider.notifier).addDownloadClients( - DownloadClient( - name: values["name"], - implementation: values["impl"], - url: values["url"], - user: _enableAuth ? values["user"] : null, - password: _enableAuth ? values["password"] : null)); - } else { - throw "validation_error"; - } - } - - return showSettingDialog( - "下载器", client.id != null, body, onSubmit, onDelete); - } - - Future showStorageDetails(Storage s) { - final _formKey = GlobalKey(); - - String selectImpl = s.implementation == null ? "local" : s.implementation!; - final widgets = - StatefulBuilder(builder: (BuildContext context, StateSetter setState) { - return FormBuilder( - key: _formKey, - autovalidateMode: AutovalidateMode.disabled, - initialValue: { - "name": s.name, - "impl": s.implementation == null ? "local" : s.implementation!, - "user": s.settings != null ? s.settings!["user"] ?? "" : "", - "password": s.settings != null ? s.settings!["password"] ?? "" : "", - "tv_path": s.settings != null ? s.settings!["tv_path"] ?? "" : "", - "url": s.settings != null ? s.settings!["url"] ?? "" : "", - "movie_path": - s.settings != null ? s.settings!["movie_path"] ?? "" : "", - "change_file_hash": s.settings != null - ? s.settings!["change_file_hash"] == "true" - ? true - : false - : false, - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FormBuilderDropdown( - name: "impl", - autovalidateMode: AutovalidateMode.onUserInteraction, - decoration: const InputDecoration(labelText: "类型"), - onChanged: (value) { - setState(() { - selectImpl = value!; - }); - }, - items: const [ - DropdownMenuItem( - value: "local", - child: Text("本地存储"), - ), - DropdownMenuItem( - value: "webdav", - child: Text("webdav"), - ) - ], - validator: FormBuilderValidators.required(), - ), - FormBuilderTextField( - name: "name", - autovalidateMode: AutovalidateMode.onUserInteraction, - initialValue: s.name, - decoration: const InputDecoration(labelText: "名称"), - validator: FormBuilderValidators.required(), - ), - selectImpl != "local" - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FormBuilderTextField( - name: "url", - autovalidateMode: AutovalidateMode.onUserInteraction, - decoration: - const InputDecoration(labelText: "Webdav地址"), - validator: FormBuilderValidators.required(), - ), - FormBuilderTextField( - name: "user", - autovalidateMode: AutovalidateMode.onUserInteraction, - decoration: const InputDecoration(labelText: "用户"), - ), - FormBuilderTextField( - name: "password", - autovalidateMode: AutovalidateMode.onUserInteraction, - decoration: const InputDecoration(labelText: "密码"), - obscureText: true, - ), - FormBuilderCheckbox( - name: "change_file_hash", - title: const Text( - "上传时更改文件哈希", - style: TextStyle(fontSize: 14), - ), - ), - ], - ) - : Container(), - FormBuilderTextField( - name: "tv_path", - autovalidateMode: AutovalidateMode.onUserInteraction, - decoration: const InputDecoration(labelText: "电视剧路径"), - validator: FormBuilderValidators.required(), - ), - FormBuilderTextField( - name: "movie_path", - autovalidateMode: AutovalidateMode.onUserInteraction, - decoration: const InputDecoration(labelText: "电影路径"), - validator: FormBuilderValidators.required(), - ) - ], - )); - }); - onSubmit() async { - if (_formKey.currentState!.saveAndValidate()) { - final values = _formKey.currentState!.value; - return ref.read(storageSettingProvider.notifier).addStorage(Storage( - name: values["name"], - implementation: selectImpl, - settings: { - "tv_path": values["tv_path"], - "movie_path": values["movie_path"], - "url": values["url"], - "user": values["user"], - "password": values["password"], - "change_file_hash": - (values["change_file_hash"] ?? false) as bool - ? "true" - : "false" - }, - )); - } else { - throw "validation_error"; - } - } - - onDelete() async { - return ref.read(storageSettingProvider.notifier).deleteStorage(s.id!); - } - - return showSettingDialog('存储', s.id != null, widgets, onSubmit, onDelete); - } - - Future showSettingDialog(String title, bool showDelete, Widget body, - Future Function() onSubmit, Future Function() onDelete) { - return showDialog( - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return AlertDialog( - title: Text(title), - content: SingleChildScrollView( - child: SizedBox( - width: 300, - child: body, - ), - ), - actions: [ - showDelete - ? TextButton( - onPressed: () { - final f = onDelete(); - f.then((v) { - Utils.showSnakeBar("删除成功"); - Navigator.of(context).pop(); - }).onError((e, s) { - Utils.showSnakeBar("删除失败:$e"); - }); - }, - child: const Text( - '删除', - style: TextStyle(color: Colors.red), - )) - : const Text(""), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('取消')), - TextButton( - child: const Text('确定'), - onPressed: () { - final f = onSubmit(); - f.then((v) { - Utils.showSnakeBar("操作成功"); - Navigator.of(context).pop(); - }).onError((e, s) { - if (e.toString() != "validation_error") { - Utils.showSnakeBar("操作失败:$e"); - } - }); - }, - ), - ], - ); - }); - } } diff --git a/ui/lib/settings/auth.dart b/ui/lib/settings/auth.dart new file mode 100644 index 0000000..29b523e --- /dev/null +++ b/ui/lib/settings/auth.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:ui/providers/login.dart'; +import 'package:ui/widgets/progress_indicator.dart'; +import 'package:ui/widgets/utils.dart'; +import 'package:ui/widgets/widgets.dart'; + +class AuthSettings extends ConsumerStatefulWidget { + static const route = "/settings"; + + const AuthSettings({super.key}); + @override + ConsumerState createState() { + return _AuthState(); + } +} + +class _AuthState extends ConsumerState { + final _formKey2 = GlobalKey(); + bool? _enableAuth; + + @override + Widget build(BuildContext context) { + var authData = ref.watch(authSettingProvider); + return authData.when( + data: (data) { + if (_enableAuth == null) { + setState(() { + _enableAuth = data.enable; + }); + } + return FormBuilder( + key: _formKey2, + initialValue: { + "user": data.user, + "password": data.password, + "enable": data.enable + }, + child: Column( + children: [ + FormBuilderSwitch( + name: "enable", + title: const Text("开启认证"), + onChanged: (v) { + setState(() { + _enableAuth = v; + }); + }), + _enableAuth! + ? Column( + children: [ + FormBuilderTextField( + name: "user", + autovalidateMode: + AutovalidateMode.onUserInteraction, + validator: FormBuilderValidators.required(), + decoration: Commons.requiredTextFieldStyle( + text: "用户名", + icon: const Icon(Icons.account_box), + )), + FormBuilderTextField( + name: "password", + obscureText: true, + enableSuggestions: false, + autocorrect: false, + autovalidateMode: + AutovalidateMode.onUserInteraction, + validator: FormBuilderValidators.required(), + decoration: Commons.requiredTextFieldStyle( + text: "密码", + icon: const Icon(Icons.password), + )) + ], + ) + : const Column(), + Center( + child: ElevatedButton( + child: const Text("保存"), + onPressed: () { + if (_formKey2.currentState!.saveAndValidate()) { + var values = _formKey2.currentState!.value; + var f = ref + .read(authSettingProvider.notifier) + .updateAuthSetting(_enableAuth!, + values["user"], values["password"]); + f.then((v) { + Utils.showSnakeBar("更新成功"); + }).onError((e, s) { + Utils.showSnakeBar("更新失败:$e"); + }); + } + })) + ], + )); + }, + error: (err, trace) => Text("$err"), + loading: () => const MyProgressIndicator()); + } + +} \ No newline at end of file diff --git a/ui/lib/settings/dialog.dart b/ui/lib/settings/dialog.dart new file mode 100644 index 0000000..dfff68b --- /dev/null +++ b/ui/lib/settings/dialog.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:ui/widgets/utils.dart'; + +Future showSettingDialog(BuildContext context,String title, bool showDelete, Widget body, + Future Function() onSubmit, Future Function() onDelete) { + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: SingleChildScrollView( + child: SizedBox( + width: 300, + child: body, + ), + ), + actions: [ + showDelete + ? TextButton( + onPressed: () { + final f = onDelete(); + f.then((v) { + Utils.showSnakeBar("删除成功"); + Navigator.of(context).pop(); + }).onError((e, s) { + Utils.showSnakeBar("删除失败:$e"); + }); + }, + child: const Text( + '删除', + style: TextStyle(color: Colors.red), + )) + : const Text(""), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('取消')), + TextButton( + child: const Text('确定'), + onPressed: () { + final f = onSubmit(); + f.then((v) { + Utils.showSnakeBar("操作成功"); + Navigator.of(context).pop(); + }).onError((e, s) { + if (e.toString() != "validation_error") { + Utils.showSnakeBar("操作失败:$e"); + } + }); + }, + ), + ], + ); + }); +} diff --git a/ui/lib/settings/downloader.dart b/ui/lib/settings/downloader.dart new file mode 100644 index 0000000..51e5ff3 --- /dev/null +++ b/ui/lib/settings/downloader.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:quiver/strings.dart'; +import 'package:ui/providers/settings.dart'; +import 'package:ui/settings/dialog.dart'; +import 'package:ui/widgets/progress_indicator.dart'; +import 'package:ui/widgets/widgets.dart'; + +class DownloaderSettings extends ConsumerStatefulWidget { + static const route = "/settings"; + + const DownloaderSettings({super.key}); + @override + ConsumerState createState() { + return _DownloaderState(); + } +} + +class _DownloaderState extends ConsumerState { + @override + Widget build(BuildContext context) { + var downloadClients = ref.watch(dwonloadClientsProvider); + return downloadClients.when( + data: (value) => Wrap( + children: List.generate(value.length + 1, (i) { + if (i < value.length) { + var client = value[i]; + return SettingsCard( + onTap: () => showDownloadClientDetails(client), + child: Text(client.name ?? "")); + } + return SettingsCard( + onTap: () => showDownloadClientDetails(DownloadClient()), + child: const Icon(Icons.add)); + })), + error: (err, trace) => Text("$err"), + loading: () => const MyProgressIndicator()); + } + + Future showDownloadClientDetails(DownloadClient client) { + final _formKey = GlobalKey(); + var _enableAuth = isNotBlank(client.user); + String selectImpl = "transmission"; + + final body = + StatefulBuilder(builder: (BuildContext context, StateSetter setState) { + return FormBuilder( + key: _formKey, + initialValue: { + "name": client.name, + "url": client.url, + "user": client.user, + "password": client.password, + "impl": "transmission" + }, + child: Column( + children: [ + FormBuilderDropdown( + name: "impl", + decoration: const InputDecoration(labelText: "类型"), + onChanged: (value) { + setState(() { + selectImpl = value!; + }); + }, + items: const [ + DropdownMenuItem( + value: "transmission", child: Text("Transmission")), + ], + ), + FormBuilderTextField( + name: "name", + decoration: const InputDecoration(labelText: "名称"), + validator: FormBuilderValidators.required(), + autovalidateMode: AutovalidateMode.onUserInteraction), + FormBuilderTextField( + name: "url", + decoration: const InputDecoration( + labelText: "地址", hintText: "http://127.0.0.1:9091"), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: FormBuilderValidators.required(), + ), + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Column( + children: [ + FormBuilderSwitch( + name: "auth", + title: const Text("需要认证"), + initialValue: _enableAuth, + onChanged: (v) { + setState(() { + _enableAuth = v!; + }); + }), + _enableAuth + ? Column( + children: [ + FormBuilderTextField( + name: "user", + decoration: Commons.requiredTextFieldStyle( + text: "用户"), + validator: FormBuilderValidators.required(), + autovalidateMode: + AutovalidateMode.onUserInteraction), + FormBuilderTextField( + name: "password", + decoration: Commons.requiredTextFieldStyle( + text: "密码"), + validator: FormBuilderValidators.required(), + obscureText: true, + autovalidateMode: + AutovalidateMode.onUserInteraction), + ], + ) + : Container() + ], + ); + }) + ], + )); + }); + onDelete() async { + return ref + .read(dwonloadClientsProvider.notifier) + .deleteDownloadClients(client.id!); + } + + onSubmit() async { + if (_formKey.currentState!.saveAndValidate()) { + var values = _formKey.currentState!.value; + return ref.read(dwonloadClientsProvider.notifier).addDownloadClients( + DownloadClient( + name: values["name"], + implementation: values["impl"], + url: values["url"], + user: _enableAuth ? values["user"] : null, + password: _enableAuth ? values["password"] : null)); + } else { + throw "validation_error"; + } + } + + return showSettingDialog( + context, "下载器", client.id != null, body, onSubmit, onDelete); + } +} diff --git a/ui/lib/settings/general.dart b/ui/lib/settings/general.dart new file mode 100644 index 0000000..bb48601 --- /dev/null +++ b/ui/lib/settings/general.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:ui/providers/settings.dart'; +import 'package:ui/widgets/utils.dart'; +import 'package:ui/widgets/progress_indicator.dart'; +import 'package:ui/widgets/widgets.dart'; + +class GeneralSettings extends ConsumerStatefulWidget { + static const route = "/settings"; + + const GeneralSettings({super.key}); + @override + ConsumerState createState() { + return _GeneralState(); + } +} + + +class _GeneralState extends ConsumerState { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + var settings = ref.watch(settingProvider); + + return settings.when( + data: (v) { + return FormBuilder( + key: _formKey, //设置globalKey,用于后面获取FormState + autovalidateMode: AutovalidateMode.onUserInteraction, + initialValue: { + "tmdb_api": v.tmdbApiKey, + "download_dir": v.downloadDIr, + "log_level": v.logLevel, + "proxy": v.proxy, + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FormBuilderTextField( + name: "tmdb_api", + decoration: Commons.requiredTextFieldStyle( + text: "TMDB Api Key", icon: const Icon(Icons.key)), + // + validator: FormBuilderValidators.required(), + ), + FormBuilderTextField( + name: "download_dir", + decoration: Commons.requiredTextFieldStyle( + text: "下载路径", + icon: const Icon(Icons.folder), + helperText: "媒体文件临时下载路径,非最终存储路径"), + // + validator: FormBuilderValidators.required(), + ), + FormBuilderTextField( + name: "proxy", + decoration: const InputDecoration( + labelText: "代理地址", + icon: Icon(Icons.folder), + helperText: "后台联网代理地址,留空表示不启用代理"), + ), + SizedBox( + width: 300, + child: FormBuilderDropdown( + name: "log_level", + decoration: const InputDecoration( + labelText: "日志级别", + icon: Icon(Icons.file_present_rounded), + ), + items: const [ + DropdownMenuItem(value: "debug", child: Text("DEBUG")), + DropdownMenuItem(value: "info", child: Text("INFO")), + DropdownMenuItem(value: "warn", child: Text("WARN")), + DropdownMenuItem(value: "error", child: Text("ERROR")), + ], + validator: FormBuilderValidators.required(), + ), + ), + Center( + child: Padding( + padding: const EdgeInsets.only(top: 28.0), + child: ElevatedButton( + child: const Padding( + padding: EdgeInsets.all(16.0), + child: Text("保存"), + ), + onPressed: () { + if (_formKey.currentState!.saveAndValidate()) { + var values = _formKey.currentState!.value; + var f = ref + .read(settingProvider.notifier) + .updateSettings(GeneralSetting( + tmdbApiKey: values["tmdb_api"], + downloadDIr: values["download_dir"], + logLevel: values["log_level"], + proxy: values["proxy"])); + f.then((v) { + Utils.showSnakeBar("更新成功"); + }).onError((e, s) { + Utils.showSnakeBar("更新失败:$e"); + }); + } + }), + ), + ) + ], + ), + ); + }, + error: (err, trace) => Text("$err"), + loading: () => const MyProgressIndicator()); + } + +} \ No newline at end of file diff --git a/ui/lib/settings/indexer.dart b/ui/lib/settings/indexer.dart new file mode 100644 index 0000000..10ca768 --- /dev/null +++ b/ui/lib/settings/indexer.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:ui/providers/settings.dart'; +import 'package:ui/settings/dialog.dart'; +import 'package:ui/widgets/progress_indicator.dart'; +import 'package:ui/widgets/widgets.dart'; + +class IndexerSettings extends ConsumerStatefulWidget { + + const IndexerSettings({super.key}); + @override + ConsumerState createState() { + return _IndexerState(); + } +} + +class _IndexerState extends ConsumerState { + @override + Widget build(BuildContext context) { + var indexers = ref.watch(indexersProvider); + return indexers.when( + data: (value) => Wrap( + children: List.generate(value.length + 1, (i) { + if (i < value.length) { + var indexer = value[i]; + return SettingsCard( + onTap: () => showIndexerDetails(indexer), + child: Text(indexer.name ?? "")); + } + return SettingsCard( + onTap: () => showIndexerDetails(Indexer()), + child: const Icon(Icons.add)); + }), + ), + error: (err, trace) => Text("$err"), + loading: () => const MyProgressIndicator()); + } + + Future showIndexerDetails(Indexer indexer) { + final _formKey = GlobalKey(); + + var body = FormBuilder( + key: _formKey, + initialValue: { + "name": indexer.name, + "url": indexer.url, + "api_key": indexer.apiKey, + "impl": "torznab" + }, + child: Column( + children: [ + FormBuilderDropdown( + name: "impl", + decoration: const InputDecoration(labelText: "类型"), + items: const [ + DropdownMenuItem(value: "torznab", child: Text("Torznab")), + ], + ), + FormBuilderTextField( + name: "name", + decoration: Commons.requiredTextFieldStyle(text: "名称"), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: FormBuilderValidators.required(), + ), + FormBuilderTextField( + name: "url", + decoration: Commons.requiredTextFieldStyle(text: "地址"), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: FormBuilderValidators.required(), + ), + FormBuilderTextField( + name: "api_key", + decoration: Commons.requiredTextFieldStyle(text: "API Key"), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: FormBuilderValidators.required(), + ), + ], + ), + ); + onDelete() async { + return ref.read(indexersProvider.notifier).deleteIndexer(indexer.id!); + } + + onSubmit() async { + if (_formKey.currentState!.saveAndValidate()) { + var values = _formKey.currentState!.value; + return ref.read(indexersProvider.notifier).addIndexer(Indexer( + name: values["name"], + url: values["url"], + apiKey: values["api_key"])); + } else { + throw "validation_error"; + } + } + + return showSettingDialog( + context, "索引器", indexer.id != null, body, onSubmit, onDelete); + } +} diff --git a/ui/lib/settings/notifier.dart b/ui/lib/settings/notifier.dart new file mode 100644 index 0000000..5d31198 --- /dev/null +++ b/ui/lib/settings/notifier.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:ui/providers/notifier.dart'; +import 'package:ui/settings/dialog.dart'; +import 'package:ui/widgets/progress_indicator.dart'; +import 'package:ui/widgets/widgets.dart'; + +class NotifierSettings extends ConsumerStatefulWidget { + static const route = "/settings"; + + const NotifierSettings({super.key}); + @override + ConsumerState createState() { + return _NotifierState(); + } +} + +class _NotifierState extends ConsumerState { + @override + Widget build(BuildContext context) { + final notifierData = ref.watch(notifiersDataProvider); + return notifierData.when( + data: (v) => Wrap( + children: List.generate(v.length + 1, (i) { + if (i < v.length) { + final client = v[i]; + return SettingsCard( + child: Text("${client.name!} (${client.service})"), + onTap: () => showNotifierDetails(client), + ); + } + return SettingsCard( + onTap: () => showNotifierDetails(NotifierData()), + child: const Icon(Icons.add)); + }), + ), + error: (err, trace) => Text("$err"), + loading: () => const MyProgressIndicator()); + } + + Future showNotifierDetails(NotifierData notifier) { + final _formKey = GlobalKey(); + + var body = FormBuilder( + key: _formKey, + initialValue: { + "name": notifier.name, + "service": notifier.service, + "enabled": notifier.enabled ?? true, + "app_token": + notifier.settings != null ? notifier.settings!["app_token"] : "", + "user_key": + notifier.settings != null ? notifier.settings!["user_key"] : "", + }, + child: Column( + children: [ + FormBuilderDropdown( + name: "service", + decoration: const InputDecoration(labelText: "类型"), + items: const [ + DropdownMenuItem(value: "pushover", child: Text("Pushover")), + ], + ), + FormBuilderTextField( + name: "name", + decoration: Commons.requiredTextFieldStyle(text: "名称"), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: FormBuilderValidators.required(), + ), + FormBuilderTextField( + name: "app_token", + decoration: Commons.requiredTextFieldStyle(text: "APP密钥"), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: FormBuilderValidators.required(), + ), + FormBuilderTextField( + name: "user_key", + decoration: Commons.requiredTextFieldStyle(text: "用户密钥"), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: FormBuilderValidators.required(), + ), + FormBuilderSwitch(name: "enabled", title: const Text("启用")) + ], + ), + ); + onDelete() async { + return ref.read(notifiersDataProvider.notifier).delete(notifier.id!); + } + + onSubmit() async { + if (_formKey.currentState!.saveAndValidate()) { + var values = _formKey.currentState!.value; + return ref.read(notifiersDataProvider.notifier).add(NotifierData( + name: values["name"], + service: values["service"], + enabled: values["enabled"], + settings: { + "app_token": values["app_token"], + "user_key": values["user_key"] + })); + } else { + throw "validation_error"; + } + } + + return showSettingDialog(context, + "通知客户端", notifier.id != null, body, onSubmit, onDelete); + } + + +} \ No newline at end of file diff --git a/ui/lib/settings/storage.dart b/ui/lib/settings/storage.dart new file mode 100644 index 0000000..478c085 --- /dev/null +++ b/ui/lib/settings/storage.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:ui/providers/settings.dart'; +import 'package:ui/settings/dialog.dart'; +import 'package:ui/widgets/progress_indicator.dart'; +import 'package:ui/widgets/widgets.dart'; + +class StorageSettings extends ConsumerStatefulWidget { + static const route = "/settings"; + + const StorageSettings({super.key}); + @override + ConsumerState createState() { + return _StorageState(); + } +} + +class _StorageState extends ConsumerState { + @override + Widget build(BuildContext context) { + var storageSettingData = ref.watch(storageSettingProvider); + return storageSettingData.when( + data: (value) => Wrap( + children: List.generate(value.length + 1, (i) { + if (i < value.length) { + var storage = value[i]; + return SettingsCard( + onTap: () => showStorageDetails(storage), + child: Text(storage.name ?? "")); + } + return SettingsCard( + onTap: () => showStorageDetails(Storage()), + child: const Icon(Icons.add)); + }), + ), + error: (err, trace) => Text("$err"), + loading: () => const MyProgressIndicator()); + } + + Future showStorageDetails(Storage s) { + final _formKey = GlobalKey(); + + String selectImpl = s.implementation == null ? "local" : s.implementation!; + final widgets = + StatefulBuilder(builder: (BuildContext context, StateSetter setState) { + return FormBuilder( + key: _formKey, + autovalidateMode: AutovalidateMode.disabled, + initialValue: { + "name": s.name, + "impl": s.implementation == null ? "local" : s.implementation!, + "user": s.settings != null ? s.settings!["user"] ?? "" : "", + "password": s.settings != null ? s.settings!["password"] ?? "" : "", + "tv_path": s.settings != null ? s.settings!["tv_path"] ?? "" : "", + "url": s.settings != null ? s.settings!["url"] ?? "" : "", + "movie_path": + s.settings != null ? s.settings!["movie_path"] ?? "" : "", + "change_file_hash": s.settings != null + ? s.settings!["change_file_hash"] == "true" + ? true + : false + : false, + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FormBuilderDropdown( + name: "impl", + autovalidateMode: AutovalidateMode.onUserInteraction, + decoration: const InputDecoration(labelText: "类型"), + onChanged: (value) { + setState(() { + selectImpl = value!; + }); + }, + items: const [ + DropdownMenuItem( + value: "local", + child: Text("本地存储"), + ), + DropdownMenuItem( + value: "webdav", + child: Text("webdav"), + ) + ], + validator: FormBuilderValidators.required(), + ), + FormBuilderTextField( + name: "name", + autovalidateMode: AutovalidateMode.onUserInteraction, + initialValue: s.name, + decoration: const InputDecoration(labelText: "名称"), + validator: FormBuilderValidators.required(), + ), + selectImpl != "local" + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FormBuilderTextField( + name: "url", + autovalidateMode: AutovalidateMode.onUserInteraction, + decoration: + const InputDecoration(labelText: "Webdav地址"), + validator: FormBuilderValidators.required(), + ), + FormBuilderTextField( + name: "user", + autovalidateMode: AutovalidateMode.onUserInteraction, + decoration: const InputDecoration(labelText: "用户"), + ), + FormBuilderTextField( + name: "password", + autovalidateMode: AutovalidateMode.onUserInteraction, + decoration: const InputDecoration(labelText: "密码"), + obscureText: true, + ), + FormBuilderCheckbox( + name: "change_file_hash", + title: const Text( + "上传时更改文件哈希", + style: TextStyle(fontSize: 14), + ), + ), + ], + ) + : Container(), + FormBuilderTextField( + name: "tv_path", + autovalidateMode: AutovalidateMode.onUserInteraction, + decoration: const InputDecoration(labelText: "电视剧路径"), + validator: FormBuilderValidators.required(), + ), + FormBuilderTextField( + name: "movie_path", + autovalidateMode: AutovalidateMode.onUserInteraction, + decoration: const InputDecoration(labelText: "电影路径"), + validator: FormBuilderValidators.required(), + ) + ], + )); + }); + onSubmit() async { + if (_formKey.currentState!.saveAndValidate()) { + final values = _formKey.currentState!.value; + return ref.read(storageSettingProvider.notifier).addStorage(Storage( + name: values["name"], + implementation: selectImpl, + settings: { + "tv_path": values["tv_path"], + "movie_path": values["movie_path"], + "url": values["url"], + "user": values["user"], + "password": values["password"], + "change_file_hash": + (values["change_file_hash"] ?? false) as bool + ? "true" + : "false" + }, + )); + } else { + throw "validation_error"; + } + } + + onDelete() async { + return ref.read(storageSettingProvider.notifier).deleteStorage(s.id!); + } + + return showSettingDialog(context,'存储', s.id != null, widgets, onSubmit, onDelete); + } + +} diff --git a/ui/lib/system_page.dart b/ui/lib/system_page.dart index c702577..837090f 100644 --- a/ui/lib/system_page.dart +++ b/ui/lib/system_page.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:ui/providers/APIs.dart'; import 'package:ui/providers/settings.dart'; -import 'package:ui/utils.dart'; +import 'package:ui/widgets/utils.dart'; import 'package:ui/widgets/progress_indicator.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/ui/lib/tv_details.dart b/ui/lib/tv_details.dart index fce724a..b30ab06 100644 --- a/ui/lib/tv_details.dart +++ b/ui/lib/tv_details.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:ui/providers/APIs.dart'; import 'package:ui/providers/series_details.dart'; import 'package:ui/providers/settings.dart'; -import 'package:ui/utils.dart'; +import 'package:ui/widgets/utils.dart'; import 'package:ui/welcome_page.dart'; import 'package:ui/widgets/progress_indicator.dart'; diff --git a/ui/lib/utils.dart b/ui/lib/widgets/utils.dart similarity index 100% rename from ui/lib/utils.dart rename to ui/lib/widgets/utils.dart