mirror of
https://github.com/simon-ding/polaris.git
synced 2026-06-09 03:27:39 +08:00
code refactor & improve search page
This commit is contained in:
12
db/const.go
12
db/const.go
@@ -33,16 +33,4 @@ const (
|
|||||||
|
|
||||||
type ResolutionType string
|
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"
|
const JwtSerectKey = "jwt_secrect_key"
|
||||||
1
db/db.go
1
db/db.go
@@ -146,6 +146,7 @@ func (c *Client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, er
|
|||||||
SetAirDate(m.AirDate).
|
SetAirDate(m.AirDate).
|
||||||
SetResolution(m.Resolution).
|
SetResolution(m.Resolution).
|
||||||
SetTargetDir(m.TargetDir).
|
SetTargetDir(m.TargetDir).
|
||||||
|
SetDownloadHistoryEpisodes(m.DownloadHistoryEpisodes).
|
||||||
AddEpisodeIDs(episodes...).
|
AddEpisodeIDs(episodes...).
|
||||||
Save(context.TODO())
|
Save(context.TODO())
|
||||||
return r, err
|
return r, err
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const DefaultResolution = Resolution1080p
|
|||||||
const (
|
const (
|
||||||
Resolution720p Resolution = "720p"
|
Resolution720p Resolution = "720p"
|
||||||
Resolution1080p Resolution = "1080p"
|
Resolution1080p Resolution = "1080p"
|
||||||
Resolution4k Resolution = "4k"
|
Resolution2160p Resolution = "2160p"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r Resolution) String() string {
|
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.
|
// ResolutionValidator is a validator for the "resolution" field enum values. It is called by the builders before save.
|
||||||
func ResolutionValidator(r Resolution) error {
|
func ResolutionValidator(r Resolution) error {
|
||||||
switch r {
|
switch r {
|
||||||
case Resolution720p, Resolution1080p, Resolution4k:
|
case Resolution720p, Resolution1080p, Resolution2160p:
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("media: invalid enum value for resolution field: %q", r)
|
return fmt.Errorf("media: invalid enum value for resolution field: %q", r)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ var (
|
|||||||
{Name: "overview", Type: field.TypeString},
|
{Name: "overview", Type: field.TypeString},
|
||||||
{Name: "created_at", Type: field.TypeTime},
|
{Name: "created_at", Type: field.TypeTime},
|
||||||
{Name: "air_date", Type: field.TypeString, Default: ""},
|
{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: "storage_id", Type: field.TypeInt, Nullable: true},
|
||||||
{Name: "target_dir", Type: field.TypeString, Nullable: true},
|
{Name: "target_dir", Type: field.TypeString, Nullable: true},
|
||||||
{Name: "download_history_episodes", Type: field.TypeBool, Nullable: true, Default: false},
|
{Name: "download_history_episodes", Type: field.TypeBool, Nullable: true, Default: false},
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (Media) Fields() []ent.Field {
|
|||||||
field.String("overview"),
|
field.String("overview"),
|
||||||
field.Time("created_at").Default(time.Now()),
|
field.Time("created_at").Default(time.Now()),
|
||||||
field.String("air_date").Default(""),
|
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.Int("storage_id").Optional(),
|
||||||
field.String("target_dir").Optional(),
|
field.String("target_dir").Optional(),
|
||||||
field.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"),
|
field.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"),
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ func (s *Server) Serve() error {
|
|||||||
tv.GET("/movie/watchlist", HttpHandler(s.GetMovieWatchlist))
|
tv.GET("/movie/watchlist", HttpHandler(s.GetMovieWatchlist))
|
||||||
tv.GET("/record/:id", HttpHandler(s.GetMediaDetails))
|
tv.GET("/record/:id", HttpHandler(s.GetMediaDetails))
|
||||||
tv.DELETE("/record/:id", HttpHandler(s.DeleteFromWatchlist))
|
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/tv/:tmdb_id", HttpHandler(s.SuggestedSeriesFolderName))
|
||||||
tv.GET("/suggest/movie/:tmdb_id", HttpHandler(s.SuggestedMovieFolderName))
|
tv.GET("/suggest/movie/:tmdb_id", HttpHandler(s.SuggestedMovieFolderName))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) {
|
|||||||
if err := c.ShouldBindJSON(&in); err != nil {
|
if err := c.ShouldBindJSON(&in); err != nil {
|
||||||
return nil, errors.Wrap(err, "bind query")
|
return nil, errors.Wrap(err, "bind query")
|
||||||
}
|
}
|
||||||
|
log.Debugf("add tv watchlist input %+v", in)
|
||||||
if in.Folder == "" {
|
if in.Folder == "" {
|
||||||
return nil, errors.New("folder should be provided")
|
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
|
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) {
|
func (s *Server) DeleteFromWatchlist(c *gin.Context) (interface{}, error) {
|
||||||
ids := c.Param("id")
|
ids := c.Param("id")
|
||||||
id, err := strconv.Atoi(ids)
|
id, err := strconv.Atoi(ids)
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class _MyAppState extends ConsumerState<MyApp> {
|
|||||||
|
|
||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
child: MaterialApp.router(
|
child: MaterialApp.router(
|
||||||
title: 'Polaris 影视追踪',
|
title: 'Polaris 影视追踪下载',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
fontFamily: "NotoSansSC",
|
fontFamily: "NotoSansSC",
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class SeriesDetails {
|
|||||||
String? airDate;
|
String? airDate;
|
||||||
String? mediaType;
|
String? mediaType;
|
||||||
Storage? storage;
|
Storage? storage;
|
||||||
|
String? targetDir;
|
||||||
|
|
||||||
SeriesDetails(
|
SeriesDetails(
|
||||||
{this.id,
|
{this.id,
|
||||||
@@ -78,6 +79,7 @@ class SeriesDetails {
|
|||||||
this.airDate,
|
this.airDate,
|
||||||
this.episodes,
|
this.episodes,
|
||||||
this.mediaType,
|
this.mediaType,
|
||||||
|
this.targetDir,
|
||||||
this.storage});
|
this.storage});
|
||||||
|
|
||||||
SeriesDetails.fromJson(Map<String, dynamic> json) {
|
SeriesDetails.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -93,6 +95,7 @@ class SeriesDetails {
|
|||||||
airDate = json["air_date"];
|
airDate = json["air_date"];
|
||||||
mediaType = json["media_type"];
|
mediaType = json["media_type"];
|
||||||
storage = Storage.fromJson(json["storage"]);
|
storage = Storage.fromJson(json["storage"]);
|
||||||
|
targetDir = json["target_dir"];
|
||||||
if (json['episodes'] != null) {
|
if (json['episodes'] != null) {
|
||||||
episodes = <Episodes>[];
|
episodes = <Episodes>[];
|
||||||
json['episodes'].forEach((v) {
|
json['episodes'].forEach((v) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:ui/providers/APIs.dart';
|
import 'package:ui/providers/APIs.dart';
|
||||||
@@ -148,116 +149,122 @@ class _SearchPageState extends ConsumerState<SearchPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showSubmitDialog(BuildContext context, SearchResult item) {
|
Future<void> _showSubmitDialog(BuildContext context, SearchResult item) {
|
||||||
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
|
||||||
return showDialog<void>(
|
return showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return Consumer(
|
return Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
String resSelected = "1080p";
|
|
||||||
int storageSelected = 0;
|
int storageSelected = 0;
|
||||||
var storage = ref.watch(storageSettingProvider);
|
var storage = ref.watch(storageSettingProvider);
|
||||||
var name = ref.watch(suggestNameDataProvider(
|
var name = ref.watch(suggestNameDataProvider(
|
||||||
(id: item.id!, mediaType: item.mediaType!)));
|
(id: item.id!, mediaType: item.mediaType!)));
|
||||||
bool downloadHistoryEpisodes = false;
|
|
||||||
|
|
||||||
var pathController = TextEditingController();
|
var pathController = TextEditingController();
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('添加剧集: ${item.name}'),
|
title: Text('添加: ${item.name}'),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 200,
|
height: 200,
|
||||||
child: Column(
|
child: FormBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
key: _formKey,
|
||||||
children: [
|
initialValue: const {
|
||||||
DropdownMenu(
|
"resolution": "1080p",
|
||||||
width: 200,
|
"storage": null,
|
||||||
label: const Text("清晰度"),
|
"folder": "",
|
||||||
initialSelection: resSelected,
|
"history_episodes": false,
|
||||||
dropdownMenuEntries: const [
|
},
|
||||||
DropdownMenuEntry(value: "720p", label: "720p"),
|
child: Column(
|
||||||
DropdownMenuEntry(value: "1080p", label: "1080p"),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
DropdownMenuEntry(value: "4k", label: "4k"),
|
children: [
|
||||||
],
|
FormBuilderDropdown(
|
||||||
onSelected: (value) {
|
name: "resolution",
|
||||||
setState(() {
|
decoration: const InputDecoration(labelText: "清晰度"),
|
||||||
resSelected = value!;
|
items: const [
|
||||||
});
|
DropdownMenuItem(
|
||||||
},
|
value: "720p", child: Text("720p")),
|
||||||
),
|
DropdownMenuItem(
|
||||||
storage.when(
|
value: "1080p", child: Text("1080p")),
|
||||||
data: (v) {
|
DropdownMenuItem(
|
||||||
return StatefulBuilder(
|
value: "2160p", child: Text("2160p")),
|
||||||
builder: (context, setState) {
|
],
|
||||||
return Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
storage.when(
|
||||||
children: [
|
data: (v) {
|
||||||
DropdownMenu(
|
return StatefulBuilder(
|
||||||
width: 200,
|
builder: (context, setState) {
|
||||||
label: const Text("存储位置"),
|
return Column(
|
||||||
initialSelection: storageSelected,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
dropdownMenuEntries: v
|
children: [
|
||||||
.map((s) => DropdownMenuEntry(
|
FormBuilderDropdown(
|
||||||
label: s.name!, value: s.id))
|
onChanged: (v) {
|
||||||
.toList(),
|
setState(
|
||||||
onSelected: (value) {
|
() {
|
||||||
setState(() {
|
storageSelected = v!;
|
||||||
storageSelected = value!;
|
},
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
),
|
name: "storage",
|
||||||
name.when(
|
decoration: const InputDecoration(
|
||||||
data: (s) {
|
labelText: "存储位置"),
|
||||||
return storageSelected == 0
|
items: v
|
||||||
? const Text("")
|
.map((s) => DropdownMenuItem(
|
||||||
: () {
|
value: s.id,
|
||||||
final storage = v
|
child: Text(s.name!)))
|
||||||
.where((e) =>
|
.toList(),
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
name.when(
|
||||||
item.mediaType == "tv"
|
data: (s) {
|
||||||
? SizedBox(
|
return storageSelected == 0
|
||||||
width: 250,
|
? const Text("")
|
||||||
child: CheckboxListTile(
|
: () {
|
||||||
|
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("是否下载往期剧集"),
|
title: const Text("是否下载往期剧集"),
|
||||||
value: downloadHistoryEpisodes,
|
),
|
||||||
onChanged: (v) {
|
)
|
||||||
setState(() {
|
: const SizedBox(),
|
||||||
downloadHistoryEpisodes = v!;
|
],
|
||||||
});
|
);
|
||||||
}),
|
});
|
||||||
)
|
},
|
||||||
: const SizedBox(),
|
error: (err, trace) => Text("$err"),
|
||||||
],
|
loading: () => const MyProgressIndicator()),
|
||||||
);
|
],
|
||||||
});
|
),
|
||||||
},
|
|
||||||
error: (err, trace) => Text("$err"),
|
|
||||||
loading: () => const MyProgressIndicator()),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
@@ -276,21 +283,25 @@ class _SearchPageState extends ConsumerState<SearchPage> {
|
|||||||
),
|
),
|
||||||
child: const Text('确定'),
|
child: const Text('确定'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var f = ref
|
if (_formKey.currentState!.saveAndValidate()) {
|
||||||
.read(searchPageDataProvider(widget.query ?? "")
|
final values = _formKey.currentState!.value;
|
||||||
.notifier)
|
//print(values);
|
||||||
.submit2Watchlist(
|
var f = ref
|
||||||
item.id!,
|
.read(searchPageDataProvider(widget.query ?? "")
|
||||||
storageSelected,
|
.notifier)
|
||||||
resSelected,
|
.submit2Watchlist(
|
||||||
item.mediaType!,
|
item.id!,
|
||||||
pathController.text,
|
values["storage"],
|
||||||
downloadHistoryEpisodes)
|
values["resolution"],
|
||||||
.then((v) {
|
item.mediaType!,
|
||||||
Navigator.of(context).pop();
|
values["folder"],
|
||||||
showSnakeBar("添加成功:${item.name}");
|
values["history_episodes"] ?? false)
|
||||||
});
|
.then((v) {
|
||||||
showLoadingWithFuture(f);
|
Navigator.of(context).pop();
|
||||||
|
showSnakeBar("添加成功:${item.name}");
|
||||||
|
});
|
||||||
|
showLoadingWithFuture(f);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
|||||||
margin: const EdgeInsets.all(4),
|
margin: const EdgeInsets.all(4),
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height*0.4),
|
constraints:
|
||||||
|
BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@@ -54,7 +55,6 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
const Text(""),
|
const Text(""),
|
||||||
Row(
|
Row(
|
||||||
@@ -64,7 +64,13 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
|||||||
width: 30,
|
width: 30,
|
||||||
),
|
),
|
||||||
Text(
|
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),
|
const Divider(thickness: 1, height: 1),
|
||||||
@@ -96,16 +102,20 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget deleteIcon() {
|
Widget deleteIcon() {
|
||||||
return IconButton(
|
return Tooltip(
|
||||||
onPressed: () {
|
message: widget.details.mediaType == "tv" ? "删除剧集" : "删除电影",
|
||||||
var f = ref
|
child: IconButton(
|
||||||
.read(mediaDetailsProvider(widget.details.id.toString()).notifier)
|
onPressed: () {
|
||||||
.delete()
|
var f = ref
|
||||||
.then((v) => context.go(widget.details.mediaType == "tv"
|
.read(
|
||||||
? WelcomePage.routeTv
|
mediaDetailsProvider(widget.details.id.toString()).notifier)
|
||||||
: WelcomePage.routeMoivie));
|
.delete()
|
||||||
showLoadingWithFuture(f);
|
.then((v) => context.go(widget.details.mediaType == "tv"
|
||||||
},
|
? WelcomePage.routeTv
|
||||||
icon: const Icon(Icons.delete));
|
: WelcomePage.routeMoivie));
|
||||||
|
showLoadingWithFuture(f);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user