mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
Fix saving, sharing, and collecting images when there are multiple images on the screen. Close #289
This commit is contained in:
@@ -494,6 +494,19 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
Future<Uint8List?> 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;
|
String? imageKey;
|
||||||
if (reader.imagesPerPage == 1) {
|
if (reader.imagesPerPage == 1) {
|
||||||
imageKey = reader.images![reader.page - 1];
|
imageKey = reader.images![reader.page - 1];
|
||||||
@@ -504,14 +517,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (imageKey == null) return null;
|
return imageKey;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,12 +1051,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
||||||
String? imageKey;
|
var imageKey = getImageKeyByOffset(offset);
|
||||||
for (var imageState in imageStates) {
|
|
||||||
if ((imageState as _ComicImageState).containsPoint(offset)) {
|
|
||||||
imageKey = (imageState.widget.image as ReaderImageProvider).imageKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (imageKey == null) return null;
|
if (imageKey == null) return null;
|
||||||
if (imageKey.startsWith("file://")) {
|
if (imageKey.startsWith("file://")) {
|
||||||
return await File(imageKey.substring(7)).readAsBytes();
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
@@ -1060,6 +1061,17 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
.readAsBytes();
|
.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(
|
ImageProvider _createImageProviderFromKey(
|
||||||
|
@@ -217,10 +217,16 @@ class _ReaderState extends State<Reader>
|
|||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
onKeyEvent: onKeyEvent,
|
onKeyEvent: onKeyEvent,
|
||||||
child: _ReaderScaffold(
|
child: Overlay(
|
||||||
|
initialEntries: [
|
||||||
|
OverlayEntry(builder: (context) {
|
||||||
|
return _ReaderScaffold(
|
||||||
child: _ReaderGestureDetector(
|
child: _ReaderGestureDetector(
|
||||||
child: _ReaderImages(key: Key(chapter.toString())),
|
child: _ReaderImages(key: Key(chapter.toString())),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -604,4 +610,6 @@ abstract interface class _ImageViewController {
|
|||||||
bool handleOnTap(Offset location);
|
bool handleOnTap(Offset location);
|
||||||
|
|
||||||
Future<Uint8List?> getImageByOffset(Offset offset);
|
Future<Uint8List?> getImageByOffset(Offset offset);
|
||||||
|
|
||||||
|
String? getImageKeyByOffset(Offset offset);
|
||||||
}
|
}
|
||||||
|
@@ -208,7 +208,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addImageFavorite() {
|
void addImageFavorite() async {
|
||||||
try {
|
try {
|
||||||
if (context.reader.images![0].contains('file://')) {
|
if (context.reader.images![0].contains('file://')) {
|
||||||
showToast(
|
showToast(
|
||||||
@@ -222,7 +222,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
String title = context.reader.history!.title;
|
String title = context.reader.history!.title;
|
||||||
String subTitle = context.reader.history!.subtitle;
|
String subTitle = context.reader.history!.subtitle;
|
||||||
int maxPage = context.reader.images!.length;
|
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 sourceKey = context.reader.type.sourceKey;
|
||||||
String imageKey = context.reader.images![page - 1];
|
String imageKey = context.reader.images![page - 1];
|
||||||
List<String> tags = context.reader.widget.tags;
|
List<String> tags = context.reader.widget.tags;
|
||||||
@@ -571,94 +573,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List?> _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<String>()
|
|
||||||
.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 {
|
void saveCurrentImage() async {
|
||||||
var data = await _getCurrentImageData();
|
var data = await selectImageToData();
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -668,7 +584,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void share() async {
|
void share() async {
|
||||||
var data = await _getCurrentImageData();
|
var data = await selectImageToData();
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -760,6 +676,74 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
return const SizedBox();
|
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<int?> 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<Uint8List?> 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<Offset?> _showSelectImageOverlay() {
|
||||||
|
if (_isOpen) {
|
||||||
|
openOrClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
var completer = Completer<Offset?>();
|
||||||
|
|
||||||
|
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 {
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user