mirror of
https://github.com/dstark5/Openlib.git
synced 2025-05-17 14:36:02 +08:00
665 lines
25 KiB
Dart
665 lines
25 KiB
Dart
// 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<ConsumerStatefulWidget> createState() =>
|
|
_ActionButtonWidgetState();
|
|
}
|
|
|
|
class _ActionButtonWidgetState extends ConsumerState<ActionButtonWidget> {
|
|
@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 {
|
|
final result = await Navigator.push(context,
|
|
MaterialPageRoute(builder: (BuildContext context) {
|
|
return Webview(url: widget.data.mirror ?? '');
|
|
}));
|
|
|
|
if (result != null) {
|
|
widget.data.mirror = result;
|
|
await downloadFileWidget(ref, context, widget.data);
|
|
}
|
|
},
|
|
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<void> downloadFileWidget(
|
|
WidgetRef ref, BuildContext context, BookInfoData data) async {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (BuildContext context) {
|
|
return _ShowDialog(title: data.title);
|
|
});
|
|
|
|
List<String> mirrors = [data.mirror!];
|
|
// 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<void> _showWarningFileDialog(BuildContext context) async {
|
|
return showDialog<void>(
|
|
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: <Widget>[
|
|
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: <Widget>[
|
|
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();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|