Skip to content

Commit c2ea21a

Browse files
authored
Implement background mirror fetching, pull-to-refresh, and fix tap navigation
Implement background mirror fetching, pull-to-refresh, and fix tap navigation
2 parents 3fa3494 + fd8ab6d commit c2ea21a

7 files changed

Lines changed: 496 additions & 117 deletions

File tree

lib/services/download_manager.dart

Lines changed: 246 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import 'package:crypto/crypto.dart';
99
// Project imports:
1010
import 'package:openlib/services/database.dart' show MyLibraryDb, MyBook;
1111
import 'package:openlib/services/download_notification.dart';
12+
import 'package:openlib/services/mirror_fetcher.dart';
1213

1314
enum DownloadStatus {
1415
queued,
16+
fetchingMirrors,
1517
downloadingMirrors,
1618
downloading,
1719
verifying,
@@ -67,12 +69,13 @@ class DownloadTask {
6769
int? totalBytes,
6870
String? errorMessage,
6971
CancelToken? cancelToken,
72+
List<String>? mirrors,
7073
}) {
7174
return DownloadTask(
7275
id: id,
7376
md5: md5,
7477
title: title,
75-
mirrors: mirrors,
78+
mirrors: mirrors ?? this.mirrors,
7679
format: format,
7780
author: author,
7881
thumbnail: thumbnail,
@@ -192,6 +195,23 @@ class DownloadManager {
192195
_startDownload(task);
193196
}
194197

198+
Future<void> addDownloadWithMirrorUrl(DownloadTask task, String mirrorUrl) async {
199+
if (_activeDownloads.containsKey(task.id)) {
200+
return;
201+
}
202+
203+
_activeDownloads[task.id] = task;
204+
_notifyListeners();
205+
206+
await _notificationService.showDownloadNotification(
207+
id: task.id.hashCode,
208+
title: 'Queued: ${task.title}',
209+
progress: 0,
210+
);
211+
212+
_startDownloadWithMirrorUrl(task, mirrorUrl);
213+
}
214+
195215
Future<void> _startDownload(DownloadTask task) async {
196216
Dio? dio;
197217
try {
@@ -389,6 +409,231 @@ class DownloadManager {
389409
}
390410
}
391411

412+
Future<void> _startDownloadWithMirrorUrl(DownloadTask task, String mirrorUrl) async {
413+
Dio? dio;
414+
try {
415+
// Update status to fetching mirrors
416+
_updateTaskStatus(task.id, DownloadStatus.fetchingMirrors);
417+
await _notificationService.showDownloadNotification(
418+
id: task.id.hashCode,
419+
title: task.title,
420+
body: 'Getting mirrors...',
421+
progress: 0,
422+
);
423+
424+
// Fetch mirrors in background using headless webview
425+
final mirrorFetcher = MirrorFetcherService();
426+
List<String> fetchedMirrors = await mirrorFetcher.fetchMirrors(mirrorUrl);
427+
428+
if (!_activeDownloads.containsKey(task.id)) {
429+
return; // Task was cancelled while fetching mirrors
430+
}
431+
432+
if (fetchedMirrors.isEmpty) {
433+
_updateTaskStatus(task.id, DownloadStatus.failed,
434+
errorMessage: 'Failed to fetch mirrors!');
435+
await _notificationService.showDownloadNotification(
436+
id: task.id.hashCode,
437+
title: task.title,
438+
body: 'Failed: Could not get mirrors',
439+
progress: -1,
440+
);
441+
return;
442+
}
443+
444+
// Update task with fetched mirrors
445+
final updatedTask = task.copyWith(mirrors: fetchedMirrors);
446+
_activeDownloads[task.id] = updatedTask;
447+
448+
// Now proceed with the regular download flow
449+
dio = Dio();
450+
String path = await _getFilePath('${updatedTask.md5}.${updatedTask.format}');
451+
List<String> orderedMirrors = _reorderMirrors(updatedTask.mirrors);
452+
453+
_updateTaskStatus(updatedTask.id, DownloadStatus.downloadingMirrors);
454+
await _notificationService.showDownloadNotification(
455+
id: updatedTask.id.hashCode,
456+
title: updatedTask.title,
457+
body: 'Finding available mirror...',
458+
progress: 0,
459+
);
460+
461+
String? workingMirror = await _getAliveMirror(orderedMirrors);
462+
463+
if (workingMirror == null) {
464+
_updateTaskStatus(updatedTask.id, DownloadStatus.failed,
465+
errorMessage: 'No working mirrors available!');
466+
await _notificationService.showDownloadNotification(
467+
id: updatedTask.id.hashCode,
468+
title: updatedTask.title,
469+
body: 'Failed: No working mirrors',
470+
progress: -1,
471+
);
472+
return;
473+
}
474+
475+
// Try to download from each mirror until successful
476+
bool downloadSuccessful = false;
477+
int mirrorIndex = orderedMirrors.indexOf(workingMirror);
478+
479+
// Create a single cancel token for the entire mirror retry sequence
480+
CancelToken cancelToken = CancelToken();
481+
_activeDownloads[updatedTask.id] =
482+
_activeDownloads[updatedTask.id]!.copyWith(cancelToken: cancelToken);
483+
484+
while (mirrorIndex < orderedMirrors.length && !downloadSuccessful) {
485+
final currentMirror = orderedMirrors[mirrorIndex];
486+
487+
try {
488+
_updateTaskStatus(updatedTask.id, DownloadStatus.downloading);
489+
490+
await dio.download(
491+
currentMirror,
492+
path,
493+
options: Options(headers: {
494+
'Connection': 'Keep-Alive',
495+
'User-Agent':
496+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'
497+
}),
498+
onReceiveProgress: (rcv, total) {
499+
if (!(rcv.isNaN || rcv.isInfinite) &&
500+
!(total.isNaN || total.isInfinite)) {
501+
double progress = rcv / total;
502+
_updateTaskProgress(updatedTask.id, progress, rcv, total);
503+
504+
_notificationService.showDownloadNotification(
505+
id: updatedTask.id.hashCode,
506+
title: updatedTask.title,
507+
body: 'Downloading...',
508+
progress: (progress * 100).toInt(),
509+
);
510+
}
511+
},
512+
deleteOnError: true,
513+
cancelToken: cancelToken,
514+
);
515+
516+
// Download completed successfully
517+
downloadSuccessful = true;
518+
519+
} on DioException catch (e) {
520+
if (e.type == DioExceptionType.cancel) {
521+
_updateTaskStatus(updatedTask.id, DownloadStatus.cancelled);
522+
await _notificationService.cancelNotification(updatedTask.id.hashCode);
523+
return;
524+
}
525+
526+
// Try next mirror if available
527+
mirrorIndex++;
528+
if (mirrorIndex < orderedMirrors.length) {
529+
_updateTaskStatus(updatedTask.id, DownloadStatus.downloadingMirrors);
530+
await _notificationService.showDownloadNotification(
531+
id: updatedTask.id.hashCode,
532+
title: updatedTask.title,
533+
body: 'Retrying with alternate mirror...',
534+
progress: 0,
535+
);
536+
537+
// Wait up to 2 seconds before retrying, but check for cancellation
538+
const totalDelay = Duration(seconds: 2);
539+
const stepDelay = Duration(milliseconds: 100);
540+
var elapsed = Duration.zero;
541+
while (elapsed < totalDelay) {
542+
await Future.delayed(stepDelay);
543+
elapsed += stepDelay;
544+
545+
// Check if task was cancelled during the delay
546+
if (!_activeDownloads.containsKey(updatedTask.id) ||
547+
_activeDownloads[updatedTask.id]?.cancelToken?.isCancelled == true) {
548+
_updateTaskStatus(updatedTask.id, DownloadStatus.cancelled);
549+
await _notificationService.cancelNotification(updatedTask.id.hashCode);
550+
return;
551+
}
552+
}
553+
} else {
554+
// No more mirrors to try; mark task as failed before re-throwing
555+
_updateTaskStatus(updatedTask.id, DownloadStatus.failed,
556+
errorMessage: 'All mirrors failed!');
557+
await _notificationService.showDownloadNotification(
558+
id: updatedTask.id.hashCode,
559+
title: updatedTask.title,
560+
body: 'Download failed: All mirrors exhausted',
561+
progress: -1,
562+
);
563+
rethrow;
564+
}
565+
}
566+
}
567+
568+
if (!_activeDownloads.containsKey(updatedTask.id)) {
569+
return;
570+
}
571+
572+
_updateTaskStatus(updatedTask.id, DownloadStatus.verifying);
573+
await _notificationService.showDownloadNotification(
574+
id: updatedTask.id.hashCode,
575+
title: updatedTask.title,
576+
body: 'Verifying file...',
577+
progress: 100,
578+
);
579+
580+
bool checkSumValid =
581+
await _verifyFileCheckSum(md5Hash: updatedTask.md5, format: updatedTask.format);
582+
583+
await _database.insert(MyBook(
584+
id: updatedTask.md5,
585+
title: updatedTask.title,
586+
author: updatedTask.author,
587+
thumbnail: updatedTask.thumbnail,
588+
link: updatedTask.link,
589+
publisher: updatedTask.publisher,
590+
info: updatedTask.info,
591+
format: updatedTask.format,
592+
description: updatedTask.description,
593+
));
594+
595+
_updateTaskStatus(updatedTask.id, DownloadStatus.completed);
596+
597+
await _notificationService.showDownloadNotification(
598+
id: updatedTask.id.hashCode,
599+
title: updatedTask.title,
600+
body: checkSumValid
601+
? 'Download completed!'
602+
: 'Download completed (checksum failed)',
603+
progress: -1,
604+
);
605+
606+
await Future.delayed(const Duration(seconds: 3));
607+
removeDownload(updatedTask.id);
608+
} on DioException catch (e) {
609+
if (e.type == DioExceptionType.cancel) {
610+
_updateTaskStatus(task.id, DownloadStatus.cancelled);
611+
await _notificationService.cancelNotification(task.id.hashCode);
612+
} else {
613+
_updateTaskStatus(task.id, DownloadStatus.failed,
614+
errorMessage: 'Download failed! Try again...');
615+
await _notificationService.showDownloadNotification(
616+
id: task.id.hashCode,
617+
title: task.title,
618+
body: 'Download failed',
619+
progress: -1,
620+
);
621+
}
622+
} catch (e) {
623+
_updateTaskStatus(task.id, DownloadStatus.failed,
624+
errorMessage: 'Download failed! Try again...');
625+
await _notificationService.showDownloadNotification(
626+
id: task.id.hashCode,
627+
title: task.title,
628+
body: 'Download failed',
629+
progress: -1,
630+
);
631+
} finally {
632+
// Always close the Dio instance to prevent resource leaks
633+
dio?.close();
634+
}
635+
}
636+
392637
void _updateTaskStatus(String taskId, DownloadStatus status,
393638
{String? errorMessage}) {
394639
if (_activeDownloads.containsKey(taskId)) {

lib/services/mirror_fetcher.dart

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Dart imports:
2+
import 'dart:async';
3+
4+
// Package imports:
5+
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
6+
7+
/// Service to fetch mirror links in the background without showing UI
8+
class MirrorFetcherService {
9+
static final MirrorFetcherService _instance = MirrorFetcherService._internal();
10+
factory MirrorFetcherService() => _instance;
11+
MirrorFetcherService._internal();
12+
13+
/// Fetch mirror links from the given URL in the background
14+
/// Returns a list of mirror download links
15+
Future<List<String>> fetchMirrors(String url) async {
16+
final Completer<List<String>> completer = Completer<List<String>>();
17+
final List<String> bookDownloadLinks = [];
18+
19+
try {
20+
// Create a headless webview to load the page
21+
final headlessWebView = HeadlessInAppWebView(
22+
initialUrlRequest: URLRequest(url: WebUri(url)),
23+
onLoadStop: (controller, url) async {
24+
if (url == null) {
25+
if (!completer.isCompleted) {
26+
completer.complete([]);
27+
}
28+
return;
29+
}
30+
31+
try {
32+
if (url.toString().contains("slow_download")) {
33+
// For slow_download pages, extract the direct link
34+
String query =
35+
"""var paragraphTag=document.querySelector('p[class="mb-4 text-xl font-bold"]');var anchorTagHref=paragraphTag.querySelector('a').href;var url=()=>{return anchorTagHref};url();""";
36+
String? mirrorLink = await controller.evaluateJavascript(source: query);
37+
if (mirrorLink != null) {
38+
bookDownloadLinks.add(mirrorLink);
39+
}
40+
} else {
41+
// For other mirror pages, extract all IPFS/mirror links
42+
String query =
43+
"""var ipfsLinkTags=document.querySelectorAll('ul>li>a');var ipfsLinks=[];var getIpfsLinks=()=>{ipfsLinkTags.forEach(e=>{ipfsLinks.push(e.href)});return ipfsLinks};getIpfsLinks();""";
44+
List<dynamic> mirrorLinks =
45+
await controller.evaluateJavascript(source: query);
46+
bookDownloadLinks.addAll(mirrorLinks.cast<String>());
47+
}
48+
49+
// Complete the future with the extracted links
50+
if (!completer.isCompleted) {
51+
completer.complete(bookDownloadLinks);
52+
}
53+
} catch (e) {
54+
// Evaluation error, complete with empty list
55+
if (!completer.isCompleted) {
56+
completer.complete([]);
57+
}
58+
}
59+
},
60+
onLoadError: (controller, url, code, message) {
61+
// Load error, complete with empty list
62+
if (!completer.isCompleted) {
63+
completer.complete([]);
64+
}
65+
},
66+
);
67+
68+
// Run the headless webview
69+
await headlessWebView.run();
70+
71+
// Wait for the page to load and links to be extracted
72+
// Maximum wait time of 15 seconds
73+
final result = await completer.future.timeout(
74+
const Duration(seconds: 15),
75+
onTimeout: () => [],
76+
);
77+
78+
// Dispose the headless webview
79+
await headlessWebView.dispose();
80+
81+
return result;
82+
} catch (e) {
83+
// Return empty list on error
84+
return [];
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)