diff --git a/ui/lib/providers/APIs.dart b/ui/lib/providers/APIs.dart index 0ed646b4..b2191d6b 100644 --- a/ui/lib/providers/APIs.dart +++ b/ui/lib/providers/APIs.dart @@ -58,6 +58,8 @@ class APIs { static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv"; static final movieParseUrl = "$_baseUrl/api/v1/setting/parse/movie"; + static final mediaSizeLimiterUrl = "$_baseUrl/api/v1/setting/limiter"; + static const tmdbApiKey = "tmdb_api_key"; static const downloadDirKey = "download_dir"; @@ -131,7 +133,7 @@ class APIs { if (sp.code != 0) { throw sp.message; } - return sp.data==null? []:sp.data as List; + return sp.data == null ? [] : sp.data as List; } static Future> downloadAllMovies() async { @@ -142,7 +144,7 @@ class APIs { if (sp.code != 0) { throw sp.message; } - return sp.data==null? []:sp.data as List; + return sp.data == null ? [] : sp.data as List; } static Future parseTvName(String s) async { diff --git a/ui/lib/providers/size_limiter.dart b/ui/lib/providers/size_limiter.dart new file mode 100644 index 00000000..1d987a32 --- /dev/null +++ b/ui/lib/providers/size_limiter.dart @@ -0,0 +1,109 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ui/providers/APIs.dart'; +import 'package:ui/providers/server_response.dart'; + +var mediaSizeLimiterDataProvider = + AsyncNotifierProvider.autoDispose( + MediaSizeLimiterData.new); + +class MediaSizeLimiterData extends AutoDisposeAsyncNotifier { + @override + FutureOr build() async { + final dio = APIs.getDio(); + var resp = await dio.get(APIs.mediaSizeLimiterUrl); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + return MediaSizeLimiter.fromJson(sp.data); + } + + Future submit(MediaSizeLimiter limiter) async { + final dio = APIs.getDio(); + var resp = await dio.post(APIs.mediaSizeLimiterUrl, data: limiter.toJson()); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + ref.invalidateSelf(); + } +} + +class MediaSizeLimiter { + SizeLimiter? tvLimiter; + SizeLimiter? movieLimiter; + + MediaSizeLimiter({this.tvLimiter, this.movieLimiter}); + + MediaSizeLimiter.fromJson(Map json) { + tvLimiter = json['tv_limiter'] != null + ? SizeLimiter.fromJson(json['tv_limiter']) + : null; + movieLimiter = json['movie_limiter'] != null + ? SizeLimiter.fromJson(json['movie_limiter']) + : null; + } + + Map toJson() { + final Map data = {}; + if (tvLimiter != null) { + data['tv_limiter'] = tvLimiter!.toJson(); + } + if (movieLimiter != null) { + data['movie_limiter'] = movieLimiter!.toJson(); + } + return data; + } +} + +class SizeLimiter { + ResLimiter? p720p; + ResLimiter? p1080p; + ResLimiter? p2160p; + + SizeLimiter({this.p720p, this.p1080p, this.p2160p}); + + SizeLimiter.fromJson(Map json) { + p720p = json['720p'] != null ? ResLimiter.fromJson(json['720p']) : null; + p1080p = json['1080p'] != null ? ResLimiter.fromJson(json['1080p']) : null; + p2160p = json['2160p'] != null ? ResLimiter.fromJson(json['2160p']) : null; + } + + Map toJson() { + final Map data = {}; + if (p720p != null) { + data['720p'] = p720p!.toJson(); + } + if (p1080p != null) { + data['1080p'] = p1080p!.toJson(); + } + if (p2160p != null) { + data['2160p'] = p2160p!.toJson(); + } + return data; + } +} + +class ResLimiter { + int? maxSize; + int? minSize; + int? preferSize; + + ResLimiter({this.maxSize, this.minSize, this.preferSize}); + + ResLimiter.fromJson(Map json) { + maxSize = json['max_size']; + minSize = json['min_size']; + preferSize = json['prefer_size']; + } + + Map toJson() { + final Map data = {}; + data['max_size'] = maxSize; + data['min_size'] = minSize; + data['prefer_size'] = preferSize; + return data; + } +} diff --git a/ui/lib/settings/downloader.dart b/ui/lib/settings/downloader.dart index 40bd268d..98fe3ef7 100644 --- a/ui/lib/settings/downloader.dart +++ b/ui/lib/settings/downloader.dart @@ -4,6 +4,7 @@ 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/providers/size_limiter.dart'; import 'package:ui/settings/dialog.dart'; import 'package:ui/widgets/progress_indicator.dart'; import 'package:ui/widgets/widgets.dart'; @@ -22,20 +23,28 @@ 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: () => showSelections(), child: const Icon(Icons.add)); - })), - error: (err, trace) => PoNetworkError(err: err), - loading: () => const MyProgressIndicator()); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: () => showSelections(), + child: const Icon(Icons.add)); + })), + error: (err, trace) => PoNetworkError(err: err), + loading: () => const MyProgressIndicator()), + Divider(), + getSizeLimiterWidget() + ], + ); } Future showDownloadClientDetails(DownloadClient client) { @@ -199,4 +208,160 @@ class _DownloaderState extends ConsumerState { ); }); } + + Widget getSizeLimiterWidget() { + var data = ref.watch(mediaSizeLimiterDataProvider); + final _formKey = GlobalKey(); + + return Container( + padding: EdgeInsets.only(left: 20, right: 20, top: 20), + child: data.when( + data: (value) { + return FormBuilder( + key: _formKey, + initialValue: { + "tv_720p_min": toMbString(value.tvLimiter!.p720p!.minSize!), + "tv_720p_max": toMbString(value.tvLimiter!.p720p!.maxSize!), + "tv_1080p_min": toMbString(value.tvLimiter!.p1080p!.minSize!), + "tv_1080p_max": toMbString(value.tvLimiter!.p1080p!.maxSize!), + "tv_2160p_min": toMbString(value.tvLimiter!.p2160p!.minSize!), + "tv_2160p_max": toMbString(value.tvLimiter!.p2160p!.maxSize!), + "movie_720p_min": + toMbString(value.movieLimiter!.p720p!.minSize!), + "movie_720p_max": + toMbString(value.movieLimiter!.p720p!.maxSize!), + "movie_1080p_min": + toMbString(value.movieLimiter!.p1080p!.minSize!), + "movie_1080p_max": + toMbString(value.movieLimiter!.p1080p!.maxSize!), + "movie_2160p_min": + toMbString(value.movieLimiter!.p2160p!.minSize!), + "movie_2160p_max": + toMbString(value.movieLimiter!.p2160p!.maxSize!), + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "剧集大小限制", + style: TextStyle(fontSize: 18), + ), + Divider(), + minMaxRow(" 720p", "tv_720p_min", "tv_720p_max"), + minMaxRow("1080p", "tv_1080p_min", "tv_1080p_max"), + minMaxRow("2160p", "tv_2160p_min", "tv_2160p_max"), + Text( + "电影大小限制", + style: TextStyle(fontSize: 18), + ), + Divider(), + minMaxRow(" 720p", "movie_720p_min", "movie_720p_max"), + minMaxRow("1080p", "movie_1080p_min", "movie_1080p_max"), + minMaxRow("2160p", "movie_2160p_min", "movie_2160p_max"), + Center( + child: Padding( + padding: EdgeInsets.all(20), + child: LoadingElevatedButton( + onPressed: () async { + if (_formKey.currentState!.saveAndValidate()) { + var values = _formKey.currentState!.value; + + return ref + .read(mediaSizeLimiterDataProvider.notifier) + .submit(MediaSizeLimiter( + tvLimiter: SizeLimiter( + p720p: ResLimiter( + minSize: + toByteInt(values["tv_720p_min"]), + maxSize: + toByteInt(values["tv_720p_max"])), + p1080p: ResLimiter( + minSize: + toByteInt(values["tv_1080p_min"]), + maxSize: toByteInt( + values["tv_1080p_max"])), + p2160p: ResLimiter( + minSize: + toByteInt(values["tv_2160p_min"]), + maxSize: toByteInt( + values["tv_2160p_max"])), + ), + movieLimiter: SizeLimiter( + p720p: ResLimiter( + minSize: toByteInt( + values["movie_720p_min"]), + maxSize: toByteInt( + values["movie_720p_max"])), + p1080p: ResLimiter( + minSize: toByteInt( + values["movie_1080p_min"]), + maxSize: toByteInt( + values["movie_1080p_max"])), + p2160p: ResLimiter( + minSize: toByteInt( + values["movie_2160p_min"]), + maxSize: toByteInt( + values["movie_2160p_max"])), + ))); + } else { + throw "validation_error"; + } + }, + label: Text("保存"), + ), + ), + ) + ], + ), + ); + }, + error: (err, trace) => Container(), + loading: () => const MyProgressIndicator()), + ); + } + + Widget minMaxRow(String title, String nameMin, String nameMax) { + return Row( + children: [ + Flexible(flex: 2, child: Container()), + Flexible( + flex: 2, + child: Text( + title, + style: TextStyle(fontSize: 16), + )), + Flexible(flex: 1, child: Container()), + Flexible( + flex: 6, + child: FormBuilderTextField( + name: nameMin, + decoration: InputDecoration(suffixText: "MB", labelText: "最小"), + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required(), + FormBuilderValidators.numeric() + ]), + )), + Flexible(flex: 1, child: Text(" - ")), + Flexible( + flex: 6, + child: FormBuilderTextField( + name: nameMax, + decoration: InputDecoration(suffixText: "MB", labelText: "最大"), + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required(), + FormBuilderValidators.numeric() + ]), + )), + Flexible(flex: 2, child: Container()), + ], + ); + } +} + +String toMbString(int size) { + return (size / 1000 / 1000).toString(); +} + +int toByteInt(String s) { + return int.parse(s) * 1000 * 1000; } diff --git a/ui/lib/widgets/widgets.dart b/ui/lib/widgets/widgets.dart index 684e7ac4..6409c835 100644 --- a/ui/lib/widgets/widgets.dart +++ b/ui/lib/widgets/widgets.dart @@ -282,6 +282,52 @@ class _LoadingTextButtonState extends State { } } +class LoadingElevatedButton extends StatefulWidget { + const LoadingElevatedButton( + {super.key, required this.onPressed, required this.label}); + final Future Function() onPressed; + final Widget label; + + @override + State createState() { + return _LoadingElevatedButtonState(); + } +} + +class _LoadingElevatedButtonState extends State { + bool loading = false; + + @override + Widget build(BuildContext context) { + return ElevatedButton.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, + ); + } +} + class PoError extends StatelessWidget { const PoError({super.key, required this.msg, required this.err}); final String msg; @@ -292,10 +338,16 @@ class PoError extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("$msg ", style: TextStyle(color:Theme.of(context).colorScheme.error),), + Text( + "$msg ", + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), Tooltip( message: "$err", - child: Icon(Icons.info,color: Theme.of(context).colorScheme.error,), + child: Icon( + Icons.info, + color: Theme.of(context).colorScheme.error, + ), ) ], ); @@ -304,9 +356,30 @@ class PoError extends StatelessWidget { class PoNetworkError extends StatelessWidget { const PoNetworkError({super.key, required this.err}); -final dynamic err; + final dynamic err; @override Widget build(BuildContext context) { return PoError(msg: "网络错误,请检查网络链接", err: err); } } + +class PoProgressIndicator extends StatelessWidget { + const PoProgressIndicator({super.key, this.backgroundColor, this.value, this.icon}); + final double? value; + final Color? backgroundColor; + final IconData? icon; + + @override + Widget build(BuildContext context) { + return Stack( + alignment: AlignmentDirectional.center, + children: [ + CircularProgressIndicator( + backgroundColor: backgroundColor, + value: value, + ), + icon != null ? Opacity(opacity: 0.7, child: Icon(icon, color: Theme.of(context).colorScheme.primary,),):Container() + ], + ); + } +}