mirror of
https://github.com/simon-ding/polaris.git
synced 2026-06-03 16:37:48 +08:00
add movie download history
This commit is contained in:
5
db/db.go
5
db/db.go
@@ -485,3 +485,8 @@ func (c *Client) SetSeasonAllEpisodeStatus(mediaID, seasonNum int, status episod
|
|||||||
func (c *Client) TmdbIdInWatchlist(tmdb_id int) bool {
|
func (c *Client) TmdbIdInWatchlist(tmdb_id int) bool {
|
||||||
return c.ent.Media.Query().Where(media.TmdbID(tmdb_id)).CountX(context.TODO()) > 0
|
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())
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"polaris/ent"
|
"polaris/ent"
|
||||||
"polaris/ent/episode"
|
"polaris/ent/episode"
|
||||||
"polaris/log"
|
"polaris/log"
|
||||||
@@ -72,3 +73,15 @@ func (s *Server) RemoveActivity(c *gin.Context) (interface{}, error) {
|
|||||||
log.Infof("history record successful deleted: %v", his.SourceTitle)
|
log.Infof("history record successful deleted: %v", his.SourceTitle)
|
||||||
return nil, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func (s *Server) Serve() error {
|
|||||||
{
|
{
|
||||||
activity.GET("/", HttpHandler(s.GetAllActivities))
|
activity.GET("/", HttpHandler(s.GetAllActivities))
|
||||||
activity.DELETE("/:id", HttpHandler(s.RemoveActivity))
|
activity.DELETE("/:id", HttpHandler(s.RemoveActivity))
|
||||||
|
activity.GET("/media/:id", HttpHandler(s.GetMediaDownloadHistory))
|
||||||
}
|
}
|
||||||
|
|
||||||
tv := api.Group("/media")
|
tv := api.Group("/media")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.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';
|
||||||
|
import 'package:ui/providers/activity.dart';
|
||||||
import 'package:ui/providers/series_details.dart';
|
import 'package:ui/providers/series_details.dart';
|
||||||
import 'package:ui/providers/settings.dart';
|
import 'package:ui/providers/settings.dart';
|
||||||
import 'package:ui/providers/welcome_data.dart';
|
import 'package:ui/providers/welcome_data.dart';
|
||||||
@@ -30,7 +31,6 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var seriesDetails = ref.watch(mediaDetailsProvider(widget.id));
|
var seriesDetails = ref.watch(mediaDetailsProvider(widget.id));
|
||||||
var torrents = ref.watch(movieTorrentsDataProvider(widget.id));
|
|
||||||
var storage = ref.watch(storageSettingProvider);
|
var storage = ref.watch(storageSettingProvider);
|
||||||
|
|
||||||
return seriesDetails.when(
|
return seriesDetails.when(
|
||||||
@@ -40,122 +40,100 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
|
|||||||
Card(
|
Card(
|
||||||
margin: const EdgeInsets.all(4),
|
margin: const EdgeInsets.all(4),
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: Padding(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(10),
|
decoration: BoxDecoration(
|
||||||
child: Row(
|
image: DecorationImage(
|
||||||
children: <Widget>[
|
fit: BoxFit.fitWidth,
|
||||||
Flexible(
|
opacity: 0.5,
|
||||||
flex: 1,
|
image: NetworkImage(
|
||||||
child: Padding(
|
"${APIs.imagesUrl}/${details.id}/backdrop.jpg",
|
||||||
padding: const EdgeInsets.all(10),
|
headers: APIs.authHeaders))),
|
||||||
child: Image.network(
|
child: Padding(
|
||||||
"${APIs.imagesUrl}/${details.id}/poster.jpg",
|
padding: const EdgeInsets.all(10),
|
||||||
fit: BoxFit.contain,
|
child: Row(
|
||||||
headers: APIs.authHeaders,
|
children: <Widget>[
|
||||||
|
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,
|
||||||
Expanded(
|
child: Row(
|
||||||
flex: 6,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("${details.resolution}"),
|
Row(
|
||||||
const SizedBox(
|
children: [
|
||||||
width: 30,
|
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),
|
Column(
|
||||||
Text(
|
children: [
|
||||||
"${details.name} (${details.airDate!.split("-")[0]})",
|
IconButton(
|
||||||
style: const TextStyle(
|
onPressed: () {
|
||||||
fontSize: 20,
|
ref
|
||||||
fontWeight: FontWeight.bold),
|
.read(mediaDetailsProvider(
|
||||||
),
|
widget.id)
|
||||||
const Text(""),
|
.notifier)
|
||||||
Text(
|
.delete()
|
||||||
details.overview!,
|
.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(
|
NestedTabBar(
|
||||||
data: (v) {
|
id: widget.id,
|
||||||
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()),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -165,3 +143,125 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
|
|||||||
loading: () => const MyProgressIndicator());
|
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<NestedTabBar>
|
||||||
|
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: <Widget>[
|
||||||
|
TabBar(
|
||||||
|
controller: _nestedTabController,
|
||||||
|
isScrollable: true,
|
||||||
|
onTap: (value) {
|
||||||
|
setState(() {
|
||||||
|
selectedTab = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tabs: const <Widget>[
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class APIs {
|
|||||||
static final loginUrl = "$_baseUrl/api/login";
|
static final loginUrl = "$_baseUrl/api/login";
|
||||||
static final loginSettingUrl = "$_baseUrl/api/v1/setting/auth";
|
static final loginSettingUrl = "$_baseUrl/api/v1/setting/auth";
|
||||||
static final activityUrl = "$_baseUrl/api/v1/activity/";
|
static final activityUrl = "$_baseUrl/api/v1/activity/";
|
||||||
|
static final activityMediaUrl = "$_baseUrl/api/v1/activity/media/";
|
||||||
static final imagesUrl = "$_baseUrl/api/v1/img";
|
static final imagesUrl = "$_baseUrl/api/v1/img";
|
||||||
|
|
||||||
static final tmdbImgBaseUrl = "$_baseUrl/api/v1/posters";
|
static final tmdbImgBaseUrl = "$_baseUrl/api/v1/posters";
|
||||||
|
|||||||
@@ -8,10 +8,27 @@ var activitiesDataProvider =
|
|||||||
AsyncNotifierProvider.autoDispose<ActivityData, List<Activity>>(
|
AsyncNotifierProvider.autoDispose<ActivityData, List<Activity>>(
|
||||||
ActivityData.new);
|
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<Activity> activities = List.empty(growable: true);
|
||||||
|
for (final a in sp.data as List) {
|
||||||
|
activities.add(Activity.fromJson(a));
|
||||||
|
}
|
||||||
|
return activities;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
class ActivityData extends AutoDisposeAsyncNotifier<List<Activity>> {
|
class ActivityData extends AutoDisposeAsyncNotifier<List<Activity>> {
|
||||||
@override
|
@override
|
||||||
FutureOr<List<Activity>> build() async {
|
FutureOr<List<Activity>> 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();
|
final dio = await APIs.getDio();
|
||||||
var resp = await dio.get(APIs.activityUrl);
|
var resp = await dio.get(APIs.activityUrl);
|
||||||
|
|||||||
Reference in New Issue
Block a user