diff --git a/lib/modules/local_reader/providers/local_reader_providers.dart b/lib/modules/local_reader/providers/local_reader_providers.dart index d421af2..99e885c 100644 --- a/lib/modules/local_reader/providers/local_reader_providers.dart +++ b/lib/modules/local_reader/providers/local_reader_providers.dart @@ -50,10 +50,10 @@ bool _isImageFile(String path) { } bool _isArchiveFile(String path) { - List imageExtensions = ['.cbz', '.zip', 'cbt', 'tar']; + List archiveExtensions = ['.cbz', '.zip', 'cbt', 'tar']; String extension = path.toLowerCase(); - for (String imageExtension in imageExtensions) { - if (extension.endsWith(imageExtension)) { + for (String archiveExtension in archiveExtensions) { + if (extension.endsWith(archiveExtension)) { return true; } } diff --git a/lib/modules/manga/download/providers/convert_to_cbz.dart b/lib/modules/manga/download/providers/convert_to_cbz.dart new file mode 100644 index 0000000..ff9574c --- /dev/null +++ b/lib/modules/manga/download/providers/convert_to_cbz.dart @@ -0,0 +1,54 @@ +import 'dart:io'; +import 'package:archive/archive_io.dart'; +import 'package:flutter/foundation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'convert_to_cbz.g.dart'; + +@riverpod +Future> convertToCBZ(ConvertToCBZRef ref, String chapterDir, + String mangaDir, String chapterName, List pageList) async { + Map data = { + "chapterDir": chapterDir, + "mangaDir": mangaDir, + "chapterName": chapterName, + "pageList": pageList + }; + return compute(_convertToCBZ, data); +} + +List _convertToCBZ(Map data) { + List imagesPaths = []; + String chapterDir = data["chapterDir"]!; + String mangaDir = data["mangaDir"]!; + String chapterName = data["chapterName"]!; + List pageList = data["pageList"]!; + + if (Directory(chapterDir).existsSync()) { + List entities = Directory(chapterDir).listSync(); + for (FileSystemEntity entity in entities) { + if (entity is File) { + String path = entity.path; + if (path.endsWith('.jpg')) { + imagesPaths.add(path); + } + } + } + imagesPaths.sort( + (a, b) { + return a.toString().compareTo(b.toString()); + }, + ); + } + + if (imagesPaths.isNotEmpty && pageList.length == imagesPaths.length) { + var encoder = ZipFileEncoder(); + encoder.create("$mangaDir/$chapterName.cbz"); + for (var path in imagesPaths) { + encoder.addFile(File(path)); + } + encoder.close(); + Directory(chapterDir).deleteSync(recursive: true); + } + + return imagesPaths; +} diff --git a/lib/modules/manga/download/providers/convert_to_cbz.g.dart b/lib/modules/manga/download/providers/convert_to_cbz.g.dart new file mode 100644 index 0000000..9574902 --- /dev/null +++ b/lib/modules/manga/download/providers/convert_to_cbz.g.dart @@ -0,0 +1,137 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'convert_to_cbz.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$convertToCBZHash() => r'b421d288e9cd1fca3079ccb5d5702ee2ad4cdfe3'; + +/// 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)); + } +} + +typedef ConvertToCBZRef = AutoDisposeFutureProviderRef>; + +/// See also [convertToCBZ]. +@ProviderFor(convertToCBZ) +const convertToCBZProvider = ConvertToCBZFamily(); + +/// See also [convertToCBZ]. +class ConvertToCBZFamily extends Family>> { + /// See also [convertToCBZ]. + const ConvertToCBZFamily(); + + /// See also [convertToCBZ]. + ConvertToCBZProvider call( + String chapterDir, + String mangaDir, + String chapterName, + List pageList, + ) { + return ConvertToCBZProvider( + chapterDir, + mangaDir, + chapterName, + pageList, + ); + } + + @override + ConvertToCBZProvider getProviderOverride( + covariant ConvertToCBZProvider provider, + ) { + return call( + provider.chapterDir, + provider.mangaDir, + provider.chapterName, + provider.pageList, + ); + } + + 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'convertToCBZProvider'; +} + +/// See also [convertToCBZ]. +class ConvertToCBZProvider extends AutoDisposeFutureProvider> { + /// See also [convertToCBZ]. + ConvertToCBZProvider( + this.chapterDir, + this.mangaDir, + this.chapterName, + this.pageList, + ) : super.internal( + (ref) => convertToCBZ( + ref, + chapterDir, + mangaDir, + chapterName, + pageList, + ), + from: convertToCBZProvider, + name: r'convertToCBZProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$convertToCBZHash, + dependencies: ConvertToCBZFamily._dependencies, + allTransitiveDependencies: + ConvertToCBZFamily._allTransitiveDependencies, + ); + + final String chapterDir; + final String mangaDir; + final String chapterName; + final List pageList; + + @override + bool operator ==(Object other) { + return other is ConvertToCBZProvider && + other.chapterDir == chapterDir && + other.mangaDir == mangaDir && + other.chapterName == chapterName && + other.pageList == pageList; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, chapterDir.hashCode); + hash = _SystemHash.combine(hash, mangaDir.hashCode); + hash = _SystemHash.combine(hash, chapterName.hashCode); + hash = _SystemHash.combine(hash, pageList.hashCode); + + return _SystemHash.finish(hash); + } +} +// 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/modules/manga/download/providers/download_provider.dart b/lib/modules/manga/download/providers/download_provider.dart index 9959825..94350b2 100644 --- a/lib/modules/manga/download/providers/download_provider.dart +++ b/lib/modules/manga/download/providers/download_provider.dart @@ -4,6 +4,7 @@ import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/download.dart'; +import 'package:mangayomi/modules/manga/download/providers/convert_to_cbz.dart'; import 'package:mangayomi/modules/more/settings/downloads/providers/downloads_state_provider.dart'; import 'package:mangayomi/providers/storage_provider.dart'; import 'package:mangayomi/services/get_chapter_url.dart'; @@ -24,6 +25,7 @@ Future> downloadChapter( final StorageProvider storageProvider = StorageProvider(); await storageProvider.requestPermission(); final tempDir = await getTemporaryDirectory(); + final mangaDir = await storageProvider.getMangaMainDirectory(chapter); bool onlyOnWifi = useWifi ?? ref.watch(onlyOnWifiStateProvider); Directory? path; bool isOk = false; @@ -125,14 +127,21 @@ Future> downloadChapter( taskIds: pageUrls, isStartDownload: false, chapterId: chapter.id); - + await ref.watch(convertToCBZProvider( + path.path, mangaDir!.path, chapter.name!, pageUrls) + .future); isar.writeTxnSync(() { isar.downloads.putSync(model..chapter.value = chapter); }); } else { await FileDownloader().downloadBatch( tasks, - batchProgressCallback: (succeeded, failed) { + batchProgressCallback: (succeeded, failed) async { + if (succeeded == tasks.length) { + await ref.watch(convertToCBZProvider( + path!.path, mangaDir!.path, chapter.name!, pageUrls) + .future); + } bool isEmpty = isar.downloads .filter() .chapterIdEqualTo(chapter.id!) @@ -165,12 +174,14 @@ Future> downloadChapter( }, taskProgressCallback: (taskProgress) async { if (taskProgress.progress == 1.0) { - await File( - "${tempDir.path}/${taskProgress.task.directory}/${taskProgress.task.filename}") - .copy("${path!.path}/${taskProgress.task.filename}"); - await File( - "${tempDir.path}/${taskProgress.task.directory}/${taskProgress.task.filename}") - .delete(); + if (Platform.isAndroid) { + await File( + "${tempDir.path}/${taskProgress.task.directory}/${taskProgress.task.filename}") + .copy("${path!.path}/${taskProgress.task.filename}"); + await File( + "${tempDir.path}/${taskProgress.task.directory}/${taskProgress.task.filename}") + .delete(); + } } }, ); diff --git a/lib/modules/manga/download/providers/download_provider.g.dart b/lib/modules/manga/download/providers/download_provider.g.dart index 8ed679b..64e94fc 100644 --- a/lib/modules/manga/download/providers/download_provider.g.dart +++ b/lib/modules/manga/download/providers/download_provider.g.dart @@ -6,7 +6,7 @@ part of 'download_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$downloadChapterHash() => r'07e6c9782618562329005e15ce973061bf70d441'; +String _$downloadChapterHash() => r'b5c9bab624536bc4ad0e0c067773f5e591aa0cc1'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/manga/reader/image_view_horizontal.dart b/lib/modules/manga/reader/image_view_horizontal.dart index ea1ee8e..bb0e9d7 100644 --- a/lib/modules/manga/reader/image_view_horizontal.dart +++ b/lib/modules/manga/reader/image_view_horizontal.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -14,6 +15,7 @@ class ImageViewHorizontal extends ConsumerWidget { final String chapter; final Directory path; final bool isLocale; + final Uint8List? localImage; final Widget? Function(ExtendedImageState state) loadStateChanged; final Function(ExtendedImageGestureState state) onDoubleTap; final GestureConfig Function(ExtendedImageState state) @@ -31,20 +33,31 @@ class ImageViewHorizontal extends ConsumerWidget { required this.onDoubleTap, required this.initGestureConfigHandler, required this.isLocale, + this.localImage, }); @override Widget build(BuildContext context, WidgetRef ref) { return isLocale - ? ExtendedImage.file( - File("${path.path}" "${padIndex(index + 1)}.jpg"), - clearMemoryCacheWhenDispose: true, - enableMemoryCache: false, - mode: ExtendedImageMode.gesture, - initGestureConfigHandler: initGestureConfigHandler, - onDoubleTap: onDoubleTap, - loadStateChanged: loadStateChanged, - ) + ? localImage != null + ? ExtendedImage.memory( + localImage!, + clearMemoryCacheWhenDispose: true, + enableMemoryCache: false, + mode: ExtendedImageMode.gesture, + initGestureConfigHandler: initGestureConfigHandler, + onDoubleTap: onDoubleTap, + loadStateChanged: loadStateChanged, + ) + : ExtendedImage.file( + File("${path.path}" "${padIndex(index + 1)}.jpg"), + clearMemoryCacheWhenDispose: true, + enableMemoryCache: false, + mode: ExtendedImageMode.gesture, + initGestureConfigHandler: initGestureConfigHandler, + onDoubleTap: onDoubleTap, + loadStateChanged: loadStateChanged, + ) : ExtendedImage.network( url, headers: ref.watch(headersProvider(source: source)), diff --git a/lib/modules/manga/reader/image_view_vertical.dart b/lib/modules/manga/reader/image_view_vertical.dart index f76f1d6..cef710a 100644 --- a/lib/modules/manga/reader/image_view_vertical.dart +++ b/lib/modules/manga/reader/image_view_vertical.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -17,6 +18,7 @@ class ImageViewVertical extends ConsumerWidget { final String source; final String chapter; final Directory path; + final Uint8List? localImage; const ImageViewVertical({ super.key, @@ -28,6 +30,7 @@ class ImageViewVertical extends ConsumerWidget { required this.source, required this.length, required this.isLocale, + this.localImage, }); @override @@ -42,11 +45,18 @@ class ImageViewVertical extends ConsumerWidget { height: MediaQuery.of(context).padding.top, ), isLocale - ? ExtendedImage.file( - fit: BoxFit.contain, - clearMemoryCacheWhenDispose: true, - enableMemoryCache: false, - File('${path.path}${padIndex(index + 1)}.jpg')) + ? localImage != null + ? ExtendedImage.memory( + localImage!, + fit: BoxFit.contain, + clearMemoryCacheWhenDispose: true, + enableMemoryCache: false, + ) + : ExtendedImage.file( + fit: BoxFit.contain, + clearMemoryCacheWhenDispose: true, + enableMemoryCache: false, + File('${path.path}${padIndex(index + 1)}.jpg')) : ExtendedImage.network(url, headers: ref.watch(headersProvider(source: source)), handleLoadingProgress: true, diff --git a/lib/modules/manga/reader/manga_reader_view.dart b/lib/modules/manga/reader/manga_reader_view.dart index 917ab34..73d3a19 100644 --- a/lib/modules/manga/reader/manga_reader_view.dart +++ b/lib/modules/manga/reader/manga_reader_view.dart @@ -38,7 +38,7 @@ class MangaReaderView extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, overlays: []); - final chapterData = ref.watch(GetChapterUrlProvider( + final chapterData = ref.watch(getChapterUrlProvider( chapter: chapter, )); final readerController = @@ -77,6 +77,7 @@ class MangaReaderView extends ConsumerWidget { readerController: readerController, isLocaleList: data.isLocaleList, chapter: chapter, + localImages: data.localImages, ); }, error: (error, stackTrace) => Scaffold( @@ -138,12 +139,14 @@ class MangaChapterPageGallery extends ConsumerStatefulWidget { required this.url, required this.readerController, required this.isLocaleList, - required this.chapter}); + required this.chapter, + required this.localImages}); final ReaderController readerController; final Directory path; final List url; final List isLocaleList; final Chapter chapter; + final List localImages; @override ConsumerState createState() { @@ -921,6 +924,9 @@ class _MangaChapterPageGalleryState }, onDoubleTap: () {}, child: ImageViewVertical( + localImage: widget.localImages.isNotEmpty + ? widget.localImages[index] + : null, titleManga: widget.readerController.getMangaName(), source: widget.readerController .getSourceName() @@ -960,6 +966,9 @@ class _MangaChapterPageGalleryState }, itemBuilder: (BuildContext context, int index) { return ImageViewHorizontal( + localImage: widget.localImages.isNotEmpty + ? widget.localImages[index] + : null, titleManga: widget.readerController.getMangaName(), source: widget.readerController .getSourceName() diff --git a/lib/services/get_chapter_url.dart b/lib/services/get_chapter_url.dart index f08469d..ad7a343 100644 --- a/lib/services/get_chapter_url.dart +++ b/lib/services/get_chapter_url.dart @@ -1,10 +1,12 @@ // ignore_for_file: depend_o import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/models/source.dart'; +import 'package:mangayomi/modules/local_reader/providers/local_reader_providers.dart'; import 'package:mangayomi/providers/storage_provider.dart'; import 'package:mangayomi/sources/multisrc/heancms/heancms.dart'; import 'package:mangayomi/sources/multisrc/madara/src/madara.dart'; @@ -24,8 +26,12 @@ class GetChapterUrlModel { Directory? path; List pageUrls = []; List isLocaleList = []; + List localImages = []; GetChapterUrlModel( - {required this.path, required this.pageUrls, required this.isLocaleList}); + {required this.path, + required this.pageUrls, + required this.isLocaleList, + required this.localImages}); } @riverpod @@ -42,8 +48,11 @@ Future getChapterUrl( final isarPageUrls = settings!.chapterPageUrlsList! .where((element) => element.chapterId == chapter.id); final incognitoMode = ref.watch(incognitoModeStateProvider); - path = await StorageProvider().getMangaChapterDirectory(chapter); + final storageProvider = StorageProvider(); + path = await storageProvider.getMangaChapterDirectory(chapter); + final mangaDirectory = await storageProvider.getMangaMainDirectory(chapter); + List localImages = []; if (isarPageUrls.isNotEmpty && isarPageUrls.first.urls != null && isarPageUrls.first.urls!.isNotEmpty) { @@ -105,6 +114,7 @@ Future getChapterUrl( else if (getMangaTypeSource(source) == TypeSource.heancms) { pageUrls = await HeanCms().getChapterUrl(chapter: chapter, ref: ref); } + /***********/ /*madara*/ /***********/ @@ -127,15 +137,29 @@ Future getChapterUrl( isar.writeTxnSync(() => isar.settings .putSync(settings..chapterPageUrlsList = chapterPageUrls)); } - for (var i = 0; i < pageUrls.length; i++) { - if (await File("${path!.path}" "${padIndex(i + 1)}.jpg").exists()) { + + if (await File("${mangaDirectory!.path}${chapter.name}.cbz").exists()) { + final local = await ref.watch(getArchiveDataFromFileProvider( + "${mangaDirectory.path}${chapter.name}.cbz") + .future); + for (var image in local.images!) { + localImages.add(image.image!); isLocaleList.add(true); - } else { - isLocaleList.add(false); + } + } else { + for (var i = 0; i < pageUrls.length; i++) { + if (await File("${path!.path}" "${padIndex(i + 1)}.jpg").exists()) { + isLocaleList.add(true); + } else { + isLocaleList.add(false); + } } } } return GetChapterUrlModel( - path: path, pageUrls: pageUrls, isLocaleList: isLocaleList); + path: path, + pageUrls: pageUrls, + isLocaleList: isLocaleList, + localImages: localImages); } diff --git a/lib/services/get_chapter_url.g.dart b/lib/services/get_chapter_url.g.dart index c294a15..c536a38 100644 --- a/lib/services/get_chapter_url.g.dart +++ b/lib/services/get_chapter_url.g.dart @@ -6,7 +6,7 @@ part of 'get_chapter_url.dart'; // RiverpodGenerator // ************************************************************************** -String _$getChapterUrlHash() => r'2ac689860d288da631afbf39ab7ad2df7e2b99f3'; +String _$getChapterUrlHash() => r'37d070779ae79c31b82820c3a78af825db45fc1d'; /// Copied from Dart SDK class _SystemHash {