diff --git a/pkg/download/model.go b/pkg/download/model.go index c838cf31..bf479a0b 100644 --- a/pkg/download/model.go +++ b/pkg/download/model.go @@ -32,6 +32,10 @@ type Task struct { Progress *Progress `json:"progress"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` + // DoneAt is the time when the task completed; nil if it has never + // finished. A done task never leaves the done state, so the value is + // written once and kept. + DoneAt *time.Time `json:"doneAt"` producer production.Source producerReceived int64 @@ -134,6 +138,12 @@ func (t *Task) MarshalJSON() ([]byte, error) { func (t *Task) updateStatus(status base.Status) { t.UpdatedAt = time.Now() t.Status = status + // A done task never leaves the done state, so there is no branch to clear + // DoneAt on other transitions. + if status == base.DownloadStatusDone { + doneAt := time.Now() + t.DoneAt = &doneAt + } } func (t *Task) clone() *Task { diff --git a/ui/flutter/lib/api/model/task.dart b/ui/flutter/lib/api/model/task.dart index fd08e781..e72c6aa2 100644 --- a/ui/flutter/lib/api/model/task.dart +++ b/ui/flutter/lib/api/model/task.dart @@ -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 json) => _$TaskFromJson(json); diff --git a/ui/flutter/lib/api/model/task.g.dart b/ui/flutter/lib/api/model/task.g.dart index f127beee..d394703f 100644 --- a/ui/flutter/lib/api/model/task.g.dart +++ b/ui/flutter/lib/api/model/task.g.dart @@ -15,6 +15,7 @@ Task _$TaskFromJson(Map json) => Task( progress: Progress.fromJson(json['progress'] as Map), 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 _$TaskToJson(Task instance) => { @@ -27,6 +28,7 @@ Map _$TaskToJson(Task instance) => { 'progress': instance.progress.toJson(), 'createdAt': instance.createdAt.toIso8601String(), 'updatedAt': instance.updatedAt.toIso8601String(), + 'doneAt': ?instance.doneAt?.toIso8601String(), }; const _$StatusEnumMap = { diff --git a/ui/flutter/lib/features/tasks/domain/task_record.dart b/ui/flutter/lib/features/tasks/domain/task_record.dart index bd066ddb..85a10cb1 100644 --- a/ui/flutter/lib/features/tasks/domain/task_record.dart +++ b/ui/flutter/lib/features/tasks/domain/task_record.dart @@ -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, diff --git a/ui/flutter/lib/features/tasks/presentation/widgets/task_drawer.dart b/ui/flutter/lib/features/tasks/presentation/widgets/task_drawer.dart index 49de9689..c668ff9b 100644 --- a/ui/flutter/lib/features/tasks/presentation/widgets/task_drawer.dart +++ b/ui/flutter/lib/features/tasks/presentation/widgets/task_drawer.dart @@ -319,6 +319,14 @@ 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.status == TaskStatus.completed) + _InfoBlock( + label: context.l10n.doneAt, + value: switch (task.doneAt) { + final doneAt? => _formatTaskDateTime(doneAt), + _ => '—', + }, + ), Container( key: const ValueKey('task-details-info-divider'), height: 1, diff --git a/ui/flutter/lib/l10n/app_ar.arb b/ui/flutter/lib/l10n/app_ar.arb index a450ab56..f25ea37b 100644 --- a/ui/flutter/lib/l10n/app_ar.arb +++ b/ui/flutter/lib/l10n/app_ar.arb @@ -246,6 +246,7 @@ "refresh": "تحديث", "status": "الحالة", "createdAt": "وقت الإنشاء", + "doneAt": "وقت الإكتمال", "speed": "السرعة", "uploadSpeed": "سرعة التشغيل", "remaining": "البقايا", diff --git a/ui/flutter/lib/l10n/app_az.arb b/ui/flutter/lib/l10n/app_az.arb index d4f297d5..4a74c52f 100644 --- a/ui/flutter/lib/l10n/app_az.arb +++ b/ui/flutter/lib/l10n/app_az.arb @@ -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", diff --git a/ui/flutter/lib/l10n/app_ca.arb b/ui/flutter/lib/l10n/app_ca.arb index 86185834..a1ddc641 100644 --- a/ui/flutter/lib/l10n/app_ca.arb +++ b/ui/flutter/lib/l10n/app_ca.arb @@ -246,6 +246,7 @@ "refresh": "Refresca", "status": "Estat", "createdAt": "Data de creació", + "doneAt": "Data de finalització", "speed": "Velocitat", "uploadSpeed": "Velocitat de pujada", "remaining": "Restant", diff --git a/ui/flutter/lib/l10n/app_de.arb b/ui/flutter/lib/l10n/app_de.arb index c0c97ad8..81f3fc7f 100644 --- a/ui/flutter/lib/l10n/app_de.arb +++ b/ui/flutter/lib/l10n/app_de.arb @@ -246,6 +246,7 @@ "refresh": "Aktualisieren", "status": "-Status", "createdAt": "Erstellungszeit", + "doneAt": "Abgeschlossen am", "speed": "Geschwindigkeit", "uploadSpeed": "Upload-Geschwindigkeit", "remaining": "Verbleibend", diff --git a/ui/flutter/lib/l10n/app_en.arb b/ui/flutter/lib/l10n/app_en.arb index a241bbfd..883bed0c 100644 --- a/ui/flutter/lib/l10n/app_en.arb +++ b/ui/flutter/lib/l10n/app_en.arb @@ -268,6 +268,7 @@ "refresh": "Refresh", "status": "Status", "createdAt": "Created", + "doneAt": "Completed at", "speed": "Speed", "uploadSpeed": "Upload speed", "remaining": "Remaining", diff --git a/ui/flutter/lib/l10n/app_es.arb b/ui/flutter/lib/l10n/app_es.arb index 3beff20c..002c8597 100644 --- a/ui/flutter/lib/l10n/app_es.arb +++ b/ui/flutter/lib/l10n/app_es.arb @@ -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", diff --git a/ui/flutter/lib/l10n/app_fa.arb b/ui/flutter/lib/l10n/app_fa.arb index 08664277..59a17a59 100644 --- a/ui/flutter/lib/l10n/app_fa.arb +++ b/ui/flutter/lib/l10n/app_fa.arb @@ -246,6 +246,7 @@ "refresh": "تازه‌سازی", "status": "وضعیت", "createdAt": "زمان ایجاد", + "doneAt": "زمان تکمیل", "speed": "سرعت", "uploadSpeed": "سرعت آپلود", "remaining": "باقی مانده", diff --git a/ui/flutter/lib/l10n/app_fr.arb b/ui/flutter/lib/l10n/app_fr.arb index 4c667ae7..66274439 100644 --- a/ui/flutter/lib/l10n/app_fr.arb +++ b/ui/flutter/lib/l10n/app_fr.arb @@ -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", diff --git a/ui/flutter/lib/l10n/app_hu.arb b/ui/flutter/lib/l10n/app_hu.arb index 7330fe85..d0fe3092 100644 --- a/ui/flutter/lib/l10n/app_hu.arb +++ b/ui/flutter/lib/l10n/app_hu.arb @@ -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", diff --git a/ui/flutter/lib/l10n/app_id.arb b/ui/flutter/lib/l10n/app_id.arb index c28ee0a7..9ad8eddd 100644 --- a/ui/flutter/lib/l10n/app_id.arb +++ b/ui/flutter/lib/l10n/app_id.arb @@ -246,6 +246,7 @@ "refresh": "Segarkan", "status": "Status", "createdAt": "Waktu dibuat", + "doneAt": "Waktu selesai", "speed": "Kecepatan", "uploadSpeed": "Kecepatan pemutakhiran", "remaining": "Sisa", diff --git a/ui/flutter/lib/l10n/app_it.arb b/ui/flutter/lib/l10n/app_it.arb index 0ae8fc65..0500ce26 100644 --- a/ui/flutter/lib/l10n/app_it.arb +++ b/ui/flutter/lib/l10n/app_it.arb @@ -246,6 +246,7 @@ "refresh": "Aggiorna", "status": "Stato", "createdAt": "Data di creazione", + "doneAt": "Data di completamento", "speed": "Velocità", "uploadSpeed": "Velocità di carico", "remaining": "Rimanere", diff --git a/ui/flutter/lib/l10n/app_ja.arb b/ui/flutter/lib/l10n/app_ja.arb index d85cb2bc..2544161c 100644 --- a/ui/flutter/lib/l10n/app_ja.arb +++ b/ui/flutter/lib/l10n/app_ja.arb @@ -246,6 +246,7 @@ "refresh": "リフレッシュ", "status": "ステータス", "createdAt": "作成日時", + "doneAt": "完了日時", "speed": "スピード", "uploadSpeed": "アップロード速度", "remaining": "メンテナンス", diff --git a/ui/flutter/lib/l10n/app_ko.arb b/ui/flutter/lib/l10n/app_ko.arb index 051a42f4..504b25b0 100644 --- a/ui/flutter/lib/l10n/app_ko.arb +++ b/ui/flutter/lib/l10n/app_ko.arb @@ -246,6 +246,7 @@ "refresh": "의붓기", "status": "상태", "createdAt": "생성 시간", + "doneAt": "완료 시간", "speed": "속도", "uploadSpeed": "연결 속도", "remaining": "의논하기", diff --git a/ui/flutter/lib/l10n/app_pl.arb b/ui/flutter/lib/l10n/app_pl.arb index bf86c584..4d512aa4 100644 --- a/ui/flutter/lib/l10n/app_pl.arb +++ b/ui/flutter/lib/l10n/app_pl.arb @@ -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", diff --git a/ui/flutter/lib/l10n/app_pt.arb b/ui/flutter/lib/l10n/app_pt.arb index b7f581e4..f93da09e 100644 --- a/ui/flutter/lib/l10n/app_pt.arb +++ b/ui/flutter/lib/l10n/app_pt.arb @@ -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", diff --git a/ui/flutter/lib/l10n/app_ru.arb b/ui/flutter/lib/l10n/app_ru.arb index aea0b6f5..57035b2b 100644 --- a/ui/flutter/lib/l10n/app_ru.arb +++ b/ui/flutter/lib/l10n/app_ru.arb @@ -246,6 +246,7 @@ "refresh": "освежиться", "status": "Статус", "createdAt": "Время создания", + "doneAt": "Время завершения", "speed": "Скорость", "uploadSpeed": "Скорость загрузки", "remaining": "оставаться", diff --git a/ui/flutter/lib/l10n/app_ta.arb b/ui/flutter/lib/l10n/app_ta.arb index a967ea8c..04193bd1 100644 --- a/ui/flutter/lib/l10n/app_ta.arb +++ b/ui/flutter/lib/l10n/app_ta.arb @@ -246,6 +246,7 @@ "refresh": "புதுப்பி", "status": "நிலை", "createdAt": "உருவாக்கிய நேரம்", + "doneAt": "முடிந்த நேரம்", "speed": "வேகம்", "uploadSpeed": "வேகம்", "remaining": "மீதம்", diff --git a/ui/flutter/lib/l10n/app_tr.arb b/ui/flutter/lib/l10n/app_tr.arb index 89be13ea..7c01725b 100644 --- a/ui/flutter/lib/l10n/app_tr.arb +++ b/ui/flutter/lib/l10n/app_tr.arb @@ -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", diff --git a/ui/flutter/lib/l10n/app_uk.arb b/ui/flutter/lib/l10n/app_uk.arb index d32d2701..65fc23a9 100644 --- a/ui/flutter/lib/l10n/app_uk.arb +++ b/ui/flutter/lib/l10n/app_uk.arb @@ -246,6 +246,7 @@ "refresh": "Реверс", "status": "Контакты", "createdAt": "Час створення", + "doneAt": "Час завершення", "speed": "ОДЯГ", "uploadSpeed": "Швидкість завантаження", "remaining": "Ременування", diff --git a/ui/flutter/lib/l10n/app_vi.arb b/ui/flutter/lib/l10n/app_vi.arb index db3e0c37..8cbd8393 100644 --- a/ui/flutter/lib/l10n/app_vi.arb +++ b/ui/flutter/lib/l10n/app_vi.arb @@ -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", diff --git a/ui/flutter/lib/l10n/app_zh.arb b/ui/flutter/lib/l10n/app_zh.arb index 0269904d..fed70a8d 100644 --- a/ui/flutter/lib/l10n/app_zh.arb +++ b/ui/flutter/lib/l10n/app_zh.arb @@ -246,6 +246,7 @@ "refresh": "刷新", "status": "状态", "createdAt": "创建时间", + "doneAt": "完成时间", "speed": "速度", "uploadSpeed": "上传速度", "remaining": "剩余时间", diff --git a/ui/flutter/lib/l10n/app_zh_TW.arb b/ui/flutter/lib/l10n/app_zh_TW.arb index 5ad3f7f7..9d8a4b94 100644 --- a/ui/flutter/lib/l10n/app_zh_TW.arb +++ b/ui/flutter/lib/l10n/app_zh_TW.arb @@ -246,6 +246,7 @@ "refresh": "重新整理", "status": "狀態", "createdAt": "建立時間", + "doneAt": "完成時間", "speed": "速度", "uploadSpeed": "上傳速度", "remaining": "剩餘時間", diff --git a/ui/flutter/macos/Podfile.lock b/ui/flutter/macos/Podfile.lock index 25728b41..ab2d2f29 100644 --- a/ui/flutter/macos/Podfile.lock +++ b/ui/flutter/macos/Podfile.lock @@ -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 diff --git a/ui/flutter/test/features/tasks/domain/task_record_test.dart b/ui/flutter/test/features/tasks/domain/task_record_test.dart index 17a8e4be..eba26e4f 100644 --- a/ui/flutter/test/features/tasks/domain/task_record_test.dart +++ b/ui/flutter/test/features/tasks/domain/task_record_test.dart @@ -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 selection, int size) => api.Task.fromJson({ + api.Task buildTask(List 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'), diff --git a/ui/flutter/test/widget_test.dart b/ui/flutter/test/widget_test.dart index 7269398e..c9427a11 100644 --- a/ui/flutter/test/widget_test.dart +++ b/ui/flutter/test/widget_test.dart @@ -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(['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); @@ -4259,6 +4267,30 @@ void main() { expect(find.text('—'), findsOneWidget); }); + testWidgets('completed task details show a dash when no done time is recorded', (WidgetTester tester) async { + await _setTestSize(tester, const Size(900, 700)); + final task = _taskRecord( + id: 'done-details', + name: 'done.zip', + status: TaskStatus.completed, + downloadDuration: const Duration(hours: 1, minutes: 2, seconds: 3), + ); + + await tester.pumpWidget( + ProviderScope( + child: shad.ShadcnApp( + theme: AppTheme.light(), + materialTheme: AppTheme.materialLight(), + home: TaskDetailsView(task: task, mobile: false, onOpenStorage: () {}, onUpdateUrl: (_) async {}), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Completed at'), findsOneWidget); + expect(find.text('—'), findsOneWidget); + }); + testWidgets('failed task details omit the unavailable error reason', (WidgetTester tester) async { await _setTestSize(tester, const Size(900, 700)); final task = TaskRecord( @@ -6164,6 +6196,7 @@ TaskRecord _taskRecord({ String? remaining, Duration? downloadDuration, DateTime? createdAt, + DateTime? doneAt, bool isFolder = false, Map requestHeaders = const {}, }) { @@ -6184,6 +6217,7 @@ TaskRecord _taskRecord({ remaining: remaining, downloadDuration: downloadDuration, createdAt: createdAt, + doneAt: doneAt, requestHeaders: requestHeaders, ); }