From 35b01b51c31a98306e9b5faba46183935651fce8 Mon Sep 17 00:00:00 2001 From: kodjodevf <107993382+kodjodevf@users.noreply.github.com> Date: Fri, 14 Apr 2023 17:50:03 +0100 Subject: [PATCH] add chapter download module --- android/app/build.gradle | 2 +- android/app/src/main/AndroidManifest.xml | 3 + android/build.gradle | 2 +- lib/main.dart | 3 + lib/providers/hive_provider.dart | 5 + lib/providers/storage_provider.dart | 49 ++ lib/services/get_manga_chapter_url.dart | 12 +- lib/services/search_manga.g.dart | 2 +- lib/utils/constant.dart | 4 +- lib/utils/headers.dart | 2 +- lib/views/browse/browse_screen.dart | 19 +- lib/views/manga/detail/manga_detail_view.dart | 11 +- lib/views/manga/download/download_model.dart | 32 ++ .../manga/download/download_model.g.dart | 62 +++ .../manga/download/download_page_widget.dart | 489 ++++++++++++++++++ .../download/download_page_widget.g.dart | 140 +++++ .../manga/reader/image_view_horizontal.dart | 60 ++- .../manga/reader/image_view_vertical.dart | 112 ++-- lib/views/manga/reader/manga_reader_view.dart | 47 +- pubspec.lock | 57 ++ pubspec.yaml | 4 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 23 files changed, 1021 insertions(+), 100 deletions(-) create mode 100644 lib/providers/storage_provider.dart create mode 100644 lib/views/manga/download/download_model.dart create mode 100644 lib/views/manga/download/download_model.g.dart create mode 100644 lib/views/manga/download/download_page_widget.dart create mode 100644 lib/views/manga/download/download_page_widget.g.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 5cb229d0..76f4d3d0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,7 +47,7 @@ android { applicationId "com.kodjodevf.mangayomi" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7a65968f..d9ec7324 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + + + (HiveConstant.hiveBoxManga); await Hive.openBox(HiveConstant.hiveBoxMangaHistory); await Hive.openBox(HiveConstant.hiveBoxReaderMode); await Hive.openBox(HiveConstant.hiveBoxMangaSource); + await Hive.openBox(HiveConstant.hiveBoxDownloads); await Hive.openBox(HiveConstant.hiveBoxMangaInfo); await Hive.openBox(HiveConstant.hiveBoxMangaFilter); await Hive.openBox(HiveConstant.hiveBoxAppSettings); diff --git a/lib/providers/hive_provider.dart b/lib/providers/hive_provider.dart index da1d072e..50015f93 100644 --- a/lib/providers/hive_provider.dart +++ b/lib/providers/hive_provider.dart @@ -4,6 +4,7 @@ import 'package:mangayomi/utils/constant.dart'; import 'package:mangayomi/models/manga_history.dart'; import 'package:mangayomi/models/model_manga.dart'; import 'package:mangayomi/source/source_model.dart'; +import 'package:mangayomi/views/manga/download/download_model.dart'; import 'package:mangayomi/views/manga/reader/providers/reader_controller_provider.dart'; final hiveBoxManga = Provider>((ref) { @@ -27,6 +28,10 @@ final hiveBoxMangaFilterProvider = Provider((ref) { final hiveBoxMangaSourceProvider = Provider>((ref) { return Hive.box(HiveConstant.hiveBoxMangaSource); }); +final hiveBoxMangaDownloads = Provider>((ref) { + return Hive.box(HiveConstant.hiveBoxDownloads); +}); + final hiveBoxSettings = Provider((ref) { return Hive.box(HiveConstant.hiveBoxAppSettings); }); diff --git a/lib/providers/storage_provider.dart b/lib/providers/storage_provider.dart new file mode 100644 index 00000000..cab3b248 --- /dev/null +++ b/lib/providers/storage_provider.dart @@ -0,0 +1,49 @@ +// ignore_for_file: depend_on_referenced_packages + +import 'dart:io'; +import 'package:mangayomi/models/model_manga.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class StorageProvider { + Future requestPermission() async { + Permission permission = Permission.manageExternalStorage; + if (Platform.isAndroid || Platform.isIOS) { + if (await permission.isGranted) { + return true; + } else { + final result = await permission.request(); + if (result == PermissionStatus.granted) { + return true; + } else { + return false; + } + } + } + return true; + } + + Future getDirectory() async { + Directory? directory; + if (Platform.isAndroid) { + directory = Directory("/storage/emulated/0/Mangayomi/"); + } else { + final dir = await getApplicationDocumentsDirectory(); + directory = Directory("${dir.path}/Mangayomi"); + } + return directory; + } + + Future getMangaChapterDirectory( + ModelManga modelManga, index) async { + final dir = await getDirectory(); + return Directory( + "${dir!.path}/downloads/${modelManga.source} (${modelManga.lang!.toUpperCase()})/${modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/${modelManga.chapterTitle![index].replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/"); + } + + Future getMangaMainDirectory(ModelManga modelManga, index) async { + final dir = await getDirectory(); + return Directory( + "${dir!.path}/downloads/${modelManga.source} (${modelManga.lang!.toUpperCase()})/${modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/"); + } +} diff --git a/lib/services/get_manga_chapter_url.dart b/lib/services/get_manga_chapter_url.dart index f71c2b6d..bfdbf1a9 100644 --- a/lib/services/get_manga_chapter_url.dart +++ b/lib/services/get_manga_chapter_url.dart @@ -8,11 +8,11 @@ import 'package:html/dom.dart' as dom; import 'package:mangayomi/models/comick/chapter_page_comick.dart'; import 'package:mangayomi/models/model_manga.dart'; import 'package:mangayomi/providers/hive_provider.dart'; +import 'package:mangayomi/providers/storage_provider.dart'; import 'package:mangayomi/services/get_popular_manga.dart'; import 'package:mangayomi/services/http_res_to_dom_html.dart'; import 'package:mangayomi/source/source_model.dart'; import 'package:mangayomi/views/more/settings/providers/incognito_mode_state_provider.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:flutter_js/flutter_js.dart'; part 'get_manga_chapter_url.g.dart'; @@ -36,15 +36,7 @@ Future getMangaChapterUrl( "${modelManga.source}/${modelManga.name}/${modelManga.chapterTitle![index]}-pageurl", defaultValue: []); final incognitoMode = ref.watch(incognitoModeStateProvider); - Directory? pathh; - if (Platform.isAndroid || Platform.isIOS) { - pathh = await getExternalStorageDirectory(); - } else { - pathh = await getApplicationDocumentsDirectory(); - } - - path = Directory( - "${pathh!.path}/${modelManga.source}/${modelManga.name}/${modelManga.chapterTitle![index]}/"); + path = await StorageProvider().getMangaChapterDirectory(modelManga, index); if (hiveUrl.isNotEmpty) { urll = hiveUrl; diff --git a/lib/services/search_manga.g.dart b/lib/services/search_manga.g.dart index eb40b6f3..b4776759 100644 --- a/lib/services/search_manga.g.dart +++ b/lib/services/search_manga.g.dart @@ -6,7 +6,7 @@ part of 'search_manga.dart'; // RiverpodGenerator // ************************************************************************** -String _$searchMangaHash() => r'e84374580686773aa67deb76ab91c00e2e6fab8b'; +String _$searchMangaHash() => r'6cb4c0eaa232a0c2b54a2c8f4841d3acfffacd40'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/utils/constant.dart b/lib/utils/constant.dart index 4d099c89..cbaf8010 100644 --- a/lib/utils/constant.dart +++ b/lib/utils/constant.dart @@ -5,7 +5,7 @@ class HiveConstant { static String get hiveBoxMangaSource => "_manga_box_source_"; static String get hiveBoxMangaFilter => "_manga_box_filter_"; static String get hiveBoxAppSettings => "_app_box_settings_"; + static String get hiveBoxDownloads => "_manga_box_downloads_"; static String get hiveBoxReaderSettings => "_reader_box_settings_"; - static String get hiveBoxReaderMode => - "_readerMode_box_settings_"; + static String get hiveBoxReaderMode => "_readerMode_box_settings_"; } diff --git a/lib/utils/headers.dart b/lib/utils/headers.dart index b7f7a988..e6fec57e 100644 --- a/lib/utils/headers.dart +++ b/lib/utils/headers.dart @@ -1,4 +1,4 @@ -Map? headers(String source) { +Map headers(String source) { return source == 'mangakawaii' ? { 'Referer': 'https://www.mangakawaii.io/', diff --git a/lib/views/browse/browse_screen.dart b/lib/views/browse/browse_screen.dart index 53dbe226..da37549e 100644 --- a/lib/views/browse/browse_screen.dart +++ b/lib/views/browse/browse_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:mangayomi/providers/storage_provider.dart'; import 'package:mangayomi/source/source_model.dart'; import 'package:mangayomi/views/browse/extension/extension_screen.dart'; import 'package:mangayomi/views/browse/migrate_screen.dart'; @@ -22,16 +23,22 @@ class _BrowseScreenState extends State _tabBarController = TabController(length: 3, vsync: this); _tabBarController.animateTo(0); _tabBarController.addListener(() { + _chekPermission(); setState(() { _textEditingController.clear(); + _entriesFilter = []; _isSearch = false; }); }); super.initState(); } - List entries = []; - List entriesFilter = []; + _chekPermission() async { + await StorageProvider().requestPermission(); + } + + List _entries = []; + List _entriesFilter = []; final _textEditingController = TextEditingController(); bool _isSearch = false; @override @@ -52,7 +59,7 @@ class _BrowseScreenState extends State ? SeachFormTextField( onChanged: (value) { setState(() { - entriesFilter = entries + _entriesFilter = _entries .where((element) => element.sourceName .toLowerCase() .contains(value.toLowerCase())) @@ -67,7 +74,7 @@ class _BrowseScreenState extends State _isSearch = false; }); _textEditingController.clear(); - entriesFilter = entries; + _entriesFilter = _entries; }, controller: _textEditingController, ) @@ -122,9 +129,9 @@ class _BrowseScreenState extends State const SourcesScreen(), ExtensionScreen( entriesData: (val) { - entries = val as List; + _entries = val as List; }, - entriesFilter: entriesFilter, + entriesFilter: _entriesFilter, ), const MigrateScreen() ]), diff --git a/lib/views/manga/detail/manga_detail_view.dart b/lib/views/manga/detail/manga_detail_view.dart index 8fb431b0..c59d5ea9 100644 --- a/lib/views/manga/detail/manga_detail_view.dart +++ b/lib/views/manga/detail/manga_detail_view.dart @@ -3,13 +3,13 @@ import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mangayomi/models/manga_reader.dart'; import 'package:mangayomi/models/model_manga.dart'; import 'package:mangayomi/utils/cached_network.dart'; import 'package:mangayomi/utils/media_query.dart'; import 'package:mangayomi/views/manga/detail/providers/state_providers.dart'; import 'package:mangayomi/views/manga/detail/readmore.dart'; +import 'package:mangayomi/views/manga/download/download_page_widget.dart'; class MangaDetailView extends ConsumerStatefulWidget { final Function(bool) isExtended; @@ -169,13 +169,12 @@ class _MangaDetailViewState extends ConsumerState modelManga: widget.modelManga!, index: reverse ? reverseIndex : finalIndex); }, - trailing: const Icon( - FontAwesomeIcons.circleDown, - size: 20, - ), + trailing: ref.watch(ChapterPageDownloadsProvider( + index: reverse ? reverseIndex : finalIndex, + modelManga: widget.modelManga!)), subtitle: Text( chapterDate[finalIndex], - style: const TextStyle(fontSize: 12), + style: const TextStyle(fontSize: 11), ), title: Text( chapterTitle[finalIndex], diff --git a/lib/views/manga/download/download_model.dart b/lib/views/manga/download/download_model.dart new file mode 100644 index 00000000..d270ebc4 --- /dev/null +++ b/lib/views/manga/download/download_model.dart @@ -0,0 +1,32 @@ +import 'package:hive/hive.dart'; +import 'package:mangayomi/models/model_manga.dart'; +part 'download_model.g.dart'; + +@HiveType(typeId: 6) +class DownloadModel { + @HiveField(0) + final ModelManga modelManga; + @HiveField(1) + final int index; + @HiveField(2) + final int succeeded; + @HiveField(3) + final int failed; + @HiveField(4) + final int total; + @HiveField(6) + final bool isDownload; + @HiveField(7) + final List taskIds; + @HiveField(8) + final bool isStartDownload; + DownloadModel( + {required this.modelManga, + required this.succeeded, + required this.failed, + required this.index, + required this.total, + required this.isDownload, + required this.taskIds, + required this.isStartDownload}); +} diff --git a/lib/views/manga/download/download_model.g.dart b/lib/views/manga/download/download_model.g.dart new file mode 100644 index 00000000..e7586704 --- /dev/null +++ b/lib/views/manga/download/download_model.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'download_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class DownloadModelAdapter extends TypeAdapter { + @override + final int typeId = 6; + + @override + DownloadModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return DownloadModel( + modelManga: fields[0] as ModelManga, + succeeded: fields[2] as int, + failed: fields[3] as int, + index: fields[1] as int, + total: fields[4] as int, + isDownload: fields[6] as bool, + taskIds: (fields[7] as List).cast(), + isStartDownload: fields[8] as bool, + ); + } + + @override + void write(BinaryWriter writer, DownloadModel obj) { + writer + ..writeByte(8) + ..writeByte(0) + ..write(obj.modelManga) + ..writeByte(1) + ..write(obj.index) + ..writeByte(2) + ..write(obj.succeeded) + ..writeByte(3) + ..write(obj.failed) + ..writeByte(4) + ..write(obj.total) + ..writeByte(6) + ..write(obj.isDownload) + ..writeByte(7) + ..write(obj.taskIds) + ..writeByte(8) + ..write(obj.isStartDownload); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DownloadModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/views/manga/download/download_page_widget.dart b/lib/views/manga/download/download_page_widget.dart new file mode 100644 index 00000000..0d1e8e8e --- /dev/null +++ b/lib/views/manga/download/download_page_widget.dart @@ -0,0 +1,489 @@ +// ignore_for_file: implementation_imports, depend_on_referenced_packages +import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:mangayomi/providers/storage_provider.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:mangayomi/models/model_manga.dart'; +import 'package:mangayomi/providers/hive_provider.dart'; +import 'package:mangayomi/services/get_manga_chapter_url.dart'; +import 'package:mangayomi/utils/constant.dart'; +import 'package:mangayomi/utils/headers.dart'; +import 'package:mangayomi/views/manga/download/download_model.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'download_page_widget.g.dart'; + +@riverpod +class ChapterPageDownloads extends _$ChapterPageDownloads { + @override + Widget build({required ModelManga modelManga, required int index}) { + return ChapterPageDownload( + index: index, + modelManga: modelManga, + ); + } + + // ... +} + +class ChapterPageDownload extends ConsumerStatefulWidget { + final ModelManga modelManga; + final int index; + const ChapterPageDownload( + {super.key, required this.modelManga, required this.index}); + + @override + ConsumerState createState() => _ChapterPageDownloadState(); +} + +class _ChapterPageDownloadState extends ConsumerState + with AutomaticKeepAliveClientMixin { + List _urll = []; + List tasks = []; + final StorageProvider _storageProvider = StorageProvider(); + _startDownload() async { + await _storageProvider.requestPermission(); + Directory? path; + bool isOk = false; + final path1 = await _storageProvider.getDirectory(); + + final finalPath = + "downloads/${widget.modelManga.source} (${widget.modelManga.lang!.toUpperCase()})/${widget.modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/${widget.modelManga.chapterTitle![widget.index].replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}"; + path = Directory( + "${path1!.path}downloads/${widget.modelManga.source} (${widget.modelManga.lang!.toUpperCase()})/${widget.modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/${widget.modelManga.chapterTitle![widget.index].replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/"); + ref + .read(getMangaChapterUrlProvider( + modelManga: widget.modelManga, + index: widget.index, + ).future) + .then((value) { + if (value.urll.isNotEmpty) { + if (mounted) { + setState(() { + _urll = value.urll; + isOk = true; + }); + } + } + }); + await Future.doWhile(() async { + await Future.delayed(const Duration(seconds: 1)); + if (isOk == true) { + return false; + } + return true; + }); + + if (_urll.isNotEmpty) { + for (var index = 0; index < _urll.length; index++) { + final path2 = Directory("${path1.path}downloads/"); + final path4 = Directory( + "${path2.path}${widget.modelManga.source} (${widget.modelManga.lang!.toUpperCase()})/"); + final path3 = Directory( + "${path2.path}${widget.modelManga.source} (${widget.modelManga.lang!.toUpperCase()})/${widget.modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/"); + final path5 = Directory( + "${path2.path}${widget.modelManga.source} (${widget.modelManga.lang!.toUpperCase()})/${widget.modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/${widget.modelManga.chapterTitle![widget.index].replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}"); + + if (!(await path1.exists())) { + path1.create(); + } + if (!(await path2.exists())) { + path2.create(); + } + if (!(await path4.exists())) { + path4.create(); + } + if (!(await path3.exists())) { + path3.create(); + } + if (!(await path5.exists())) { + path5.create(); + } + + if ((await path.exists())) { + if (await File("${path.path}" "${index + 1}.jpg").exists()) { + } else { + tasks.add(DownloadTask( + taskId: _urll[index], + headers: headers(widget.modelManga.source!), + url: _urll[index], + filename: "${index + 1}.jpg", + baseDirectory: + Platform.isWindows || Platform.isMacOS || Platform.isLinux + ? BaseDirectory.applicationDocuments + : BaseDirectory.temporary, + directory: + Platform.isWindows || Platform.isMacOS || Platform.isLinux + ? 'Mangayomi/$finalPath' + : finalPath, + updates: Updates.statusAndProgress, + allowPause: true, + )); + } + } else { + path.create(); + if (await File("${path.path}" "${index + 1}.jpg").exists()) { + } else { + tasks.add(DownloadTask( + taskId: _urll[index], + headers: headers(widget.modelManga.source!), + url: _urll[index], + filename: "${index + 1}.jpg", + baseDirectory: + Platform.isWindows || Platform.isMacOS || Platform.isLinux + ? BaseDirectory.applicationDocuments + : BaseDirectory.temporary, + directory: + Platform.isWindows || Platform.isMacOS || Platform.isLinux + ? 'Mangayomi/$finalPath' + : finalPath, + updates: Updates.statusAndProgress, + allowPause: true, + )); + } + } + } + if (tasks.isEmpty && _urll.isNotEmpty) { + final model = DownloadModel( + modelManga: widget.modelManga, + succeeded: 0, + failed: 0, + index: widget.index, + total: 0, + isDownload: true, + taskIds: _urll, + isStartDownload: false); + + ref + .watch(hiveBoxMangaDownloads) + .put(widget.modelManga.chapterTitle![widget.index], model); + } else { + await FileDownloader().downloadBatch( + tasks, + batchProgressCallback: (succeeded, failed) { + final model = DownloadModel( + modelManga: widget.modelManga, + succeeded: succeeded, + failed: failed, + index: widget.index, + total: tasks.length, + isDownload: (succeeded == tasks.length) ? true : false, + taskIds: _urll, + isStartDownload: true); + + Hive.box(HiveConstant.hiveBoxDownloads) + .put(widget.modelManga.chapterTitle![widget.index], model); + }, + taskProgressCallback: (task, progress) async { + if (progress == 1.0) { + final downloadTask = DownloadTask( + creationTime: task.creationTime, + taskId: task.taskId, + headers: task.headers, + url: task.url, + filename: task.filename, + baseDirectory: task.baseDirectory, + directory: task.directory, + updates: task.updates, + allowPause: task.allowPause, + ); + if (Platform.isAndroid || Platform.isIOS) { + await FileDownloader().moveToSharedStorage( + downloadTask, SharedStorage.external, + directory: finalPath); + } + } + }, + ); + } + } + } + + _deleteFile(List pageUrl) async { + final path = await _storageProvider.getMangaChapterDirectory( + widget.modelManga, widget.index); + + try { + path!.deleteSync(recursive: true); + ref.watch(hiveBoxMangaDownloads).delete( + widget.modelManga.chapterTitle![widget.index], + ); + } catch (e) { + ref.watch(hiveBoxMangaDownloads).delete( + widget.modelManga.chapterTitle![widget.index], + ); + } + } + + bool _isStarted = false; + @override + Widget build(BuildContext context) { + super.build(context); + return SizedBox( + height: 41, + width: 35, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 3), + child: ValueListenableBuilder>( + valueListenable: ref.watch(hiveBoxMangaDownloads).listenable(), + builder: (context, val, child) { + final entries = val.values + .where((element) => + element.modelManga.chapterTitle![element.index] == + widget.modelManga.chapterTitle![widget.index]) + .toList(); + + if (entries.isNotEmpty) { + return entries.first.isDownload + ? PopupMenuButton( + child: Icon( + size: 25, + Icons.check_circle, + color: + Theme.of(context).iconTheme.color!.withOpacity(0.7), + ), + onSelected: (value) { + if (value.toString() == 'Delete') { + setState(() { + _isStarted = false; + }); + _deleteFile(entries.first.taskIds); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 'Send', child: Text("Send")), + const PopupMenuItem( + value: 'Delete', child: Text('Delete')), + ], + ) + : entries.first.isStartDownload && + entries.first.succeeded == 0 + ? SizedBox( + height: 41, + width: 35, + child: PopupMenuButton( + child: _downloadWidget(context, false), + onSelected: (value) { + if (value.toString() == 'Cancel') { + setState(() { + _isStarted = false; + }); + List taskIds = []; + for (var id in entries.first.taskIds) { + taskIds.add(id); + } + FileDownloader() + .cancelTasksWithIds(taskIds) + .then((value) async { + await Future.delayed( + const Duration(seconds: 2)); + ref.watch(hiveBoxMangaDownloads).delete( + widget.modelManga + .chapterTitle![widget.index], + ); + }); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'Cancel', child: Text("Cancel")), + ], + )) + : entries.first.succeeded != 0 + ? SizedBox( + height: 41, + width: 35, + child: PopupMenuButton( + child: Stack( + children: [ + Align( + alignment: Alignment.center, + child: Icon( + Icons.arrow_downward_sharp, + color: Theme.of(context) + .iconTheme + .color! + .withOpacity(0.7), + )), + Align( + alignment: Alignment.center, + child: TweenAnimationBuilder( + duration: + const Duration(milliseconds: 250), + curve: Curves.easeInOut, + tween: Tween( + begin: 0, + end: (entries.first.succeeded / + entries.first.total), + ), + builder: (context, value, _) => + SizedBox( + height: 2, + width: 2, + child: CircularProgressIndicator( + strokeWidth: 19, + value: value, + color: Theme.of(context) + .iconTheme + .color! + .withOpacity(0.7), + ), + ), + ), + ), + Align( + alignment: Alignment.center, + child: Icon( + Icons.arrow_downward_sharp, + color: Theme.of(context) + .scaffoldBackgroundColor, + )), + ], + ), + onSelected: (value) { + if (value.toString() == 'Cancel') { + setState(() { + _isStarted = false; + }); + List taskIds = []; + for (var id in entries.first.taskIds) { + taskIds.add(id); + } + FileDownloader() + .cancelTasksWithIds(taskIds) + .then((value) async { + await Future.delayed( + const Duration(seconds: 2)); + ref.watch(hiveBoxMangaDownloads).delete( + widget.modelManga + .chapterTitle![widget.index], + ); + }); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'Cancel', child: Text("Cancel")), + ], + )) + : entries.first.succeeded == 0 + ? IconButton( + onPressed: () { + // _startDownload(); + setState(() { + _isStarted = true; + }); + }, + icon: Icon( + FontAwesomeIcons.circleDown, + color: Theme.of(context) + .iconTheme + .color! + .withOpacity(0.7), + size: 25, + )) + : SizedBox( + height: 50, + width: 50, + child: PopupMenuButton( + child: const Icon( + Icons.error_outline_outlined, + color: Colors.red, + size: 25, + ), + onSelected: (value) { + if (value.toString() == 'Retry') { + ref.watch(hiveBoxMangaDownloads).delete( + widget.modelManga + .chapterTitle![widget.index], + ); + _startDownload(); + setState(() { + _isStarted = true; + }); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'Retry', child: Text("Retry")), + ], + )); + } + return _isStarted + ? SizedBox( + height: 50, + width: 50, + child: PopupMenuButton( + child: _downloadWidget(context, true), + onSelected: (value) { + if (value.toString() == 'Cancel') { + setState(() { + _isStarted = false; + }); + List taskIds = []; + for (var id in _urll) { + taskIds.add(id); + } + FileDownloader() + .cancelTasksWithIds(taskIds) + .then((value) async { + await Future.delayed(const Duration(seconds: 2)); + ref.watch(hiveBoxMangaDownloads).delete( + widget.modelManga.chapterTitle![widget.index], + ); + }); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'Cancel', child: Text("Cancel")), + ], + )) + : IconButton( + splashRadius: 5, + iconSize: 17, + onPressed: () { + _startDownload(); + setState(() { + _isStarted = true; + }); + }, + icon: _downloadWidget(context, false), + ); + }, + ), + ), + ); + } + + @override + bool get wantKeepAlive => true; +} + +Widget _downloadWidget(BuildContext context, bool isLoading) { + return Stack( + children: [ + Align( + alignment: Alignment.center, + child: Icon( + size: 18, + Icons.arrow_downward_sharp, + color: Theme.of(context).iconTheme.color!.withOpacity(0.7), + )), + Align( + alignment: Alignment.center, + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + value: isLoading ? null : 1, + color: Theme.of(context).iconTheme.color!.withOpacity(0.7), + strokeWidth: 2, + ), + ), + ), + ], + ); +} diff --git a/lib/views/manga/download/download_page_widget.g.dart b/lib/views/manga/download/download_page_widget.g.dart new file mode 100644 index 00000000..a26392af --- /dev/null +++ b/lib/views/manga/download/download_page_widget.g.dart @@ -0,0 +1,140 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'download_page_widget.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$chapterPageDownloadsHash() => + r'0b3eaf9a3ca4786287616a87e5de62af24259b68'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$ChapterPageDownloads + extends BuildlessAutoDisposeNotifier { + late final ModelManga modelManga; + late final int index; + + Widget build({ + required ModelManga modelManga, + required int index, + }); +} + +/// See also [ChapterPageDownloads]. +@ProviderFor(ChapterPageDownloads) +const chapterPageDownloadsProvider = ChapterPageDownloadsFamily(); + +/// See also [ChapterPageDownloads]. +class ChapterPageDownloadsFamily extends Family { + /// See also [ChapterPageDownloads]. + const ChapterPageDownloadsFamily(); + + /// See also [ChapterPageDownloads]. + ChapterPageDownloadsProvider call({ + required ModelManga modelManga, + required int index, + }) { + return ChapterPageDownloadsProvider( + modelManga: modelManga, + index: index, + ); + } + + @override + ChapterPageDownloadsProvider getProviderOverride( + covariant ChapterPageDownloadsProvider provider, + ) { + return call( + modelManga: provider.modelManga, + index: provider.index, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'chapterPageDownloadsProvider'; +} + +/// See also [ChapterPageDownloads]. +class ChapterPageDownloadsProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [ChapterPageDownloads]. + ChapterPageDownloadsProvider({ + required this.modelManga, + required this.index, + }) : super.internal( + () => ChapterPageDownloads() + ..modelManga = modelManga + ..index = index, + from: chapterPageDownloadsProvider, + name: r'chapterPageDownloadsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$chapterPageDownloadsHash, + dependencies: ChapterPageDownloadsFamily._dependencies, + allTransitiveDependencies: + ChapterPageDownloadsFamily._allTransitiveDependencies, + ); + + final ModelManga modelManga; + final int index; + + @override + bool operator ==(Object other) { + return other is ChapterPageDownloadsProvider && + other.modelManga == modelManga && + other.index == index; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, modelManga.hashCode); + hash = _SystemHash.combine(hash, index.hashCode); + + return _SystemHash.finish(hash); + } + + @override + Widget runNotifierBuild( + covariant ChapterPageDownloads notifier, + ) { + return notifier.build( + modelManga: modelManga, + index: index, + ); + } +} +// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions diff --git a/lib/views/manga/reader/image_view_horizontal.dart b/lib/views/manga/reader/image_view_horizontal.dart index 311f9f5b..026dbe9a 100644 --- a/lib/views/manga/reader/image_view_horizontal.dart +++ b/lib/views/manga/reader/image_view_horizontal.dart @@ -36,21 +36,53 @@ class ImageViewHorizontal extends StatefulWidget { typedef DoubleClickAnimationListener = void Function(); class _ImageViewHorizontalState extends State { + @override + void initState() { + _localCheck(); + super.initState(); + } + + _localCheck() async { + if (await File("${widget.path.path}" "${widget.index + 1}.jpg").exists()) { + if (mounted) { + setState(() { + _isLocale = true; + }); + } + } else { + if (mounted) { + setState(() { + _isLocale = false; + }); + } + } + } + + bool _isLocale = false; @override Widget build(BuildContext context) { - return ExtendedImage.network( - widget.url, - cache: true, - clearMemoryCacheWhenDispose: true, - enableMemoryCache: false, - cacheMaxAge: const Duration(days: 7), - headers: headers(widget.source), - mode: ExtendedImageMode.gesture, - initGestureConfigHandler: widget.initGestureConfigHandler, - onDoubleTap: widget.onDoubleTap, - handleLoadingProgress: true, - loadStateChanged: widget.loadStateChanged, - ); + return _isLocale + ? ExtendedImage.file( + File("${widget.path.path}" "${widget.index + 1}.jpg"), + clearMemoryCacheWhenDispose: true, + enableMemoryCache: false, + mode: ExtendedImageMode.gesture, + initGestureConfigHandler: widget.initGestureConfigHandler, + onDoubleTap: widget.onDoubleTap, + loadStateChanged: widget.loadStateChanged, + ) + : ExtendedImage.network( + widget.url, + cache: true, + clearMemoryCacheWhenDispose: true, + enableMemoryCache: false, + cacheMaxAge: const Duration(days: 7), + headers: headers(widget.source), + mode: ExtendedImageMode.gesture, + initGestureConfigHandler: widget.initGestureConfigHandler, + onDoubleTap: widget.onDoubleTap, + handleLoadingProgress: true, + loadStateChanged: widget.loadStateChanged, + ); } } - diff --git a/lib/views/manga/reader/image_view_vertical.dart b/lib/views/manga/reader/image_view_vertical.dart index 305e3013..ddf1015c 100644 --- a/lib/views/manga/reader/image_view_vertical.dart +++ b/lib/views/manga/reader/image_view_vertical.dart @@ -33,6 +33,29 @@ class ImageViewVertical extends ConsumerStatefulWidget { class _ImageViewVerticalState extends ConsumerState with AutomaticKeepAliveClientMixin { @override + void initState() { + _localCheck(); + super.initState(); + } + + _localCheck() async { + if (await File("${widget.path.path}" "${widget.index + 1}.jpg").exists()) { + if (mounted) { + setState(() { + _isLocale = true; + }); + } + } else { + if (mounted) { + setState(() { + _isLocale = false; + }); + } + } + } + + bool _isLocale = false; + @override Widget build(BuildContext context) { super.build(context); return Container( @@ -44,44 +67,57 @@ class _ImageViewVerticalState extends ConsumerState SizedBox( height: MediaQuery.of(context).padding.top, ), - ExtendedImage.network(widget.url, - headers: headers(widget.source), - handleLoadingProgress: true, - fit: BoxFit.contain, - cacheMaxAge: const Duration(days: 7), - clearMemoryCacheWhenDispose: true, - enableMemoryCache: false, - loadStateChanged: (ExtendedImageState state) { - if (state.extendedImageLoadState == LoadState.loading) { - final ImageChunkEvent? loadingProgress = state.loadingProgress; - final double? progress = - loadingProgress?.expectedTotalBytes != null - ? loadingProgress!.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null; - return SizedBox( - height: mediaHeight(context, 0.5), - child: Center( - child: CircularProgressIndicator( - value: progress, - ), - ), - ); - } - if (state.extendedImageLoadState == LoadState.failed) { - return Center( - child: ElevatedButton( - onPressed: () { - state.reLoadImage(); - }, - child: const Icon( - Icons.replay_outlined, - size: 30, - )), - ); - } - return null; - }), + _isLocale + ? ExtendedImage.file( + fit: BoxFit.contain, + clearMemoryCacheWhenDispose: true, + enableMemoryCache: false, + File('${widget.path.path}${widget.index + 1}.jpg')) + : ExtendedImage.network(widget.url, + headers: headers(widget.source), + handleLoadingProgress: true, + fit: BoxFit.contain, + cacheMaxAge: const Duration(days: 7), + clearMemoryCacheWhenDispose: true, + enableMemoryCache: false, + loadStateChanged: (ExtendedImageState state) { + if (state.extendedImageLoadState == LoadState.loading) { + final ImageChunkEvent? loadingProgress = + state.loadingProgress; + final double? progress = + loadingProgress?.expectedTotalBytes != null + ? loadingProgress!.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null; + return Container( + color: Colors.black, + height: mediaHeight(context, 0.8), + child: Center( + child: CircularProgressIndicator( + value: progress, + ), + ), + ); + } + if (state.extendedImageLoadState == LoadState.failed) { + return Container( + color: Colors.black, + height: mediaHeight(context, 0.8), + child: Column( + children: [ + ElevatedButton( + onPressed: () { + state.reLoadImage(); + }, + child: const Icon( + Icons.replay_outlined, + size: 30, + )), + ], + )); + } + return null; + }), if (widget.index + 1 == widget.length) Column( children: [ diff --git a/lib/views/manga/reader/manga_reader_view.dart b/lib/views/manga/reader/manga_reader_view.dart index 5005a9cb..0d7e60b2 100644 --- a/lib/views/manga/reader/manga_reader_view.dart +++ b/lib/views/manga/reader/manga_reader_view.dart @@ -169,20 +169,20 @@ class _MangaChapterPageGalleryState widget.readerController.setPageIndex(index); } - void _onAddButtonTapped(int ok, bool isPrev, {bool isSlide = false}) { + void _onAddButtonTapped(int index, bool isPrev, {bool isSlide = false}) { if (isPrev) { if (_selectedValue == ReaderMode.verticalContinuous || _selectedValue == ReaderMode.webtoon) { - if (ok != -1) { + if (index != -1) { _itemScrollController.scrollTo( curve: Curves.ease, - index: ok, + index: index, duration: Duration(milliseconds: isSlide ? 2 : 150)); } } else { - if (ok != -1) { + if (index != -1) { if (_extendedController.hasClients) { - _extendedController.animateToPage(ok.toInt(), + _extendedController.animateToPage(index, duration: Duration(milliseconds: isSlide ? 2 : 150), curve: Curves.ease); } @@ -191,16 +191,16 @@ class _MangaChapterPageGalleryState } else { if (_selectedValue == ReaderMode.verticalContinuous || _selectedValue == ReaderMode.webtoon) { - if (widget.readerController.getPageLength(widget.url) != ok) { + if (widget.readerController.getPageLength(widget.url) != index) { _itemScrollController.scrollTo( curve: Curves.ease, - index: ok, + index: index, duration: Duration(milliseconds: isSlide ? 2 : 150)); } } else { - if (widget.readerController.getPageLength(widget.url) != ok) { + if (widget.readerController.getPageLength(widget.url) != index) { if (_extendedController.hasClients) { - _extendedController.animateToPage(ok.toInt(), + _extendedController.animateToPage(index.toInt(), duration: Duration(milliseconds: isSlide ? 2 : 150), curve: Curves.ease); } @@ -923,7 +923,9 @@ class _MangaChapterPageGalleryState reverse: _isReversHorizontal, physics: const ClampingScrollPhysics(), canScrollPage: (GestureDetails? gestureDetails) { - return !(gestureDetails!.totalScale! > 1.0); + return gestureDetails != null + ? !(gestureDetails.totalScale! > 1.0) + : true; }, itemBuilder: (BuildContext context, int index) { return ImageViewHorizontal( @@ -981,16 +983,21 @@ class _MangaChapterPageGalleryState } if (state.extendedImageLoadState == LoadState.failed) { - return Center( - child: ElevatedButton( - onPressed: () { - state.reLoadImage(); - }, - child: const Icon( - Icons.replay_outlined, - size: 30, - )), - ); + return Container( + color: Colors.black, + height: mediaHeight(context, 0.8), + child: Column( + children: [ + ElevatedButton( + onPressed: () { + state.reLoadImage(); + }, + child: const Icon( + Icons.replay_outlined, + size: 30, + )), + ], + )); } return Container(); }, diff --git a/pubspec.lock b/pubspec.lock index e92921b3..f3bd51ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.10.0" + background_downloader: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "7b778222546d53fcd41a84f6aee90b87c2593930" + url: "https://github.com/kodjodevf/background_downloader.git" + source: git + version: "5.4.5" boolean_selector: dependency: transitive description: @@ -544,6 +553,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + localstore: + dependency: transitive + description: + name: localstore + sha256: "42a0afb7696cfab1b4bd7d08355b4ee01f975fd364553b28d51496eccaf11cce" + url: "https://pub.dev" + source: hosted + version: "1.3.5" logging: dependency: transitive description: @@ -688,6 +705,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85 + url: "https://pub.dev" + source: hosted + version: "9.0.8" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + url: "https://pub.dev" + source: hosted + version: "3.9.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + url: "https://pub.dev" + source: hosted + version: "0.1.2" photo_view: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index baa013d7..02f3537f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,10 @@ dependencies: # draggable_menu: ^0.2.0 url_launcher: ^6.1.10 package_info_plus: ^3.0.2 + background_downloader: + git: + url: https://github.com/kodjodevf/background_downloader.git + permission_handler: ^10.2.0 # The following adds the Cupertino Icons font to your application. diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 61dfc983..a536e18d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterJsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterJsPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index cabb9093..57c1eacd 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_js + permission_handler_windows url_launcher_windows )