Merge PR #1536 into 2.0.0-beta.2

# Conflicts:
#	ui/flutter/test/widget_test.dart
This commit is contained in:
Levi committed 2026-09-25 16:23:55 +08:00
commit a5ebbca318
30 files changed
+112 -22

No files matched your search

+10
View File
@@ -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 {
+2
View File
@@ -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);
+2
View File
@@ -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,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,
+1
View File
@@ -246,6 +246,7 @@
"refresh": "تحديث",
"status": "الحالة",
"createdAt": "وقت الإنشاء",
"doneAt": "وقت الإكتمال",
"speed": "السرعة",
"uploadSpeed": "سرعة التشغيل",
"remaining": "البقايا",
+1
View File
@@ -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",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "Refresca",
"status": "Estat",
"createdAt": "Data de creació",
"doneAt": "Data de finalització",
"speed": "Velocitat",
"uploadSpeed": "Velocitat de pujada",
"remaining": "Restant",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "Aktualisieren",
"status": "-Status",
"createdAt": "Erstellungszeit",
"doneAt": "Abgeschlossen am",
"speed": "Geschwindigkeit",
"uploadSpeed": "Upload-Geschwindigkeit",
"remaining": "Verbleibend",
+1
View File
@@ -268,6 +268,7 @@
"refresh": "Refresh",
"status": "Status",
"createdAt": "Created",
"doneAt": "Completed at",
"speed": "Speed",
"uploadSpeed": "Upload speed",
"remaining": "Remaining",
+1
View File
@@ -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",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "تازه‌سازی",
"status": "وضعیت",
"createdAt": "زمان ایجاد",
"doneAt": "زمان تکمیل",
"speed": "سرعت",
"uploadSpeed": "سرعت آپلود",
"remaining": "باقی مانده",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "Segarkan",
"status": "Status",
"createdAt": "Waktu dibuat",
"doneAt": "Waktu selesai",
"speed": "Kecepatan",
"uploadSpeed": "Kecepatan pemutakhiran",
"remaining": "Sisa",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "Aggiorna",
"status": "Stato",
"createdAt": "Data di creazione",
"doneAt": "Data di completamento",
"speed": "Velocità",
"uploadSpeed": "Velocità di carico",
"remaining": "Rimanere",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "リフレッシュ",
"status": "ステータス",
"createdAt": "作成日時",
"doneAt": "完了日時",
"speed": "スピード",
"uploadSpeed": "アップロード速度",
"remaining": "メンテナンス",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "의붓기",
"status": "상태",
"createdAt": "생성 시간",
"doneAt": "완료 시간",
"speed": "속도",
"uploadSpeed": "연결 속도",
"remaining": "의논하기",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "освежиться",
"status": "Статус",
"createdAt": "Время создания",
"doneAt": "Время завершения",
"speed": "Скорость",
"uploadSpeed": "Скорость загрузки",
"remaining": "оставаться",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "புதுப்பி",
"status": "நிலை",
"createdAt": "உருவாக்கிய நேரம்",
"doneAt": "முடிந்த நேரம்",
"speed": "வேகம்",
"uploadSpeed": "வேகம்",
"remaining": "மீதம்",
+1
View File
@@ -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",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "Реверс",
"status": "Контакты",
"createdAt": "Час створення",
"doneAt": "Час завершення",
"speed": "ОДЯГ",
"uploadSpeed": "Швидкість завантаження",
"remaining": "Ременування",
+1
View File
@@ -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",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "刷新",
"status": "状态",
"createdAt": "创建时间",
"doneAt": "完成时间",
"speed": "速度",
"uploadSpeed": "上传速度",
"remaining": "剩余时间",
+1
View File
@@ -246,6 +246,7 @@
"refresh": "重新整理",
"status": "狀態",
"createdAt": "建立時間",
"doneAt": "完成時間",
"speed": "速度",
"uploadSpeed": "上傳速度",
"remaining": "剩餘時間",
+22 -21
View File
@@ -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'),
+34
View File
@@ -4129,6 +4129,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(
@@ -4199,7 +4200,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);
@@ -4261,6 +4269,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(
@@ -6166,6 +6198,7 @@ TaskRecord _taskRecord({
String? remaining,
Duration? downloadDuration,
DateTime? createdAt,
DateTime? doneAt,
bool isFolder = false,
Map<String, String> requestHeaders = const {},
}) {
@@ -6186,6 +6219,7 @@ TaskRecord _taskRecord({
remaining: remaining,
downloadDuration: downloadDuration,
createdAt: createdAt,
doneAt: doneAt,
requestHeaders: requestHeaders,
);
}