From 993dfd57f14dec3d582461af645761d7a7b0dfaa Mon Sep 17 00:00:00 2001 From: Simon Ding Date: Thu, 18 Jul 2024 12:15:04 +0800 Subject: [PATCH] feat: update setting page --- server/server.go | 4 +- server/setting.go | 37 +-- ui/lib/movie_watchlist.dart | 2 +- ui/lib/providers/APIs.dart | 1 + ui/lib/providers/settings.dart | 74 +++--- ui/lib/system_settings.dart | 467 ++++++++++++++------------------- ui/lib/tv_details.dart | 10 +- ui/lib/utils.dart | 14 +- ui/lib/widgets/widgets.dart | 41 +++ 9 files changed, 319 insertions(+), 331 deletions(-) create mode 100644 ui/lib/widgets/widgets.dart diff --git a/server/server.go b/server/server.go index 692e0f3..0f8ddc9 100644 --- a/server/server.go +++ b/server/server.go @@ -53,8 +53,8 @@ func (s *Server) Serve() error { setting := api.Group("/setting") { - setting.POST("/do", HttpHandler(s.SetSetting)) - setting.GET("/do", HttpHandler(s.GetSetting)) + setting.POST("/general", HttpHandler(s.SetSetting)) + setting.GET("/general", HttpHandler(s.GetSetting)) setting.POST("/auth", HttpHandler(s.EnableAuth)) setting.GET("/auth", HttpHandler(s.GetAuthSetting)) } diff --git a/server/setting.go b/server/setting.go index 8c55781..24678b3 100644 --- a/server/setting.go +++ b/server/setting.go @@ -1,33 +1,40 @@ package server import ( - "polaris/log" + "polaris/db" "github.com/gin-gonic/gin" "github.com/pkg/errors" ) -type setSettingIn struct { - Key string `json:"key"` - Value string `json:"value"` +type GeneralSettings struct { + TmdbApiKey string `json:"tmdb_api_key"` + DownloadDir string `json:"download_dir"` } func (s *Server) SetSetting(c *gin.Context) (interface{}, error) { - var in setSettingIn + var in GeneralSettings if err := c.ShouldBindJSON(&in); err != nil { return nil, errors.Wrap(err, "bind json") } - err := s.db.SetSetting(in.Key, in.Value) - return nil, err + if in.TmdbApiKey != "" { + if err := s.db.SetSetting(db.SettingTmdbApiKey, in.TmdbApiKey); err != nil { + return nil, errors.Wrap(err, "save tmdb api") + } + } + if in.DownloadDir == "" { + if err := s.db.SetSetting(db.SettingDownloadDir, in.DownloadDir); err != nil { + return nil, errors.Wrap(err, "save download dir") + } + } + return nil, nil } func (s *Server) GetSetting(c *gin.Context) (interface{}, error) { - q := c.Query("key") - log.Infof("query key: %v", q) - if q == "" { - return nil, nil - } - v := s.db.GetSetting(q) - log.Infof("get value for key %v: %v", q, v) - return gin.H{q: v}, nil + tmdb := s.db.GetSetting(db.SettingTmdbApiKey) + downloadDir := s.db.GetSetting(db.SettingDownloadDir) + return &GeneralSettings{ + TmdbApiKey: tmdb, + DownloadDir: downloadDir, + }, nil } diff --git a/ui/lib/movie_watchlist.dart b/ui/lib/movie_watchlist.dart index 5f4575d..5acc92b 100644 --- a/ui/lib/movie_watchlist.dart +++ b/ui/lib/movie_watchlist.dart @@ -138,7 +138,7 @@ class _MovieDetailsPageState extends ConsumerState { DataCell(Text("${torrent.size?.readableFileSize()}")), DataCell(Text("${torrent.seeders}")), DataCell(Text("${torrent.peers}")), - DataCell(IconButton.filledTonal( + DataCell(IconButton( icon: const Icon(Icons.download), onPressed: () async { await ref diff --git a/ui/lib/providers/APIs.dart b/ui/lib/providers/APIs.dart index 590f728..8e933d2 100644 --- a/ui/lib/providers/APIs.dart +++ b/ui/lib/providers/APIs.dart @@ -10,6 +10,7 @@ class APIs { static final _baseUrl = baseUrl(); static final searchUrl = "$_baseUrl/api/v1/media/search"; static final settingsUrl = "$_baseUrl/api/v1/setting/do"; + static final settingsGeneralUrl = "$_baseUrl/api/v1/setting/general"; static final watchlistTvUrl = "$_baseUrl/api/v1/media/tv/watchlist"; static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist"; static final availableMoviesUrl = "$_baseUrl/api/v1/media/movie/resources/"; diff --git a/ui/lib/providers/settings.dart b/ui/lib/providers/settings.dart index 995d02c..7cc21cc 100644 --- a/ui/lib/providers/settings.dart +++ b/ui/lib/providers/settings.dart @@ -6,46 +6,38 @@ import 'package:quiver/strings.dart'; import 'package:ui/providers/APIs.dart'; import 'package:ui/providers/server_response.dart'; -var settingProvider = - AsyncNotifierProvider.autoDispose.family( - EditSettingData.new); +var settingProvider = AsyncNotifierProvider.autoDispose + (EditSettingData.new); var indexersProvider = - AsyncNotifierProvider.autoDispose>(IndexerSetting.new); + AsyncNotifierProvider.autoDispose>( + IndexerSetting.new); -var dwonloadClientsProvider = - AsyncNotifierProvider.autoDispose>( - DownloadClientSetting.new); +var dwonloadClientsProvider = AsyncNotifierProvider.autoDispose< + DownloadClientSetting, List>(DownloadClientSetting.new); var storageSettingProvider = AsyncNotifierProvider.autoDispose>( StorageSettingData.new); -class EditSettingData extends AutoDisposeFamilyAsyncNotifier { - String? key; - +class EditSettingData extends AutoDisposeAsyncNotifier { @override - FutureOr build(String arg) async { + FutureOr build() async { final dio = await APIs.getDio(); - key = arg; - var resp = await dio.get(APIs.settingsUrl, queryParameters: {"key": arg}); + + var resp = await dio.get(APIs.settingsGeneralUrl); var rrr = ServerResponse.fromJson(resp.data); if (rrr.code != 0) { throw rrr.message; } - var data = rrr.data as Map; - var value = data[arg] as String; - - return value; + final ss = GeneralSetting.fromJson(rrr.data); + return ss; } - Future updateSettings(String v) async { + Future updateSettings(GeneralSetting gs) async { final dio = await APIs.getDio(); - var resp = await dio.post(APIs.settingsUrl, data: { - "key": key, - "value": v, - }); - var sp = ServerResponse.fromJson(resp.data as Map); + var resp = await dio.post(APIs.settingsGeneralUrl, data: gs.toJson()); + var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { throw sp.message; } @@ -53,6 +45,25 @@ class EditSettingData extends AutoDisposeFamilyAsyncNotifier { } } +class GeneralSetting { + String? tmdbApiKey; + String? downloadDIr; + + GeneralSetting({this.tmdbApiKey, this.downloadDIr}); + + factory GeneralSetting.fromJson(Map json) { + return GeneralSetting( + tmdbApiKey: json["tmdb_api_key"], downloadDIr: json["download_dir"]); + } + + Map toJson() { + final Map data = {}; + data['tmdb_api_key'] = tmdbApiKey; + data['download_dir'] = downloadDIr; + return data; + } +} + class IndexerSetting extends AutoDisposeAsyncNotifier> { @override FutureOr> build() async { @@ -121,7 +132,8 @@ class Indexer { } } -class DownloadClientSetting extends AutoDisposeAsyncNotifier> { +class DownloadClientSetting + extends AutoDisposeAsyncNotifier> { @override FutureOr> build() async { final dio = await APIs.getDio(); @@ -247,7 +259,8 @@ class StorageSettingData extends AutoDisposeAsyncNotifier> { } class Storage { - Storage({this.id, this.name, this.implementation, this.settings, this.isDefault}); + Storage( + {this.id, this.name, this.implementation, this.settings, this.isDefault}); final int? id; final String? name; @@ -257,12 +270,11 @@ class Storage { factory Storage.fromJson(Map json1) { return Storage( - id: json1["id"], - name: json1["name"], - implementation: json1["implementation"], - settings: json.decode(json1["settings"]), - isDefault: json1["default"] - ); + id: json1["id"], + name: json1["name"], + implementation: json1["implementation"], + settings: json.decode(json1["settings"]), + isDefault: json1["default"]); } Map toJson() => { diff --git a/ui/lib/system_settings.dart b/ui/lib/system_settings.dart index bdfa0ea..e4d5d12 100644 --- a/ui/lib/system_settings.dart +++ b/ui/lib/system_settings.dart @@ -4,8 +4,7 @@ import 'package:ui/providers/login.dart'; import 'package:ui/providers/settings.dart'; import 'package:ui/utils.dart'; import 'package:ui/widgets/progress_indicator.dart'; - -import 'providers/APIs.dart'; +import 'package:ui/widgets/widgets.dart'; class SystemSettingsPage extends ConsumerStatefulWidget { static const route = "/settings"; @@ -19,227 +18,134 @@ class SystemSettingsPage extends ConsumerStatefulWidget { class _SystemSettingsPageState extends ConsumerState { final GlobalKey _formKey = GlobalKey(); - Future? _pendingTmdb; - Future? _pendingIndexer; - Future? _pendingDownloadClient; - Future? _pendingStorage; + final _tmdbApiController = TextEditingController(); final _downloadDirController = TextEditingController(); bool? _enableAuth; @override Widget build(BuildContext context) { - var tmdbKey = ref.watch(settingProvider(APIs.tmdbApiKey)); - var dirKey = ref.watch(settingProvider(APIs.downloadDirKey)); + var settings = ref.watch(settingProvider); - var tmdbSetting = FutureBuilder( - // We listen to the pending operation, to update the UI accordingly. - future: _pendingTmdb, - builder: (context, snapshot) { + var tmdbSetting = settings.when( + data: (v) { + _tmdbApiController.text = v.tmdbApiKey!; + _downloadDirController.text = v.downloadDIr!; return Container( - padding: const EdgeInsets.fromLTRB(40, 10, 40, 0), - child: Form( - key: _formKey, //设置globalKey,用于后面获取FormState - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - children: [ - tmdbKey.when( - data: (value) { - _tmdbApiController.text = value; - return TextFormField( - autofocus: true, - controller: _tmdbApiController, - decoration: const InputDecoration( - labelText: "TMDB Api Key", - icon: Icon(Icons.key), - ), - // - validator: (v) { - return v!.trim().isNotEmpty ? null : "ApiKey 不能为空"; - }, - onSaved: (newValue) {}, - ); + padding: const EdgeInsets.fromLTRB(40, 10, 40, 0), + child: Form( + key: _formKey, //设置globalKey,用于后面获取FormState + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + TextFormField( + autofocus: true, + controller: _tmdbApiController, + decoration: Commons.requiredTextFieldStyle( + text: "TMDB Api Key", icon: const Icon(Icons.key)), + // + validator: (v) { + return v!.trim().isNotEmpty ? null : "ApiKey 不能为空"; }, - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()), - dirKey.when( - data: (data) { - _downloadDirController.text = data; - return TextFormField( - autofocus: true, - controller: _downloadDirController, - decoration: const InputDecoration( - labelText: "下载路径", - icon: Icon(Icons.folder), - ), - // - validator: (v) { - return v!.trim().isNotEmpty ? null : "ApiKey 不能为空"; - }, - onSaved: (newValue) {}, - ); - }, - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()), - Center( - child: Padding( - padding: const EdgeInsets.only(top: 28.0), - child: ElevatedButton( - child: const Padding( - padding: EdgeInsets.all(16.0), - child: Text("保存"), - ), - onPressed: () { - // 通过_formKey.currentState 获取FormState后, - // 调用validate()方法校验用户名密码是否合法,校验 - // 通过后再提交数据。 - if ((_formKey.currentState as FormState).validate()) { - var furture = ref - .read(settingProvider(APIs.tmdbApiKey).notifier) - .updateSettings(_tmdbApiController.text); - ref - .read(settingProvider(APIs.downloadDirKey) - .notifier) - .updateSettings(_downloadDirController.text); - setState(() { - _pendingTmdb = furture; - }); - showError(snapshot); - } - }, - ), + onSaved: (newValue) {}, ), - ) - ], - ), - ), - ); - }); + TextFormField( + autofocus: true, + controller: _downloadDirController, + decoration: Commons.requiredTextFieldStyle( + text: "下载路径", icon: const Icon(Icons.folder)), + // + validator: (v) { + return v!.trim().isNotEmpty ? null : "下载路径不能为空"; + }, + onSaved: (newValue) {}, + ), + 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 as FormState) + .validate()) { + var f = ref + .read(settingProvider.notifier) + .updateSettings(GeneralSetting( + tmdbApiKey: _tmdbApiController.text, + downloadDIr: + _downloadDirController.text)); + f.whenComplete(() { + Utils.showSnakeBar("更新成功"); + }).onError((e, s) { + Utils.showSnakeBar("更新失败:$e"); + }); + } + }, + ), + ), + ) + ], + ), + )); + }, + error: (err, trace) => Text("$err"), + loading: () => const MyProgressIndicator()); var indexers = ref.watch(indexersProvider); - var indexerSetting = FutureBuilder( - // We listen to the pending operation, to update the UI accordingly. - future: _pendingIndexer, - builder: (context, snapshot) { - return indexers.when( - data: (value) => GridView.builder( - itemCount: value.length + 1, - scrollDirection: Axis.vertical, - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6), - itemBuilder: (context, i) { - if (i < value.length) { - var indexer = value[i]; - return Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - showIndexerDetails(snapshot, context, indexer); - }, - child: Center(child: Text(indexer.name!)))); - } - return Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - showIndexerDetails(snapshot, context, Indexer()); - }, - child: const Center( - child: Icon(Icons.add), - ))); - }), - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()); - }); + 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 = FutureBuilder( - // We listen to the pending operation, to update the UI accordingly. - future: _pendingDownloadClient, - builder: (context, snapshot) { - return downloadClients.when( - data: (value) => GridView.builder( - itemCount: value.length + 1, - scrollDirection: Axis.vertical, - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6), - itemBuilder: (context, i) { - if (i < value.length) { - var client = value[i]; - return Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - showDownloadClientDetails( - snapshot, context, client); - }, - child: Center(child: Text(client.name!)))); - } - return Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - showDownloadClientDetails( - snapshot, context, DownloadClient()); - }, - child: const Center( - child: Icon(Icons.add), - ))); - }), - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()); - }); + 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 = FutureBuilder( - // We listen to the pending operation, to update the UI accordingly. - future: _pendingStorage, - builder: (context, snapshot) { - return storageSettingData.when( - data: (value) => GridView.builder( - itemCount: value.length + 1, - scrollDirection: Axis.vertical, - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6), - itemBuilder: (context, i) { - if (i < value.length) { - var storage = value[i]; - return Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - showStorageDetails(snapshot, context, storage); - }, - child: Center(child: Text(storage.name!)))); - } - return Card( - margin: const EdgeInsets.all(4), - clipBehavior: Clip.hardEdge, - child: InkWell( - //splashColor: Colors.blue.withAlpha(30), - onTap: () { - showStorageDetails(snapshot, context, Storage()); - }, - child: const Center( - child: Icon(Icons.add), - ))); - }), - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()); - }); + 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); TextEditingController userController = TextEditingController(); @@ -267,15 +173,18 @@ class _SystemSettingsPageState extends ConsumerState { children: [ TextFormField( controller: userController, - decoration: const InputDecoration( - labelText: "用户名", - icon: Icon(Icons.verified_user), + decoration: Commons.requiredTextFieldStyle( + text: "用户名", + icon: const Icon(Icons.account_box), )), TextFormField( + obscureText: true, + enableSuggestions: false, + autocorrect: false, controller: passController, - decoration: const InputDecoration( - labelText: "密码", - icon: Icon(Icons.verified_user), + decoration: Commons.requiredTextFieldStyle( + text: "密码", + icon: const Icon(Icons.password), )) ], ) @@ -284,10 +193,15 @@ class _SystemSettingsPageState extends ConsumerState { child: ElevatedButton( child: const Text("保存"), onPressed: () { - ref + var f = ref .read(authSettingProvider.notifier) .updateAuthSetting(_enableAuth!, userController.text, passController.text); + f.whenComplete(() { + Utils.showSnakeBar("更新成功"); + }).onError((e, s) { + Utils.showSnakeBar("更新失败:$e"); + }); })) ], ); @@ -298,6 +212,7 @@ class _SystemSettingsPageState extends ConsumerState { return ListView( children: [ ExpansionTile( + expandedAlignment: Alignment.centerLeft, tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0), childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0), initiallyExpanded: true, @@ -305,23 +220,26 @@ class _SystemSettingsPageState extends ConsumerState { children: [tmdbSetting], ), ExpansionTile( + expandedAlignment: Alignment.centerLeft, tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0), childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0), - initiallyExpanded: true, + initiallyExpanded: false, title: const Text("索引器设置"), children: [indexerSetting], ), ExpansionTile( + expandedAlignment: Alignment.centerLeft, tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0), childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0), - initiallyExpanded: true, + initiallyExpanded: false, title: const Text("下载客户端设置"), children: [downloadSetting], ), ExpansionTile( + expandedAlignment: Alignment.centerLeft, tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0), childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0), - initiallyExpanded: true, + initiallyExpanded: false, title: const Text("存储设置"), children: [storageSetting], ), @@ -336,8 +254,7 @@ class _SystemSettingsPageState extends ConsumerState { ); } - Future showIndexerDetails( - AsyncSnapshot snapshot, BuildContext context, Indexer indexer) { + Future showIndexerDetails(Indexer indexer) { var nameController = TextEditingController(text: indexer.name); var urlController = TextEditingController(text: indexer.url); var apiKeyController = TextEditingController(text: indexer.apiKey); @@ -351,15 +268,15 @@ class _SystemSettingsPageState extends ConsumerState { child: ListBody( children: [ TextField( - decoration: const InputDecoration(labelText: "名称"), + decoration: Commons.requiredTextFieldStyle(text: "名称"), controller: nameController, ), TextField( - decoration: const InputDecoration(labelText: "地址"), + decoration: Commons.requiredTextFieldStyle(text: "地址"), controller: urlController, ), TextField( - decoration: const InputDecoration(labelText: "API Key"), + decoration: Commons.requiredTextFieldStyle(text: "API Key"), controller: apiKeyController, ), ], @@ -373,14 +290,17 @@ class _SystemSettingsPageState extends ConsumerState { var f = ref .read(indexersProvider.notifier) .deleteIndexer(indexer.id!); - setState(() { - _pendingIndexer = f; - }); - if (!showError(snapshot)) { + f.whenComplete(() { + Utils.showSnakeBar("操作成功"); Navigator.of(context).pop(); - } + }).onError((e, s) { + Utils.showSnakeBar("操作失败:$e"); + }); }, - child: const Text('删除')), + child: const Text( + '删除', + style: TextStyle(color: Colors.red), + )), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消')), @@ -392,13 +312,12 @@ class _SystemSettingsPageState extends ConsumerState { name: nameController.text, url: urlController.text, apiKey: apiKeyController.text)); - setState(() { - _pendingIndexer = f; - }); - - if (!showError(snapshot)) { + f.whenComplete(() { + Utils.showSnakeBar("操作成功"); Navigator.of(context).pop(); - } + }).onError((e, s) { + Utils.showSnakeBar("操作失败:$e"); + }); }, ), ], @@ -406,8 +325,7 @@ class _SystemSettingsPageState extends ConsumerState { }); } - Future showDownloadClientDetails(AsyncSnapshot snapshot, - BuildContext context, DownloadClient client) { + Future showDownloadClientDetails(DownloadClient client) { var nameController = TextEditingController(text: client.name); var urlController = TextEditingController(text: client.url); @@ -421,11 +339,11 @@ class _SystemSettingsPageState extends ConsumerState { child: ListBody( children: [ TextField( - decoration: const InputDecoration(labelText: "名称"), + decoration: Commons.requiredTextFieldStyle(text: "名称"), controller: nameController, ), TextField( - decoration: const InputDecoration(labelText: "地址"), + decoration: Commons.requiredTextFieldStyle(text: "地址"), controller: urlController, ), ], @@ -439,14 +357,17 @@ class _SystemSettingsPageState extends ConsumerState { var f = ref .read(dwonloadClientsProvider.notifier) .deleteDownloadClients(client.id!); - setState(() { - _pendingDownloadClient = f; - }); - if (!showError(snapshot)) { + f.whenComplete(() { + Utils.showSnakeBar("操作成功"); Navigator.of(context).pop(); - } + }).onError((e, s) { + Utils.showSnakeBar("操作失败:$e"); + }); }, - child: const Text('删除')), + child: const Text( + '删除', + style: TextStyle(color: Colors.red), + )), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消')), @@ -457,12 +378,12 @@ class _SystemSettingsPageState extends ConsumerState { .read(dwonloadClientsProvider.notifier) .addDownloadClients( nameController.text, urlController.text); - setState(() { - _pendingDownloadClient = f; - }); - if (!showError(snapshot)) { + f.whenComplete(() { + Utils.showSnakeBar("操作成功"); Navigator.of(context).pop(); - } + }).onError((e, s) { + Utils.showSnakeBar("操作失败:$e"); + }); }, ), ], @@ -470,8 +391,7 @@ class _SystemSettingsPageState extends ConsumerState { }); } - Future showStorageDetails( - AsyncSnapshot snapshot, BuildContext context, Storage s) { + Future showStorageDetails(Storage s) { return showDialog( context: context, barrierDismissible: true, // user must tap button! @@ -483,9 +403,9 @@ class _SystemSettingsPageState extends ConsumerState { var userController = TextEditingController(); var passController = TextEditingController(); if (s.settings != null) { - tvPathController.text = s.settings!["tv_path"] ?? ""; + tvPathController.text = s.settings!["tv_path"] ?? ""; moviePathController.text = s.settings!["movie_path"] ?? ""; - urlController.text = s.settings!["url"]?? ""; + urlController.text = s.settings!["url"] ?? ""; userController.text = s.settings!["user"] ?? ""; passController.text = s.settings!["password"] ?? ""; } @@ -516,8 +436,7 @@ class _SystemSettingsPageState extends ConsumerState { controller: nameController, ), selectImpl != "local" - ? - Column( + ? Column( children: [ TextField( decoration: const InputDecoration( @@ -535,15 +454,16 @@ class _SystemSettingsPageState extends ConsumerState { controller: passController, ), ], - ): Container(), - TextField( - decoration: const InputDecoration(labelText: "电视剧路径"), - controller: tvPathController, - ), - TextField( - decoration: const InputDecoration(labelText: "电影路径"), - controller: moviePathController, ) + : Container(), + TextField( + decoration: const InputDecoration(labelText: "电视剧路径"), + controller: tvPathController, + ), + TextField( + decoration: const InputDecoration(labelText: "电影路径"), + controller: moviePathController, + ) ], ), ), @@ -555,12 +475,12 @@ class _SystemSettingsPageState extends ConsumerState { var f = ref .read(storageSettingProvider.notifier) .deleteStorage(s.id!); - setState(() { - _pendingStorage = f; - }); - if (!showError(snapshot)) { + f.whenComplete(() { + Utils.showSnakeBar("操作成功"); Navigator.of(context).pop(); - } + }).onError((e, s) { + Utils.showSnakeBar("操作失败:$e"); + }); }, child: const Text('删除')), TextButton( @@ -569,7 +489,7 @@ class _SystemSettingsPageState extends ConsumerState { TextButton( child: const Text('确定'), onPressed: () async { - ref + final f = ref .read(storageSettingProvider.notifier) .addStorage(Storage( name: nameController.text, @@ -582,9 +502,12 @@ class _SystemSettingsPageState extends ConsumerState { "password": passController.text }, )); - if (!showError(snapshot)) { + f.whenComplete(() { + Utils.showSnakeBar("操作成功"); Navigator.of(context).pop(); - } + }).onError((e, s) { + Utils.showSnakeBar("操作失败:$e"); + }); }, ), ], @@ -597,7 +520,7 @@ class _SystemSettingsPageState extends ConsumerState { final isErrored = snapshot.hasError && snapshot.connectionState != ConnectionState.waiting; if (isErrored) { - Utils.showSnakeBar(context, "当前操作出错: ${snapshot.error}"); + Utils.showSnakeBar("当前操作出错: ${snapshot.error}"); return true; } return false; diff --git a/ui/lib/tv_details.dart b/ui/lib/tv_details.dart index 289785f..dcde099 100644 --- a/ui/lib/tv_details.dart +++ b/ui/lib/tv_details.dart @@ -62,11 +62,11 @@ class _TvDetailsPageState extends ConsumerState { ? const Icon(Icons.downloading) : (ep.status == "downloaded" ? const Icon(Icons.download_done) - : const Icon(Icons.cloud_off))), + : const Icon(Icons.warning_rounded))), ), DataCell(Row( children: [ - IconButton.filledTonal( + IconButton( onPressed: () async { var f = ref .read( @@ -78,14 +78,12 @@ class _TvDetailsPageState extends ConsumerState { }); if (!Utils.showError(context, snapshot)) { var name = await f; - if (context.mounted) { - Utils.showSnakeBar(context, "开始下载: $name"); - } + Utils.showSnakeBar("开始下载: $name"); } }, icon: const Icon(Icons.search)), const SizedBox(width: 10,), - IconButton.filledTonal( + IconButton( onPressed: () {}, icon: const Icon(Icons.manage_search)) ], diff --git a/ui/lib/utils.dart b/ui/lib/utils.dart index 3de65f6..842f4f9 100644 --- a/ui/lib/utils.dart +++ b/ui/lib/utils.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:ui/providers/APIs.dart'; class Utils { static Future showAlertDialog(BuildContext context, String msg) async { @@ -31,22 +32,27 @@ class Utils { ); } - static showSnakeBar(BuildContext context, String msg) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg), showCloseIcon: true,)); + static showSnakeBar(String msg) { + final context = APIs.navigatorKey.currentContext; + if (context != null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(msg), + showCloseIcon: true, + )); + } } static bool showError(BuildContext context, AsyncSnapshot snapshot) { final isErrored = snapshot.hasError && snapshot.connectionState != ConnectionState.waiting; if (isErrored) { - Utils.showSnakeBar(context, "当前操作出错: ${snapshot.error}"); + Utils.showSnakeBar("当前操作出错: ${snapshot.error}"); return true; } return false; } } - extension FileFormatter on num { String readableFileSize({bool base1024 = true}) { final base = base1024 ? 1024 : 1000; diff --git a/ui/lib/widgets/widgets.dart b/ui/lib/widgets/widgets.dart new file mode 100644 index 0000000..0a01942 --- /dev/null +++ b/ui/lib/widgets/widgets.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class Commons { + static InputDecoration requiredTextFieldStyle({ + required String text, + Widget? icon, + }) { + return InputDecoration( + label: Row( + children: [ + Text(text), + const Text( + "*", + style: TextStyle(color: Colors.red), + ) + ], + ), + icon: icon, + ); + } +} + +class SettingsCard extends StatelessWidget { + final Widget? child; + final GestureTapCallback? onTap; + + const SettingsCard({super.key, required this.child, this.onTap}); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.all(4), + clipBehavior: Clip.hardEdge, + child: InkWell( + //splashColor: Colors.blue.withAlpha(30), + onTap: onTap, + child: + SizedBox(width: 150, height: 150, child: Center(child: child))), + ); + } +}