diff --git a/lib/modules/local_reader/local_reader_screen.dart b/lib/modules/local_reader/local_reader_screen.dart new file mode 100644 index 00000000..919b4c77 --- /dev/null +++ b/lib/modules/local_reader/local_reader_screen.dart @@ -0,0 +1,205 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mangayomi/modules/local_reader/models/models.dart'; +import 'package:mangayomi/modules/local_reader/providers/local_reader_providers.dart'; +import 'package:mangayomi/modules/widgets/progress_center.dart'; +import 'package:mangayomi/utils/media_query.dart'; + +class LocalReaderScreen extends ConsumerStatefulWidget { + const LocalReaderScreen({super.key}); + + @override + ConsumerState createState() => _LocalReaderScreenState(); +} + +class _LocalReaderScreenState extends ConsumerState { + List images = []; + bool isLoading = false; + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Scaffold( + appBar: AppBar( + title: const Text('Local Reader'), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton( + onPressed: () async { + setState(() { + isLoading = true; + }); + //File + FilePickerResult? result = await FilePicker.platform + .pickFiles( + type: FileType.custom, + allowedExtensions: [ + 'cbz', + 'zip', + 'cbt', + 'tar' + ]); + if (result != null) { + //File + final ddd = await ref.watch( + getArchiveDataFromFileProvider( + result.files.first.path!) + .future); + + setState(() { + images.add(ddd); + isLoading = false; + }); + } else { + setState(() { + isLoading = false; + }); + } + }, + icon: const Icon(Icons.file_open)), + IconButton( + onPressed: () async { + setState(() { + isLoading = true; + }); + //Directory + String? result = + await FilePicker.platform.getDirectoryPath(); + + if (result != null) { + //Directory + final ddd = await ref.watch( + getArchiveDataFromDirectoryProvider(result) + .future); + setState(() { + images = ddd; + isLoading = false; + }); + } else { + setState(() { + isLoading = false; + }); + } + }, + icon: const Icon(Icons.create_new_folder_rounded)), + ], + ), + Expanded( + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: 0.68, crossAxisCount: 3), + itemCount: images.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Material( + borderRadius: BorderRadius.circular(10), + clipBehavior: Clip.antiAliasWithSaveLayer, + child: InkWell( + onTap: () { + context.push("/localReaderReaderView", + extra: images[index]); + }, + child: Ink.image( + height: 200, + fit: BoxFit.cover, + image: MemoryImage(images[index].coverImage!), + child: Container( + height: 70, + decoration: BoxDecoration( + color: + Theme.of(context).scaffoldBackgroundColor, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.6) + ], + stops: const [0, 1], + ), + ), + child: Stack( + children: [ + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + images[index].name!, + style: const TextStyle( + fontSize: 12, + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(0.5, 0.9), + blurRadius: 3.0) + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(5), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(5)), + child: Padding( + padding: const EdgeInsets.all(2), + child: Text( + getTypeExtension(images[index] + .extensionType!) + .toUpperCase(), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 10), + ), + )), + ) + ], + ), + ), + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + if (isLoading) + Container( + width: mediaWidth(context, 1), + height: mediaHeight(context, 1), + color: Colors.black45, + child: UnconstrainedBox( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).scaffoldBackgroundColor, + ), + height: 200, + width: 200, + child: const Center(child: ProgressCenter())), + ), + ) + ], + ); + } +} diff --git a/lib/modules/local_reader/models/models.dart b/lib/modules/local_reader/models/models.dart new file mode 100644 index 00000000..129f4c5a --- /dev/null +++ b/lib/modules/local_reader/models/models.dart @@ -0,0 +1,20 @@ +import 'package:flutter/foundation.dart'; + +class LocalArchive { + String? name; + + Uint8List? coverImage; + + List? images = []; + + LocalExtensionType? extensionType; + + String? path; +} + +enum LocalExtensionType { cbz, zip, cbt, tar } + +class LocalImage { + String? name; + Uint8List? image; +} diff --git a/lib/modules/local_reader/providers/local_reader_providers.dart b/lib/modules/local_reader/providers/local_reader_providers.dart new file mode 100644 index 00000000..d085f779 --- /dev/null +++ b/lib/modules/local_reader/providers/local_reader_providers.dart @@ -0,0 +1,121 @@ +import 'dart:io'; +import 'package:archive/archive.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mangayomi/modules/local_reader/models/models.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'local_reader_providers.g.dart'; + +@riverpod +Future> getArchiveDataFromDirectory( + GetArchiveDataFromDirectoryRef ref, String path) async { + return compute(_extract, path); +} + +@riverpod +Future getArchiveDataFromFile( + GetArchiveDataFromFileRef ref, String path) async { + return compute(_extractArchive, path); +} + +List _extract(String data) { + return _searchForArchive(Directory(data)); +} + +List _list = []; +List _searchForArchive(Directory dir) { + List entities = dir.listSync(); + for (FileSystemEntity entity in entities) { + if (entity is Directory) { + _searchForArchive(entity); + } else if (entity is File) { + String path = entity.path; + if (_isArchiveFile(path)) { + final dd = _extractArchive(path); + _list.add(dd); + } + } + } + return _list; +} + +bool _isImageFile(String path) { + List imageExtensions = ['.png', '.jpg', '.jpeg']; + String extension = path.toLowerCase(); + for (String imageExtension in imageExtensions) { + if (extension.endsWith(imageExtension)) { + return true; + } + } + return false; +} + +bool _isArchiveFile(String path) { + List imageExtensions = ['.cbz', '.zip', 'cbt', 'tar']; + String extension = path.toLowerCase(); + for (String imageExtension in imageExtensions) { + if (extension.endsWith(imageExtension)) { + return true; + } + } + return false; +} + +LocalArchive _extractArchive(String path) { + final bytes = File(path).readAsBytesSync(); + final localArchive = LocalArchive() + ..path = path + ..extensionType = + setTypeExtension(path.split('/').last.split("\\").last.split(".").last) + ..name = path + .split('/') + .last + .split("\\") + .last + .replaceAll(RegExp(r'\.(cbz|zip|cbt|tar)'), ''); + Archive? archive; + final extensionType = localArchive.extensionType; + if (extensionType == LocalExtensionType.cbt || + extensionType == LocalExtensionType.tar) { + archive = TarDecoder().decodeBytes(bytes); + } else { + archive = ZipDecoder().decodeBytes(bytes); + } + + for (final file in archive) { + final filename = file.name; + if (file.isFile) { + if (_isImageFile(filename)) { + if (filename.contains("cover")) { + final data = file.content as Uint8List; + localArchive.coverImage = Uint8List.fromList(data); + } else { + final data = file.content as Uint8List; + localArchive.images!.add(LocalImage() + ..image = Uint8List.fromList(data) + ..name = filename.split('/').last.split("\\").last); + } + } + } + } + localArchive.images!.sort((a, b) => a.name!.compareTo(b.name!)); + localArchive.coverImage ??= localArchive.images!.first.image; + return localArchive; +} + +String getTypeExtension(LocalExtensionType type) { + return switch (type) { + LocalExtensionType.cbt => type.name, + LocalExtensionType.zip => type.name, + LocalExtensionType.tar => type.name, + _ => type.name, + }; +} + +LocalExtensionType setTypeExtension(String extension) { + return switch (extension) { + "cbt" => LocalExtensionType.cbt, + "zip" => LocalExtensionType.zip, + "tar" => LocalExtensionType.tar, + _ => LocalExtensionType.cbz, + }; +} diff --git a/lib/modules/local_reader/providers/local_reader_providers.g.dart b/lib/modules/local_reader/providers/local_reader_providers.g.dart new file mode 100644 index 00000000..0ee9266a --- /dev/null +++ b/lib/modules/local_reader/providers/local_reader_providers.g.dart @@ -0,0 +1,200 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'local_reader_providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$getArchiveDataFromDirectoryHash() => + r'fb85bd2b43ae73f083bdfa0760d8185ef989dd09'; + +/// 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 GetArchiveDataFromDirectoryRef + = AutoDisposeFutureProviderRef>; + +/// See also [getArchiveDataFromDirectory]. +@ProviderFor(getArchiveDataFromDirectory) +const getArchiveDataFromDirectoryProvider = GetArchiveDataFromDirectoryFamily(); + +/// See also [getArchiveDataFromDirectory]. +class GetArchiveDataFromDirectoryFamily + extends Family>> { + /// See also [getArchiveDataFromDirectory]. + const GetArchiveDataFromDirectoryFamily(); + + /// See also [getArchiveDataFromDirectory]. + GetArchiveDataFromDirectoryProvider call( + String path, + ) { + return GetArchiveDataFromDirectoryProvider( + path, + ); + } + + @override + GetArchiveDataFromDirectoryProvider getProviderOverride( + covariant GetArchiveDataFromDirectoryProvider provider, + ) { + return call( + provider.path, + ); + } + + 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'getArchiveDataFromDirectoryProvider'; +} + +/// See also [getArchiveDataFromDirectory]. +class GetArchiveDataFromDirectoryProvider + extends AutoDisposeFutureProvider> { + /// See also [getArchiveDataFromDirectory]. + GetArchiveDataFromDirectoryProvider( + this.path, + ) : super.internal( + (ref) => getArchiveDataFromDirectory( + ref, + path, + ), + from: getArchiveDataFromDirectoryProvider, + name: r'getArchiveDataFromDirectoryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$getArchiveDataFromDirectoryHash, + dependencies: GetArchiveDataFromDirectoryFamily._dependencies, + allTransitiveDependencies: + GetArchiveDataFromDirectoryFamily._allTransitiveDependencies, + ); + + final String path; + + @override + bool operator ==(Object other) { + return other is GetArchiveDataFromDirectoryProvider && other.path == path; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, path.hashCode); + + return _SystemHash.finish(hash); + } +} + +String _$getArchiveDataFromFileHash() => + r'e5dc60fea6c36346c47542c141703bb027173215'; +typedef GetArchiveDataFromFileRef = AutoDisposeFutureProviderRef; + +/// See also [getArchiveDataFromFile]. +@ProviderFor(getArchiveDataFromFile) +const getArchiveDataFromFileProvider = GetArchiveDataFromFileFamily(); + +/// See also [getArchiveDataFromFile]. +class GetArchiveDataFromFileFamily extends Family> { + /// See also [getArchiveDataFromFile]. + const GetArchiveDataFromFileFamily(); + + /// See also [getArchiveDataFromFile]. + GetArchiveDataFromFileProvider call( + String path, + ) { + return GetArchiveDataFromFileProvider( + path, + ); + } + + @override + GetArchiveDataFromFileProvider getProviderOverride( + covariant GetArchiveDataFromFileProvider provider, + ) { + return call( + provider.path, + ); + } + + 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'getArchiveDataFromFileProvider'; +} + +/// See also [getArchiveDataFromFile]. +class GetArchiveDataFromFileProvider + extends AutoDisposeFutureProvider { + /// See also [getArchiveDataFromFile]. + GetArchiveDataFromFileProvider( + this.path, + ) : super.internal( + (ref) => getArchiveDataFromFile( + ref, + path, + ), + from: getArchiveDataFromFileProvider, + name: r'getArchiveDataFromFileProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$getArchiveDataFromFileHash, + dependencies: GetArchiveDataFromFileFamily._dependencies, + allTransitiveDependencies: + GetArchiveDataFromFileFamily._allTransitiveDependencies, + ); + + final String path; + + @override + bool operator ==(Object other) { + return other is GetArchiveDataFromFileProvider && other.path == path; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, path.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/local_reader/reader/local_reader_reader_view.dart b/lib/modules/local_reader/reader/local_reader_reader_view.dart new file mode 100644 index 00000000..a1b1c817 --- /dev/null +++ b/lib/modules/local_reader/reader/local_reader_reader_view.dart @@ -0,0 +1,938 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:draggable_menu/draggable_menu.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter/material.dart'; +import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/settings.dart'; +import 'package:mangayomi/modules/local_reader/models/models.dart'; +import 'package:mangayomi/utils/image_detail_info.dart'; +import 'package:mangayomi/utils/media_query.dart'; +import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicator_animate_rotate.dart'; +import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +typedef DoubleClickAnimationListener = void Function(); + +class LocalReaderReaderView extends ConsumerWidget { + final LocalArchive localArchive; + const LocalReaderReaderView({ + super.key, + required this.localArchive, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, + overlays: []); + + return MangaChapterPageGallery(localArchive: localArchive); + } +} + +class MangaChapterPageGallery extends ConsumerStatefulWidget { + const MangaChapterPageGallery({super.key, required this.localArchive}); + final LocalArchive localArchive; + + @override + ConsumerState createState() { + return _MangaChapterPageGalleryState(); + } +} + +class _MangaChapterPageGalleryState + extends ConsumerState + with TickerProviderStateMixin { + late final ItemScrollController _itemScrollController = + ItemScrollController(); + late AnimationController _scaleAnimationController; + late Animation _animation; + late int _currentIndex = 0; + @override + void dispose() { + _rebuildDetail.close(); + _doubleClickAnimationController.dispose(); + clearGestureDetailsCache(); + super.dispose(); + } + + bool animatePageTransitions = + isar.settings.getSync(227)!.animatePageTransitions!; + Duration? _doubleTapAnimationDuration() { + int doubleTapAnimationValue = + isar.settings.getSync(227)!.doubleTapAnimationSpeed!; + if (doubleTapAnimationValue == 0) { + return const Duration(milliseconds: 10); + } else if (doubleTapAnimationValue == 1) { + return const Duration(milliseconds: 800); + } + return const Duration(milliseconds: 200); + } + + Future setIndex(int index) async { + setState(() { + _currentIndex = index; + }); + } + + @override + void initState() { + _doubleClickAnimationController = AnimationController( + duration: _doubleTapAnimationDuration(), vsync: this); + + _scaleAnimationController = AnimationController( + duration: _doubleTapAnimationDuration(), vsync: this); + _animation = Tween(begin: 1.0, end: 2.0).animate( + CurvedAnimation(curve: Curves.ease, parent: _scaleAnimationController)); + _animation.addListener(() => _photoViewController.scale = _animation.value); + _initCurrentIndex(); + _itemPositionsListener.itemPositions.addListener(_readProgressListener); + super.initState(); + } + + _readProgressListener() { + var posIndex = _itemPositionsListener.itemPositions.value.first.index; + if (posIndex >= 0 && posIndex < widget.localArchive.images!.length) { + if (_currentIndex != posIndex) { + setState(() { + _currentIndex = posIndex; + }); + } + } + } + + _initCurrentIndex() async { + await Future.delayed(const Duration(milliseconds: 1)); + _selectedValue = ReaderMode.vertical; + _(_selectedValue!, true); + } + + void _onPageChanged(int index) { + setState(() { + _currentIndex = index; + }); + if (_imageDetailY != 0) { + _imageDetailY = 0; + _rebuildDetail.sink.add(_imageDetailY); + } + } + + void _onBtnTapped(int index, bool isPrev, {bool isSlide = false}) { + if (isPrev) { + if (_selectedValue == ReaderMode.verticalContinuous || + _selectedValue == ReaderMode.webtoon) { + if (index != -1) { + if (isSlide) { + _itemScrollController.jumpTo( + index: index, + ); + } else { + animatePageTransitions + ? _itemScrollController.scrollTo( + curve: Curves.ease, + index: index, + duration: Duration(milliseconds: isSlide ? 2 : 150)) + : _itemScrollController.jumpTo( + index: index, + ); + } + } + } else { + if (index != -1) { + if (_extendedController.hasClients) { + setState(() { + _isZoom = false; + }); + animatePageTransitions + ? _extendedController.animateToPage(index, + duration: Duration(milliseconds: isSlide ? 2 : 150), + curve: Curves.ease) + : _extendedController.jumpToPage(index); + } + } + } + } else { + if (_selectedValue == ReaderMode.verticalContinuous || + _selectedValue == ReaderMode.webtoon) { + if (widget.localArchive.images!.length != index) { + if (isSlide) { + _itemScrollController.jumpTo( + index: index, + ); + } else { + animatePageTransitions + ? _itemScrollController.scrollTo( + curve: Curves.ease, + index: index, + duration: Duration(milliseconds: isSlide ? 2 : 150)) + : _itemScrollController.jumpTo( + index: index, + ); + } + } + } else { + if (widget.localArchive.images!.length != index) { + if (_extendedController.hasClients) { + setState(() { + _isZoom = false; + }); + animatePageTransitions + ? _extendedController.animateToPage(index, + duration: Duration(milliseconds: isSlide ? 2 : 150), + curve: Curves.ease) + : _extendedController.jumpToPage(index); + } + } + } + } + } + + ReaderMode? _selectedValue; + bool _isView = false; + Alignment _scalePosition = Alignment.center; + final PhotoViewController _photoViewController = PhotoViewController(); + final PhotoViewScaleStateController _photoViewScaleStateController = + PhotoViewScaleStateController(); + + final ItemPositionsListener _itemPositionsListener = + ItemPositionsListener.create(); + void _onScaleEnd(BuildContext context, ScaleEndDetails details, + PhotoViewControllerValue controllerValue) { + if (controllerValue.scale! < 1) { + _photoViewScaleStateController.reset(); + } + } + + late final _extendedController = ExtendedPageController( + initialPage: _currentIndex, + shouldIgnorePointerWhenScrolling: false, + ); + + double get pixelRatio => View.of(context).devicePixelRatio; + + Size get size => View.of(context).physicalSize / pixelRatio; + Alignment _computeAlignmentByTapOffset(Offset offset) { + return Alignment((offset.dx - size.width / 2) / (size.width / 2), + (offset.dy - size.height / 2) / (size.height / 2)); + } + + void _toggleScale(Offset tapPosition) { + setState(() { + if (_scaleAnimationController.isAnimating) { + return; + } + + if (_photoViewController.scale == 1.0) { + _scalePosition = _computeAlignmentByTapOffset(tapPosition); + + if (_scaleAnimationController.isCompleted) { + _scaleAnimationController.reset(); + } + + _scaleAnimationController.forward(); + return; + } + + if (_photoViewController.scale == 2.0) { + _scaleAnimationController.reverse(); + return; + } + + _photoViewScaleStateController.reset(); + }); + } + + Axis _scrollDirection = Axis.vertical; + bool _isReversHorizontal = false; + + late bool _showPagesNumber = true; + _(ReaderMode value, bool isInit) async { + if (value == ReaderMode.vertical) { + if (mounted) { + setState(() { + _selectedValue = value; + _scrollDirection = Axis.vertical; + _isReversHorizontal = false; + }); + if (isInit) { + await Future.delayed(const Duration(milliseconds: 30)); + } + _extendedController.jumpToPage(_currentIndex); + } + } else if (value == ReaderMode.ltr || value == ReaderMode.rtl) { + if (mounted) { + setState(() { + if (value == ReaderMode.rtl) { + _isReversHorizontal = true; + } else { + _isReversHorizontal = false; + } + _selectedValue = value; + _scrollDirection = Axis.horizontal; + }); + if (isInit) { + await Future.delayed(const Duration(milliseconds: 30)); + } + _extendedController.jumpToPage(_currentIndex); + } + } else { + if (mounted) { + setState(() { + _selectedValue = value; + _isReversHorizontal = false; + }); + if (isInit) { + await Future.delayed(const Duration(milliseconds: 30)); + } + _itemScrollController.scrollTo( + index: _currentIndex, duration: const Duration(milliseconds: 1)); + } + } + } + + Color _backgroundColor(BuildContext context) => + Theme.of(context).scaffoldBackgroundColor.withOpacity(0.9); + + Widget _showMore() { + return Consumer( + builder: (context, ref, child) { + final currentIndex = _currentIndex; + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AnimatedContainer( + height: _isView ? 80 : 0, + curve: Curves.ease, + duration: const Duration(milliseconds: 200), + child: PreferredSize( + preferredSize: Size.fromHeight(_isView ? 80 : 0), + child: AppBar( + centerTitle: false, + automaticallyImplyLeading: false, + titleSpacing: 0, + leading: BackButton( + onPressed: () { + Navigator.pop(context); + }, + ), + title: ListTile( + dense: true, + title: SizedBox( + width: mediaWidth(context, 0.8), + child: Text( + '${widget.localArchive.name} ', + style: const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + subtitle: SizedBox( + width: mediaWidth(context, 0.8), + child: Text( + "", + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ), + backgroundColor: _backgroundColor(context), + ), + ), + ), + AnimatedContainer( + curve: Curves.ease, + duration: const Duration(milliseconds: 300), + width: mediaWidth(context, 1), + height: _isView ? 130 : 0, + child: Column( + children: [ + Flexible( + child: Row( + children: [ + Expanded( + child: Transform.scale( + scaleX: !_isReversHorizontal ? 1 : -1, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Container( + height: 70, + decoration: BoxDecoration( + color: _backgroundColor(context), + borderRadius: BorderRadius.circular(25)), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 12), + child: Transform.scale( + scaleX: !_isReversHorizontal ? 1 : -1, + child: SizedBox( + width: 25, + child: Text( + "${currentIndex + 1} ", + style: const TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + Flexible( + child: Slider( + onChanged: (newValue) { + _onBtnTapped(newValue.toInt(), true, + isSlide: true); + }, + divisions: max( + widget.localArchive.images!.length - + 1, + 1), + value: min( + _currentIndex.toDouble(), + widget.localArchive.images!.length + .toDouble()), + min: 0, + max: (widget.localArchive.images! + .length - + 1) + .toDouble(), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 12), + child: Transform.scale( + scaleX: !_isReversHorizontal ? 1 : -1, + child: SizedBox( + width: 25, + child: Text( + "${widget.localArchive.images!.length}", + style: const TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + Flexible( + child: Container( + height: 65, + color: _backgroundColor(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + PopupMenuButton( + color: Colors.black, + child: const Icon( + Icons.app_settings_alt_outlined, + ), + onSelected: (value) { + if (mounted) { + setState(() { + _selectedValue = value; + }); + } + _(value, true); + }, + itemBuilder: (context) => [ + for (var readerMode in ReaderMode.values) + PopupMenuItem( + value: readerMode, + child: Row( + children: [ + Icon( + Icons.check, + color: _selectedValue == readerMode + ? Colors.white + : Colors.transparent, + ), + const SizedBox( + width: 7, + ), + Text( + getReaderModeName(readerMode), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ], + )), + ], + ), + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.screen_rotation, + ), + ), + IconButton( + onPressed: () { + _showModalSettings(); + }, + icon: const Icon( + Icons.settings_rounded, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + + Widget _showPage() { + return Consumer( + builder: (context, ref, child) { + final currentIndex = _currentIndex; + return _isView + ? Container() + : _showPagesNumber + ? Align( + alignment: Alignment.bottomCenter, + child: Text( + '${currentIndex + 1} / ${widget.localArchive.images!.length}', + style: const TextStyle( + color: Colors.white, + fontSize: 12.0, + shadows: [ + Shadow(offset: Offset(0.0, 0.0), blurRadius: 10.0) + ], + ), + textAlign: TextAlign.center, + ), + ) + : Container(); + }, + ); + } + + _isViewFunction() { + if (mounted) { + setState(() { + _isView = !_isView; + }); + } + if (_isView) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + } else { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, + overlays: []); + } + } + + Widget _gestureRightLeft() { + return Consumer( + builder: (context, ref, child) { + return Row( + children: [ + /// left region + Expanded( + flex: 2, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + if (_isReversHorizontal) { + _onBtnTapped(_currentIndex + 1, false); + } else { + _onBtnTapped(_currentIndex - 1, true); + } + }, + onDoubleTapDown: _isVerticalContinous() + ? (TapDownDetails details) { + _toggleScale(details.globalPosition); + } + : null, + onDoubleTap: _isVerticalContinous() ? () {} : null, + ), + ), + + /// center region + Expanded( + flex: 2, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + _isViewFunction(); + }, + onDoubleTapDown: _isVerticalContinous() + ? (TapDownDetails details) { + _toggleScale(details.globalPosition); + } + : null, + onDoubleTap: _isVerticalContinous() ? () {} : null, + ), + ), + + /// right region + Expanded( + flex: 2, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + if (_isReversHorizontal) { + _onBtnTapped(_currentIndex - 1, true); + } else { + _onBtnTapped(_currentIndex + 1, false); + } + }, + onDoubleTapDown: _isVerticalContinous() + ? (TapDownDetails details) { + _toggleScale(details.globalPosition); + } + : null, + onDoubleTap: _isVerticalContinous() ? () {} : null, + ), + ), + ], + ); + }, + ); + } + + Widget _gestureTopBottom() { + return Consumer( + builder: (context, ref, child) { + return Column( + children: [ + /// top region + Expanded( + flex: 2, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + _onBtnTapped(_currentIndex - 1, true); + }, + onDoubleTapDown: _isVerticalContinous() + ? (TapDownDetails details) { + _toggleScale(details.globalPosition); + } + : null, + onDoubleTap: _isVerticalContinous() ? () {} : null, + ), + ), + + /// center region + Expanded(flex: 5, child: Container()), + + /// bottom region + Expanded( + flex: 2, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + _onBtnTapped(_currentIndex + 1, false); + }, + onDoubleTapDown: _isVerticalContinous() + ? (TapDownDetails details) { + _toggleScale(details.globalPosition); + } + : null, + onDoubleTap: _isVerticalContinous() ? () {} : null, + ), + ), + ], + ); + }, + ); + } + + bool _isZoom = false; + bool _isVerticalContinous() { + return _selectedValue == ReaderMode.verticalContinuous || + _selectedValue == ReaderMode.webtoon; + } + + final StreamController _rebuildDetail = + StreamController.broadcast(); + final Map detailKeys = {}; + late AnimationController _doubleClickAnimationController; + + Animation? _doubleClickAnimation; + late DoubleClickAnimationListener _doubleClickAnimationListener; + List doubleTapScales = [1.0, 2.0]; + GlobalKey slidePagekey = + GlobalKey(); + double _imageDetailY = 0; + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + Navigator.pop(context); + + return false; + }, + child: Stack( + children: [ + _isVerticalContinous() + ? PhotoViewGallery.builder( + itemCount: 1, + builder: (_, __) => PhotoViewGalleryPageOptions.customChild( + controller: _photoViewController, + scaleStateController: _photoViewScaleStateController, + basePosition: _scalePosition, + onScaleEnd: _onScaleEnd, + child: ScrollablePositionedList.separated( + physics: const ClampingScrollPhysics(), + minCacheExtent: 8 * (MediaQuery.of(context).size.height), + initialScrollIndex: _currentIndex, + itemCount: widget.localArchive.images!.length, + itemScrollController: _itemScrollController, + itemPositionsListener: _itemPositionsListener, + itemBuilder: (context, index) => GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTapDown: (TapDownDetails details) { + _toggleScale(details.globalPosition); + }, + onDoubleTap: () {}, + child: Image.memory( + widget.localArchive.images![index].image!)), + separatorBuilder: (_, __) => Divider( + color: Colors.black, + height: _selectedValue == ReaderMode.webtoon ? 0 : 6), + ), + ), + ) + : Material( + color: Colors.black, + shadowColor: Colors.black, + child: ExtendedImageGesturePageView.builder( + controller: _extendedController, + scrollDirection: _scrollDirection, + reverse: _isReversHorizontal, + physics: const ClampingScrollPhysics(), + preloadPagesCount: + _isZoom ? 0 : widget.localArchive.images!.length, + canScrollPage: (GestureDetails? gestureDetails) { + return gestureDetails != null + ? !(gestureDetails.totalScale! > 1.0) + : true; + }, + itemBuilder: (BuildContext context, int index) { + return ExtendedImage.memory( + widget.localArchive.images![index].image!, + clearMemoryCacheWhenDispose: true, + enableMemoryCache: false, + mode: ExtendedImageMode.gesture, + loadStateChanged: (ExtendedImageState state) { + if (state.extendedImageLoadState == + LoadState.loading) { + final ImageChunkEvent? loadingProgress = + state.loadingProgress; + final double progress = + loadingProgress?.expectedTotalBytes != null + ? loadingProgress!.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : 0; + return Container( + color: Colors.black, + height: mediaHeight(context, 0.8), + child: CircularProgressIndicatorAnimateRotate( + progress: progress), + ); + } + if (state.extendedImageLoadState == + LoadState.completed) { + return StreamBuilder( + builder: (BuildContext context, + AsyncSnapshot data) { + return ExtendedImageGesture( + state, + canScaleImage: (_) => _imageDetailY == 0, + imageBuilder: (Widget image) { + return Stack( + children: [ + Positioned.fill( + top: _imageDetailY, + bottom: -_imageDetailY, + child: image, + ), + ], + ); + }, + ); + }, + initialData: _imageDetailY, + stream: _rebuildDetail.stream, + ); + } + if (state.extendedImageLoadState == + LoadState.failed) { + return Container( + color: Colors.black, + height: mediaHeight(context, 0.8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + state.reLoadImage(); + }, + child: const Icon( + Icons.replay_outlined, + size: 30, + )), + ], + )); + } + return Container(); + }, + initGestureConfigHandler: (ExtendedImageState state) { + double? initialScale = 1.0; + final size = MediaQuery.of(context).size; + if (state.extendedImageInfo != null) { + initialScale = initScale( + size: size, + initialScale: initialScale, + imageSize: Size( + state.extendedImageInfo!.image.width + .toDouble(), + state.extendedImageInfo!.image.height + .toDouble())); + } + return GestureConfig( + inertialSpeed: 200, + inPageView: true, + initialScale: initialScale!, + maxScale: 8, + animationMaxScale: 8, + initialAlignment: InitialAlignment.center, + cacheGesture: true, + hitTestBehavior: HitTestBehavior.translucent, + ); + }, + onDoubleTap: (ExtendedImageGestureState state) { + final Offset? pointerDownPosition = + state.pointerDownPosition; + final double? begin = + state.gestureDetails!.totalScale; + double end; + + //remove old + _doubleClickAnimation + ?.removeListener(_doubleClickAnimationListener); + + //stop pre + _doubleClickAnimationController.stop(); + + //reset to use + _doubleClickAnimationController.reset(); + + if (begin == doubleTapScales[0]) { + setState(() { + _isZoom = true; + }); + end = doubleTapScales[1]; + } else { + setState(() { + _isZoom = false; + }); + end = doubleTapScales[0]; + } + + _doubleClickAnimationListener = () { + state.handleDoubleTap( + scale: _doubleClickAnimation!.value, + doubleTapPosition: pointerDownPosition); + }; + + _doubleClickAnimation = Tween( + begin: begin, end: end) + .animate(CurvedAnimation( + curve: Curves.ease, + parent: _doubleClickAnimationController)); + + _doubleClickAnimation! + .addListener(_doubleClickAnimationListener); + + _doubleClickAnimationController.forward(); + }, + ); + }, + itemCount: widget.localArchive.images!.length, + onPageChanged: _onPageChanged)), + _gestureRightLeft(), + _gestureTopBottom(), + _showMore(), + _showPage(), + ], + ), + ); + } + + _showModalSettings() { + DraggableMenu.open( + context, + DraggableMenu( + ui: ClassicDraggableMenu(barItem: Container()), + expandable: false, + maxHeight: mediaHeight(context, 0.4), + fastDrag: false, + minimizeBeforeFastDrag: false, + child: StatefulBuilder( + builder: (context, setState) { + return Scaffold( + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 10, + ), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'Settings', + style: TextStyle( + fontSize: 17, fontWeight: FontWeight.bold), + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + SwitchListTile( + dense: true, + title: const Text('Show Page Number'), + value: _showPagesNumber, + onChanged: (value) { + setState(() { + _showPagesNumber = value; + }); + }, + ), + ], + ), + ) + ], + ), + ); + }, + ))); + } +} diff --git a/lib/modules/main_view/main_screen.dart b/lib/modules/main_view/main_screen.dart index c9f63718..99b4bd98 100644 --- a/lib/modules/main_view/main_screen.dart +++ b/lib/modules/main_view/main_screen.dart @@ -24,7 +24,7 @@ class _MainScreenState extends State { final route = GoRouter.of(context); int currentIndex = route.location == '/library' ? 0 - : route.location == '/updates' + : route.location == '/localReader' ? 1 : route.location == '/history' ? 2 @@ -79,7 +79,7 @@ class _MainScreenState extends State { width: isLongPressed ? 0 : route.location != '/library' && - route.location != '/updates' && + route.location != '/localReader' && route.location != '/history' && route.location != '/browse' && route.location != '/more' @@ -106,14 +106,14 @@ class _MainScreenState extends State { child: Text('Library'))), NavigationRailDestination( selectedIcon: Icon( - Icons.new_releases, + Icons.library_books, ), icon: Icon( - Icons.new_releases_outlined, + Icons.library_books_outlined, ), label: Padding( padding: EdgeInsets.only(top: 5), - child: Text('Updates'))), + child: Text('Local Reader'))), NavigationRailDestination( selectedIcon: Icon( Icons.history, @@ -158,7 +158,7 @@ class _MainScreenState extends State { if (newIndex == 0) { route.go('/library'); } else if (newIndex == 1) { - route.go('/updates'); + route.go('/localReader'); } else if (newIndex == 2) { route.go('/history'); } else if (newIndex == 3) { @@ -186,7 +186,7 @@ class _MainScreenState extends State { height: isLongPressed ? 0 : route.location != '/library' && - route.location != '/updates' && + route.location != '/localReader' && route.location != '/history' && route.location != '/browse' && route.location != '/more' @@ -212,12 +212,12 @@ class _MainScreenState extends State { label: 'Library'), NavigationDestination( selectedIcon: Icon( - Icons.new_releases, + Icons.library_books, ), icon: Icon( - Icons.new_releases_outlined, + Icons.library_books_outlined, ), - label: 'Updates'), + label: 'Local Reader'), NavigationDestination( selectedIcon: Icon( Icons.history, @@ -252,7 +252,7 @@ class _MainScreenState extends State { if (newIndex == 0) { route.go('/library'); } else if (newIndex == 1) { - route.go('/updates'); + route.go('/localReader'); } else if (newIndex == 2) { route.go('/history'); } else if (newIndex == 3) { diff --git a/lib/router/router.dart b/lib/router/router.dart index e5d7ef17..f25b396f 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -3,6 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/manga_type.dart'; +import 'package:mangayomi/modules/local_reader/local_reader_screen.dart'; +import 'package:mangayomi/modules/local_reader/models/models.dart'; +import 'package:mangayomi/modules/local_reader/reader/local_reader_reader_view.dart'; import 'package:mangayomi/modules/webview/webview.dart'; import 'package:mangayomi/modules/browse/browse_screen.dart'; import 'package:mangayomi/modules/browse/extension/extension_lang.dart'; @@ -23,7 +26,6 @@ import 'package:mangayomi/modules/more/settings/browse/browse_screen.dart'; import 'package:mangayomi/modules/more/settings/general/general_screen.dart'; import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart'; import 'package:mangayomi/modules/more/settings/settings_screen.dart'; -import 'package:mangayomi/modules/updates/updates_screen.dart'; final routerProvider = Provider((ref) { final router = AsyncRouterNotifier(); @@ -51,12 +53,12 @@ class AsyncRouterNotifier extends ChangeNotifier { ), ), GoRoute( - name: "updates", - path: '/updates', - builder: (context, state) => const UpdatesScreen(), + name: "localReader", + path: '/localReader', + builder: (context, state) => const LocalReaderScreen(), pageBuilder: (context, state) => CustomTransition( key: state.pageKey, - child: const UpdatesScreen(), + child: const LocalReaderScreen(), ), ), GoRoute( @@ -319,6 +321,25 @@ class AsyncRouterNotifier extends ChangeNotifier { ); }, ), + GoRoute( + path: "/localReaderReaderView", + name: "localReaderReaderView", + builder: (context, state) { + final localArchive = state.extra as LocalArchive; + return LocalReaderReaderView( + localArchive: localArchive, + ); + }, + pageBuilder: (context, state) { + final localArchive = state.extra as LocalArchive; + return CustomTransition( + key: state.pageKey, + child: LocalReaderReaderView( + localArchive: localArchive, + ), + ); + }, + ), ]; } diff --git a/lib/services/http_service/cloudflare/providers/cookie_providers.g.dart b/lib/services/http_service/cloudflare/providers/cookie_providers.g.dart index 435cd4e1..8a822e8c 100644 --- a/lib/services/http_service/cloudflare/providers/cookie_providers.g.dart +++ b/lib/services/http_service/cloudflare/providers/cookie_providers.g.dart @@ -6,7 +6,7 @@ part of 'cookie_providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$cookieStateHash() => r'1ae831fd859a7f084f26be985ebf1e126b38a788'; +String _$cookieStateHash() => r'73fbf2fed21118db48d07ae0bdd213e1d4789fbd'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/utils/headers.g.dart b/lib/utils/headers.g.dart index e00361cd..342a2c15 100644 --- a/lib/utils/headers.g.dart +++ b/lib/utils/headers.g.dart @@ -6,7 +6,7 @@ part of 'headers.dart'; // RiverpodGenerator // ************************************************************************** -String _$headersHash() => r'371ee77f40f010b4e2d5e354c30e666754b34291'; +String _$headersHash() => r'b25fd0415020bf0585f8ecad168689935188bfad'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/utils/test.dart b/lib/utils/test.dart new file mode 100644 index 00000000..33e779d7 --- /dev/null +++ b/lib/utils/test.dart @@ -0,0 +1,322 @@ +import 'dart:developer'; +import 'dart:io'; +import 'package:archive/archive.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:mangayomi/modules/widgets/progress_center.dart'; +import 'package:mangayomi/utils/media_query.dart'; + +class ImageFinderPage extends StatefulWidget { + const ImageFinderPage({super.key}); + + @override + State createState() => _ImageFinderPageState(); +} + +class LocalArchive { + String? name; + + Uint8List? coverImage; + + List? images = []; + + LocalExtensionType? extensionType; +} + +enum LocalExtensionType { cbz, zip, rar, cbt, tar } + +class LocalImage { + String? name; + Uint8List? image; +} + +class _ImageFinderPageState extends State { + List imagePaths = []; + bool isLoading = false; + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Scaffold( + appBar: AppBar( + title: const Text('Image Finder'), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () async { + setState(() { + isLoading = true; + }); + //File + FilePickerResult? result = + await FilePicker.platform.pickFiles(); + if (result != null) { + //File + final ddd = await ArchiveSS() + .getArchiveDatas(result.files.first.path!); + setState(() { + imagePaths.add(ddd); + isLoading = false; + }); + } else { + setState(() { + isLoading = false; + }); + } + }, + child: const Text("File ")), + ElevatedButton( + onPressed: () async { + setState(() { + isLoading = true; + }); + //Directory + String? result = + await FilePicker.platform.getDirectoryPath(); + + if (result != null) { + //Directory + final ddd = await ArchiveSS().directory(result); + setState(() { + imagePaths = ddd; + isLoading = false; + }); + } else { + setState(() { + isLoading = false; + }); + } + }, + child: const Text("Directory")), + ], + ), + Expanded( + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: 0.68, crossAxisCount: 3), + itemCount: imagePaths.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Material( + borderRadius: BorderRadius.circular(10), + clipBehavior: Clip.antiAliasWithSaveLayer, + child: InkWell( + onTap: () {}, + child: Ink.image( + height: 200, + fit: BoxFit.cover, + image: MemoryImage(imagePaths[index].coverImage!), + child: Container( + height: 70, + decoration: BoxDecoration( + color: + Theme.of(context).scaffoldBackgroundColor, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.6) + ], + stops: const [0, 1], + ), + ), + child: Stack( + children: [ + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + imagePaths[index].name!, + style: const TextStyle( + fontSize: 12, + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(0.5, 0.9), + blurRadius: 3.0) + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(5), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(5)), + child: Padding( + padding: const EdgeInsets.all(2), + child: Text( + getTypeExtension(imagePaths[index] + .extensionType!) + .toUpperCase(), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 10), + ), + )), + ) + ], + ), + ), + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + if (isLoading) + Container( + width: mediaWidth(context, 1), + height: mediaHeight(context, 1), + color: Colors.black45, + child: UnconstrainedBox( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).scaffoldBackgroundColor, + ), + height: 200, + width: 200, + child: const Center(child: ProgressCenter())), + ), + ) + ], + ); + } +} + +class ArchiveSS { + Future> directory(String path) async { + return compute(extract, path); + } + + Future getArchiveDatas(String path) async { + return compute(extractArchive, path); + } + + List extract(String data) { + return searchForArchive(Directory(data)); + } + + List list = []; + List searchForArchive(Directory dir) { + List entities = dir.listSync(); + for (FileSystemEntity entity in entities) { + if (entity is Directory) { + searchForArchive(entity); + } else if (entity is File) { + String path = entity.path; + if (isArchiveFile(path)) { + final dd = extractArchive(path); + list.add(dd); + } + } + } + return list; + } + + bool isImageFile(String path) { + List imageExtensions = ['.png', '.jpg', '.jpeg']; + String extension = path.toLowerCase(); + for (String imageExtension in imageExtensions) { + if (extension.endsWith(imageExtension)) { + return true; + } + } + return false; + } + + bool isArchiveFile(String path) { + List imageExtensions = ['.cbz', '.zip', '.cbr', 'cbt', 'tar']; + String extension = path.toLowerCase(); + for (String imageExtension in imageExtensions) { + if (extension.endsWith(imageExtension)) { + return true; + } + } + return false; + } + + LocalArchive extractArchive(String path) { + log(path.split('/').last.split("\\").last.split(".").last); + final bytes = File(path).readAsBytesSync(); + final localArchive = LocalArchive() + ..extensionType = setTypeExtension( + path.split('/').last.split("\\").last.split(".").last) + ..name = path + .split('/') + .last + .split("\\") + .last + .replaceAll(RegExp(r'\.(cbz|zip|cbt|tar)'), ''); + Archive? archive; + final extensionType = localArchive.extensionType; + if (extensionType == LocalExtensionType.cbt || + extensionType == LocalExtensionType.tar) { + archive = TarDecoder().decodeBytes(bytes); + } else { + archive = ZipDecoder().decodeBytes(bytes); + } + + for (final file in archive) { + final filename = file.name; + if (file.isFile) { + if (isImageFile(filename)) { + if (filename.contains("cover")) { + final data = file.content as Uint8List; + localArchive.coverImage = Uint8List.fromList(data); + } else { + final data = file.content as Uint8List; + localArchive.images!.add(LocalImage() + ..image = Uint8List.fromList(data) + ..name = filename.split('/').last.split("\\").last); + } + } + } + } + localArchive.images!.sort((a, b) => a.name!.compareTo(b.name!)); + localArchive.coverImage ??= localArchive.images!.first.image; + return localArchive; + } +} + +String getTypeExtension(LocalExtensionType type) { + return switch (type) { + LocalExtensionType.cbt => type.name, + LocalExtensionType.zip => type.name, + LocalExtensionType.rar => type.name, + LocalExtensionType.tar => type.name, + _ => type.name, + }; +} + +LocalExtensionType setTypeExtension(String extension) { + return switch (extension) { + "cbt" => LocalExtensionType.cbt, + "zip" => LocalExtensionType.zip, + "rar" => LocalExtensionType.rar, + "tar" => LocalExtensionType.tar, + _ => LocalExtensionType.cbz, + }; +} diff --git a/pubspec.lock b/pubspec.lock index 616f67a9..fcf6fc03 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "98d1d33ed129b372846e862de23a0fc365745f4d7b5e786ce667fcbbb7ac5c07" + sha256: "405666cd3cf0ee0a48d21ec67e65406aad2c726d9fa58840d3375e7bdcd32a07" url: "https://pub.dev" source: hosted - version: "55.0.0" + version: "60.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "881348aed9b0b425882c97732629a6a31093c8ff20fc4b3b03fb9d3d50a3a126" + sha256: "1952250bd005bacb895a01bf1b4dc00e3ba1c526cf47dca54dfe24979c65f5b3" url: "https://pub.dev" source: hosted - version: "5.7.1" + version: "5.12.0" analyzer_plugin: dependency: transitive description: @@ -26,7 +26,7 @@ packages: source: hosted version: "0.11.2" archive: - dependency: transitive + dependency: "direct main" description: name: archive sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" @@ -254,18 +254,18 @@ packages: dependency: transitive description: name: custom_lint - sha256: e87176016465263daf10c209df1f50a52e9e46e7612fab7462da1e6d984638f6 + sha256: "3ce36c04d30c60cde295588c6185b3f9800e6c18f6670a7ffdb3d5eab39bb942" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.4.0" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: f42f688bc26bdf4c081e011ba27a00439f17c20d9aeca4312f8022e577f8363f + sha256: "9170d9db2daf774aa2251a3bc98e4ba903c7702ab07aa438bc83bd3c9a0de57f" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.4.0" dart_style: dependency: transitive description: @@ -362,6 +362,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf" + url: "https://pub.dev" + source: hosted + version: "5.3.1" fixnum: dependency: transitive description: @@ -452,14 +460,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + url: "https://pub.dev" + source: hosted + version: "2.0.15" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "9692634c2c00d2a1a5e96bbde0b79a7c6f0f266aa266d76cd52841f791949a89" + sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4 url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.6" flutter_test: dependency: "direct dev" description: flutter @@ -706,10 +722,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: cbff87676c352d97116af6dbea05aa28c4d65eb0f6d5677a520c11a69ca9a24d + sha256: ceb027f6bc6a60674a233b4a90a7658af1aebdea833da0b5b53c1e9821a78c7b url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.2" package_info_plus_platform_interface: dependency: transitive description: @@ -727,13 +743,13 @@ packages: source: hosted version: "1.8.3" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider - sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.15" path_provider_android: dependency: transitive description: @@ -906,34 +922,34 @@ packages: dependency: transitive description: name: riverpod - sha256: ec5641067d111681ef825754d1327565c987985c7cb52e09bc867b78248854b2 + sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.6" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "7c2d4de69ba06107c3d7f1b3f43dc3fbdcb2f666b480560af654b4eb89af0d6d" + sha256: "1b2632a6fc0b659c923a4dcc7cd5da42476f5b3294c70c86c971e63bdd443384" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.1" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: c0f51b3fc5a0cefcbcddb35a10ad542d6c38919c081a25279045158ac7955cfb + sha256: cedd6a54b6f5764ffd5c05df57b6676bfc8c01978e14ee60a2c16891038820fe url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.1" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "2c08a6fbbe80d489f1c5208e5358bfdd4d612f1777c47fdb9ff91bb7a2670529" + sha256: cd0595de57ccf5d944ff4b0f68289e11ac6a2eff1e3dfd1d884a43f6f3bcee5e url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.3" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 156b627e..35b97d3f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,7 +50,7 @@ dependencies: intl: ^0.18.0 google_fonts: ^4.0.3 url_launcher: ^6.1.10 - package_info_plus: ^3.0.2 + package_info_plus: ^4.0.2 background_downloader: git: url: https://github.com/kodjodevf/background_downloader.git @@ -62,6 +62,9 @@ dependencies: share_plus: ^7.0.0 xpath_selector_html_parser: ^3.0.1 desktop_webview_window: ^0.2.0 + archive: ^3.3.7 + file_picker: ^5.3.0 + path_provider: ^2.0.15 # The following adds the Cupertino Icons font to your application. @@ -72,7 +75,7 @@ dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.3.3 - riverpod_generator: ^2.1.4 + riverpod_generator: ^2.2.3 flutter_launcher_icons: ^0.13.1 isar_generator: 3.1.0+1 flutter_lints: ^2.0.1