diff --git a/lib/api/task.dart b/lib/api/task.dart index a5c1727..05085c2 100644 --- a/lib/api/task.dart +++ b/lib/api/task.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:json_annotation/json_annotation.dart'; part 'task.g.dart'; @@ -10,7 +12,21 @@ enum TaskType { @JsonValue(2) updateMeiliSearchData, @JsonValue(3) - fixGalleryPage, + fixGalleryPage; + + String text(BuildContext context) { + final i18n = AppLocalizations.of(context)!; + switch (this) { + case TaskType.download: + return i18n.downloadTask; + case TaskType.exportZip: + return i18n.exportZipTask; + case TaskType.updateMeiliSearchData: + return i18n.updateMeiliSearchDataTask; + case TaskType.fixGalleryPage: + return i18n.fixGalleryPageTask; + } + } } @JsonSerializable() @@ -189,7 +205,21 @@ enum TaskStatus { @JsonValue(2) finished, @JsonValue(3) - failed, + failed; + + String text(BuildContext context) { + final i18n = AppLocalizations.of(context)!; + switch (this) { + case TaskStatus.wait: + return i18n.waiting; + case TaskStatus.running: + return i18n.running; + case TaskStatus.finished: + return i18n.finished; + case TaskStatus.failed: + return i18n.failed; + } + } } class TaskDetail { @@ -237,7 +267,7 @@ class TaskError { @JsonSerializable() class DownloadConfig { - DownloadConfig ({ + DownloadConfig({ this.maxDownloadImgCount, this.mpv, this.downloadOriginalImg, diff --git a/lib/components/task.dart b/lib/components/task.dart index 5b0ed05..ead0ec5 100644 --- a/lib/components/task.dart +++ b/lib/components/task.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:go_router/go_router.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; import '../api/task.dart'; import '../globals.dart'; @@ -83,27 +84,31 @@ class _TaskView extends State { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(8.0), - child: GestureDetector( - onTap: () {}, - child: Row(children: [ - Expanded( + child: Row(children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: ReorderableDragStartListener( + index: widget.index, child: const Icon(Icons.reorder))), + Expanded( + child: GestureDetector( + onTap: () { + context.push("/dialog/task/${widget.task.base.id}"); + }, child: Column(children: [ - _buildText(context), - LinearPercentIndicator( - animation: true, - animateFromLastPercent: true, - progressColor: Colors.green, - lineHeight: 20.0, - barRadius: const Radius.circular(10), - padding: EdgeInsets.zero, - center: Text(percentText, - style: const TextStyle(color: Colors.black)), - percent: percent, - ), - ])), - ReorderableDragStartListener( - index: widget.index, child: const Icon(Icons.reorder)), - ])), + _buildText(context), + LinearPercentIndicator( + animation: true, + animateFromLastPercent: true, + progressColor: Colors.green, + lineHeight: 20.0, + barRadius: const Radius.circular(10), + padding: EdgeInsets.zero, + center: Text(percentText, + style: const TextStyle(color: Colors.black)), + percent: percent, + ), + ]))), + ]), ); } } diff --git a/lib/dialog/new_download_task_page.dart b/lib/dialog/new_download_task_page.dart index 64640be..9e226b3 100644 --- a/lib/dialog/new_download_task_page.dart +++ b/lib/dialog/new_download_task_page.dart @@ -1,9 +1,9 @@ import 'package:dio/dio.dart'; -import 'package:eh_downloader_flutter/components/number_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; +import '../components/number_field.dart'; import '../globals.dart'; import '../utils/parse_url.dart'; diff --git a/lib/dialog/task_page.dart b/lib/dialog/task_page.dart new file mode 100644 index 0000000..6348b65 --- /dev/null +++ b/lib/dialog/task_page.dart @@ -0,0 +1,261 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:go_router/go_router.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; +import '../api/task.dart'; +import '../globals.dart'; +import '../utils/filesize.dart'; + +class _KeyValue extends StatelessWidget { + const _KeyValue(this.name, this.value, {this.fontSize}); + final String name; + final String value; + final double? fontSize; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Row(children: [ + SizedBox( + width: 80, + child: Center( + child: Text(name, + textAlign: TextAlign.center, + style: TextStyle(color: cs.primary, fontSize: fontSize)))), + Expanded( + child: SelectableText(value, + style: TextStyle(color: cs.secondary, fontSize: fontSize)), + ) + ]); + } +} + +class TaskPage extends StatefulWidget { + const TaskPage(this.id, {super.key}); + final int id; + + @override + State createState() => _TaskPage(); +} + +class _TaskPage extends State { + void _onStateChanged(dynamic _) { + setState(() {}); + } + + void _onProgressUpdated(dynamic arg) { + final id = arg as int; + if (id != widget.id) return; + setState(() {}); + } + + @override + void initState() { + listener.on("task_list_changed", _onStateChanged); + listener.on("task_meta_updated", _onStateChanged); + listener.on("task_progress_updated", _onProgressUpdated); + super.initState(); + } + + @override + void dispose() { + listener.removeEventListener("task_list_changed", _onStateChanged); + listener.removeEventListener("task_meta_updated", _onStateChanged); + listener.removeEventListener("task_progress_updated", _onProgressUpdated); + super.dispose(); + } + + Widget _buildBasicInfo(BuildContext context) { + if (!tasks.tasksList.contains(widget.id)) return Container(); + final i18n = AppLocalizations.of(context)!; + final task = tasks.tasks[widget.id]!; + final typ = task.base.type; + String gid = ""; + if (task.base.gid != 0) { + gid = task.base.gid.toString(); + } + if (task.base.gid == 0 && + (typ == TaskType.fixGalleryPage || + typ == TaskType.updateMeiliSearchData)) { + gid = i18n.allGalleries; + } + return Column( + children: [ + _KeyValue(i18n.taskId, widget.id.toString(), fontSize: 16), + _KeyValue(i18n.taskType, typ.text(context), fontSize: 16), + _KeyValue(i18n.gid, gid, fontSize: 16), + task.base.token.isEmpty + ? Container() + : _KeyValue(i18n.galleryToken, task.base.token, fontSize: 16), + _KeyValue(i18n.processId, task.base.pid.toString(), fontSize: 16), + _KeyValue(i18n.taskStatus, task.status.text(context), fontSize: 16), + task.fataled == null + ? Container() + : _KeyValue(i18n.fatalError, task.fataled! ? i18n.yes : i18n.no, + fontSize: 16), + task.error == null + ? Container() + : SelectableText(task.error!, + style: const TextStyle(color: Colors.red)), + ], + ); + } + + bool get haveProgress => tasks.tasksList.contains(widget.id) + ? tasks.tasks[widget.id]!.status == TaskStatus.running && + tasks.tasks[widget.id]!.progress != null + : false; + + Widget _buildProgress(BuildContext context) { + if (!haveProgress) return Container(); + final task = tasks.tasks[widget.id]!; + final typ = task.base.type; + if (typ == TaskType.download) { + final p = task.progress as TaskDownloadProgess; + final i18n = AppLocalizations.of(context)!; + if (p.totalPage == 0) { + return Text(i18n.fetchingMetadata); + } + if (p.failedPage == 0) { + final percent = p.downloadedPage / p.totalPage; + final percentText = "${(percent * 100).toStringAsFixed(2)}%"; + return Row(children: [ + Expanded( + child: LinearPercentIndicator( + animation: true, + animateFromLastPercent: true, + progressColor: Colors.green, + lineHeight: 20.0, + barRadius: const Radius.circular(10), + padding: EdgeInsets.zero, + center: + Text(percentText, style: const TextStyle(color: Colors.black)), + percent: percent, + )), + Text("${p.downloadedPage}/${p.totalPage}"), + ]); + } + return Column(children: [ + _KeyValue(i18n.downloadedPages, p.downloadedPage.toString(), + fontSize: 16), + _KeyValue(i18n.failedPages, p.failedPage.toString(), fontSize: 16), + _KeyValue(i18n.totalPages, p.totalPage.toString(), fontSize: 16), + ]); + } + int now = 0; + int total = 0; + switch (typ) { + case TaskType.exportZip: + final p = task.progress as TaskExportZipProgress; + now = p.addedPage; + total = p.totalPage; + case TaskType.fixGalleryPage: + final p = task.progress as TaskFixGalleryPageProgress; + now = p.checkedGallery; + total = p.totalGallery; + case TaskType.updateMeiliSearchData: + final p = task.progress as TaskUpdateMeiliSearchDataProgress; + now = p.updatedGallery; + total = p.totalGallery; + default: + } + if (total == 0) return Container(); + final percent = now / total; + final percentText = "${(percent * 100).toStringAsFixed(2)}%"; + return Row(children: [ + Expanded( + child: LinearPercentIndicator( + animation: true, + animateFromLastPercent: true, + progressColor: Colors.green, + lineHeight: 20.0, + barRadius: const Radius.circular(10), + padding: EdgeInsets.zero, + center: Text(percentText, style: const TextStyle(color: Colors.black)), + percent: percent, + )), + Text("$now/$total"), + ]); + } + + Widget _buildMoreProgress(BuildContext context) { + if (!haveProgress) return SliverToBoxAdapter(child: Container()); + final task = tasks.tasks[widget.id]!; + if (task.base.type != TaskType.download) { + return SliverToBoxAdapter(child: Container()); + } + final p = task.progress as TaskDownloadProgess; + if (p.details.isEmpty) return SliverToBoxAdapter(child: Container()); + return SliverList.builder( + itemCount: p.details.length, + itemBuilder: (context, index) { + final d = p.details[index]; + final percent = d.downloaded / d.total; + final percentText = "${(percent * 100).toStringAsFixed(2)}%"; + return Column(children: [ + Text("${d.name}(${d.width}x${d.height})"), + Row(children: [ + Expanded( + child: LinearPercentIndicator( + animation: true, + animateFromLastPercent: true, + progressColor: Colors.green, + lineHeight: 20.0, + barRadius: const Radius.circular(10), + padding: EdgeInsets.zero, + center: Text(percentText, + style: const TextStyle(color: Colors.black)), + percent: percent, + )), + Text("${getFileSize(d.downloaded)}/${getFileSize(d.total)}"), + ]), + ]); + }, + ); + } + + @override + Widget build(BuildContext context) { + tryInitApi(context); + final i18n = AppLocalizations.of(context)!; + final maxWidth = MediaQuery.of(context).size.width; + final indent = maxWidth < 400 ? 5.0 : 10.0; + return Container( + padding: maxWidth < 400 + ? const EdgeInsets.symmetric(vertical: 20, horizontal: 5) + : const EdgeInsets.all(20), + width: maxWidth < 810 ? null : 800, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(10)), + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Stack( + alignment: Alignment.center, + children: [ + Text( + i18n.taskDetails, + style: Theme.of(context).textTheme.headlineSmall, + ), + Align( + alignment: Alignment.centerRight, + child: IconButton( + onPressed: () => context.canPop() + ? context.pop() + : context.go("/task_manager"), + icon: const Icon(Icons.close), + )), + ], + ), + ), + SliverToBoxAdapter(child: _buildBasicInfo(context)), + SliverToBoxAdapter( + child: haveProgress + ? Divider(indent: indent, endIndent: indent) + : Container()), + SliverToBoxAdapter(child: _buildProgress(context)), + _buildMoreProgress(context), + ], + ), + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0d527d9..4947c1e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -156,5 +156,19 @@ "galleryURL": "Gallery URL", "galleryToken": "Gallery Token", "randomFileSecret": "The secret of token to access random file without login", - "downloadTask": "Download Task" + "downloadTask": "Download Task", + "taskDetails": "Task details", + "taskId": "Task ID", + "taskType": "Task type", + "exportZipTask": "Export as ZIP file task", + "updateMeiliSearchDataTask": "Sync meilisearch server's data task", + "fixGalleryPageTask": "Fix gallery page data task", + "allGalleries": "All galleries", + "processId": "Process ID", + "taskStatus": "Task status", + "fatalError": "Fatal error", + "fetchingMetadata": "Fetching metadata ...", + "downloadedPages": "Downloaded pages", + "failedPages": "Download failed pages", + "totalPages": "Total pages" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6f20bac..233e060 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -156,5 +156,19 @@ "galleryURL": "画廊地址", "galleryToken": "画廊令牌", "randomFileSecret": "生成无需登录即可访问随机文件的令牌的密钥", - "downloadTask": "下载任务" + "downloadTask": "下载任务", + "taskDetails": "任务详情", + "taskId": "任务ID", + "taskType": "任务类型", + "exportZipTask": "导出为ZIP文件任务", + "updateMeiliSearchDataTask": "同步meilisearch服务器数据任务", + "fixGalleryPageTask": "修复画廊页面数据任务", + "allGalleries": "所有画廊", + "processId": "进程ID", + "taskStatus": "任务状态", + "fatalError": "致命错误", + "fetchingMetadata": "获取元数据中…", + "downloadedPages": "已下载页数", + "failedPages": "下载失败的页数", + "totalPages": "总页数" } diff --git a/lib/main.dart b/lib/main.dart index d135338..f383b23 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ import 'dialog/dialog_page.dart'; import 'dialog/download_zip_page.dart'; import 'dialog/gallery_details_page.dart'; import 'dialog/new_download_task_page.dart'; +import 'dialog/task_page.dart'; import 'galleries.dart'; import 'gallery.dart'; import 'globals.dart'; @@ -183,7 +184,25 @@ final _router = GoRouter( builder: (context) { return NewDownloadTaskPage(gid: gid, token: token); }); - }) + }), + GoRoute( + path: "/dialog/task/:id", + pageBuilder: (context, state) { + return DialogPage( + key: state.pageKey, + builder: (context) { + return TaskPage(int.parse(state.pathParameters["id"]!)); + }); + }, + redirect: (context, state) { + try { + int.parse(state.pathParameters["id"]!); + return null; + } catch (e) { + _routerLog.warning("Failed to parse id:", e); + return "/task_manager"; + } + }), ], );