// Dart imports: // import 'dart:convert'; // Flutter imports: import 'package:flutter/material.dart'; // import 'package:flutter/scheduler.dart'; // Package imports: import 'package:dio/dio.dart' show CancelToken; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:openlib/services/share_book.dart'; // import 'package:flutter_svg/svg.dart'; // Project imports: import 'package:openlib/services/annas_archieve.dart' show BookInfoData; import 'package:openlib/services/database.dart'; import 'package:openlib/services/download_file.dart'; import 'package:openlib/ui/components/book_info_widget.dart'; import 'package:openlib/ui/components/error_widget.dart'; import 'package:openlib/ui/components/file_buttons_widget.dart'; import 'package:openlib/ui/components/snack_bar_widget.dart'; import 'package:openlib/ui/webview_page.dart'; import 'package:openlib/state/state.dart' show bookInfoProvider, totalFileSizeInBytes, downloadedFileSizeInBytes, downloadProgressProvider, getTotalFileSize, getDownloadedFileSize, cancelCurrentDownload, mirrorStatusProvider, ProcessState, CheckSumProcessState, downloadState, checkSumState, checkIdExists, myLibraryProvider; class BookInfoPage extends ConsumerWidget { const BookInfoPage({super.key, required this.url}); final String url; @override Widget build(BuildContext context, WidgetRef ref) { final bookInfo = ref.watch(bookInfoProvider(url)); return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.surface, title: const Text("Openlib"), titleTextStyle: Theme.of(context).textTheme.displayLarge, actions: [ bookInfo.maybeWhen(data: (data) { return IconButton( icon: Icon( Icons.share_sharp, color: Theme.of(context).colorScheme.tertiary, ), iconSize: 19.0, onPressed: () async { await shareBook(data.title, data.link, data.thumbnail ?? ''); }, ); }, orElse: () { return const SizedBox.shrink(); }) ], ), body: bookInfo.when( skipLoadingOnRefresh: false, data: (data) { return BookInfoWidget( data: data, child: ActionButtonWidget(data: data)); }, error: (err, _) { // if (err.toString().contains("403")) { // var errJson = jsonDecode(err.toString()); // if (SchedulerBinding.instance.schedulerPhase == // SchedulerPhase.persistentCallbacks) { // SchedulerBinding.instance.addPostFrameCallback((_) { // Future.delayed( // const Duration(seconds: 3), // () => Navigator.pushReplacement(context, // MaterialPageRoute(builder: (BuildContext context) { // return Webview(url: errJson["url"]); // }))); // }); // } // return Column( // mainAxisAlignment: MainAxisAlignment.center, // crossAxisAlignment: CrossAxisAlignment.stretch, // children: [ // SizedBox( // width: 210, // child: SvgPicture.asset( // 'assets/captcha.svg', // width: 210, // ), // ), // const SizedBox( // height: 30, // ), // Text( // "Captcha required", // textAlign: TextAlign.center, // style: TextStyle( // fontSize: 18, // fontWeight: FontWeight.bold, // color: Theme.of(context).textTheme.headlineMedium?.color, // overflow: TextOverflow.ellipsis, // ), // ), // Padding( // padding: const EdgeInsets.all(8.0), // child: Text( // "you will be redirected to solve captcha", // textAlign: TextAlign.center, // style: TextStyle( // fontSize: 13, // fontWeight: FontWeight.bold, // color: Theme.of(context).textTheme.headlineSmall?.color, // overflow: TextOverflow.ellipsis, // ), // ), // ), // Padding( // padding: const EdgeInsets.fromLTRB(30, 15, 30, 10), // child: Container( // width: double.infinity, // decoration: BoxDecoration( // color: const Color.fromARGB(255, 255, 186, 186), // borderRadius: BorderRadius.circular(5)), // child: const Padding( // padding: EdgeInsets.all(10.0), // child: Text( // "If you have solved the captcha then you will be automatically redirected to the results page . In case you seeing this page even after completing try using a VPN .", // textAlign: TextAlign.start, // style: TextStyle( // fontSize: 13, // fontWeight: FontWeight.bold, // color: Colors.black, // ), // ), // ), // ), // ) // ], // ); // } else { return CustomErrorWidget( error: err, stackTrace: _, onRefresh: () { // ignore: unused_result ref.refresh(bookInfoProvider(url)); }, ); // } }, loading: () { return Center( child: SizedBox( width: 25, height: 25, child: CircularProgressIndicator( color: Theme.of(context).colorScheme.secondary, strokeCap: StrokeCap.round, ), )); }, ), ); } } class ActionButtonWidget extends ConsumerStatefulWidget { const ActionButtonWidget({super.key, required this.data}); final BookInfoData data; @override ConsumerState createState() => _ActionButtonWidgetState(); } class _ActionButtonWidgetState extends ConsumerState { @override Widget build(BuildContext context) { final isBookExist = ref.watch(checkIdExists(widget.data.md5)); return isBookExist.when( data: (isExists) { if (isExists) { return FileOpenAndDeleteButtons( id: widget.data.md5, format: widget.data.format!, onDelete: () async { await Future.delayed(const Duration(seconds: 1)); // ignore: unused_result ref.refresh(checkIdExists(widget.data.md5)); }, ); } else { return Padding( padding: const EdgeInsets.only(top: 21, bottom: 21), child: Row( mainAxisAlignment: MainAxisAlignment.start, // Aligns buttons properly children: [ // Button for "Add To My Library" TextButton( style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.secondary, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), textStyle: const TextStyle( fontSize: 13, fontWeight: FontWeight.w900, color: Colors.white, ), ), onPressed: () async { if (widget.data.mirror != null && widget.data.mirror != '') { final result = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { return Webview(url: widget.data.mirror ?? ''); })); if (result != null) { await downloadFileWidget( ref, context, widget.data, result); } } else { showSnackBar( context: context, message: 'No mirrors available!'); } }, child: const Text('Add To My Library'), ) ], ), ); } }, error: (error, stackTrace) { return Text(error.toString()); }, loading: () { return CircularProgressIndicator( color: Theme.of(context).colorScheme.secondary, strokeCap: StrokeCap.round, ); }, ); } } Future downloadFileWidget(WidgetRef ref, BuildContext context, BookInfoData data, List mirrors) async { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return _ShowDialog(title: data.title); }); // print(mirrors); downloadFile( mirrors: mirrors, md5: data.md5, format: data.format!, onStart: () { ref.read(downloadState.notifier).state = ProcessState.running; }, onProgress: (int rcv, int total) async { if (ref.read(totalFileSizeInBytes) != total) { ref.read(totalFileSizeInBytes.notifier).state = total; } ref.read(downloadedFileSizeInBytes.notifier).state = rcv; ref.read(downloadProgressProvider.notifier).state = rcv / total; if (rcv / total == 1.0) { MyLibraryDb dataBase = MyLibraryDb.instance; await dataBase.insert(MyBook( id: data.md5, title: data.title, author: data.author, thumbnail: data.thumbnail, link: data.link, publisher: data.publisher, info: data.info, format: data.format, description: data.description)); ref.read(downloadState.notifier).state = ProcessState.complete; ref.read(checkSumState.notifier).state = CheckSumProcessState.running; try { final checkSum = await verifyFileCheckSum( md5Hash: data.md5, format: data.format!); if (checkSum == true) { ref.read(checkSumState.notifier).state = CheckSumProcessState.success; } else { ref.read(checkSumState.notifier).state = CheckSumProcessState.failed; } } catch (_) { ref.read(checkSumState.notifier).state = CheckSumProcessState.failed; } // ignore: unused_result ref.refresh(checkIdExists(data.md5)); // ignore: unused_result ref.refresh(myLibraryProvider); // ignore: use_build_context_synchronously showSnackBar(context: context, message: 'Book has been downloaded!'); } }, cancelDownlaod: (CancelToken downloadToken) { ref.read(cancelCurrentDownload.notifier).state = downloadToken; }, mirrorStatus: (val) { ref.read(mirrorStatusProvider.notifier).state = val; }, onDownlaodFailed: (msg) { Navigator.of(context).pop(); showSnackBar(context: context, message: msg.toString()); }); } class _ShowDialog extends ConsumerWidget { final String title; const _ShowDialog({required this.title}); @override Widget build(BuildContext context, WidgetRef ref) { final downloadProgress = ref.watch(downloadProgressProvider); final fileSize = ref.watch(getTotalFileSize); final downloadedFileSize = ref.watch(getDownloadedFileSize); final mirrorStatus = ref.watch(mirrorStatusProvider); final downloadProcessState = ref.watch(downloadState); final checkSumVerifyState = ref.watch(checkSumState); if (downloadProgress == 1.0 && (checkSumVerifyState == CheckSumProcessState.failed || checkSumVerifyState == CheckSumProcessState.success)) { Future.delayed(const Duration(seconds: 1), () { Navigator.of(context).pop(); if (checkSumVerifyState == CheckSumProcessState.failed) { _showWarningFileDialog(context); } }); } return Stack( alignment: Alignment.center, children: [ Padding( padding: const EdgeInsets.all(15.0), child: Container( width: double.infinity, height: 345, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: Theme.of(context).colorScheme.tertiaryContainer, ), padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(8), child: Text( "Downloading Book", style: TextStyle( fontSize: 19, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.tertiary, decoration: TextDecoration.none), ), ), Padding( padding: const EdgeInsets.all(8), child: Text( title, style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Theme.of(context) .colorScheme .tertiary .withAlpha(170), decoration: TextDecoration.none), overflow: TextOverflow.ellipsis, maxLines: 2, textAlign: TextAlign.start, ), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ mirrorStatus ? const Icon( Icons.check_circle, size: 15, color: Colors.green, ) : SizedBox( width: 9, height: 9, child: CircularProgressIndicator( color: Theme.of(context).colorScheme.secondary, strokeWidth: 2.5, strokeCap: StrokeCap.round, ), ), const SizedBox( width: 3, ), Text( "Checking mirror availability", style: TextStyle( fontSize: 11.5, fontWeight: FontWeight.bold, color: Theme.of(context) .colorScheme .tertiary .withAlpha(140), decoration: TextDecoration.none), overflow: TextOverflow.ellipsis, maxLines: 2, textAlign: TextAlign.start, ), ]), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ switch (downloadProcessState) { ProcessState.waiting => Icon( Icons.timer_sharp, size: 15, color: Theme.of(context) .colorScheme .tertiary .withAlpha(140), ), ProcessState.running => SizedBox( width: 9, height: 9, child: CircularProgressIndicator( color: Theme.of(context).colorScheme.secondary, strokeWidth: 2.5, strokeCap: StrokeCap.round, ), ), ProcessState.complete => const Icon( Icons.check_circle, size: 15, color: Colors.green, ), }, const SizedBox( width: 3, ), Text( "Downloading", style: TextStyle( fontSize: 11.5, fontWeight: FontWeight.bold, color: Theme.of(context) .colorScheme .tertiary .withAlpha(140), decoration: TextDecoration.none), overflow: TextOverflow.ellipsis, maxLines: 2, textAlign: TextAlign.start, ), ]), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ switch (checkSumVerifyState) { CheckSumProcessState.waiting => Icon( Icons.timer_sharp, size: 15, color: Theme.of(context) .colorScheme .tertiary .withAlpha(140), ), CheckSumProcessState.running => SizedBox( width: 9, height: 9, child: CircularProgressIndicator( color: Theme.of(context).colorScheme.secondary, strokeWidth: 2.5, strokeCap: StrokeCap.round, ), ), CheckSumProcessState.failed => const Icon( Icons.close, size: 15, color: Colors.red, ), CheckSumProcessState.success => const Icon( Icons.check_circle, size: 15, color: Colors.green, ), }, const SizedBox( width: 3, ), Text( "Verifying file checksum", style: TextStyle( fontSize: 11.5, fontWeight: FontWeight.bold, color: Theme.of(context) .colorScheme .tertiary .withAlpha(140), decoration: TextDecoration.none), overflow: TextOverflow.ellipsis, maxLines: 2, textAlign: TextAlign.start, ), ]), ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Padding( padding: const EdgeInsets.all(8), child: Text( '$downloadedFileSize/$fileSize', style: TextStyle( fontSize: 9, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.secondary, decoration: TextDecoration.none, letterSpacing: 1), overflow: TextOverflow.ellipsis, maxLines: 1, textAlign: TextAlign.start, ), ), ], ), Padding( padding: const EdgeInsets.all(8.0), child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(50)), child: LinearProgressIndicator( color: Theme.of(context).colorScheme.secondary, backgroundColor: Theme.of(context) .colorScheme .tertiary .withAlpha(50), value: downloadProgress, minHeight: 4, ), ), ), Padding( padding: const EdgeInsets.all(10.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.secondary, textStyle: const TextStyle( fontSize: 11, fontWeight: FontWeight.w900, color: Colors.white, )), onPressed: () { ref.read(cancelCurrentDownload).cancel(); Navigator.of(context).pop(); }, child: const Padding( padding: EdgeInsets.all(3.0), child: Text('Cancel'), ), ) ], ), ) ], ), ), ), ), ], ); } } Future _showWarningFileDialog(BuildContext context) async { return showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: Text( 'Checksum failed!', style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.secondary, decoration: TextDecoration.none, letterSpacing: 1), ), content: SingleChildScrollView( child: ListBody( children: [ Text( 'The downloaded book may be malicious. Delete it and get the same book from another source, or use the book at your own risk.', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.tertiary.withAlpha(170), ), ), ], ), ), actions: [ TextButton( child: Text( 'Okay', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.secondary, decoration: TextDecoration.none, letterSpacing: 1), ), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); }