Add task detail page

This commit is contained in:
2024-05-25 11:07:01 +08:00
parent 9b16aa3224
commit 3704233d3d
7 changed files with 370 additions and 27 deletions

View File

@@ -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,

View File

@@ -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<TaskView> {
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,
),
]))),
]),
);
}
}

View File

@@ -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';

261
lib/dialog/task_page.dart Normal file
View File

@@ -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<StatefulWidget> createState() => _TaskPage();
}
class _TaskPage extends State<TaskPage> {
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),
],
),
);
}
}

View File

@@ -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"
}

View File

@@ -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": "总页数"
}

View File

@@ -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";
}
}),
],
);