mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 16:40:32 +00:00
feat(task): record and display task completion time
Add DoneAt to the download task model: set when a task finishes and cleared when it is downloaded again. The value flows through the API model and task record, and is shown as "Completed at" in the task drawer info tab next to the creation time.
This commit is contained in:
1 parent
694b43e3f9
commit
4b231f52c5
30 files changed
+82
-23
No files matched your search
@@ -32,6 +32,9 @@ type Task struct {
|
||||
Progress *Progress `json:"progress"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
// DoneAt is the time when the task last completed; nil while the task has
|
||||
// never finished or is being downloaded again.
|
||||
DoneAt *time.Time `json:"doneAt"`
|
||||
|
||||
producer production.Source
|
||||
producerReceived int64
|
||||
@@ -134,6 +137,12 @@ func (t *Task) MarshalJSON() ([]byte, error) {
|
||||
func (t *Task) updateStatus(status base.Status) {
|
||||
t.UpdatedAt = time.Now()
|
||||
t.Status = status
|
||||
if status == base.DownloadStatusDone {
|
||||
doneAt := time.Now()
|
||||
t.DoneAt = &doneAt
|
||||
} else {
|
||||
t.DoneAt = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Task) clone() *Task {
|
||||
|
||||
@@ -35,6 +35,7 @@ class Task {
|
||||
Progress progress;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? doneAt;
|
||||
|
||||
Task({
|
||||
required this.id,
|
||||
@@ -45,6 +46,7 @@ class Task {
|
||||
required this.progress,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.doneAt,
|
||||
});
|
||||
|
||||
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
|
||||
|
||||
@@ -15,6 +15,7 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
doneAt: json['doneAt'] == null ? null : DateTime.parse(json['doneAt'] as String),
|
||||
)..protocol = $enumDecodeNullable(_$ProtocolEnumMap, json['protocol']);
|
||||
|
||||
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||
@@ -27,6 +28,7 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||
'progress': instance.progress.toJson(),
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'updatedAt': instance.updatedAt.toIso8601String(),
|
||||
'doneAt': ?instance.doneAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
const _$StatusEnumMap = {
|
||||
|
||||
@@ -76,6 +76,7 @@ class TaskRecord {
|
||||
this.remaining,
|
||||
this.downloadDuration,
|
||||
this.createdAt,
|
||||
this.doneAt,
|
||||
this.progress,
|
||||
this.error,
|
||||
this.completedLabel,
|
||||
@@ -97,6 +98,7 @@ class TaskRecord {
|
||||
final String? remaining;
|
||||
final Duration? downloadDuration;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? doneAt;
|
||||
final double? progress;
|
||||
final String url;
|
||||
final String storagePath;
|
||||
@@ -145,6 +147,7 @@ class TaskRecord {
|
||||
remainingSeconds: _remainingSeconds(task, totalBytes, downloadedBytes),
|
||||
downloadDuration: task.progress.used > 0 ? Duration(microseconds: (task.progress.used + 999) ~/ 1000) : null,
|
||||
createdAt: task.createdAt,
|
||||
doneAt: task.doneAt,
|
||||
waiting: task.status == api_task.Status.wait,
|
||||
progress: progress,
|
||||
url: displayUrl,
|
||||
|
||||
@@ -319,6 +319,8 @@ class _TaskInfoTab extends StatelessWidget {
|
||||
_InfoBlock(label: context.l10n.remaining, value: remaining),
|
||||
if (task.createdAt case final createdAt?)
|
||||
_InfoBlock(label: context.l10n.createdAt, value: _formatTaskDateTime(createdAt)),
|
||||
if (task.doneAt case final doneAt?)
|
||||
_InfoBlock(label: context.l10n.doneAt, value: _formatTaskDateTime(doneAt)),
|
||||
Container(
|
||||
key: const ValueKey('task-details-info-divider'),
|
||||
height: 1,
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "تحديث",
|
||||
"status": "الحالة",
|
||||
"createdAt": "وقت الإنشاء",
|
||||
"doneAt": "وقت الإكتمال",
|
||||
"speed": "السرعة",
|
||||
"uploadSpeed": "سرعة التشغيل",
|
||||
"remaining": "البقايا",
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"refresh": "Yenilə",
|
||||
"status": "Vəziyyət",
|
||||
"createdAt": "Yaradılıb",
|
||||
"doneAt": "Tamamlanıb",
|
||||
"speed": "Sürət",
|
||||
"uploadSpeed": "Yükləmə sürəti",
|
||||
"remaining": "Qalıb",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Refresca",
|
||||
"status": "Estat",
|
||||
"createdAt": "Data de creació",
|
||||
"doneAt": "Data de finalització",
|
||||
"speed": "Velocitat",
|
||||
"uploadSpeed": "Velocitat de pujada",
|
||||
"remaining": "Restant",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Aktualisieren",
|
||||
"status": "-Status",
|
||||
"createdAt": "Erstellungszeit",
|
||||
"doneAt": "Abgeschlossen am",
|
||||
"speed": "Geschwindigkeit",
|
||||
"uploadSpeed": "Upload-Geschwindigkeit",
|
||||
"remaining": "Verbleibend",
|
||||
|
||||
@@ -268,6 +268,7 @@
|
||||
"refresh": "Refresh",
|
||||
"status": "Status",
|
||||
"createdAt": "Created",
|
||||
"doneAt": "Completed at",
|
||||
"speed": "Speed",
|
||||
"uploadSpeed": "Upload speed",
|
||||
"remaining": "Remaining",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Actualizar",
|
||||
"status": "Situación",
|
||||
"createdAt": "Fecha de creación",
|
||||
"doneAt": "Fecha de finalización",
|
||||
"speed": "Velocidad",
|
||||
"uploadSpeed": "Velocidad de carga",
|
||||
"remaining": "Permaneciendo",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "تازهسازی",
|
||||
"status": "وضعیت",
|
||||
"createdAt": "زمان ایجاد",
|
||||
"doneAt": "زمان تکمیل",
|
||||
"speed": "سرعت",
|
||||
"uploadSpeed": "سرعت آپلود",
|
||||
"remaining": "باقی مانده",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Rafraîchir",
|
||||
"status": "État",
|
||||
"createdAt": "Date de création",
|
||||
"doneAt": "Terminé le",
|
||||
"speed": "Vitesse",
|
||||
"uploadSpeed": "Vitesse de chargement",
|
||||
"remaining": "Reste",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Frissítés",
|
||||
"status": "állapot",
|
||||
"createdAt": "Létrehozás ideje",
|
||||
"doneAt": "Befejezés ideje",
|
||||
"speed": "sebesség",
|
||||
"uploadSpeed": "Feltöltési sebesség",
|
||||
"remaining": "Maradt",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Segarkan",
|
||||
"status": "Status",
|
||||
"createdAt": "Waktu dibuat",
|
||||
"doneAt": "Waktu selesai",
|
||||
"speed": "Kecepatan",
|
||||
"uploadSpeed": "Kecepatan pemutakhiran",
|
||||
"remaining": "Sisa",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Aggiorna",
|
||||
"status": "Stato",
|
||||
"createdAt": "Data di creazione",
|
||||
"doneAt": "Data di completamento",
|
||||
"speed": "Velocità",
|
||||
"uploadSpeed": "Velocità di carico",
|
||||
"remaining": "Rimanere",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "リフレッシュ",
|
||||
"status": "ステータス",
|
||||
"createdAt": "作成日時",
|
||||
"doneAt": "完了日時",
|
||||
"speed": "スピード",
|
||||
"uploadSpeed": "アップロード速度",
|
||||
"remaining": "メンテナンス",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "의붓기",
|
||||
"status": "상태",
|
||||
"createdAt": "생성 시간",
|
||||
"doneAt": "완료 시간",
|
||||
"speed": "속도",
|
||||
"uploadSpeed": "연결 속도",
|
||||
"remaining": "의논하기",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Odśwież",
|
||||
"status": "Stan",
|
||||
"createdAt": "Czas utworzenia",
|
||||
"doneAt": "Czas ukończenia",
|
||||
"speed": "Prędkość",
|
||||
"uploadSpeed": "Prędkość wysyłania",
|
||||
"remaining": "Pozostały",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Actualizar",
|
||||
"status": "Estado",
|
||||
"createdAt": "Data de criação",
|
||||
"doneAt": "Data de conclusão",
|
||||
"speed": "Velocidade",
|
||||
"uploadSpeed": "Velocidade de envio",
|
||||
"remaining": "Restante",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "освежиться",
|
||||
"status": "Статус",
|
||||
"createdAt": "Время создания",
|
||||
"doneAt": "Время завершения",
|
||||
"speed": "Скорость",
|
||||
"uploadSpeed": "Скорость загрузки",
|
||||
"remaining": "оставаться",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "புதுப்பி",
|
||||
"status": "நிலை",
|
||||
"createdAt": "உருவாக்கிய நேரம்",
|
||||
"doneAt": "முடிந்த நேரம்",
|
||||
"speed": "வேகம்",
|
||||
"uploadSpeed": "வேகம்",
|
||||
"remaining": "மீதம்",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Yenile",
|
||||
"status": "Durum durumu",
|
||||
"createdAt": "Oluşturulma zamanı",
|
||||
"doneAt": "Tamamlanma zamanı",
|
||||
"speed": "Hız",
|
||||
"uploadSpeed": "Yük hızı",
|
||||
"remaining": "Kalanlar",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Реверс",
|
||||
"status": "Контакты",
|
||||
"createdAt": "Час створення",
|
||||
"doneAt": "Час завершення",
|
||||
"speed": "ОДЯГ",
|
||||
"uploadSpeed": "Швидкість завантаження",
|
||||
"remaining": "Ременування",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "Cập nhật",
|
||||
"status": "Trạng thái",
|
||||
"createdAt": "Thời gian tạo",
|
||||
"doneAt": "Thời gian hoàn thành",
|
||||
"speed": "Tốc độ",
|
||||
"uploadSpeed": "Tốc độ",
|
||||
"remaining": "Ở lại",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "刷新",
|
||||
"status": "状态",
|
||||
"createdAt": "创建时间",
|
||||
"doneAt": "完成时间",
|
||||
"speed": "速度",
|
||||
"uploadSpeed": "上传速度",
|
||||
"remaining": "剩余时间",
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"refresh": "重新整理",
|
||||
"status": "狀態",
|
||||
"createdAt": "建立時間",
|
||||
"doneAt": "完成時間",
|
||||
"speed": "速度",
|
||||
"uploadSpeed": "上傳速度",
|
||||
"remaining": "剩餘時間",
|
||||
|
||||
@@ -7,7 +7,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_picker (0.0.1):
|
||||
- file_picker_darwin (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- flutter_inappwebview_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
@@ -41,7 +42,7 @@ DEPENDENCIES:
|
||||
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
||||
- desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||
- file_picker_darwin (from `Flutter/ephemeral/.symlinks/plugins/file_picker_darwin/darwin`)
|
||||
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
|
||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
@@ -68,8 +69,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
file_picker:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||
file_picker_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker_darwin/darwin
|
||||
flutter_inappwebview_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
||||
flutter_local_notifications:
|
||||
@@ -96,25 +97,25 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
||||
desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43
|
||||
desktop_multi_window: 93667594ccc4b88d91a97972fd3b1b89667fa80a
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
|
||||
app_links: c3185399a5cabc2e610ee5ad52fb7269b84ff869
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_picker_darwin: 27e043831d68462d54f1c40e724b1f53c9d1e8be
|
||||
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
|
||||
flutter_local_notifications: 14e285ca39907db50704f7f46c9ab7a526bd7ead
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
open_dir_macos: 79810d7921d777f8732bf6358b3a0839a8fad9b5
|
||||
open_dir_macos: 9649018f88e71eb9fb8aa3d95caec30a73c08c5b
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
screen_retriever_macos: c5508cc3c66ff0d4db650480cf0ab691e220d933
|
||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
|
||||
uri_content: 5f98ff12ded3f2e1681b689828d747bafb03ba5f
|
||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||
window_manager: b729e31d38fb04905235df9ea896128991cad99e
|
||||
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||
screen_retriever_macos: 038d3f7b34a779b06f536842003e41e8ec1fb4b7
|
||||
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
|
||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||
tray_manager: c2a678ee19e2a094f0c2d03660ee2c3e81cea8ab
|
||||
uri_content: e002074bcf6fbf905a3909603f9a2b1b490fce67
|
||||
url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce
|
||||
window_manager: e25faf20d88283a0d46e7b1a759d07261ca27575
|
||||
|
||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||
|
||||
COCOAPODS: 1.17.0
|
||||
COCOAPODS: 1.15.2
|
||||
@@ -4,7 +4,7 @@ import 'package:gopeed/core/icons/gopeed_icons.dart';
|
||||
import 'package:gopeed/features/tasks/domain/task_record.dart';
|
||||
|
||||
void main() {
|
||||
api.Task buildTask(List<int> selection, int size) => api.Task.fromJson({
|
||||
api.Task buildTask(List<int> selection, int size, {String? doneAt}) => api.Task.fromJson({
|
||||
'id': 'bt-selection',
|
||||
'name': 'bundle',
|
||||
'protocol': 'bt',
|
||||
@@ -12,6 +12,7 @@ void main() {
|
||||
'uploading': false,
|
||||
'createdAt': '2026-01-01T00:00:00Z',
|
||||
'updatedAt': '2026-01-01T00:00:00Z',
|
||||
'doneAt': doneAt,
|
||||
'meta': {
|
||||
'req': {'url': 'magnet:?xt=urn:btih:test'},
|
||||
'opts': {'path': '/downloads', 'selectFiles': selection},
|
||||
@@ -43,6 +44,13 @@ void main() {
|
||||
expect(record.totalBytes, 60);
|
||||
});
|
||||
|
||||
test('completion time maps from api and stays null for legacy tasks', () {
|
||||
final completed = TaskRecord.fromApi(buildTask([], 60, doneAt: '2026-01-02T03:04:05Z'));
|
||||
expect(completed.doneAt, DateTime.parse('2026-01-02T03:04:05Z'));
|
||||
final legacy = TaskRecord.fromApi(buildTask([], 60));
|
||||
expect(legacy.doneAt, isNull);
|
||||
});
|
||||
|
||||
test('file names preserve hash and question mark characters for type detection', () {
|
||||
expect(
|
||||
taskFileTypeIcon('Slow English Podcast #2 _ Eating, Talking, and Learning Natural English.mp4'),
|
||||
|
||||
@@ -2649,7 +2649,7 @@ void main() {
|
||||
await tester.tap(find.byKey(const ValueKey('settings-language-select')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('English'), findsOneWidget);
|
||||
expect(SettingsLanguageSelect.supportedValues, hasLength(21));
|
||||
expect(SettingsLanguageSelect.supportedValues, hasLength(22));
|
||||
expect(SettingsLanguageSelect.supportedValues, containsAll(<String>['zh', 'zh_TW', 'pt']));
|
||||
expect(SettingsLanguageSelect.supportedValues, isNot(contains('zh_CN')));
|
||||
expect(SettingsLanguageSelect.supportedValues, isNot(contains('pt_BR')));
|
||||
@@ -4127,6 +4127,7 @@ void main() {
|
||||
remaining: '1 minute remaining',
|
||||
downloadDuration: const Duration(hours: 1, minutes: 2, seconds: 3),
|
||||
createdAt: DateTime(2026, 8, 27, 12, 34, 56),
|
||||
doneAt: DateTime(2026, 8, 28, 1, 2, 3),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -4197,7 +4198,14 @@ void main() {
|
||||
expect(find.text('50 B / 100 B'), findsNothing);
|
||||
expect(find.text('Created'), findsOneWidget);
|
||||
expect(find.text('2026-08-27 12:34:56'), findsOneWidget);
|
||||
expect(find.text('Completed at'), findsOneWidget);
|
||||
expect(find.text('2026-08-28 01:02:03'), findsOneWidget);
|
||||
expect(
|
||||
tester.getBottomLeft(find.text('2026-08-27 12:34:56')).dy,
|
||||
lessThan(tester.getTopLeft(find.text('Completed at')).dy),
|
||||
);
|
||||
final infoDivider = find.byKey(const ValueKey('task-details-info-divider'));
|
||||
expect(tester.getBottomLeft(find.text('Completed at')).dy, lessThan(tester.getTopLeft(infoDivider).dy));
|
||||
expect(tester.getBottomLeft(find.text('Created')).dy, lessThan(tester.getTopLeft(infoDivider).dy));
|
||||
expect(tester.getBottomLeft(infoDivider).dy, lessThan(tester.getTopLeft(find.text('Download Link')).dy));
|
||||
expect(find.text('1 minute remaining'), findsNothing);
|
||||
@@ -6164,6 +6172,7 @@ TaskRecord _taskRecord({
|
||||
String? remaining,
|
||||
Duration? downloadDuration,
|
||||
DateTime? createdAt,
|
||||
DateTime? doneAt,
|
||||
bool isFolder = false,
|
||||
Map<String, String> requestHeaders = const {},
|
||||
}) {
|
||||
@@ -6184,6 +6193,7 @@ TaskRecord _taskRecord({
|
||||
remaining: remaining,
|
||||
downloadDuration: downloadDuration,
|
||||
createdAt: createdAt,
|
||||
doneAt: doneAt,
|
||||
requestHeaders: requestHeaders,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user