mirror of
https://github.com/simon-ding/polaris.git
synced 2026-04-22 11:47:30 +08:00
feat: complete size limiter feature
This commit is contained in:
@@ -58,6 +58,8 @@ class APIs {
|
|||||||
static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv";
|
static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv";
|
||||||
static final movieParseUrl = "$_baseUrl/api/v1/setting/parse/movie";
|
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 tmdbApiKey = "tmdb_api_key";
|
||||||
static const downloadDirKey = "download_dir";
|
static const downloadDirKey = "download_dir";
|
||||||
|
|
||||||
@@ -131,7 +133,7 @@ class APIs {
|
|||||||
if (sp.code != 0) {
|
if (sp.code != 0) {
|
||||||
throw sp.message;
|
throw sp.message;
|
||||||
}
|
}
|
||||||
return sp.data==null? []:sp.data as List<String>;
|
return sp.data == null ? [] : sp.data as List<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<String>> downloadAllMovies() async {
|
static Future<List<String>> downloadAllMovies() async {
|
||||||
@@ -142,7 +144,7 @@ class APIs {
|
|||||||
if (sp.code != 0) {
|
if (sp.code != 0) {
|
||||||
throw sp.message;
|
throw sp.message;
|
||||||
}
|
}
|
||||||
return sp.data==null? []:sp.data as List<String>;
|
return sp.data == null ? [] : sp.data as List<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String> parseTvName(String s) async {
|
static Future<String> parseTvName(String s) async {
|
||||||
|
|||||||
109
ui/lib/providers/size_limiter.dart
Normal file
109
ui/lib/providers/size_limiter.dart
Normal file
@@ -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, MediaSizeLimiter>(
|
||||||
|
MediaSizeLimiterData.new);
|
||||||
|
|
||||||
|
class MediaSizeLimiterData extends AutoDisposeAsyncNotifier<MediaSizeLimiter> {
|
||||||
|
@override
|
||||||
|
FutureOr<MediaSizeLimiter> 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<void> 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<String, dynamic> json) {
|
||||||
|
tvLimiter = json['tv_limiter'] != null
|
||||||
|
? SizeLimiter.fromJson(json['tv_limiter'])
|
||||||
|
: null;
|
||||||
|
movieLimiter = json['movie_limiter'] != null
|
||||||
|
? SizeLimiter.fromJson(json['movie_limiter'])
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
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<String, dynamic> 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<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
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<String, dynamic> json) {
|
||||||
|
maxSize = json['max_size'];
|
||||||
|
minSize = json['min_size'];
|
||||||
|
preferSize = json['prefer_size'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
data['max_size'] = maxSize;
|
||||||
|
data['min_size'] = minSize;
|
||||||
|
data['prefer_size'] = preferSize;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
import 'package:quiver/strings.dart';
|
import 'package:quiver/strings.dart';
|
||||||
import 'package:ui/providers/settings.dart';
|
import 'package:ui/providers/settings.dart';
|
||||||
|
import 'package:ui/providers/size_limiter.dart';
|
||||||
import 'package:ui/settings/dialog.dart';
|
import 'package:ui/settings/dialog.dart';
|
||||||
import 'package:ui/widgets/progress_indicator.dart';
|
import 'package:ui/widgets/progress_indicator.dart';
|
||||||
import 'package:ui/widgets/widgets.dart';
|
import 'package:ui/widgets/widgets.dart';
|
||||||
@@ -22,20 +23,28 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var downloadClients = ref.watch(dwonloadClientsProvider);
|
var downloadClients = ref.watch(dwonloadClientsProvider);
|
||||||
return downloadClients.when(
|
return Column(
|
||||||
data: (value) => Wrap(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: List.generate(value.length + 1, (i) {
|
children: [
|
||||||
if (i < value.length) {
|
downloadClients.when(
|
||||||
var client = value[i];
|
data: (value) => Wrap(
|
||||||
return SettingsCard(
|
children: List.generate(value.length + 1, (i) {
|
||||||
onTap: () => showDownloadClientDetails(client),
|
if (i < value.length) {
|
||||||
child: Text(client.name ?? ""));
|
var client = value[i];
|
||||||
}
|
return SettingsCard(
|
||||||
return SettingsCard(
|
onTap: () => showDownloadClientDetails(client),
|
||||||
onTap: () => showSelections(), child: const Icon(Icons.add));
|
child: Text(client.name ?? ""));
|
||||||
})),
|
}
|
||||||
error: (err, trace) => PoNetworkError(err: err),
|
return SettingsCard(
|
||||||
loading: () => const MyProgressIndicator());
|
onTap: () => showSelections(),
|
||||||
|
child: const Icon(Icons.add));
|
||||||
|
})),
|
||||||
|
error: (err, trace) => PoNetworkError(err: err),
|
||||||
|
loading: () => const MyProgressIndicator()),
|
||||||
|
Divider(),
|
||||||
|
getSizeLimiterWidget()
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showDownloadClientDetails(DownloadClient client) {
|
Future<void> showDownloadClientDetails(DownloadClient client) {
|
||||||
@@ -199,4 +208,160 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget getSizeLimiterWidget() {
|
||||||
|
var data = ref.watch(mediaSizeLimiterDataProvider);
|
||||||
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,6 +282,52 @@ class _LoadingTextButtonState extends State<LoadingTextButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LoadingElevatedButton extends StatefulWidget {
|
||||||
|
const LoadingElevatedButton(
|
||||||
|
{super.key, required this.onPressed, required this.label});
|
||||||
|
final Future<void> Function() onPressed;
|
||||||
|
final Widget label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _LoadingElevatedButtonState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoadingElevatedButtonState extends State<LoadingElevatedButton> {
|
||||||
|
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 {
|
class PoError extends StatelessWidget {
|
||||||
const PoError({super.key, required this.msg, required this.err});
|
const PoError({super.key, required this.msg, required this.err});
|
||||||
final String msg;
|
final String msg;
|
||||||
@@ -292,10 +338,16 @@ class PoError extends StatelessWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text("$msg ", style: TextStyle(color:Theme.of(context).colorScheme.error),),
|
Text(
|
||||||
|
"$msg ",
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "$err",
|
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 {
|
class PoNetworkError extends StatelessWidget {
|
||||||
const PoNetworkError({super.key, required this.err});
|
const PoNetworkError({super.key, required this.err});
|
||||||
final dynamic err;
|
final dynamic err;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PoError(msg: "网络错误,请检查网络链接", err: err);
|
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()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user