mirror of
https://github.com/simon-ding/polaris.git
synced 2026-06-26 02:34:58 +08:00
feat: edit media details
This commit is contained in:
27
db/db.go
27
db/db.go
@@ -11,6 +11,7 @@ import (
|
|||||||
"polaris/ent/history"
|
"polaris/ent/history"
|
||||||
"polaris/ent/indexers"
|
"polaris/ent/indexers"
|
||||||
"polaris/ent/media"
|
"polaris/ent/media"
|
||||||
|
"polaris/ent/schema"
|
||||||
"polaris/ent/settings"
|
"polaris/ent/settings"
|
||||||
"polaris/ent/storage"
|
"polaris/ent/storage"
|
||||||
"polaris/log"
|
"polaris/log"
|
||||||
@@ -87,8 +88,8 @@ func (c *Client) generateDefaultLocalStorage() error {
|
|||||||
return c.AddStorage(&StorageInfo{
|
return c.AddStorage(&StorageInfo{
|
||||||
Name: "local",
|
Name: "local",
|
||||||
Implementation: "local",
|
Implementation: "local",
|
||||||
TvPath: "/data/tv/",
|
TvPath: "/data/tv/",
|
||||||
MoviePath: "/data/movies/",
|
MoviePath: "/data/movies/",
|
||||||
Default: true,
|
Default: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -249,7 +250,6 @@ type TorznabSetting struct {
|
|||||||
ApiKey string `json:"api_key"`
|
ApiKey string `json:"api_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (c *Client) SaveIndexer(in *ent.Indexers) error {
|
func (c *Client) SaveIndexer(in *ent.Indexers) error {
|
||||||
|
|
||||||
if in.ID != 0 {
|
if in.ID != 0 {
|
||||||
@@ -265,7 +265,7 @@ func (c *Client) SaveIndexer(in *ent.Indexers) error {
|
|||||||
|
|
||||||
_, err := c.ent.Indexers.Create().
|
_, err := c.ent.Indexers.Create().
|
||||||
SetName(in.Name).SetImplementation(in.Implementation).SetPriority(in.Priority).SetSettings(in.Settings).SetSeedRatio(in.SeedRatio).
|
SetName(in.Name).SetImplementation(in.Implementation).SetPriority(in.Priority).SetSettings(in.Settings).SetSeedRatio(in.SeedRatio).
|
||||||
SetDisabled(in.Disabled).Save(context.TODO())
|
SetDisabled(in.Disabled).Save(context.TODO())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "save db")
|
return errors.Wrap(err, "save db")
|
||||||
}
|
}
|
||||||
@@ -290,6 +290,7 @@ func (c *Client) GetIndexer(id int) (*TorznabInfo, error) {
|
|||||||
}
|
}
|
||||||
return &TorznabInfo{Indexers: res, TorznabSetting: ss}, nil
|
return &TorznabInfo{Indexers: res, TorznabSetting: ss}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type TorznabInfo struct {
|
type TorznabInfo struct {
|
||||||
*ent.Indexers
|
*ent.Indexers
|
||||||
TorznabSetting
|
TorznabSetting
|
||||||
@@ -307,7 +308,7 @@ func (c *Client) GetAllTorznabInfo() []*TorznabInfo {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
l = append(l, &TorznabInfo{
|
l = append(l, &TorznabInfo{
|
||||||
Indexers: r,
|
Indexers: r,
|
||||||
TorznabSetting: ss,
|
TorznabSetting: ss,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -356,7 +357,7 @@ type StorageInfo struct {
|
|||||||
Settings map[string]string `json:"settings" binding:"required"`
|
Settings map[string]string `json:"settings" binding:"required"`
|
||||||
TvPath string `json:"tv_path" binding:"required"`
|
TvPath string `json:"tv_path" binding:"required"`
|
||||||
MoviePath string `json:"movie_path" binding:"required"`
|
MoviePath string `json:"movie_path" binding:"required"`
|
||||||
Default bool `json:"default"`
|
Default bool `json:"default"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StorageInfo) ToWebDavSetting() WebdavSetting {
|
func (s *StorageInfo) ToWebDavSetting() WebdavSetting {
|
||||||
@@ -371,7 +372,6 @@ func (s *StorageInfo) ToWebDavSetting() WebdavSetting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type WebdavSetting struct {
|
type WebdavSetting struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
@@ -555,7 +555,18 @@ func (c *Client) GetDownloadClient(id int) (*ent.DownloadClients, error) {
|
|||||||
return c.ent.DownloadClients.Query().Where(downloadclients.ID(id)).First(context.Background())
|
return c.ent.DownloadClients.Query().Where(downloadclients.ID(id)).First(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (c *Client) SetEpisodeMonitoring(id int, b bool) error {
|
func (c *Client) SetEpisodeMonitoring(id int, b bool) error {
|
||||||
return c.ent.Episode.Update().Where(episode.ID(id)).SetMonitored(b).Exec(context.Background())
|
return c.ent.Episode.Update().Where(episode.ID(id)).SetMonitored(b).Exec(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EditMediaData struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Resolution media.Resolution `json:"resolution"`
|
||||||
|
TargetDir string `json:"target_dir"`
|
||||||
|
Limiter *schema.MediaLimiter `json:"limiter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) EditMediaMetadata(in EditMediaData) error {
|
||||||
|
return c.ent.Media.Update().Where(media.ID(in.ID)).SetResolution(in.Resolution).SetTargetDir(in.TargetDir).SetLimiter(in.Limiter).
|
||||||
|
Exec(context.Background())
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func init() {
|
|||||||
MaxSize: 50, // megabytes
|
MaxSize: 50, // megabytes
|
||||||
MaxBackups: 3,
|
MaxBackups: 3,
|
||||||
MaxAge: 30, // days
|
MaxAge: 30, // days
|
||||||
|
Compress: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"polaris/log"
|
"polaris/log"
|
||||||
"polaris/pkg/notifier"
|
"polaris/pkg/notifier"
|
||||||
"polaris/pkg/storage"
|
"polaris/pkg/storage"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@@ -55,6 +56,10 @@ func (c *Client) writePlexmatch(seriesId int, episodeId int, targetDir, name str
|
|||||||
} else {
|
} else {
|
||||||
buff.Write(data)
|
buff.Write(data)
|
||||||
}
|
}
|
||||||
|
if strings.Contains(buff.String(), name) {
|
||||||
|
log.Debugf("already write plex episode line: %v", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
buff.WriteString(fmt.Sprintf("\nep: %d: %s\n", ep.EpisodeNumber, name))
|
buff.WriteString(fmt.Sprintf("\nep: %d: %s\n", ep.EpisodeNumber, name))
|
||||||
log.Infof("write season plexmatch file content: %s", buff.String())
|
log.Infof("write season plexmatch file content: %s", buff.String())
|
||||||
return st.WriteFile(seasonPlex, buff.Bytes())
|
return st.WriteFile(seasonPlex, buff.Bytes())
|
||||||
|
|||||||
@@ -116,16 +116,16 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//如果种子是路径,则会把路径展开,只移动文件,类似 move dir/* dir2/, 如果种子是文件,则会直接移动文件,类似 move file dir/
|
|
||||||
if err := stImpl.Copy(filepath.Join(c.db.GetDownloadDir(), torrentName), r.TargetDir); err != nil {
|
|
||||||
return errors.Wrap(err, "move file")
|
|
||||||
}
|
|
||||||
|
|
||||||
// .plexmatch file
|
// .plexmatch file
|
||||||
if err := c.writePlexmatch(r.MediaID, r.EpisodeID, r.TargetDir, torrentName); err != nil {
|
if err := c.writePlexmatch(r.MediaID, r.EpisodeID, r.TargetDir, torrentName); err != nil {
|
||||||
log.Errorf("create .plexmatch file error: %v", err)
|
log.Errorf("create .plexmatch file error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//如果种子是路径,则会把路径展开,只移动文件,类似 move dir/* dir2/, 如果种子是文件,则会直接移动文件,类似 move file dir/
|
||||||
|
if err := stImpl.Copy(filepath.Join(c.db.GetDownloadDir(), torrentName), r.TargetDir); err != nil {
|
||||||
|
return errors.Wrap(err, "move file")
|
||||||
|
}
|
||||||
|
|
||||||
c.db.SetHistoryStatus(r.ID, history.StatusSuccess)
|
c.db.SetHistoryStatus(r.ID, history.StatusSuccess)
|
||||||
if r.EpisodeID != 0 {
|
if r.EpisodeID != 0 {
|
||||||
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusDownloaded)
|
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusDownloaded)
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ func (s *Server) Serve() error {
|
|||||||
tv := api.Group("/media")
|
tv := api.Group("/media")
|
||||||
{
|
{
|
||||||
tv.GET("/search", HttpHandler(s.SearchMedia))
|
tv.GET("/search", HttpHandler(s.SearchMedia))
|
||||||
|
tv.POST("/edit", HttpHandler(s.EditMediaMetadata))
|
||||||
tv.POST("/tv/watchlist", HttpHandler(s.AddTv2Watchlist))
|
tv.POST("/tv/watchlist", HttpHandler(s.AddTv2Watchlist))
|
||||||
tv.GET("/tv/watchlist", HttpHandler(s.GetTvWatchlist))
|
tv.GET("/tv/watchlist", HttpHandler(s.GetTvWatchlist))
|
||||||
tv.POST("/torrents", HttpHandler(s.SearchAvailableTorrents))
|
tv.POST("/torrents", HttpHandler(s.SearchAvailableTorrents))
|
||||||
|
|||||||
@@ -195,8 +195,8 @@ func (s *Server) DeleteDownloadCLient(c *gin.Context) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type episodeMonitoringIn struct {
|
type episodeMonitoringIn struct {
|
||||||
EpisodeID int `json:"episode_id"`
|
EpisodeID int `json:"episode_id"`
|
||||||
Monitor bool `json:"monitor"`
|
Monitor bool `json:"monitor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) ChangeEpisodeMonitoring(c *gin.Context) (interface{}, error) {
|
func (s *Server) ChangeEpisodeMonitoring(c *gin.Context) (interface{}, error) {
|
||||||
@@ -207,3 +207,15 @@ func (s *Server) ChangeEpisodeMonitoring(c *gin.Context) (interface{}, error) {
|
|||||||
s.db.SetEpisodeMonitoring(in.EpisodeID, in.Monitor)
|
s.db.SetEpisodeMonitoring(in.EpisodeID, in.Monitor)
|
||||||
return "success", nil
|
return "success", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) EditMediaMetadata(c *gin.Context) (interface{}, error) {
|
||||||
|
var in db.EditMediaData
|
||||||
|
if err := c.ShouldBindJSON(&in); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "bind")
|
||||||
|
}
|
||||||
|
err := s.db.EditMediaMetadata(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "save db")
|
||||||
|
}
|
||||||
|
return "success", nil
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import 'package:ui/providers/server_response.dart';
|
|||||||
class APIs {
|
class APIs {
|
||||||
static final _baseUrl = baseUrl();
|
static final _baseUrl = baseUrl();
|
||||||
static final searchUrl = "$_baseUrl/api/v1/media/search";
|
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 settingsUrl = "$_baseUrl/api/v1/setting/do";
|
||||||
static final settingsGeneralUrl = "$_baseUrl/api/v1/setting/general";
|
static final settingsGeneralUrl = "$_baseUrl/api/v1/setting/general";
|
||||||
static final watchlistTvUrl = "$_baseUrl/api/v1/media/tv/watchlist";
|
static final watchlistTvUrl = "$_baseUrl/api/v1/media/tv/watchlist";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:ui/providers/APIs.dart';
|
import 'package:ui/providers/APIs.dart';
|
||||||
import 'package:ui/providers/server_response.dart';
|
import 'package:ui/providers/server_response.dart';
|
||||||
@@ -34,7 +35,7 @@ class SeriesDetailData
|
|||||||
|
|
||||||
Future<String> searchAndDownload(
|
Future<String> searchAndDownload(
|
||||||
String seriesId, int seasonNum, int episodeNum) async {
|
String seriesId, int seasonNum, int episodeNum) async {
|
||||||
final dio = await APIs.getDio();
|
final dio = APIs.getDio();
|
||||||
var resp = await dio.post(APIs.searchAndDownloadUrl, data: {
|
var resp = await dio.post(APIs.searchAndDownloadUrl, data: {
|
||||||
"id": int.parse(seriesId),
|
"id": int.parse(seriesId),
|
||||||
"season": seasonNum,
|
"season": seasonNum,
|
||||||
@@ -61,6 +62,22 @@ class SeriesDetailData
|
|||||||
}
|
}
|
||||||
ref.invalidateSelf();
|
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 {
|
class SeriesDetails {
|
||||||
@@ -79,6 +96,7 @@ class SeriesDetails {
|
|||||||
Storage? storage;
|
Storage? storage;
|
||||||
String? targetDir;
|
String? targetDir;
|
||||||
bool? downloadHistoryEpisodes;
|
bool? downloadHistoryEpisodes;
|
||||||
|
Limiter? limiter;
|
||||||
|
|
||||||
SeriesDetails(
|
SeriesDetails(
|
||||||
{this.id,
|
{this.id,
|
||||||
@@ -95,7 +113,8 @@ class SeriesDetails {
|
|||||||
this.mediaType,
|
this.mediaType,
|
||||||
this.targetDir,
|
this.targetDir,
|
||||||
this.storage,
|
this.storage,
|
||||||
this.downloadHistoryEpisodes});
|
this.downloadHistoryEpisodes,
|
||||||
|
this.limiter});
|
||||||
|
|
||||||
SeriesDetails.fromJson(Map<String, dynamic> json) {
|
SeriesDetails.fromJson(Map<String, dynamic> json) {
|
||||||
id = json['id'];
|
id = json['id'];
|
||||||
@@ -118,6 +137,19 @@ class SeriesDetails {
|
|||||||
episodes!.add(Episodes.fromJson(v));
|
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
|
enabledSizedLimiter
|
||||||
? FormBuilderRangeSlider(
|
? const MyRangeSlider(name: "size_limiter")
|
||||||
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 SizedBox(),
|
: const SizedBox(),
|
||||||
widget.item.mediaType == "tv"
|
widget.item.mediaType == "tv"
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
@@ -193,7 +165,7 @@ class _SubmitSearchResultState extends ConsumerState<SubmitSearchResult> {
|
|||||||
if (_formKey.currentState!.saveAndValidate()) {
|
if (_formKey.currentState!.saveAndValidate()) {
|
||||||
final values = _formKey.currentState!.value;
|
final values = _formKey.currentState!.value;
|
||||||
var f = ref
|
var f = ref
|
||||||
.read(searchPageDataProvider(widget.query ?? "").notifier)
|
.read(searchPageDataProvider(widget.query).notifier)
|
||||||
.submit2Watchlist(
|
.submit2Watchlist(
|
||||||
widget.item.id!,
|
widget.item.id!,
|
||||||
values["storage"],
|
values["storage"],
|
||||||
@@ -223,3 +195,4 @@ class _SubmitSearchResultState extends ConsumerState<SubmitSearchResult> {
|
|||||||
return "$v MB";
|
return "$v MB";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,53 +41,7 @@ class WelcomePage extends ConsumerWidget {
|
|||||||
]
|
]
|
||||||
: List.generate(value.length, (i) {
|
: List.generate(value.length, (i) {
|
||||||
final item = value[i];
|
final item = value[i];
|
||||||
return Card(
|
return MediaCard(item: item);
|
||||||
//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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -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/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:form_builder_validators/form_builder_validators.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';
|
||||||
import 'package:ui/providers/series_details.dart';
|
import 'package:ui/providers/series_details.dart';
|
||||||
@@ -68,10 +70,12 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 30,
|
width: 30,
|
||||||
),
|
),
|
||||||
Expanded(child: Text(
|
Expanded(
|
||||||
"${widget.details.mediaType == "tv" ? widget.details.storage!.tvPath : widget.details.storage!.moviePath}"
|
child: Text(
|
||||||
"${widget.details.targetDir}"),)
|
"${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),
|
||||||
Text(
|
Text(
|
||||||
@@ -88,6 +92,7 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
|||||||
)),
|
)),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
editIcon(widget.details),
|
||||||
deleteIcon(),
|
deleteIcon(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -120,4 +125,77 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
|||||||
icon: const Icon(Icons.delete)),
|
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/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:ui/providers/APIs.dart';
|
import 'package:ui/providers/APIs.dart';
|
||||||
|
|
||||||
class Commons {
|
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