refactor: settings page

This commit is contained in:
Simon Ding
2024-07-29 16:34:32 +08:00
parent fcff47041a
commit eed72c5eb9
14 changed files with 841 additions and 699 deletions

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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<SystemSettingsPage> {
final _formKey = GlobalKey<FormBuilderState>();
final _formKey2 = GlobalKey<FormBuilderState>();
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<void> showNotifierDetails(NotifierData notifier) {
final _formKey = GlobalKey<FormBuilderState>();
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<void> showIndexerDetails(Indexer indexer) {
final _formKey = GlobalKey<FormBuilderState>();
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<void> showDownloadClientDetails(DownloadClient client) {
final _formKey = GlobalKey<FormBuilderState>();
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<String>(
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<void> showStorageDetails(Storage s) {
final _formKey = GlobalKey<FormBuilderState>();
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: <Widget>[
FormBuilderDropdown<String>(
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<void> showSettingDialog(String title, bool showDelete, Widget body,
Future Function() onSubmit, Future Function() onDelete) {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: Text(title),
content: SingleChildScrollView(
child: SizedBox(
width: 300,
child: body,
),
),
actions: <Widget>[
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");
}
});
},
),
],
);
});
}
}

102
ui/lib/settings/auth.dart Normal file
View File

@@ -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<ConsumerStatefulWidget> createState() {
return _AuthState();
}
}
class _AuthState extends ConsumerState<AuthSettings> {
final _formKey2 = GlobalKey<FormBuilderState>();
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());
}
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:ui/widgets/utils.dart';
Future<void> showSettingDialog(BuildContext context,String title, bool showDelete, Widget body,
Future Function() onSubmit, Future Function() onDelete) {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: Text(title),
content: SingleChildScrollView(
child: SizedBox(
width: 300,
child: body,
),
),
actions: <Widget>[
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");
}
});
},
),
],
);
});
}

View File

@@ -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<ConsumerStatefulWidget> createState() {
return _DownloaderState();
}
}
class _DownloaderState extends ConsumerState<DownloaderSettings> {
@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<void> showDownloadClientDetails(DownloadClient client) {
final _formKey = GlobalKey<FormBuilderState>();
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<String>(
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);
}
}

View File

@@ -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<ConsumerStatefulWidget> createState() {
return _GeneralState();
}
}
class _GeneralState extends ConsumerState<GeneralSettings> {
final _formKey = GlobalKey<FormBuilderState>();
@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());
}
}

View File

@@ -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<ConsumerStatefulWidget> createState() {
return _IndexerState();
}
}
class _IndexerState extends ConsumerState<IndexerSettings> {
@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<void> showIndexerDetails(Indexer indexer) {
final _formKey = GlobalKey<FormBuilderState>();
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);
}
}

View File

@@ -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<ConsumerStatefulWidget> createState() {
return _NotifierState();
}
}
class _NotifierState extends ConsumerState<NotifierSettings> {
@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<void> showNotifierDetails(NotifierData notifier) {
final _formKey = GlobalKey<FormBuilderState>();
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);
}
}

View File

@@ -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<ConsumerStatefulWidget> createState() {
return _StorageState();
}
}
class _StorageState extends ConsumerState<StorageSettings> {
@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<void> showStorageDetails(Storage s) {
final _formKey = GlobalKey<FormBuilderState>();
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: <Widget>[
FormBuilderDropdown<String>(
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);
}
}

View File

@@ -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';

View File

@@ -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';