From 85f8750908a4b65905d96cf8abd20f66ce71d938 Mon Sep 17 00:00:00 2001 From: Simon Ding Date: Sat, 13 Jul 2024 17:23:27 +0800 Subject: [PATCH] feat: activity page --- ent/history.go | 2 +- ent/schema/history.go | 2 +- server/activity.go | 20 ++++++++- server/resources.go | 2 +- server/scheduler.go | 16 ++++++-- server/server.go | 7 ++-- ui/lib/activity.dart | 48 ++++++++++++++++++++-- ui/lib/main.dart | 25 +++++++++--- ui/lib/navdrawer.dart | 3 +- ui/lib/providers/APIs.dart | 1 + ui/lib/providers/activity.dart | 74 ++++++++++++++++++++++++++++++++++ 11 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 ui/lib/providers/activity.dart diff --git a/ent/history.go b/ent/history.go index ecf97b4..08e12cc 100644 --- a/ent/history.go +++ b/ent/history.go @@ -28,7 +28,7 @@ type History struct { // TargetDir holds the value of the "target_dir" field. TargetDir string `json:"target_dir,omitempty"` // Completed holds the value of the "completed" field. - Completed bool `json:"completed,omitempty"` + Completed bool `json:"completed"` // Saved holds the value of the "saved" field. Saved string `json:"saved,omitempty"` selectValues sql.SelectValues diff --git a/ent/schema/history.go b/ent/schema/history.go index 206398d..d6e94ac 100644 --- a/ent/schema/history.go +++ b/ent/schema/history.go @@ -18,7 +18,7 @@ func (History) Fields() []ent.Field { field.String("source_title"), field.Time("date"), field.String("target_dir"), - field.Bool("completed").Default(false), + field.Bool("completed").Default(false).StructTag("json:\"completed\""), field.String("saved").Optional(), } } diff --git a/server/activity.go b/server/activity.go index 44b059d..d272328 100644 --- a/server/activity.go +++ b/server/activity.go @@ -1,6 +1,7 @@ package server import ( + "polaris/ent" "polaris/log" "strconv" @@ -8,10 +9,27 @@ import ( "github.com/pkg/errors" ) +type Activity struct { + *ent.History + InBackgroud bool `json:"in_backgroud"` +} + func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) { his := s.db.GetHistories() + var activities = make([]Activity, 0, len(his)) + for _, h := range his { + a := Activity{ + History: h, + } + for id, task := range s.tasks { + if h.ID == id && task.Processing { + a.InBackgroud = true + } + } + activities = append(activities, a) + } - return his, nil + return activities, nil } func (s *Server) RemoveActivity(c *gin.Context) (interface{}, error) { diff --git a/server/resources.go b/server/resources.go index ac2536c..86c2c6f 100644 --- a/server/resources.go +++ b/server/resources.go @@ -131,7 +131,7 @@ func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string if err != nil { return nil, errors.Wrap(err, "save record") } - s.tasks[history.ID] = torrent + s.tasks[history.ID] = &Task{Torrent: torrent} log.Infof("success add %s to download task", r1.Name) return &r1.Name, nil diff --git a/server/scheduler.go b/server/scheduler.go index 8cbbd65..e2474d1 100644 --- a/server/scheduler.go +++ b/server/scheduler.go @@ -4,6 +4,7 @@ import ( "path/filepath" "polaris/db" "polaris/log" + "polaris/pkg" "polaris/pkg/storage" "github.com/pkg/errors" @@ -28,9 +29,14 @@ func (s *Server) checkTasks() { log.Infof("task no longer exists: %v", id) continue } + if t.Processing { + continue + } + log.Infof("task (%s) percentage done: %d%%", t.Name(), t.Progress()) if t.Progress() == 100 { log.Infof("task is done: %v", t.Name()) + t.Processing = true go func() { if err := s.moveCompletedTask(id); err != nil { log.Infof("post tasks for id %v fail: %v", id, err) @@ -43,9 +49,6 @@ func (s *Server) checkTasks() { func (s *Server) moveCompletedTask(id int) error { torrent := s.tasks[id] r := s.db.GetHistory(id) - s.db.SetHistoryComplete(r.ID) - - delete(s.tasks, r.ID) series := s.db.GetSeriesDetails(r.SeriesID) st := s.db.GetStorage(series.StorageID) @@ -68,5 +71,12 @@ func (s *Server) moveCompletedTask(id int) error { } log.Infof("move downloaded files to target dir success, file: %v, target dir: %v", torrent.Name(), r.TargetDir) torrent.Remove() + delete(s.tasks, r.ID) + s.db.SetHistoryComplete(r.ID) return nil } + +type Task struct { + Processing bool + pkg.Torrent +} \ No newline at end of file diff --git a/server/server.go b/server/server.go index ec277e4..23238fe 100644 --- a/server/server.go +++ b/server/server.go @@ -3,7 +3,6 @@ package server import ( "polaris/db" "polaris/log" - "polaris/pkg" "polaris/pkg/tmdb" "polaris/pkg/transmission" "polaris/ui" @@ -21,7 +20,7 @@ func NewServer(db *db.Client) *Server { r: r, db: db, cron: cron.New(), - tasks: make(map[int]pkg.Torrent), + tasks: make(map[int]*Task), } } @@ -30,7 +29,7 @@ type Server struct { db *db.Client cron *cron.Cron language string - tasks map[int]pkg.Torrent + tasks map[int]*Task } func (s *Server) Serve() error { @@ -118,6 +117,6 @@ func (s *Server) reloadTasks() { log.Errorf("relaod task %s failed: %v", t.SourceTitle, err) continue } - s.tasks[t.ID] = torrent + s.tasks[t.ID] = &Task{Torrent: torrent} } } diff --git a/ui/lib/activity.dart b/ui/lib/activity.dart index c98e02b..efac420 100644 --- a/ui/lib/activity.dart +++ b/ui/lib/activity.dart @@ -1,11 +1,51 @@ +import 'dart:js_interop_unsafe'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ui/providers/activity.dart'; class ActivityPage extends ConsumerWidget { + static const route = "/activities"; @override Widget build(BuildContext context, WidgetRef ref) { - // TODO: implement build - throw UnimplementedError(); + var activitiesWatcher = ref.watch(activitiesDataProvider); + + return activitiesWatcher.when( + data: (activities) { + return SingleChildScrollView( + child: DataTable( + columns: const [ + DataColumn(label: Text("id"), numeric: true), + DataColumn(label: Text("名称")), + // DataColumn(label: Text("目标路径")), + DataColumn(label: Text("是否完成")), + DataColumn(label: Text("后台操作")), + DataColumn(label: Text("操作")) + ], + rows: List.generate(activities.length, (i) { + var activity = activities[i]; + + return DataRow(cells: [ + DataCell(Text("${activity.id}")), + DataCell(Text("${activity.sourceTitle}")), + //DataCell(Text("${activity.targetDir}")), + DataCell(Text("${activity.completed}")), + DataCell(Text("${activity.inBackgroud}")), + DataCell(IconButton( + onPressed: () { + ref + .read(activitiesDataProvider.notifier) + .deleteActivity(activity.id!); + }, + icon: const Icon(Icons.delete))) + ]); + }), + ), + ); + }, + error: (err, trace) => Text("$err"), + loading: () => const Center( + child: SizedBox( + width: 30, height: 30, child: CircularProgressIndicator()))); } - -} \ No newline at end of file +} diff --git a/ui/lib/main.dart b/ui/lib/main.dart index 7fdf953..95b8b90 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -1,6 +1,8 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:ui/activity.dart'; import 'package:ui/login_page.dart'; import 'package:ui/navdrawer.dart'; import 'package:ui/providers/APIs.dart'; @@ -34,12 +36,19 @@ class MyApp extends StatelessWidget { backgroundColor: Theme.of(context).colorScheme.inversePrimary, // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. - title: const Text("Polaris追剧"), + title: Row( + children: [ + const Text("Polaris 追剧"), + const SizedBox( + width: 100, + ), + IconButton( + tooltip: "搜索剧集", + onPressed: () => context.go(SearchPage.route), + icon: const Icon(Icons.search)), + ], + ), actions: [ - IconButton( - tooltip: "搜索剧集", - onPressed: () => context.go(SearchPage.route), - icon: const Icon(Icons.search)), IconButton( onPressed: () => context.go(SystemSettingsPage.route), icon: const Icon(Icons.settings)) @@ -77,6 +86,10 @@ class MyApp extends StatelessWidget { builder: (context, state) => TvDetailsPage(seriesId: state.pathParameters['id']!), ), + GoRoute( + path: ActivityPage.route, + builder: (context, state) => ActivityPage(), + ) ], ); @@ -87,7 +100,7 @@ class MyApp extends StatelessWidget { _shellRoute, GoRoute( path: LoginScreen.route, - builder: (context, state) =>const LoginScreen(), + builder: (context, state) => const LoginScreen(), ) ], ); diff --git a/ui/lib/navdrawer.dart b/ui/lib/navdrawer.dart index 4fa32f8..c4f693a 100644 --- a/ui/lib/navdrawer.dart +++ b/ui/lib/navdrawer.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:ui/activity.dart'; import 'package:ui/search.dart'; import 'package:ui/system_settings.dart'; import 'package:ui/weclome.dart'; @@ -34,7 +35,7 @@ class _NavDrawerState extends State { if (value == 0) { context.go(WelcomePage.route); } else if (value == 1) { - context.go(SearchPage.route); + context.go(ActivityPage.route); } else if (value == 2) { context.go(SystemSettingsPage.route); } diff --git a/ui/lib/providers/APIs.dart b/ui/lib/providers/APIs.dart index 74b3bba..f266d2e 100644 --- a/ui/lib/providers/APIs.dart +++ b/ui/lib/providers/APIs.dart @@ -20,6 +20,7 @@ class APIs { static final storageUrl = "$_baseUrl/api/v1/storage/"; static final loginUrl = "$_baseUrl/api/login"; static final loginSettingUrl = "$_baseUrl/api/v1/setting/auth"; + static final activityUrl = "$_baseUrl/api/v1/activity/"; static const tmdbImgBaseUrl = "https://image.tmdb.org/t/p/w500/"; diff --git a/ui/lib/providers/activity.dart b/ui/lib/providers/activity.dart new file mode 100644 index 0000000..319f04d --- /dev/null +++ b/ui/lib/providers/activity.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ui/providers/APIs.dart'; +import 'package:ui/providers/server_response.dart'; + +var activitiesDataProvider = AsyncNotifierProvider.autoDispose>( + ActivityData.new); + +class ActivityData extends AutoDisposeAsyncNotifier> { + @override + FutureOr> build() async { + final dio = await APIs.getDio(); + var resp = await dio.get(APIs.activityUrl); + 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; + } + + Future deleteActivity(int id) async { + final dio = await APIs.getDio(); + var resp = await dio.delete("${APIs.activityUrl}$id"); + final sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + ref.invalidateSelf(); + } +} + +class Activity { + Activity({ + required this.id, + required this.seriesId, + required this.episodeId, + required this.sourceTitle, + required this.date, + required this.targetDir, + required this.completed, + required this.saved, + required this.inBackgroud, + }); + + final int? id; + final int? seriesId; + final int? episodeId; + final String? sourceTitle; + final DateTime? date; + final String? targetDir; + final bool? completed; + final String? saved; + final bool? inBackgroud; + + factory Activity.fromJson(Map json){ + return Activity( + id: json["id"], + seriesId: json["series_id"], + episodeId: json["episode_id"], + sourceTitle: json["source_title"], + date: DateTime.tryParse(json["date"] ?? ""), + targetDir: json["target_dir"], + completed: json["completed"], + saved: json["saved"], + inBackgroud: json["in_backgroud"], + ); + } + +}