diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 2591ba0..752606a 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -494,6 +494,19 @@ class _GalleryModeState extends State<_GalleryMode> @override Future getImageByOffset(Offset offset) async { + var imageKey = getImageKeyByOffset(offset); + if (imageKey == null) return null; + if (imageKey.startsWith("file://")) { + return await File(imageKey.substring(7)).readAsBytes(); + } else { + return (await CacheManager().findCache( + "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! + .readAsBytes(); + } + } + + @override + String? getImageKeyByOffset(Offset offset) { String? imageKey; if (reader.imagesPerPage == 1) { imageKey = reader.images![reader.page - 1]; @@ -504,14 +517,7 @@ class _GalleryModeState extends State<_GalleryMode> } } } - if (imageKey == null) return null; - if (imageKey.startsWith("file://")) { - return await File(imageKey.substring(7)).readAsBytes(); - } else { - return (await CacheManager().findCache( - "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! - .readAsBytes(); - } + return imageKey; } } @@ -1045,12 +1051,7 @@ class _ContinuousModeState extends State<_ContinuousMode> @override Future getImageByOffset(Offset offset) async { - String? imageKey; - for (var imageState in imageStates) { - if ((imageState as _ComicImageState).containsPoint(offset)) { - imageKey = (imageState.widget.image as ReaderImageProvider).imageKey; - } - } + var imageKey = getImageKeyByOffset(offset); if (imageKey == null) return null; if (imageKey.startsWith("file://")) { return await File(imageKey.substring(7)).readAsBytes(); @@ -1060,6 +1061,17 @@ class _ContinuousModeState extends State<_ContinuousMode> .readAsBytes(); } } + + @override + String? getImageKeyByOffset(Offset offset) { + String? imageKey; + for (var imageState in imageStates) { + if ((imageState as _ComicImageState).containsPoint(offset)) { + imageKey = (imageState.widget.image as ReaderImageProvider).imageKey; + } + } + return imageKey; + } } ImageProvider _createImageProviderFromKey( diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 10c4029..0686bf3 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -217,10 +217,16 @@ class _ReaderState extends State focusNode: focusNode, autofocus: true, onKeyEvent: onKeyEvent, - child: _ReaderScaffold( - child: _ReaderGestureDetector( - child: _ReaderImages(key: Key(chapter.toString())), - ), + child: Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return _ReaderScaffold( + child: _ReaderGestureDetector( + child: _ReaderImages(key: Key(chapter.toString())), + ), + ); + }) + ], ), ); } @@ -604,4 +610,6 @@ abstract interface class _ImageViewController { bool handleOnTap(Offset location); Future getImageByOffset(Offset offset); + + String? getImageKeyByOffset(Offset offset); } diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 1408c11..9986766 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -208,7 +208,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ); } - void addImageFavorite() { + void addImageFavorite() async { try { if (context.reader.images![0].contains('file://')) { showToast( @@ -222,7 +222,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { String title = context.reader.history!.title; String subTitle = context.reader.history!.subtitle; int maxPage = context.reader.images!.length; - int page = context.reader.page; + int? page = await selectImage(); + if (page == null) return; + page += 1; String sourceKey = context.reader.type.sourceKey; String imageKey = context.reader.images![page - 1]; List tags = context.reader.widget.tags; @@ -571,94 +573,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ); } - Future _getCurrentImageData() async { - var imageKey = context.reader.images![context.reader.page - 1]; - var reader = context.reader; - if (context.reader.mode.isContinuous) { - var continuesState = - context.reader._imageViewController as _ContinuousModeState; - var imagesOnScreen = - continuesState.itemPositionsListener.itemPositions.value; - var images = imagesOnScreen - .map((e) => context.reader.images!.elementAtOrNull(e.index - 1)) - .whereType() - .toList(); - String? selected; - if (images.length > 1) { - await showPopUpWidget( - context, - PopUpWidgetScaffold( - title: "Select an image on screen".tl, - body: GridView.builder( - itemCount: images.length, - itemBuilder: (context, index) { - ImageProvider image; - var imageKey = images[index]; - if (imageKey.startsWith('file://')) { - image = FileImage(File(imageKey.replaceFirst("file://", ''))); - } else { - image = ReaderImageProvider( - imageKey, - reader.type.comicSource!.key, - reader.cid, - reader.eid, - reader.page, - ); - } - return InkWell( - borderRadius: const BorderRadius.all(Radius.circular(16)), - onTap: () { - selected = images[index]; - App.rootContext.pop(); - }, - child: Container( - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context).colorScheme.outline, - ), - ), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - ), - width: double.infinity, - height: double.infinity, - child: Image( - width: double.infinity, - height: double.infinity, - image: image, - ), - ), - ).padding(const EdgeInsets.all(8)); - }, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - childAspectRatio: 0.7, - ), - ), - ), - ); - } else { - selected = images.first; - } - if (selected == null) { - return null; - } else { - imageKey = selected!; - } - } - if (imageKey.startsWith("file://")) { - return await File(imageKey.substring(7)).readAsBytes(); - } else { - return (await CacheManager().findCache( - "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! - .readAsBytes(); - } - } - void saveCurrentImage() async { - var data = await _getCurrentImageData(); + var data = await selectImageToData(); if (data == null) { return; } @@ -668,7 +584,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } void share() async { - var data = await _getCurrentImageData(); + var data = await selectImageToData(); if (data == null) { return; } @@ -760,6 +676,74 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } return const SizedBox(); } + + /// If there is only one image on screen, return it. + /// + /// If there are multiple images on screen, + /// show an overlay to let the user select an image. + /// + /// The return value is the index of the selected image. + Future selectImage() async { + var reader = context.reader; + var imageViewController = context.reader._imageViewController; + if (imageViewController is _GalleryModeState && reader.imagesPerPage == 1) { + return reader.page - 1; + } else { + var location = await _showSelectImageOverlay(); + if (location == null) { + return null; + } + var imageKey = imageViewController!.getImageKeyByOffset(location); + if (imageKey == null) { + return null; + } + return reader.images!.indexOf(imageKey); + } + } + + /// Same as [selectImage], but return the image data. + Future selectImageToData() async { + var i = await selectImage(); + if (i == null) { + return null; + } + var imageKey = context.reader.images![i]; + if (imageKey.startsWith("file://")) { + return await File(imageKey.substring(7)).readAsBytes(); + } else { + return (await CacheManager().findCache( + "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! + .readAsBytes(); + } + } + + Future _showSelectImageOverlay() { + if (_isOpen) { + openOrClose(); + } + + var completer = Completer(); + + var overlay = Overlay.of(context); + OverlayEntry? entry; + entry = OverlayEntry( + builder: (context) { + return Positioned.fill( + child: _SelectImageOverlayContent(onTap: (offset) { + completer.complete(offset); + entry!.remove(); + }, onDispose: () { + if (!completer.isCompleted) { + completer.complete(null); + } + }), + ); + }, + ); + overlay.insert(entry); + + return completer.future; + } } class _BatteryWidget extends StatefulWidget { @@ -940,3 +924,69 @@ class _ClockWidgetState extends State<_ClockWidget> { ); } } + +class _SelectImageOverlayContent extends StatefulWidget { + const _SelectImageOverlayContent({ + required this.onTap, + required this.onDispose, + }); + + final void Function(Offset) onTap; + + final void Function() onDispose; + + @override + State<_SelectImageOverlayContent> createState() => _SelectImageOverlayContentState(); +} + +class _SelectImageOverlayContentState extends State<_SelectImageOverlayContent> { + @override + void dispose() { + widget.onDispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTapUp: (details) { + widget.onTap(details.globalPosition); + }, + child: Container( + color: Colors.black.withAlpha(50), + child: Align( + alignment: Alignment( + 0, + -0.6, + ), + child: Container( + width: 232, + height: 42, + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: context.colorScheme.outlineVariant, + ), + ), + child: Row( + children: [ + const SizedBox(width: 8), + const Icon(Icons.info_outline), + const SizedBox(width: 16), + Text( + "Click to select an image".tl, + style: TextStyle( + fontSize: 16, + color: context.colorScheme.onSurface, + ), + ), + ], + ), + ), + ), + ), + ); + } +}