From faa603d5df833f3b0e43378e0509ed7a8f44a3a7 Mon Sep 17 00:00:00 2001 From: Simon Ding Date: Wed, 31 Jul 2024 15:26:34 +0800 Subject: [PATCH] code refactor & improve search page --- db/const.go | 12 -- db/db.go | 1 + ent/media/media.go | 4 +- ent/migrate/schema.go | 2 +- ent/schema/media.go | 2 +- server/server.go | 1 - server/watchlist.go | 9 +- ui/lib/main.dart | 2 +- ui/lib/providers/series_details.dart | 3 + ui/lib/search.dart | 227 ++++++++++++++------------- ui/lib/widgets/detail_card.dart | 38 +++-- 11 files changed, 153 insertions(+), 148 deletions(-) diff --git a/db/const.go b/db/const.go index deb14f1..b675fd0 100644 --- a/db/const.go +++ b/db/const.go @@ -33,16 +33,4 @@ const ( type ResolutionType string -const ( - Any ResolutionType = "any" - R720p ResolutionType = "720p" - R1080p ResolutionType = "1080p" - R4k ResolutionType = "4k" -) - -func (r ResolutionType) String() string { - return string(r) -} - - const JwtSerectKey = "jwt_secrect_key" \ No newline at end of file diff --git a/db/db.go b/db/db.go index 8557dd9..34c6321 100644 --- a/db/db.go +++ b/db/db.go @@ -146,6 +146,7 @@ func (c *Client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, er SetAirDate(m.AirDate). SetResolution(m.Resolution). SetTargetDir(m.TargetDir). + SetDownloadHistoryEpisodes(m.DownloadHistoryEpisodes). AddEpisodeIDs(episodes...). Save(context.TODO()) return r, err diff --git a/ent/media/media.go b/ent/media/media.go index 200a86a..209b45f 100644 --- a/ent/media/media.go +++ b/ent/media/media.go @@ -124,7 +124,7 @@ const DefaultResolution = Resolution1080p const ( Resolution720p Resolution = "720p" Resolution1080p Resolution = "1080p" - Resolution4k Resolution = "4k" + Resolution2160p Resolution = "2160p" ) func (r Resolution) String() string { @@ -134,7 +134,7 @@ func (r Resolution) String() string { // ResolutionValidator is a validator for the "resolution" field enum values. It is called by the builders before save. func ResolutionValidator(r Resolution) error { switch r { - case Resolution720p, Resolution1080p, Resolution4k: + case Resolution720p, Resolution1080p, Resolution2160p: return nil default: return fmt.Errorf("media: invalid enum value for resolution field: %q", r) diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index 7c01335..bc97693 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -100,7 +100,7 @@ var ( {Name: "overview", Type: field.TypeString}, {Name: "created_at", Type: field.TypeTime}, {Name: "air_date", Type: field.TypeString, Default: ""}, - {Name: "resolution", Type: field.TypeEnum, Enums: []string{"720p", "1080p", "4k"}, Default: "1080p"}, + {Name: "resolution", Type: field.TypeEnum, Enums: []string{"720p", "1080p", "2160p"}, Default: "1080p"}, {Name: "storage_id", Type: field.TypeInt, Nullable: true}, {Name: "target_dir", Type: field.TypeString, Nullable: true}, {Name: "download_history_episodes", Type: field.TypeBool, Nullable: true, Default: false}, diff --git a/ent/schema/media.go b/ent/schema/media.go index 962a336..de3bc24 100644 --- a/ent/schema/media.go +++ b/ent/schema/media.go @@ -25,7 +25,7 @@ func (Media) Fields() []ent.Field { field.String("overview"), field.Time("created_at").Default(time.Now()), field.String("air_date").Default(""), - field.Enum("resolution").Values("720p", "1080p", "4k").Default("1080p"), + field.Enum("resolution").Values("720p", "1080p", "2160p").Default("1080p"), field.Int("storage_id").Optional(), field.String("target_dir").Optional(), field.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"), diff --git a/server/server.go b/server/server.go index 672b712..64d1349 100644 --- a/server/server.go +++ b/server/server.go @@ -91,7 +91,6 @@ func (s *Server) Serve() error { tv.GET("/movie/watchlist", HttpHandler(s.GetMovieWatchlist)) tv.GET("/record/:id", HttpHandler(s.GetMediaDetails)) tv.DELETE("/record/:id", HttpHandler(s.DeleteFromWatchlist)) - tv.GET("/resolutions", HttpHandler(s.GetAvailableResolutions)) tv.GET("/suggest/tv/:tmdb_id", HttpHandler(s.SuggestedSeriesFolderName)) tv.GET("/suggest/movie/:tmdb_id", HttpHandler(s.SuggestedMovieFolderName)) } diff --git a/server/watchlist.go b/server/watchlist.go index 344edea..5afcfae 100644 --- a/server/watchlist.go +++ b/server/watchlist.go @@ -69,6 +69,7 @@ func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) { if err := c.ShouldBindJSON(&in); err != nil { return nil, errors.Wrap(err, "bind query") } + log.Debugf("add tv watchlist input %+v", in) if in.Folder == "" { return nil, errors.New("folder should be provided") } @@ -320,14 +321,6 @@ func (s *Server) GetMediaDetails(c *gin.Context) (interface{}, error) { return MediaDetails{MediaDetails: detail, Storage: &st.Storage}, nil } -func (s *Server) GetAvailableResolutions(c *gin.Context) (interface{}, error) { - return []db.ResolutionType{ - db.R720p, - db.R1080p, - db.R4k, - }, nil -} - func (s *Server) DeleteFromWatchlist(c *gin.Context) (interface{}, error) { ids := c.Param("id") id, err := strconv.Atoi(ids) diff --git a/ui/lib/main.dart b/ui/lib/main.dart index 9cc3b7a..7629eaa 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -121,7 +121,7 @@ class _MyAppState extends ConsumerState { return ProviderScope( child: MaterialApp.router( - title: 'Polaris 影视追踪', + title: 'Polaris 影视追踪下载', theme: ThemeData( fontFamily: "NotoSansSC", colorScheme: ColorScheme.fromSeed( diff --git a/ui/lib/providers/series_details.dart b/ui/lib/providers/series_details.dart index 68014fb..3d9b8b7 100644 --- a/ui/lib/providers/series_details.dart +++ b/ui/lib/providers/series_details.dart @@ -64,6 +64,7 @@ class SeriesDetails { String? airDate; String? mediaType; Storage? storage; + String? targetDir; SeriesDetails( {this.id, @@ -78,6 +79,7 @@ class SeriesDetails { this.airDate, this.episodes, this.mediaType, + this.targetDir, this.storage}); SeriesDetails.fromJson(Map json) { @@ -93,6 +95,7 @@ class SeriesDetails { airDate = json["air_date"]; mediaType = json["media_type"]; storage = Storage.fromJson(json["storage"]); + targetDir = json["target_dir"]; if (json['episodes'] != null) { episodes = []; json['episodes'].forEach((v) { diff --git a/ui/lib/search.dart b/ui/lib/search.dart index ce1ace8..d5b9718 100644 --- a/ui/lib/search.dart +++ b/ui/lib/search.dart @@ -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/providers/APIs.dart'; @@ -148,116 +149,122 @@ class _SearchPageState extends ConsumerState { } Future _showSubmitDialog(BuildContext context, SearchResult item) { + final _formKey = GlobalKey(); + return showDialog( context: context, builder: (BuildContext context) { return Consumer( builder: (context, ref, _) { - String resSelected = "1080p"; int storageSelected = 0; var storage = ref.watch(storageSettingProvider); var name = ref.watch(suggestNameDataProvider( (id: item.id!, mediaType: item.mediaType!))); - bool downloadHistoryEpisodes = false; var pathController = TextEditingController(); return AlertDialog( - title: Text('添加剧集: ${item.name}'), + title: Text('添加: ${item.name}'), content: SizedBox( width: 500, height: 200, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DropdownMenu( - width: 200, - label: const Text("清晰度"), - initialSelection: resSelected, - dropdownMenuEntries: const [ - DropdownMenuEntry(value: "720p", label: "720p"), - DropdownMenuEntry(value: "1080p", label: "1080p"), - DropdownMenuEntry(value: "4k", label: "4k"), - ], - onSelected: (value) { - setState(() { - resSelected = value!; - }); - }, - ), - storage.when( - data: (v) { - return StatefulBuilder( - builder: (context, setState) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DropdownMenu( - width: 200, - label: const Text("存储位置"), - initialSelection: storageSelected, - dropdownMenuEntries: v - .map((s) => DropdownMenuEntry( - label: s.name!, value: s.id)) - .toList(), - onSelected: (value) { - setState(() { - storageSelected = value!; - }); - }, - ), - name.when( - data: (s) { - return storageSelected == 0 - ? const Text("") - : () { - final storage = v - .where((e) => - e.id == storageSelected) - .first; - final path = item.mediaType == - "tv" - ? storage.settings!["tv_path"] - : storage - .settings!["movie_path"]; - - pathController.text = s; - return SizedBox( - //width: 300, - child: TextField( - controller: pathController, - decoration: InputDecoration( - labelText: "存储路径", - prefix: Text(path)), - ), - ); - }(); - }, - error: (error, stackTrace) => - Text("$error"), - loading: () => const MyProgressIndicator( - size: 20, + child: FormBuilder( + key: _formKey, + initialValue: const { + "resolution": "1080p", + "storage": null, + "folder": "", + "history_episodes": false, + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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")), + ], + ), + storage.when( + data: (v) { + return StatefulBuilder( + builder: (context, setState) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FormBuilderDropdown( + onChanged: (v) { + setState( + () { + storageSelected = v!; + }, + ); + }, + name: "storage", + decoration: const InputDecoration( + labelText: "存储位置"), + items: v + .map((s) => DropdownMenuItem( + value: s.id, + child: Text(s.name!))) + .toList(), ), - ), - item.mediaType == "tv" - ? SizedBox( - width: 250, - child: CheckboxListTile( + name.when( + data: (s) { + return storageSelected == 0 + ? const Text("") + : () { + final storage = v + .where((e) => + e.id == storageSelected) + .first; + final path = + item.mediaType == "tv" + ? storage.tvPath + : storage.moviePath; + + pathController.text = s; + return SizedBox( + //width: 300, + child: FormBuilderTextField( + name: "folder", + controller: pathController, + decoration: InputDecoration( + labelText: "存储路径", + prefix: Text( + path ?? "unknown")), + ), + ); + }(); + }, + error: (error, stackTrace) => + Text("$error"), + loading: () => const MyProgressIndicator( + size: 20, + ), + ), + item.mediaType == "tv" + ? SizedBox( + width: 250, + child: FormBuilderCheckbox( + name: "history_episodes", title: const Text("是否下载往期剧集"), - value: downloadHistoryEpisodes, - onChanged: (v) { - setState(() { - downloadHistoryEpisodes = v!; - }); - }), - ) - : const SizedBox(), - ], - ); - }); - }, - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()), - ], + ), + ) + : const SizedBox(), + ], + ); + }); + }, + error: (err, trace) => Text("$err"), + loading: () => const MyProgressIndicator()), + ], + ), ), ), actions: [ @@ -276,21 +283,25 @@ class _SearchPageState extends ConsumerState { ), child: const Text('确定'), onPressed: () async { - var f = ref - .read(searchPageDataProvider(widget.query ?? "") - .notifier) - .submit2Watchlist( - item.id!, - storageSelected, - resSelected, - item.mediaType!, - pathController.text, - downloadHistoryEpisodes) - .then((v) { - Navigator.of(context).pop(); - showSnakeBar("添加成功:${item.name}"); - }); - showLoadingWithFuture(f); + if (_formKey.currentState!.saveAndValidate()) { + final values = _formKey.currentState!.value; + //print(values); + var f = ref + .read(searchPageDataProvider(widget.query ?? "") + .notifier) + .submit2Watchlist( + item.id!, + values["storage"], + values["resolution"], + item.mediaType!, + values["folder"], + values["history_episodes"] ?? false) + .then((v) { + Navigator.of(context).pop(); + showSnakeBar("添加成功:${item.name}"); + }); + showLoadingWithFuture(f); + } }, ), ], diff --git a/ui/lib/widgets/detail_card.dart b/ui/lib/widgets/detail_card.dart index b5ed352..c794e46 100644 --- a/ui/lib/widgets/detail_card.dart +++ b/ui/lib/widgets/detail_card.dart @@ -25,7 +25,8 @@ class _DetailCardState extends ConsumerState { margin: const EdgeInsets.all(4), clipBehavior: Clip.hardEdge, child: Container( - constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height*0.4), + constraints: + BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.4), decoration: BoxDecoration( image: DecorationImage( fit: BoxFit.cover, @@ -54,7 +55,6 @@ class _DetailCardState extends ConsumerState { Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text(""), Row( @@ -64,7 +64,13 @@ class _DetailCardState extends ConsumerState { width: 30, ), Text( - "${widget.details.storage!.name} (${widget.details.storage!.implementation})") + "${widget.details.storage!.name} (${widget.details.storage!.implementation})"), + const SizedBox( + width: 30, + ), + Text( + "${widget.details.mediaType == "tv" ? widget.details.storage!.tvPath : widget.details.storage!.moviePath}" + "${widget.details.targetDir}") ], ), const Divider(thickness: 1, height: 1), @@ -96,16 +102,20 @@ class _DetailCardState extends ConsumerState { } Widget deleteIcon() { - return IconButton( - onPressed: () { - var f = ref - .read(mediaDetailsProvider(widget.details.id.toString()).notifier) - .delete() - .then((v) => context.go(widget.details.mediaType == "tv" - ? WelcomePage.routeTv - : WelcomePage.routeMoivie)); - showLoadingWithFuture(f); - }, - icon: const Icon(Icons.delete)); + return Tooltip( + message: widget.details.mediaType == "tv" ? "删除剧集" : "删除电影", + child: IconButton( + onPressed: () { + var f = ref + .read( + mediaDetailsProvider(widget.details.id.toString()).notifier) + .delete() + .then((v) => context.go(widget.details.mediaType == "tv" + ? WelcomePage.routeTv + : WelcomePage.routeMoivie)); + showLoadingWithFuture(f); + }, + icon: const Icon(Icons.delete)), + ); } }