From b34e39889cce08b4c7cfe08b1c74344c5b7e28fe Mon Sep 17 00:00:00 2001 From: Simon Ding Date: Thu, 8 Aug 2024 10:56:03 +0800 Subject: [PATCH] feat: ui improvement --- ui/lib/search_page/submit_dialog.dart | 13 ++-- ui/lib/settings/dialog.dart | 18 ++--- ui/lib/tv_details.dart | 101 ++++++++++++-------------- ui/lib/widgets/detail_card.dart | 59 +++++++++------ ui/lib/widgets/resource_list.dart | 7 +- ui/lib/widgets/widgets.dart | 91 +++++++++++++++++++++++ 6 files changed, 190 insertions(+), 99 deletions(-) diff --git a/ui/lib/search_page/submit_dialog.dart b/ui/lib/search_page/submit_dialog.dart index b50027f..fd4f669 100644 --- a/ui/lib/search_page/submit_dialog.dart +++ b/ui/lib/search_page/submit_dialog.dart @@ -161,15 +161,15 @@ class _SubmitSearchResultState extends ConsumerState { Navigator.of(context).pop(); }, ), - TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.labelLarge, - ), - child: const Text('确定'), + LoadingTextButton( + // style: TextButton.styleFrom( + // textStyle: Theme.of(context).textTheme.labelLarge, + // ), + label: const Text('确定'), onPressed: () async { if (_formKey.currentState!.saveAndValidate()) { final values = _formKey.currentState!.value; - var f = ref + await ref .read(searchPageDataProvider(widget.query).notifier) .submit2Watchlist( widget.item.id!, @@ -185,7 +185,6 @@ class _SubmitSearchResultState extends ConsumerState { Navigator.of(context).pop(); showSnakeBar("添加成功:${widget.item.name}"); }); - showLoadingWithFuture(f); } }, ), diff --git a/ui/lib/settings/dialog.dart b/ui/lib/settings/dialog.dart index c982385..f6b07ad 100644 --- a/ui/lib/settings/dialog.dart +++ b/ui/lib/settings/dialog.dart @@ -22,12 +22,11 @@ Future showSettingDialog( ), actions: [ showDelete - ? TextButton( - onPressed: () { - final f = onDelete().then((v) => Navigator.of(context).pop()); - showLoadingWithFuture(f); + ? LoadingTextButton( + onPressed: () async { + await onDelete().then((v) => Navigator.of(context).pop()); }, - child: const Text( + label: const Text( '删除', style: TextStyle(color: Colors.red), )) @@ -35,11 +34,10 @@ Future showSettingDialog( TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消')), - TextButton( - child: const Text('确定'), - onPressed: () { - final f = onSubmit().then((v) => Navigator.of(context).pop()); - showLoadingWithFuture(f); + LoadingTextButton( + label: const Text('确定'), + onPressed: () async { + await onSubmit().then((v) => Navigator.of(context).pop()); }, ), ], diff --git a/ui/lib/tv_details.dart b/ui/lib/tv_details.dart index d341b14..7e05717 100644 --- a/ui/lib/tv_details.dart +++ b/ui/lib/tv_details.dart @@ -48,62 +48,55 @@ class _TvDetailsPageState extends ConsumerState { Opacity( opacity: 0.7, child: ep.status == "downloading" - ? const Tooltip( - message: "下载中", - child: IconButton(onPressed: null, icon: Icon(Icons.downloading)), - ) + ? const IconButton( + tooltip: "下载中", + onPressed: null, + icon: Icon(Icons.downloading)) : (ep.status == "downloaded" - ? const Tooltip( - message: "已下载", - child: IconButton(onPressed: null, icon: Icon(Icons.download_done)), - ) + ? const IconButton( + tooltip: "已下载", + onPressed: null, + icon: Icon(Icons.download_done)) : (ep.monitored == true - ? Tooltip( - message: "监控中", + ? IconButton( + tooltip: "监控中", + onPressed: () { + ref + .read(mediaDetailsProvider( + widget.seriesId) + .notifier) + .changeMonitoringStatus( + ep.id!, false); + }, + icon: const Icon(Icons.alarm)) + : Opacity( + opacity: 0.7, child: IconButton( + tooltip: "未监控", onPressed: () { ref .read(mediaDetailsProvider( widget.seriesId) .notifier) .changeMonitoringStatus( - ep.id!, false); + ep.id!, true); }, - icon: const Icon(Icons.alarm)), - ) - : Opacity( - opacity: 0.7, - child: Tooltip( - message: "未监控", - child: IconButton( - onPressed: () { - ref - .read(mediaDetailsProvider( - widget.seriesId) - .notifier) - .changeMonitoringStatus( - ep.id!, true); - }, - icon: const Icon(Icons.alarm_off)), - ), + icon: const Icon(Icons.alarm_off)), )))), ), DataCell(Row( children: [ - Tooltip( - message: "搜索下载对应剧集", - child: IconButton( - onPressed: () { - var f = ref - .read(mediaDetailsProvider(widget.seriesId) - .notifier) - .searchAndDownload(widget.seriesId, - ep.seasonNumber!, ep.episodeNumber!) - .then((v) => showSnakeBar("开始下载: $v")); - showLoadingWithFuture(f); - }, - icon: const Icon(Icons.download)), - ), + LoadingIconButton( + tooltip: "搜索下载对应剧集", + onPressed: () async { + await ref + .read( + mediaDetailsProvider(widget.seriesId).notifier) + .searchAndDownload(widget.seriesId, + ep.seasonNumber!, ep.episodeNumber!) + .then((v) => showSnakeBar("开始下载: $v")); + }, + icon: Icons.download), const SizedBox( width: 10, ), @@ -142,19 +135,17 @@ class _TvDetailsPageState extends ConsumerState { DataColumn( label: Row( children: [ - Tooltip( - message: "搜索下载全部剧集", - child: IconButton( - onPressed: () { - final f = ref - .read(mediaDetailsProvider(widget.seriesId) - .notifier) - .searchAndDownload(widget.seriesId, k, 0) - .then((v) => showSnakeBar("开始下载: $v")); - showLoadingWithFuture(f); - }, - icon: const Icon(Icons.download)), - ), + LoadingIconButton( + tooltip: "搜索下载全部剧集", + onPressed: () async { + await ref + .read(mediaDetailsProvider(widget.seriesId) + .notifier) + .searchAndDownload(widget.seriesId, k, 0) + .then((v) => showSnakeBar("开始下载: $v")); + //showLoadingWithFuture(f); + }, + icon: Icons.download), const SizedBox( width: 10, ), diff --git a/ui/lib/widgets/detail_card.dart b/ui/lib/widgets/detail_card.dart index cd2cc3f..f7a1e31 100644 --- a/ui/lib/widgets/detail_card.dart +++ b/ui/lib/widgets/detail_card.dart @@ -66,8 +66,7 @@ class _DetailCardState extends ConsumerState { const SizedBox( width: 30, ), - Text( - "${widget.details.storage!.name}:"), + Text("${widget.details.storage!.name}:"), Text( "${widget.details.mediaType == "tv" ? widget.details.storage!.tvPath : widget.details.storage!.moviePath}" "${widget.details.targetDir}"), @@ -90,13 +89,15 @@ class _DetailCardState extends ConsumerState { const Text(""), Expanded( child: Text( - overflow: TextOverflow.ellipsis, + overflow: TextOverflow.visible, maxLines: 9, widget.details.overview ?? "", )), Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - editIcon(widget.details), + downloadButton(), + editIcon(), deleteIcon(), ], ) @@ -113,11 +114,10 @@ class _DetailCardState extends ConsumerState { } Widget deleteIcon() { - return Tooltip( - message: widget.details.mediaType == "tv" ? "删除剧集" : "删除电影", - child: IconButton( - onPressed: () => showConfirmDialog(), icon: const Icon(Icons.delete)), - ); + return IconButton( + tooltip: widget.details.mediaType == "tv" ? "删除剧集" : "删除电影", + onPressed: () => showConfirmDialog(), + icon: const Icon(Icons.delete)); } Future showConfirmDialog() { @@ -150,19 +150,21 @@ class _DetailCardState extends ConsumerState { ); } - Widget editIcon(SeriesDetails details) { + Widget editIcon() { return IconButton( - onPressed: () => showEditDialog(details), icon: const Icon(Icons.edit)); + tooltip: "编辑", + onPressed: () => showEditDialog(), + icon: const Icon(Icons.edit)); } - showEditDialog(SeriesDetails details) { + showEditDialog() { final _formKey = GlobalKey(); return showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return AlertDialog( - title: Text("编辑 ${details.name}"), + title: Text("编辑 ${widget.details.name}"), content: SelectionArea( child: SizedBox( width: MediaQuery.of(context).size.width * 0.3, @@ -171,11 +173,16 @@ class _DetailCardState extends ConsumerState { child: FormBuilder( key: _formKey, initialValue: { - "resolution": details.resolution, - "target_dir": details.targetDir, - "limiter": details.limiter != null - ? RangeValues(details.limiter!.sizeMin.toDouble()/1000/1000, - details.limiter!.sizeMax.toDouble()/1000/1000) + "resolution": widget.details.resolution, + "target_dir": widget.details.targetDir, + "limiter": widget.details.limiter != null + ? RangeValues( + widget.details.limiter!.sizeMin.toDouble() / + 1000 / + 1000, + widget.details.limiter!.sizeMax.toDouble() / + 1000 / + 1000) : const RangeValues(0, 0) }, child: Column( @@ -204,23 +211,29 @@ class _DetailCardState extends ConsumerState { TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text("取消")), - TextButton( - onPressed: () { + LoadingTextButton( + onPressed: () async { if (_formKey.currentState!.saveAndValidate()) { final values = _formKey.currentState!.value; - var f = ref + await ref .read(mediaDetailsProvider(widget.details.id.toString()) .notifier) .edit(values["resolution"], values["target_dir"], values["limiter"]) .then((v) => Navigator.of(context).pop()); - showLoadingWithFuture(f); } }, - child: const Text("确认")) + label: const Text("确认")) ], ); }, ); } + + Widget downloadButton() { + return IconButton( + tooltip: widget.details.mediaType == "tv" ? "查找并下载所有监控剧集" : "查找并下载此电影", + onPressed: () {}, + icon: const Icon(Icons.download_rounded)); + } } diff --git a/ui/lib/widgets/resource_list.dart b/ui/lib/widgets/resource_list.dart index 8f640c4..a944505 100644 --- a/ui/lib/widgets/resource_list.dart +++ b/ui/lib/widgets/resource_list.dart @@ -59,10 +59,10 @@ class ResourceList extends ConsumerWidget { : "-"))); } - rows.add(DataCell(IconButton( - icon: const Icon(Icons.download), + rows.add(DataCell(LoadingIconButton( + icon: Icons.download, onPressed: () async { - var f = ref + await ref .read(mediaTorrentsDataProvider(( mediaId: mediaId, seasonNumber: seasonNum, @@ -70,7 +70,6 @@ class ResourceList extends ConsumerWidget { )).notifier) .download(torrent) .then((v) => showSnakeBar("开始下载:${torrent.name}")); - showLoadingWithFuture(f); }, ))); return DataRow(cells: rows); diff --git a/ui/lib/widgets/widgets.dart b/ui/lib/widgets/widgets.dart index 250adf5..978d123 100644 --- a/ui/lib/widgets/widgets.dart +++ b/ui/lib/widgets/widgets.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:ui/providers/APIs.dart'; +import 'package:ui/widgets/utils.dart'; class Commons { static InputDecoration requiredTextFieldStyle({ @@ -139,3 +140,93 @@ class _MySliderState extends State { return "$v MB"; } } + +class LoadingIconButton extends StatefulWidget { + LoadingIconButton({required this.onPressed, required this.icon, this.tooltip}); + final Future Function() onPressed; + final IconData icon; + final String? tooltip; + + @override + State createState() { + return _LoadingIconButtonState(); + } +} + +class _LoadingIconButtonState extends State { + bool loading = false; + + @override + Widget build(BuildContext context) { + return IconButton( + tooltip: widget.tooltip, + onPressed: loading + ? null + : () async { + setState(() => loading = true); + try { + await widget.onPressed(); + } catch (e) { + showSnakeBar("操作失败:$e"); + } finally { + setState(() => loading = false); + } + }, + icon: loading + ? Container( + width: 24, + height: 24, + padding: const EdgeInsets.all(2.0), + child: const CircularProgressIndicator( + color: Colors.grey, + strokeWidth: 3, + ), + ) + : Icon(widget.icon)); + } +} + +class LoadingTextButton extends StatefulWidget { + LoadingTextButton({required this.onPressed, required this.label}); + final Future Function() onPressed; + final Widget label; + + @override + State createState() { + return _LoadingTextButtonState(); + } +} + +class _LoadingTextButtonState extends State { + bool loading = false; + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: loading + ? null + : () async { + setState(() => loading = true); + try { + await widget.onPressed(); + } catch (e) { + showSnakeBar("操作失败:$e"); + } finally { + setState(() => loading = false); + } + }, + icon: loading + ? Container( + width: 24, + height: 24, + padding: const EdgeInsets.all(2.0), + child: const CircularProgressIndicator( + color: Colors.grey, + strokeWidth: 3, + ), + ) + : Text(""), + label: widget.label, + ); + } +}