feat: add refresh button & parse dialog

This commit is contained in:
Simon Ding
2024-11-01 21:53:38 +08:00
parent 2da02fa706
commit e67413cec2
8 changed files with 291 additions and 59 deletions

View File

@@ -175,6 +175,10 @@ func (s *Server) DownloadAll(c *gin.Context) (interface{}, error) {
if err != nil {
return nil, errors.Wrap(err, "convert")
}
return s.downloadAllEpisodes(id)
}
func (s *Server) downloadAllEpisodes(id int) (interface{}, error) {
m, err := s.db.GetMedia(id)
if err != nil {
return nil, errors.Wrap(err, "get media")
@@ -186,3 +190,27 @@ func (s *Server) DownloadAll(c *gin.Context) (interface{}, error) {
return []string{name}, err
}
func (s *Server) DownloadAllTv(c *gin.Context) (interface{}, error) {
tvs := s.db.GetMediaWatchlist(media.MediaTypeTv)
var allNames []string
for _, tv := range tvs {
names, err := s.downloadAllEpisodes(tv.ID)
if err == nil {
allNames = append(allNames, names.([]string)...)
}
}
return allNames, nil
}
func (s *Server) DownloadAllMovies(c *gin.Context) (interface{}, error) {
movies := s.db.GetMediaWatchlist(media.MediaTypeMovie)
var allNames []string
for _, mv := range movies {
names, err := s.downloadAllEpisodes(mv.ID)
if err == nil {
allNames = append(allNames, names.([]string)...)
}
}
return allNames, nil
}

View File

@@ -96,6 +96,8 @@ func (s *Server) Serve() error {
tv.GET("/suggest/tv/:tmdb_id", HttpHandler(s.SuggestedSeriesFolderName))
tv.GET("/suggest/movie/:tmdb_id", HttpHandler(s.SuggestedMovieFolderName))
tv.GET("/downloadall/:id", HttpHandler(s.DownloadAll))
tv.GET("/download/tv", HttpHandler(s.DownloadAllTv))
tv.GET("/download/movie", HttpHandler(s.DownloadAllMovies))
}
indexer := api.Group("/indexer")
{

View File

@@ -105,6 +105,7 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
return nil, nil
}
func (s *Server) GetSetting(c *gin.Context) (interface{}, error) {
tmdb := s.db.GetSetting(db.SettingTmdbApiKey)
downloadDir := s.db.GetSetting(db.SettingDownloadDir)

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -8,9 +10,12 @@ class APIs {
static final _baseUrl = baseUrl();
static final searchUrl = "$_baseUrl/api/v1/media/search";
static final editMediaUrl = "$_baseUrl/api/v1/media/edit";
static final downloadAllUrl = "$_baseUrl/api/v1/media/downloadall/";
static final downloadAllEpisodesUrl = "$_baseUrl/api/v1/media/downloadall/";
static final downloadAllTvUrl = "$_baseUrl/api/v1/media/download/tv";
static final downloadAllMovieUrl = "$_baseUrl/api/v1/media/download/movie";
static final settingsUrl = "$_baseUrl/api/v1/setting/do";
static final settingsGeneralUrl = "$_baseUrl/api/v1/setting/general";
//static final singleSettingUrl = "$_baseUrl/api/v1/setting/";
static final watchlistTvUrl = "$_baseUrl/api/v1/media/tv/watchlist";
static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist";
static final availableTorrentsUrl = "$_baseUrl/api/v1/media/torrents/";
@@ -50,6 +55,9 @@ class APIs {
static final cronJobUrl = "$_baseUrl/api/v1/setting/cron/trigger";
static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv";
static final movieParseUrl = "$_baseUrl/api/v1/setting/parse/movie";
static const tmdbApiKey = "tmdb_api_key";
static const downloadDirKey = "download_dir";
@@ -114,4 +122,50 @@ class APIs {
throw sp.message;
}
}
static Future<List<String>> downloadAllTv() async {
var resp = await getDio().get(APIs.downloadAllTvUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
return sp.data as List<String>;
}
static Future<List<String>> downloadAllMovies() async {
var resp = await getDio().get(APIs.downloadAllMovieUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
return sp.data as List<String>;
}
static Future<String> parseTvName(String s) async {
var resp = await getDio().post(APIs.tvParseUrl, data: {"s": s});
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
JsonEncoder encoder = new JsonEncoder.withIndent(' ');
return encoder.convert(sp.data);
}
static Future<String> parseMovieName(String s) async {
var resp = await getDio().post(APIs.movieParseUrl, data: {"s": s});
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
JsonEncoder encoder = new JsonEncoder.withIndent(' ');
return encoder.convert(sp.data);
}
}

View File

@@ -84,7 +84,7 @@ class SeriesDetailData
Future<dynamic> downloadall() async {
final dio = APIs.getDio();
var resp = await dio.get(APIs.downloadAllUrl + id!);
var resp = await dio.get(APIs.downloadAllEpisodesUrl + id!);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;

View File

@@ -32,7 +32,7 @@ var prowlarrSettingDataProvider =
class EditSettingData extends AutoDisposeAsyncNotifier<GeneralSetting> {
@override
FutureOr<GeneralSetting> build() async {
final dio = await APIs.getDio();
final dio = APIs.getDio();
var resp = await dio.get(APIs.settingsGeneralUrl);
var rrr = ServerResponse.fromJson(resp.data);

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:ui/movie_watchlist.dart';
@@ -6,6 +7,8 @@ import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/welcome_data.dart';
import 'package:ui/tv_details.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/widgets/widgets.dart';
class WelcomePage extends ConsumerStatefulWidget {
const WelcomePage({super.key});
@@ -20,7 +23,7 @@ class WelcomePage extends ConsumerStatefulWidget {
class WelcomePageState extends ConsumerState<WelcomePage> {
//WelcomePageState({super.key});
final _formKey = GlobalKey<FormBuilderState>();
bool onlyShowUnfinished = false;
@override
@@ -50,66 +53,94 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
_ => const MyProgressIndicator(),
};
}(),
Row(
getMoreButtonAndActions(uri)
],
);
}
Widget getMoreButtonAndActions(String uri) {
return Row(
children: [
Expanded(child: Container()),
Column(
children: [
Expanded(child: Container()),
Column(
children: [
Expanded(child: Container()),
Padding(
padding: EdgeInsets.all(20),
child: MenuAnchor(
style: MenuStyle(
//minimumSize: WidgetStatePropertyAll(Size(400, 300)),
backgroundColor: WidgetStatePropertyAll(Theme.of(context)
.colorScheme
.inversePrimary
.withOpacity(0.9)),
),
menuChildren: [
MenuItemButton(
onPressed: null,
child: CheckboxListTile(
value: onlyShowUnfinished,
onChanged: (b) {
setState(() {
onlyShowUnfinished = b!;
});
},
title: const Text(
"未完成",
style: TextStyle(fontSize: 16),
softWrap: false,
),
controlAffinity: ListTileControlAffinity.leading,
),
),
],
builder: (context, controller, child) {
return Opacity(
opacity: 0.7,
child: FloatingActionButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Icon(Icons.more_horiz),
));
},
),
Padding(
padding: EdgeInsets.all(20),
child: MenuAnchor(
style: MenuStyle(
alignment: Alignment.topLeft,
backgroundColor: WidgetStatePropertyAll(Theme.of(context)
.colorScheme
.inversePrimary
.withOpacity(0.7)),
),
],
)
menuChildren: [parseName(), onlyUnfinished(), refreshAll(uri)],
builder: (context, controller, child) {
return Opacity(
opacity: 0.7,
child: FloatingActionButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Icon(Icons.more_horiz),
));
},
),
),
],
),
)
],
);
}
Widget onlyUnfinished() {
return CheckboxListTile(
value: onlyShowUnfinished,
onChanged: (b) {
setState(() {
onlyShowUnfinished = b!;
});
},
title: const Text(
"未完成",
style: TextStyle(fontSize: 16),
softWrap: false,
),
controlAffinity: ListTileControlAffinity.leading,
);
}
Widget refreshAll(String uri) {
return LoadingListTile(
icon: Icons.refresh,
text: "全部更新",
onPressed: () async {
if (uri == WelcomePage.routeMoivie) {
await APIs.downloadAllMovies().then((v) {
showSnakeBar("开始下载电影:$v");
});
} else {
await APIs.downloadAllTv().then((v) {
showSnakeBar("开始下载剧集:$v");
});
}
},
);
}
Widget parseName() {
return ListTile(
leading: Icon(Icons.calculate),
title: Text("测试解析"),
onTap: () => _showNameParsingDialog(),
);
}
bool isSmallScreen(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return screenWidth < 600;
@@ -135,6 +166,71 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
return MediaCard(item: item);
});
}
Future<void> _showNameParsingDialog() async {
final resultController = TextEditingController();
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('测试名称解析'),
content: SizedBox(
width: 500,
height: 400,
child: FormBuilder(
key: _formKey,
initialValue: {"name": "", "type": "tv"},
child: Column(
children: [
FormBuilderTextField(
name: "name",
decoration: InputDecoration(labelText: "要解析的名字"),
),
FormBuilderDropdown(
name: "type",
items: [
DropdownMenuItem(
value: "tv",
child: const Text("电视剧"),
),
DropdownMenuItem(value: "movie", child: const Text("电影"))
],
),
Center(
child: Padding(
padding: EdgeInsets.all(10),
child: LoadingTextButton(
onPressed: () async {
if (_formKey.currentState!.saveAndValidate()) {
final values = _formKey.currentState!.value;
//print(values);
if (values["type"] == "tv") {
var s = await APIs.parseTvName(values["name"]);
resultController.text = s;
} else {
var s =
await APIs.parseMovieName(values["name"]);
resultController.text = s;
}
}
return;
},
label: Text("解析")),
),
),
TextField(
maxLines: 8,
controller: resultController,
)
],
),
),
),
);
},
);
}
}
class MediaCard extends StatelessWidget {

View File

@@ -141,8 +141,58 @@ class _MySliderState extends State<MyRangeSlider> {
}
}
class LoadingListTile extends StatefulWidget {
const LoadingListTile(
{super.key,
required this.onPressed,
required this.icon,
required this.text});
final Future<void> Function() onPressed;
final IconData icon;
final String text;
@override
State<StatefulWidget> createState() {
return LoadingListTileState();
}
}
class LoadingListTileState extends State<LoadingListTile> {
bool loading = false;
@override
Widget build(BuildContext context) {
return ListTile(
onTap: loading
? null
: () async {
setState(() => loading = true);
try {
await widget.onPressed();
} catch (e) {
showSnakeBar("操作失败:$e");
} finally {
setState(() => loading = false);
}
},
title: Text(widget.text),
leading: loading
? Container(
width: 24,
height: 24,
padding: const EdgeInsets.all(2.0),
child: const CircularProgressIndicator(
color: Colors.grey,
strokeWidth: 3,
),
)
: Icon(widget.icon));
}
}
class LoadingIconButton extends StatefulWidget {
const LoadingIconButton({super.key, required this.onPressed, required this.icon, this.tooltip});
const LoadingIconButton(
{super.key, required this.onPressed, required this.icon, this.tooltip});
final Future<void> Function() onPressed;
final IconData icon;
final String? tooltip;
@@ -159,7 +209,7 @@ class _LoadingIconButtonState extends State<LoadingIconButton> {
@override
Widget build(BuildContext context) {
return IconButton(
tooltip: widget.tooltip,
tooltip: widget.tooltip,
onPressed: loading
? null
: () async {
@@ -187,7 +237,8 @@ class _LoadingIconButtonState extends State<LoadingIconButton> {
}
class LoadingTextButton extends StatefulWidget {
const LoadingTextButton({super.key, required this.onPressed, required this.label});
const LoadingTextButton(
{super.key, required this.onPressed, required this.label});
final Future<void> Function() onPressed;
final Widget label;