mirror of
https://github.com/simon-ding/polaris.git
synced 2026-06-09 11:39:46 +08:00
feat: edit media details
This commit is contained in:
@@ -7,6 +7,7 @@ import 'package:ui/providers/server_response.dart';
|
||||
class APIs {
|
||||
static final _baseUrl = baseUrl();
|
||||
static final searchUrl = "$_baseUrl/api/v1/media/search";
|
||||
static final editMediaUrl = "$_baseUrl/api/v1/media/edit";
|
||||
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";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/server_response.dart';
|
||||
@@ -34,7 +35,7 @@ class SeriesDetailData
|
||||
|
||||
Future<String> searchAndDownload(
|
||||
String seriesId, int seasonNum, int episodeNum) async {
|
||||
final dio = await APIs.getDio();
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.post(APIs.searchAndDownloadUrl, data: {
|
||||
"id": int.parse(seriesId),
|
||||
"season": seasonNum,
|
||||
@@ -61,6 +62,22 @@ class SeriesDetailData
|
||||
}
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
Future<void> edit(
|
||||
String resolution, String targetDir, RangeValues limiter) async {
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.post(APIs.editMediaUrl, data: {
|
||||
"id": int.parse(id!),
|
||||
"resolution": resolution,
|
||||
"target_dir": targetDir,
|
||||
"limiter": {"size_min": limiter.start.toInt(), "size_max": limiter.end.toInt()},
|
||||
});
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
class SeriesDetails {
|
||||
@@ -79,6 +96,7 @@ class SeriesDetails {
|
||||
Storage? storage;
|
||||
String? targetDir;
|
||||
bool? downloadHistoryEpisodes;
|
||||
Limiter? limiter;
|
||||
|
||||
SeriesDetails(
|
||||
{this.id,
|
||||
@@ -95,7 +113,8 @@ class SeriesDetails {
|
||||
this.mediaType,
|
||||
this.targetDir,
|
||||
this.storage,
|
||||
this.downloadHistoryEpisodes});
|
||||
this.downloadHistoryEpisodes,
|
||||
this.limiter});
|
||||
|
||||
SeriesDetails.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
@@ -118,6 +137,19 @@ class SeriesDetails {
|
||||
episodes!.add(Episodes.fromJson(v));
|
||||
});
|
||||
}
|
||||
if (json["limiter"] != null) {
|
||||
limiter = Limiter.fromJson(json["limiter"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Limiter {
|
||||
int sizeMax;
|
||||
int sizeMin;
|
||||
Limiter({required this.sizeMax, required this.sizeMin});
|
||||
|
||||
factory Limiter.fromJson(Map<String, dynamic> json) {
|
||||
return Limiter(sizeMax: json["size_max"], sizeMin: json["size_min"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,35 +125,7 @@ class _SubmitSearchResultState extends ConsumerState<SubmitSearchResult> {
|
||||
},
|
||||
),
|
||||
enabledSizedLimiter
|
||||
? FormBuilderRangeSlider(
|
||||
maxValueWidget: (max) =>
|
||||
Text("${sizeMax / 1000} GB"),
|
||||
minValueWidget: (min) => Text("0"),
|
||||
valueWidget: (value) {
|
||||
final sss = value.split(" ");
|
||||
return Text(
|
||||
"${readableSize(sss[0])} - ${readableSize(sss[2])}");
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
if (value.end > sizeMax * 0.9) {
|
||||
setState(
|
||||
() {
|
||||
sizeMax = sizeMax * 5;
|
||||
},
|
||||
);
|
||||
} else if (value.end < sizeMax * 0.2) {
|
||||
if (sizeMax > 5000) {
|
||||
setState(
|
||||
() {
|
||||
sizeMax = sizeMax / 5;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
name: "size_limiter",
|
||||
min: 0,
|
||||
max: sizeMax)
|
||||
? const MyRangeSlider(name: "size_limiter")
|
||||
: const SizedBox(),
|
||||
widget.item.mediaType == "tv"
|
||||
? SizedBox(
|
||||
@@ -193,7 +165,7 @@ class _SubmitSearchResultState extends ConsumerState<SubmitSearchResult> {
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
final values = _formKey.currentState!.value;
|
||||
var f = ref
|
||||
.read(searchPageDataProvider(widget.query ?? "").notifier)
|
||||
.read(searchPageDataProvider(widget.query).notifier)
|
||||
.submit2Watchlist(
|
||||
widget.item.id!,
|
||||
values["storage"],
|
||||
@@ -223,3 +195,4 @@ class _SubmitSearchResultState extends ConsumerState<SubmitSearchResult> {
|
||||
return "$v MB";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,53 +41,7 @@ class WelcomePage extends ConsumerWidget {
|
||||
]
|
||||
: List.generate(value.length, (i) {
|
||||
final item = value[i];
|
||||
return Card(
|
||||
//margin: const EdgeInsets.all(4),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
elevation: 5,
|
||||
child: InkWell(
|
||||
//splashColor: Colors.blue.withAlpha(30),
|
||||
onTap: () {
|
||||
if (uri == routeMoivie) {
|
||||
context.go(MovieDetailsPage.toRoute(item.id!));
|
||||
} else {
|
||||
context.go(TvDetailsPage.toRoute(item.id!));
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 140,
|
||||
height: 210,
|
||||
child: Ink.image(
|
||||
image: NetworkImage(
|
||||
"${APIs.imagesUrl}/${item.id}/poster.jpg",
|
||||
)),
|
||||
),
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: Column(
|
||||
children: [
|
||||
LinearProgressIndicator(
|
||||
value: 1,
|
||||
color: item.downloadedNum! >=
|
||||
item.monitoredNum!
|
||||
? Colors.green
|
||||
: Colors.blue,
|
||||
),
|
||||
Text(
|
||||
item.name!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 2.5),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
));
|
||||
return MediaCard(item: item);
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -95,3 +49,58 @@ class WelcomePage extends ConsumerWidget {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MediaCard extends StatelessWidget {
|
||||
final MediaDetail item;
|
||||
|
||||
const MediaCard({super.key, required this.item});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
//margin: const EdgeInsets.all(4),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
elevation: 10,
|
||||
child: InkWell(
|
||||
//splashColor: Colors.blue.withAlpha(30),
|
||||
onTap: () {
|
||||
if (item.mediaType == "movie") {
|
||||
context.go(MovieDetailsPage.toRoute(item.id!));
|
||||
} else {
|
||||
context.go(TvDetailsPage.toRoute(item.id!));
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 140,
|
||||
height: 210,
|
||||
child: Ink.image(
|
||||
image: NetworkImage(
|
||||
"${APIs.imagesUrl}/${item.id}/poster.jpg",
|
||||
)),
|
||||
),
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: Column(
|
||||
children: [
|
||||
LinearProgressIndicator(
|
||||
value: 1,
|
||||
color: item.downloadedNum! >= item.monitoredNum!
|
||||
? Colors.green
|
||||
: Colors.blue,
|
||||
),
|
||||
Text(
|
||||
item.name!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 2.5),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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:go_router/go_router.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/series_details.dart';
|
||||
@@ -68,10 +70,12 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
const SizedBox(
|
||||
width: 30,
|
||||
),
|
||||
Expanded(child: Text(
|
||||
"${widget.details.mediaType == "tv" ? widget.details.storage!.tvPath : widget.details.storage!.moviePath}"
|
||||
"${widget.details.targetDir}"),)
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
"${widget.details.mediaType == "tv" ? widget.details.storage!.tvPath : widget.details.storage!.moviePath}"
|
||||
"${widget.details.targetDir}"),
|
||||
)
|
||||
],
|
||||
),
|
||||
const Divider(thickness: 1, height: 1),
|
||||
Text(
|
||||
@@ -88,6 +92,7 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
)),
|
||||
Row(
|
||||
children: [
|
||||
editIcon(widget.details),
|
||||
deleteIcon(),
|
||||
],
|
||||
)
|
||||
@@ -120,4 +125,77 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
icon: const Icon(Icons.delete)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget editIcon(SeriesDetails details) {
|
||||
return IconButton(
|
||||
onPressed: () => showEditDialog(details), icon: const Icon(Icons.edit));
|
||||
}
|
||||
|
||||
showEditDialog(SeriesDetails details) {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text("编辑 ${details.name}"),
|
||||
content: SelectionArea(
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
height: MediaQuery.of(context).size.height * 0.3,
|
||||
child: SingleChildScrollView(
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
initialValue: {
|
||||
"resolution": details.resolution,
|
||||
"target_dir": details.targetDir,
|
||||
"limiter": details.limiter != null
|
||||
? RangeValues(details.limiter!.sizeMin.toDouble(),
|
||||
details.limiter!.sizeMax.toDouble())
|
||||
: const RangeValues(0, 0)
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderDropdown(
|
||||
name: "resolution",
|
||||
decoration: const InputDecoration(labelText: "清晰度"),
|
||||
items: const [
|
||||
DropdownMenuItem(value: "720p", child: Text("720p")),
|
||||
DropdownMenuItem(value: "1080p", child: Text("1080p")),
|
||||
DropdownMenuItem(value: "2160p", child: Text("2160p")),
|
||||
],
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: "target_dir",
|
||||
validator: FormBuilderValidators.required(),
|
||||
),
|
||||
const MyRangeSlider(name: "limiter"),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("取消")),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
final values = _formKey.currentState!.value;
|
||||
var f = 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("确认"))
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
|
||||
class Commons {
|
||||
@@ -86,3 +87,54 @@ showLoadingWithFuture(Future f) {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class MyRangeSlider extends StatefulWidget {
|
||||
final String name;
|
||||
const MyRangeSlider({super.key, required this.name});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _MySliderState();
|
||||
}
|
||||
}
|
||||
|
||||
class _MySliderState extends State<MyRangeSlider> {
|
||||
double sizeMax = 5000;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderRangeSlider(
|
||||
maxValueWidget: (max) => Text("${sizeMax / 1000} GB"),
|
||||
minValueWidget: (min) => Text("0"),
|
||||
valueWidget: (value) {
|
||||
final sss = value.split(" ");
|
||||
return Text("${readableSize(sss[0])} - ${readableSize(sss[2])}");
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
if (value.end > sizeMax * 0.9) {
|
||||
setState(
|
||||
() {
|
||||
sizeMax = sizeMax * 5;
|
||||
},
|
||||
);
|
||||
} else if (value.end < sizeMax * 0.2) {
|
||||
if (sizeMax > 5000) {
|
||||
setState(
|
||||
() {
|
||||
sizeMax = sizeMax / 5;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
name: widget.name,
|
||||
min: 0,
|
||||
max: sizeMax);
|
||||
}
|
||||
|
||||
String readableSize(String v) {
|
||||
if (v.endsWith("K")) {
|
||||
return v.replaceAll("K", " GB");
|
||||
}
|
||||
return "$v MB";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user