Line data Source code
1 : import 'package:flutter/material.dart';
2 : import '../events.dart';
3 : import '../task_handler.dart';
4 :
5 : /// A widget that builds itself based on the latest [TaskProgress] of a task.
6 : ///
7 : /// Use this to easily reactive-ly update your UI as a background task
8 : /// reports its progress, speed, and ETA.
9 : ///
10 : /// This is a high-level wrapper around [StreamBuilder] that automatically
11 : /// handles the [TaskHandler.progress] stream.
12 : ///
13 : /// ## Example
14 : ///
15 : /// ```dart
16 : /// TaskProgressBuilder(
17 : /// handler: myTaskHandler,
18 : /// builder: (context, progress) {
19 : /// if (progress == null) return Text('Waiting for progress...');
20 : /// return LinearProgressIndicator(value: progress.progress / 100);
21 : /// },
22 : /// )
23 : /// ```
24 : class TaskProgressBuilder extends StatelessWidget {
25 : /// The handler for the task to track.
26 : final TaskHandler handler;
27 :
28 : /// The builder function that is called every time a new progress update arrives.
29 : ///
30 : /// `progress` is the latest update, or null if no update has arrived yet.
31 : final Widget Function(BuildContext context, TaskProgress? progress) builder;
32 :
33 : /// An optional initial progress value to use before the first update arrives.
34 : final TaskProgress? initialProgress;
35 :
36 0 : const TaskProgressBuilder({
37 : super.key,
38 : required this.handler,
39 : required this.builder,
40 : this.initialProgress,
41 : });
42 :
43 0 : @override
44 : Widget build(BuildContext context) {
45 0 : return StreamBuilder<TaskProgress>(
46 0 : stream: handler.progress,
47 0 : initialData: initialProgress,
48 0 : builder: (context, snapshot) {
49 0 : return builder(context, snapshot.data);
50 : },
51 : );
52 : }
53 : }
54 :
55 : /// A pre-styled Material 3 card that displays the progress of a background task.
56 : ///
57 : /// Displays:
58 : /// - A title and optional subtitle/message.
59 : /// - A progress bar.
60 : /// - Percentage, network speed, and time remaining (ETA).
61 : /// - Current step information (if available).
62 : ///
63 : /// Perfect for download managers, file processing screens, or any
64 : /// task-heavy application.
65 : class TaskProgressCard extends StatelessWidget {
66 : /// The handler for the task to display.
67 : final TaskHandler handler;
68 :
69 : /// An optional title for the card. Defaults to the task ID.
70 : final String? title;
71 :
72 : /// An optional icon to display in the header.
73 : final Widget? icon;
74 :
75 : /// Whether to show the network speed and time remaining.
76 : final bool showMetrics;
77 :
78 : /// Whether to show the current message from the task.
79 : final bool showMessage;
80 :
81 : /// Optional padding for the card content.
82 : final EdgeInsetsGeometry padding;
83 :
84 0 : const TaskProgressCard({
85 : super.key,
86 : required this.handler,
87 : this.title,
88 : this.icon,
89 : this.showMetrics = true,
90 : this.showMessage = true,
91 : this.padding = const EdgeInsets.all(16.0),
92 : });
93 :
94 0 : @override
95 : Widget build(BuildContext context) {
96 0 : final theme = Theme.of(context);
97 :
98 0 : return TaskProgressBuilder(
99 0 : handler: handler,
100 0 : builder: (context, progress) {
101 : final p = progress;
102 0 : final pct = (p?.progress ?? 0) / 100.0;
103 0 : final hasMetrics = p?.networkSpeed != null || p?.timeRemaining != null;
104 0 : final hasSteps = p?.currentStep != null && p?.totalSteps != null;
105 :
106 0 : return Card(
107 : clipBehavior: Clip.antiAlias,
108 0 : child: Padding(
109 0 : padding: padding,
110 0 : child: Column(
111 : crossAxisAlignment: CrossAxisAlignment.start,
112 : mainAxisSize: MainAxisSize.min,
113 0 : children: [
114 : // Header
115 0 : Row(
116 0 : children: [
117 0 : if (icon != null) ...[
118 0 : icon!,
119 : const SizedBox(width: 12),
120 : ],
121 0 : Expanded(
122 0 : child: Column(
123 : crossAxisAlignment: CrossAxisAlignment.start,
124 0 : children: [
125 0 : Text(
126 0 : title ?? handler.taskId,
127 0 : style: theme.textTheme.titleMedium?.copyWith(
128 : fontWeight: FontWeight.bold,
129 : ),
130 : maxLines: 1,
131 : overflow: TextOverflow.ellipsis,
132 : ),
133 0 : if (showMessage && p?.message != null)
134 0 : Text(
135 0 : p!.message!,
136 0 : style: theme.textTheme.bodySmall,
137 : maxLines: 1,
138 : overflow: TextOverflow.ellipsis,
139 : ),
140 : ],
141 : ),
142 : ),
143 0 : const SizedBox(width: 8),
144 0 : Text(
145 0 : '${(pct * 100).toInt()}%',
146 0 : style: theme.textTheme.titleMedium?.copyWith(
147 0 : color: theme.colorScheme.primary,
148 : fontWeight: FontWeight.bold,
149 : ),
150 : ),
151 : ],
152 : ),
153 : const SizedBox(height: 16),
154 :
155 : // Progress Bar
156 0 : LinearProgressIndicator(
157 : value: pct,
158 : minHeight: 8,
159 0 : borderRadius: BorderRadius.circular(4),
160 : ),
161 :
162 : // Footer Metrics
163 0 : if (showMetrics && (hasMetrics || hasSteps)) ...[
164 : const SizedBox(height: 12),
165 0 : Row(
166 0 : children: [
167 0 : if (hasSteps) ...[
168 0 : Icon(Icons.layers_outlined,
169 0 : size: 14, color: theme.hintColor),
170 : const SizedBox(width: 4),
171 0 : Text(
172 0 : 'Step ${p!.currentStep}/${p.totalSteps}',
173 0 : style: theme.textTheme.labelSmall,
174 : ),
175 : const SizedBox(width: 16),
176 : ],
177 0 : if (p?.networkSpeed != null) ...[
178 0 : Icon(Icons.speed, size: 14, color: theme.hintColor),
179 : const SizedBox(width: 4),
180 0 : Text(
181 0 : p!.networkSpeedHuman,
182 0 : style: theme.textTheme.labelSmall,
183 : ),
184 : const Spacer(),
185 : ],
186 0 : if (p?.timeRemaining != null) ...[
187 0 : Icon(Icons.timer_outlined,
188 0 : size: 14, color: theme.hintColor),
189 : const SizedBox(width: 4),
190 0 : Text(
191 0 : p!.timeRemainingHuman,
192 0 : style: theme.textTheme.labelSmall,
193 : ),
194 : ],
195 : ],
196 : ),
197 : ],
198 : ],
199 : ),
200 : ),
201 : );
202 : },
203 : );
204 : }
205 : }
|