1- import 'dart:convert' ;
21import 'dart:io' ;
32
43import 'package:flutter/material.dart' ;
54import 'package:get/get.dart' ;
65import 'package:share_plus/share_plus.dart' ;
76
87import 'package:insta/features/downloader/download_job_model.dart' ;
9- import 'package:insta/core/services/analytics_service.dart' ;
108import 'package:insta/features/downloader/download_queue_controller.dart' ;
119import 'package:insta/core/services/notification_service.dart' ;
1210import 'package:insta/features/status_saver/video_player_page.dart' ;
@@ -21,22 +19,6 @@ class DownloadQueueScreen extends StatefulWidget {
2119class _DownloadQueueScreenState extends State <DownloadQueueScreen > {
2220 final queue = DownloadQueueService .to;
2321 final searchController = TextEditingController ();
24- List <String > analyticsRows = const [];
25-
26- @override
27- void initState () {
28- super .initState ();
29- _loadAnalytics ();
30- }
31-
32- Future <void > _loadAnalytics () async {
33- if (! Get .isRegistered <AnalyticsService >()) return ;
34- final rows = await AnalyticsService .to.readRecentEvents (limit: 100 );
35- if (! mounted) return ;
36- setState (() {
37- analyticsRows = rows;
38- });
39- }
4022
4123 @override
4224 void dispose () {
@@ -110,219 +92,144 @@ class _DownloadQueueScreenState extends State<DownloadQueueScreen> {
11092
11193 @override
11294 Widget build (BuildContext context) {
113- return DefaultTabController (
114- length: 3 ,
115- child: Scaffold (
116- appBar: AppBar (
117- title: const Text ('Downloads' ),
118- bottom: const TabBar (
119- tabs: [
120- Tab (text: 'Queue' ),
121- Tab (text: 'History' ),
122- Tab (text: 'Diagnostics' ),
123- ],
95+ return Scaffold (
96+ appBar: AppBar (
97+ title: const Text ('Downloads' ),
98+ ),
99+ body: Column (
100+ children: [
101+ Padding (
102+ padding: const EdgeInsets .all (12 ),
103+ child: TextField (
104+ controller: searchController,
105+ decoration: const InputDecoration (
106+ labelText: 'Search downloads' ,
107+ border: OutlineInputBorder (),
108+ prefixIcon: Icon (Icons .search),
109+ ),
110+ onChanged: (_) => setState (() {}),
111+ ),
124112 ),
125- ),
126- body: TabBarView (
127- children: [
128- _buildQueueTab (),
129- _buildHistoryTab (),
130- _buildDiagnosticsTab (),
131- ],
132- ),
113+ Expanded (child: _buildMergedDownloadsList ()),
114+ ],
133115 ),
134116 );
135117 }
136118
137- Widget _buildQueueTab () {
119+ int _statusPriority (DownloadStatus status) {
120+ switch (status) {
121+ case DownloadStatus .running:
122+ return 0 ;
123+ case DownloadStatus .paused:
124+ return 1 ;
125+ case DownloadStatus .queued:
126+ return 2 ;
127+ case DownloadStatus .failed:
128+ return 3 ;
129+ case DownloadStatus .canceled:
130+ return 4 ;
131+ case DownloadStatus .success:
132+ return 5 ;
133+ }
134+ }
135+
136+ Widget _buildMergedDownloadsList () {
138137 return Obx (() {
139- final queueJobs = queue.jobs.where ((j) {
140- return j.status == DownloadStatus .queued ||
141- j.status == DownloadStatus .running ||
142- j.status == DownloadStatus .paused;
138+ final query = searchController.text.trim ().toLowerCase ();
139+ final indexedJobs = queue.jobs.asMap ().entries.toList ();
140+
141+ final filtered = indexedJobs.where ((entry) {
142+ if (query.isEmpty) return true ;
143+ final job = entry.value;
144+ return job.fileName.toLowerCase ().contains (query) ||
145+ NotificationService .to.getTypeLabel (job.type).toLowerCase ().contains (query) ||
146+ job.status.name.toLowerCase ().contains (query);
143147 }).toList ();
144148
145- if (queueJobs.isEmpty) {
146- return const Center (child: Text ('No active downloads' ));
147- }
149+ filtered.sort ((a, b) {
150+ final byPriority = _statusPriority (a.value.status).compareTo (
151+ _statusPriority (b.value.status),
152+ );
153+ if (byPriority != 0 ) return byPriority;
154+ return a.key.compareTo (b.key);
155+ });
148156
149- return ReorderableListView . builder (
150- itemCount : queueJobs.length,
151- onReorder : (oldIndex, newIndex) async {
152- final oldJob = queueJobs[oldIndex];
153- final oldGlobal = queue.jobs. indexWhere ((j) => j.id == oldJob.id );
154- if (oldGlobal == - 1 ) return ;
157+ if (queue.jobs.isEmpty) {
158+ return const Center (child : Text ( 'No downloads yet' ));
159+ }
160+ if (filtered.isEmpty) {
161+ return const Center (child : Text ( 'No matching downloads' ) );
162+ }
155163
156- final adjusted = newIndex > oldIndex ? newIndex - 1 : newIndex;
157- final targetJob = queueJobs[adjusted.clamp (0 , queueJobs.length - 1 )];
158- final newGlobal = queue.jobs.indexWhere ((j) => j.id == targetJob.id);
159- if (newGlobal == - 1 ) return ;
160- await queue.reorder (oldGlobal, newGlobal);
161- },
164+ return ListView .separated (
165+ itemCount: filtered.length,
166+ separatorBuilder: (_, _) => const Divider (height: 1 ),
162167 itemBuilder: (context, index) {
163- final job = queueJobs [index];
168+ final job = filtered [index].value ;
164169 return ListTile (
165170 key: ValueKey (job.id),
166- title: Text (job.fileName, maxLines: 1 , overflow: TextOverflow .ellipsis),
167- subtitle: Text ('${job .status .name } - ${job .progress }%' ),
168- trailing: Row (
169- mainAxisSize: MainAxisSize .min,
170- children: [
171- if (job.status == DownloadStatus .running)
172- IconButton (
173- icon: const Icon (Icons .pause),
174- onPressed: () => queue.pause (job.id),
175- ),
176- if (job.status == DownloadStatus .paused)
177- IconButton (
178- icon: const Icon (Icons .play_arrow),
179- onPressed: () => queue.resume (job.id),
180- ),
181- IconButton (
182- icon: const Icon (Icons .cancel),
183- onPressed: () => queue.cancel (job.id),
184- ),
185- ],
171+ title: Text (
172+ job.fileName,
173+ maxLines: 1 ,
174+ overflow: TextOverflow .ellipsis,
175+ ),
176+ subtitle: Text (
177+ '${NotificationService .to .getTypeLabel (job .type )} - ${job .status .name } - ${job .progress }%' ,
186178 ),
179+ trailing: _buildTrailingActions (job),
187180 );
188181 },
189182 );
190183 });
191184 }
192185
193- Widget _buildHistoryTab () {
194- return Column (
186+ Widget _buildTrailingActions (DownloadJob job) {
187+ final isActive = job.status == DownloadStatus .queued ||
188+ job.status == DownloadStatus .running ||
189+ job.status == DownloadStatus .paused;
190+
191+ return Row (
192+ mainAxisSize: MainAxisSize .min,
195193 children: [
196- Padding (
197- padding: const EdgeInsets .all (12 ),
198- child: TextField (
199- controller: searchController,
200- decoration: const InputDecoration (
201- labelText: 'Search history' ,
202- border: OutlineInputBorder (),
203- prefixIcon: Icon (Icons .search),
204- ),
205- onChanged: (_) => setState (() {}),
194+ if (job.status == DownloadStatus .running)
195+ IconButton (
196+ tooltip: 'Pause' ,
197+ icon: const Icon (Icons .pause),
198+ onPressed: () => queue.pause (job.id),
206199 ),
207- ),
208- Expanded (
209- child: Obx (() {
210- final query = searchController.text.trim ().toLowerCase ();
211- final history = queue.jobs.where ((j) {
212- final done = j.status == DownloadStatus .success ||
213- j.status == DownloadStatus .failed ||
214- j.status == DownloadStatus .canceled;
215- if (! done) return false ;
216- if (query.isEmpty) return true ;
217- return j.fileName.toLowerCase ().contains (query) ||
218- NotificationService .to.getTypeLabel (j.type).toLowerCase ().contains (query);
219- }).toList ();
220-
221- if (history.isEmpty) {
222- return const Center (child: Text ('No history found' ));
223- }
224-
225- return ListView .separated (
226- itemCount: history.length,
227- separatorBuilder: (_, _) => const Divider (height: 1 ),
228- itemBuilder: (context, index) {
229- final job = history[index];
230- return ListTile (
231- title: Text (job.fileName, maxLines: 1 , overflow: TextOverflow .ellipsis),
232- subtitle: Text (
233- '${NotificationService .to .getTypeLabel (job .type )} - ${job .status .name }' ,
234- ),
235- trailing: Row (
236- mainAxisSize: MainAxisSize .min,
237- children: [
238- if (job.status == DownloadStatus .success && job.filePath != null )
239- IconButton (
240- tooltip: 'Share' ,
241- icon: const Icon (Icons .share),
242- onPressed: () => _shareDownloadedFile (job),
243- ),
244- if (job.status == DownloadStatus .success &&
245- job.filePath != null &&
246- _isPlayableVideo (job.filePath! ))
247- IconButton (
248- tooltip: 'Play' ,
249- icon: const Icon (Icons .play_circle),
250- onPressed: () => _playDownloadedVideo (job),
251- ),
252- if (job.status == DownloadStatus .failed)
253- IconButton (
254- icon: const Icon (Icons .refresh),
255- onPressed: () => queue.retry (job.id),
256- ),
257- ],
258- ),
259- );
260- },
261- );
262- }),
263- ),
264- ],
265- );
266- }
267-
268- Widget _buildDiagnosticsTab () {
269- return RefreshIndicator (
270- onRefresh: _loadAnalytics,
271- child: ListView (
272- padding: const EdgeInsets .all (12 ),
273- children: [
274- const Text (
275- 'Failed Jobs' ,
276- style: TextStyle (fontSize: 16 , fontWeight: FontWeight .bold),
200+ if (job.status == DownloadStatus .paused)
201+ IconButton (
202+ tooltip: 'Resume' ,
203+ icon: const Icon (Icons .play_arrow),
204+ onPressed: () => queue.resume (job.id),
277205 ),
278- const SizedBox (height: 8 ),
279- Obx (() {
280- final failed = queue.jobs.where ((j) => j.status == DownloadStatus .failed).toList ();
281- if (failed.isEmpty) {
282- return const Padding (
283- padding: EdgeInsets .symmetric (vertical: 12 ),
284- child: Text ('No failed jobs' ),
285- );
286- }
287- return Column (
288- children: failed
289- .map (
290- (j) => Card (
291- child: ListTile (
292- title: Text (j.fileName, maxLines: 1 , overflow: TextOverflow .ellipsis),
293- subtitle: Text (j.errorCode ?? 'unknown' ),
294- trailing: IconButton (
295- icon: const Icon (Icons .refresh),
296- onPressed: () => queue.retry (j.id),
297- ),
298- ),
299- ),
300- )
301- .toList (),
302- );
303- }),
304- const SizedBox (height: 16 ),
305- const Text (
306- 'Recent Events' ,
307- style: TextStyle (fontSize: 16 , fontWeight: FontWeight .bold),
206+ if (job.status == DownloadStatus .failed)
207+ IconButton (
208+ tooltip: 'Retry' ,
209+ icon: const Icon (Icons .refresh),
210+ onPressed: () => queue.retry (job.id),
308211 ),
309- const SizedBox (height: 8 ),
310- if (analyticsRows.isEmpty)
311- const Text ('No analytics events yet' )
312- else
313- ...analyticsRows.map ((line) {
314- String title = line;
315- try {
316- final json = jsonDecode (line) as Map <String , dynamic >;
317- title = '${json ['event' ]} @ ${json ['ts' ]}' ;
318- } catch (_) {}
319- return Padding (
320- padding: const EdgeInsets .only (bottom: 6 ),
321- child: Text (title, style: const TextStyle (fontSize: 12 )),
322- );
323- }),
324- ],
325- ),
212+ if (job.status == DownloadStatus .success && job.filePath != null )
213+ IconButton (
214+ tooltip: 'Share' ,
215+ icon: const Icon (Icons .share),
216+ onPressed: () => _shareDownloadedFile (job),
217+ ),
218+ if (job.status == DownloadStatus .success &&
219+ job.filePath != null &&
220+ _isPlayableVideo (job.filePath! ))
221+ IconButton (
222+ tooltip: 'Play' ,
223+ icon: const Icon (Icons .play_circle),
224+ onPressed: () => _playDownloadedVideo (job),
225+ ),
226+ if (isActive)
227+ IconButton (
228+ tooltip: 'Cancel' ,
229+ icon: const Icon (Icons .cancel),
230+ onPressed: () => queue.cancel (job.id),
231+ ),
232+ ],
326233 );
327234 }
328235}
0 commit comments