From 11f7b51eb56b8ff6a46408547c209a0e31f8aec9 Mon Sep 17 00:00:00 2001 From: Simon Ding Date: Tue, 23 Jul 2024 19:01:24 +0800 Subject: [PATCH] add movie download history --- db/db.go | 5 + server/activity.go | 13 ++ server/server.go | 1 + ui/lib/movie_watchlist.dart | 318 ++++++++++++++++++++++----------- ui/lib/providers/APIs.dart | 1 + ui/lib/providers/activity.dart | 19 +- 6 files changed, 247 insertions(+), 110 deletions(-) diff --git a/db/db.go b/db/db.go index 46f1de5..39ca472 100644 --- a/db/db.go +++ b/db/db.go @@ -485,3 +485,8 @@ func (c *Client) SetSeasonAllEpisodeStatus(mediaID, seasonNum int, status episod func (c *Client) TmdbIdInWatchlist(tmdb_id int) bool { return c.ent.Media.Query().Where(media.TmdbID(tmdb_id)).CountX(context.TODO()) > 0 } + + +func (c *Client) GetDownloadHistory(mediaID int) ([]*ent.History, error) { + return c.ent.History.Query().Where(history.MediaID(mediaID)).All(context.TODO()) +} \ No newline at end of file diff --git a/server/activity.go b/server/activity.go index b1c1ab3..ce4399e 100644 --- a/server/activity.go +++ b/server/activity.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "polaris/ent" "polaris/ent/episode" "polaris/log" @@ -72,3 +73,15 @@ func (s *Server) RemoveActivity(c *gin.Context) (interface{}, error) { log.Infof("history record successful deleted: %v", his.SourceTitle) return nil, nil } +func (s *Server) GetMediaDownloadHistory(c *gin.Context) (interface{}, error) { + var ids = c.Param("id") + id, err := strconv.Atoi(ids) + if err != nil { + return nil, fmt.Errorf("id is not correct: %v", ids) + } + his, err := s.db.GetDownloadHistory(id) + if err != nil { + return nil, errors.Wrap(err, "db") + } + return his, nil +} diff --git a/server/server.go b/server/server.go index b9eddbc..c16ae0a 100644 --- a/server/server.go +++ b/server/server.go @@ -62,6 +62,7 @@ func (s *Server) Serve() error { { activity.GET("/", HttpHandler(s.GetAllActivities)) activity.DELETE("/:id", HttpHandler(s.RemoveActivity)) + activity.GET("/media/:id", HttpHandler(s.GetMediaDownloadHistory)) } tv := api.Group("/media") diff --git a/ui/lib/movie_watchlist.dart b/ui/lib/movie_watchlist.dart index 3ff7a3e..83b5741 100644 --- a/ui/lib/movie_watchlist.dart +++ b/ui/lib/movie_watchlist.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:ui/providers/APIs.dart'; +import 'package:ui/providers/activity.dart'; import 'package:ui/providers/series_details.dart'; import 'package:ui/providers/settings.dart'; import 'package:ui/providers/welcome_data.dart'; @@ -30,7 +31,6 @@ class _MovieDetailsPageState extends ConsumerState { @override Widget build(BuildContext context) { var seriesDetails = ref.watch(mediaDetailsProvider(widget.id)); - var torrents = ref.watch(movieTorrentsDataProvider(widget.id)); var storage = ref.watch(storageSettingProvider); return seriesDetails.when( @@ -40,122 +40,100 @@ class _MovieDetailsPageState extends ConsumerState { Card( margin: const EdgeInsets.all(4), clipBehavior: Clip.hardEdge, - child: Padding( - padding: const EdgeInsets.all(10), - child: Row( - children: [ - Flexible( - flex: 1, - child: Padding( - padding: const EdgeInsets.all(10), - child: Image.network( - "${APIs.imagesUrl}/${details.id}/poster.jpg", - fit: BoxFit.contain, - headers: APIs.authHeaders, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.fitWidth, + opacity: 0.5, + image: NetworkImage( + "${APIs.imagesUrl}/${details.id}/backdrop.jpg", + headers: APIs.authHeaders))), + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Flexible( + flex: 1, + child: Padding( + padding: const EdgeInsets.all(10), + child: Image.network( + "${APIs.imagesUrl}/${details.id}/poster.jpg", + fit: BoxFit.contain, + headers: APIs.authHeaders, + ), + ), ), - ), - ), - Expanded( - flex: 6, - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Expanded( + flex: 6, + child: Row( children: [ - Row( + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("${details.resolution}"), - const SizedBox( - width: 30, + Row( + children: [ + Text("${details.resolution}"), + const SizedBox( + width: 30, + ), + storage.when( + data: (value) { + for (final s in value) { + if (s.id == details.storageId) { + return Text( + "${s.name}(${s.implementation})"); + } + } + return const Text("未知存储"); + }, + error: (error, stackTrace) => + Text("$error"), + loading: () => + const MyProgressIndicator()), + ], + ), + const Divider(thickness: 1, height: 1), + Text( + "${details.name} (${details.airDate!.split("-")[0]})", + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold), + ), + const Text(""), + Text( + details.overview!, ), - storage.when( - data: (value) { - for (final s in value) { - if (s.id == details.storageId) { - return Text( - "${s.name}(${s.implementation})"); - } - } - return const Text("未知存储"); - }, - error: (error, stackTrace) => - Text("$error"), - loading: () => - const MyProgressIndicator()), ], - ), - const Divider(thickness: 1, height: 1), - Text( - "${details.name} (${details.airDate!.split("-")[0]})", - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold), - ), - const Text(""), - Text( - details.overview!, - ), + )), + Column( + children: [ + IconButton( + onPressed: () { + ref + .read(mediaDetailsProvider( + widget.id) + .notifier) + .delete() + .whenComplete(() => context + .go(WelcomePage.routeMoivie)) + .onError((error, trace) => + Utils.showSnakeBar( + "删除失败:$error")); + }, + icon: const Icon(Icons.delete)) + ], + ) ], - )), - Column( - children: [ - IconButton( - onPressed: () { - ref - .read(mediaDetailsProvider(widget.id) - .notifier) - .delete() - .whenComplete(() => context - .go(WelcomePage.routeMoivie)) - .onError((error, trace) => - Utils.showSnakeBar( - "删除失败:$error")); - }, - icon: const Icon(Icons.delete)) - ], - ) - ], - ), + ), + ), + ], ), - ], - ), - ), + )), ), - torrents.when( - data: (v) { - return DataTable( - columns: const [ - DataColumn(label: Text("名称")), - DataColumn(label: Text("大小")), - DataColumn(label: Text("seeders")), - DataColumn(label: Text("peers")), - DataColumn(label: Text("操作")) - ], - rows: List.generate(v.length, (i) { - final torrent = v[i]; - return DataRow(cells: [ - DataCell(Text("${torrent.name}")), - DataCell(Text("${torrent.size?.readableFileSize()}")), - DataCell(Text("${torrent.seeders}")), - DataCell(Text("${torrent.peers}")), - DataCell(IconButton( - icon: const Icon(Icons.download), - onPressed: () { - ref - .read(movieTorrentsDataProvider(widget.id) - .notifier) - .download(torrent.link!) - .whenComplete(() => Utils.showSnakeBar( - "开始下载:${torrent.name}")).onError((error, trace) => Utils.showSnakeBar("操作失败: $error")); - }, - )) - ]); - }), - ); - }, - error: (error, trace) => Text("$error"), - loading: () => const MyProgressIndicator()), + NestedTabBar( + id: widget.id, + ) ], ); }, @@ -165,3 +143,125 @@ class _MovieDetailsPageState extends ConsumerState { loading: () => const MyProgressIndicator()); } } + +class NestedTabBar extends ConsumerStatefulWidget { + final String id; + + const NestedTabBar({super.key, required this.id}); + + @override + _NestedTabBarState createState() => _NestedTabBarState(); +} + +class _NestedTabBarState extends ConsumerState + with TickerProviderStateMixin { + late TabController _nestedTabController; + @override + void initState() { + super.initState(); + _nestedTabController = new TabController(length: 2, vsync: this); + } + + @override + void dispose() { + super.dispose(); + _nestedTabController.dispose(); + } + + int selectedTab = 0; + + @override + Widget build(BuildContext context) { + var torrents = ref.watch(movieTorrentsDataProvider(widget.id)); + var histories = ref.watch(mediaHistoryDataProvider(widget.id)); + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TabBar( + controller: _nestedTabController, + isScrollable: true, + onTap: (value) { + setState(() { + selectedTab = value; + }); + }, + tabs: const [ + Tab( + text: "下载记录", + ), + Tab( + text: "资源", + ), + ], + ), + Builder(builder: (context) { + if (selectedTab == 0) { + return histories.when( + data: (v) { + if (v.isEmpty) { + return const Center( + child: Text("无下载记录"), + ); + } + return DataTable( + columns: const [ + DataColumn(label: Text("#"), numeric: true), + DataColumn(label: Text("名称")), + DataColumn(label: Text("下载时间")), + ], + rows: List.generate(v.length, (i) { + final activity = v[i]; + return DataRow(cells: [ + DataCell(Text("${activity.id}")), + DataCell(Text("${activity.sourceTitle}")), + DataCell(Text("${activity.date!.toLocal()}")), + ]); + })); + }, + error: (error, trace) => Text("$error"), + loading: () => const MyProgressIndicator()); + } else { + return torrents.when( + data: (v) { + return DataTable( + columns: const [ + DataColumn(label: Text("名称")), + DataColumn(label: Text("大小")), + DataColumn(label: Text("seeders")), + DataColumn(label: Text("peers")), + DataColumn(label: Text("操作")) + ], + rows: List.generate(v.length, (i) { + final torrent = v[i]; + return DataRow(cells: [ + DataCell(Text("${torrent.name}")), + DataCell(Text("${torrent.size?.readableFileSize()}")), + DataCell(Text("${torrent.seeders}")), + DataCell(Text("${torrent.peers}")), + DataCell(IconButton( + icon: const Icon(Icons.download), + onPressed: () { + ref + .read(movieTorrentsDataProvider(widget.id) + .notifier) + .download(torrent.link!) + .whenComplete(() => + Utils.showSnakeBar("开始下载:${torrent.name}")) + .onError((error, trace) => + Utils.showSnakeBar("操作失败: $error")); + }, + )) + ]); + }), + ); + }, + error: (error, trace) => Text("$error"), + loading: () => const MyProgressIndicator()); + } + }) + ], + ); + } +} diff --git a/ui/lib/providers/APIs.dart b/ui/lib/providers/APIs.dart index 966b3db..7d01df5 100644 --- a/ui/lib/providers/APIs.dart +++ b/ui/lib/providers/APIs.dart @@ -27,6 +27,7 @@ class APIs { static final loginUrl = "$_baseUrl/api/login"; static final loginSettingUrl = "$_baseUrl/api/v1/setting/auth"; static final activityUrl = "$_baseUrl/api/v1/activity/"; + static final activityMediaUrl = "$_baseUrl/api/v1/activity/media/"; static final imagesUrl = "$_baseUrl/api/v1/img"; static final tmdbImgBaseUrl = "$_baseUrl/api/v1/posters"; diff --git a/ui/lib/providers/activity.dart b/ui/lib/providers/activity.dart index d40881d..5672e24 100644 --- a/ui/lib/providers/activity.dart +++ b/ui/lib/providers/activity.dart @@ -8,10 +8,27 @@ var activitiesDataProvider = AsyncNotifierProvider.autoDispose>( ActivityData.new); +var mediaHistoryDataProvider = FutureProvider.autoDispose.family( + (ref, arg) async { + final dio = await APIs.getDio(); + var resp = await dio.get("${APIs.activityMediaUrl}$arg"); + final sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + List activities = List.empty(growable: true); + for (final a in sp.data as List) { + activities.add(Activity.fromJson(a)); + } + return activities; + }, +); + class ActivityData extends AutoDisposeAsyncNotifier> { @override FutureOr> build() async { - Timer(const Duration(seconds: 5), ref.invalidateSelf);//Periodically Refresh + Timer( + const Duration(seconds: 5), ref.invalidateSelf); //Periodically Refresh final dio = await APIs.getDio(); var resp = await dio.get(APIs.activityUrl);