feat(tasks): group extension folder downloads (#1594)

This commit is contained in:
WuFeng authored and GitHub committed 2026-11-02 15:39:08 +08:00
1 parent fc0a1e0ef9
commit 1e36599d52
84 files changed
+6690 -881

No files matched your search

@@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart';
import '../../../../../core/utils/breakpoints.dart';
import '../../../../../shared/theme/app_design_tokens.dart';
import '../../../domain/task_record.dart';
import '../task_context_menu.dart';
import 'task_card_desktop.dart';
@@ -10,6 +11,8 @@ class TaskCard extends StatelessWidget {
const TaskCard({
super.key,
required this.task,
this.expanded = false,
this.nested = false,
required this.selected,
required this.batchMode,
required this.selectedInBatch,
@@ -25,6 +28,8 @@ class TaskCard extends StatelessWidget {
});
final TaskRecord task;
final bool expanded;
final bool nested;
final bool selected;
final bool batchMode;
final bool selectedInBatch;
@@ -42,40 +47,44 @@ class TaskCard extends StatelessWidget {
Widget build(BuildContext context) {
final isMobile = MediaQuery.sizeOf(context).width < Breakpoints.mobile;
if (isMobile) {
final card = TaskCardMobile(
task: task,
selected: selected,
batchMode: batchMode,
selectedInBatch: selectedInBatch,
onPressed: onPressed,
onToggleBatch: onToggleBatch,
onPause: onPause,
onResume: onResume,
onDelete: onDelete,
onOpen: onOpen,
onReveal: onReveal,
listeningForUpdate: listeningForUpdate,
);
final actions = contextActions;
return actions == null ? card : TaskContextMenu(task: task, actions: actions, child: card);
}
final card = TaskCardDesktop(
task: task,
selected: selected,
batchMode: batchMode,
selectedInBatch: selectedInBatch,
onPressed: onPressed,
onToggleBatch: onToggleBatch,
onPause: onPause,
onResume: onResume,
onDelete: onDelete,
onOpen: onOpen,
onReveal: onReveal,
listeningForUpdate: listeningForUpdate,
);
final card = isMobile
? TaskCardMobile(
task: task,
expanded: expanded,
selected: selected,
batchMode: batchMode,
selectedInBatch: selectedInBatch,
onPressed: onPressed,
onToggleBatch: onToggleBatch,
onPause: task.canPause ? onPause : null,
onResume: task.canResume ? onResume : null,
onDelete: onDelete,
onOpen: onOpen,
onReveal: onReveal,
listeningForUpdate: listeningForUpdate,
)
: TaskCardDesktop(
task: task,
expanded: expanded,
selected: selected,
batchMode: batchMode,
selectedInBatch: selectedInBatch,
onPressed: onPressed,
onToggleBatch: onToggleBatch,
onPause: task.canPause ? onPause : null,
onResume: task.canResume ? onResume : null,
onDelete: onDelete,
onOpen: onOpen,
onReveal: onReveal,
listeningForUpdate: listeningForUpdate,
);
final actions = contextActions;
return actions == null ? card : TaskContextMenu(task: task, actions: actions, child: card);
return Padding(
padding: EdgeInsetsDirectional.only(start: nested ? AppDesignTokens.space24 : 0),
child: Semantics(
expanded: task.isGroup ? expanded : null,
child: actions == null ? card : TaskContextMenu(task: task, actions: actions, child: card),
),
);
}
}
@@ -14,6 +14,7 @@ class TaskCardDesktop extends StatefulWidget {
const TaskCardDesktop({
super.key,
required this.task,
this.expanded = false,
required this.selected,
required this.batchMode,
required this.selectedInBatch,
@@ -28,6 +29,7 @@ class TaskCardDesktop extends StatefulWidget {
});
final TaskRecord task;
final bool expanded;
final bool selected;
final bool batchMode;
final bool selectedInBatch;
@@ -64,7 +66,7 @@ class _TaskCardDesktopState extends State<TaskCardDesktop> {
_lastTapTime = isDoubleTap ? null : now;
_lastTapPosition = isDoubleTap ? null : details.globalPosition;
widget.onPressed();
if (isDoubleTap) {
if (isDoubleTap && !widget.task.isGroup) {
widget.onOpen?.call();
}
}
@@ -134,7 +136,7 @@ class _TaskCardDesktopState extends State<TaskCardDesktop> {
)
: const SizedBox.shrink(),
),
TaskCardIcon(task: task),
TaskCardIcon(task: task, expanded: widget.expanded),
const SizedBox(width: 16),
Expanded(
child: SizedBox(
@@ -234,15 +236,21 @@ class _ProgressContent extends StatelessWidget {
}
List<Widget> _buildActionButtons(TaskRecord task, AppPalette palette, bool isDark) {
final actions = switch (task.status) {
TaskStatus.downloading => [(Icons.pause, onPause), (Icons.close, onDelete)],
TaskStatus.paused => [(Icons.play_arrow, onResume), (Icons.close, onDelete)],
TaskStatus.failed => [(Icons.refresh, onResume), (Icons.delete_outline, onDelete)],
TaskStatus.completed => [
if (AppWindowChrome.isDesktopWindow) (Icons.folder_open_outlined, null),
(Icons.delete_outline, onDelete),
],
};
final actions = task.isGroup && task.status != TaskStatus.completed
? [
if (task.canPause) (Icons.pause, onPause),
if (task.canResume) (Icons.play_arrow, onResume),
(Icons.close, onDelete),
]
: switch (task.status) {
TaskStatus.downloading => [(Icons.pause, onPause), (Icons.close, onDelete)],
TaskStatus.paused => [(Icons.play_arrow, onResume), (Icons.close, onDelete)],
TaskStatus.failed => [(Icons.refresh, onResume), (Icons.delete_outline, onDelete)],
TaskStatus.completed => [
if (AppWindowChrome.isDesktopWindow) (Icons.folder_open_outlined, null),
(Icons.delete_outline, onDelete),
],
};
return actions
.map(
@@ -13,6 +13,7 @@ class TaskCardMobile extends StatefulWidget {
const TaskCardMobile({
super.key,
required this.task,
this.expanded = false,
required this.selected,
required this.batchMode,
required this.selectedInBatch,
@@ -27,6 +28,7 @@ class TaskCardMobile extends StatefulWidget {
});
final TaskRecord task;
final bool expanded;
final bool selected;
final bool batchMode;
final bool selectedInBatch;
@@ -62,7 +64,7 @@ class _TaskCardMobileState extends State<TaskCardMobile> {
onExit: (_) => setState(() => _hovered = false),
child: GestureDetector(
onTap: widget.onPressed,
onDoubleTap: !widget.batchMode && task.status == TaskStatus.completed ? widget.onOpen : null,
onDoubleTap: !widget.batchMode && !task.isGroup && task.status == TaskStatus.completed ? widget.onOpen : null,
child: Container(
key: const ValueKey('task-card-mobile-surface'),
decoration: BoxDecoration(
@@ -117,7 +119,7 @@ class _TaskCardMobileState extends State<TaskCardMobile> {
)
: const SizedBox.shrink(),
),
TaskCardIcon(task: task),
TaskCardIcon(task: task, expanded: widget.expanded),
const SizedBox(width: 12),
Expanded(
child: Text(
@@ -168,15 +170,21 @@ class _TaskCardMobileState extends State<TaskCardMobile> {
}
List<Widget> _buildActionButtons(TaskRecord task, AppPalette palette, bool isDark) {
final actions = switch (task.status) {
TaskStatus.downloading => [(Icons.pause, widget.onPause), (Icons.close, widget.onDelete)],
TaskStatus.paused => [(Icons.play_arrow, widget.onResume), (Icons.close, widget.onDelete)],
TaskStatus.failed => [(Icons.refresh, widget.onResume), (Icons.delete_outline, widget.onDelete)],
TaskStatus.completed => [
if (AppWindowChrome.isDesktopWindow) (Icons.folder_open_outlined, widget.onReveal),
(Icons.delete_outline, widget.onDelete),
],
};
final actions = task.isGroup && task.status != TaskStatus.completed
? [
if (task.canPause) (Icons.pause, widget.onPause),
if (task.canResume) (Icons.play_arrow, widget.onResume),
(Icons.close, widget.onDelete),
]
: switch (task.status) {
TaskStatus.downloading => [(Icons.pause, widget.onPause), (Icons.close, widget.onDelete)],
TaskStatus.paused => [(Icons.play_arrow, widget.onResume), (Icons.close, widget.onDelete)],
TaskStatus.failed => [(Icons.refresh, widget.onResume), (Icons.delete_outline, widget.onDelete)],
TaskStatus.completed => [
if (AppWindowChrome.isDesktopWindow) (Icons.folder_open_outlined, widget.onReveal),
(Icons.delete_outline, widget.onDelete),
],
};
return actions
.map(
@@ -46,25 +46,35 @@ class TaskUpdateListeningIndicator extends StatelessWidget {
}
class TaskCardIcon extends StatelessWidget {
const TaskCardIcon({super.key, required this.task});
const TaskCardIcon({super.key, required this.task, this.expanded = false});
final TaskRecord task;
final bool expanded;
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
return SizedBox(
width: 32,
height: 32,
child: ClipRRect(
borderRadius: BorderRadius.circular(AppDesignTokens.controlRadius),
clipBehavior: Clip.hardEdge,
child: ColoredBox(
color: palette.taskCardIconBg,
child: Center(child: Icon(task.icon, size: 20, color: palette.textSecondary)),
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (task.isGroup) ...[
Icon(expanded ? Icons.expand_more : Icons.chevron_right, size: 18, color: palette.textSecondary),
const SizedBox(width: AppDesignTokens.space4),
],
SizedBox(
width: 32,
height: 32,
child: ClipRRect(
borderRadius: BorderRadius.circular(AppDesignTokens.controlRadius),
clipBehavior: Clip.hardEdge,
child: ColoredBox(
color: palette.taskCardIconBg,
child: Center(child: Icon(task.icon, size: 20, color: palette.textSecondary)),
),
),
),
),
],
);
}
}