Add image copy functionality.

Currently only supports Windows.
Close #260
This commit is contained in:
2025-03-26 22:50:00 +08:00
parent dee8d17b1e
commit d812332613
7 changed files with 151 additions and 2 deletions

View File

@@ -24,6 +24,8 @@ class ComicImage extends StatefulWidget {
Map<String, String>? headers,
int? cacheWidth,
int? cacheHeight,
this.onInit,
this.onDispose,
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0);
@@ -60,6 +62,10 @@ class ComicImage extends StatefulWidget {
final bool isAntiAlias;
final void Function(State<ComicImage> state)? onInit;
final void Function(State<ComicImage> state)? onDispose;
static void clear() => _ComicImageState.clear();
@override
@@ -87,6 +93,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
super.initState();
WidgetsBinding.instance.addObserver(this);
_scrollAwareContext = DisposableBuildContext<State<ComicImage>>(this);
widget.onInit?.call(this);
}
@override
@@ -97,6 +104,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
_completerHandle?.dispose();
_scrollAwareContext.dispose();
_replaceImage(info: null);
widget.onDispose?.call(this);
super.dispose();
}
@@ -136,6 +144,15 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
super.reassemble();
}
bool containsPoint(Offset point) {
if (!mounted) {
return false;
}
var renderBox = context.findRenderObject() as RenderBox;
var localPoint = renderBox.globalToLocal(point);
return renderBox.paintBounds.contains(localPoint);
}
void _updateInvertColors() {
_invertColors = MediaQuery.maybeInvertColorsOf(context) ??
SemanticsBinding.instance.accessibilityFeatures.invertColors;

View File

@@ -281,6 +281,12 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
context.pop();
},
),
if (App.isDesktop && !reader.isLoading)
MenuEntry(
icon: Icons.copy,
text: "Copy Image".tl,
onClick: () => copyImage(location),
),
],
);
}
@@ -303,6 +309,16 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
@override
Object? get key => "reader_gesture";
void copyImage(Offset location) async {
var controller = reader._imageViewController;
var image = await controller!.getImageByOffset(location);
if (image != null) {
writeImageToClipboard(image);
} else {
context.showMessage(message: "No Image");
}
}
}
class _DragListener {

View File

@@ -113,6 +113,8 @@ class _GalleryModeState extends State<_GalleryMode>
int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil();
var imageStates = <State<ComicImage>>{};
@override
void initState() {
reader = context.reader;
@@ -234,6 +236,8 @@ class _GalleryModeState extends State<_GalleryMode>
child: ComicImage(
image: imageProvider,
fit: BoxFit.contain,
onInit: (state) => imageStates.add(state),
onDispose: (state) => imageStates.remove(state),
),
);
}).toList();
@@ -370,6 +374,28 @@ class _GalleryModeState extends State<_GalleryMode>
bool handleOnTap(Offset location) {
return false;
}
@override
Future<Uint8List?> getImageByOffset(Offset offset) async {
String? imageKey;
if (reader.imagesPerPage == 1) {
imageKey = reader.images![reader.page - 1];
} else {
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.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();
}
}
}
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
@@ -414,6 +440,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
/// To handle the tap event, we need to know if the user was scrolling before the delay.
bool delayedIsScrolling = false;
var imageStates = <State<ComicImage>>{};
void delayedSetIsScrolling(bool value) {
Future.delayed(
const Duration(milliseconds: 300),
@@ -574,6 +602,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
width: width,
height: height,
fit: BoxFit.contain,
onInit: (state) => imageStates.add(state),
onDispose: (state) => imageStates.remove(state),
),
);
},
@@ -857,6 +887,24 @@ class _ContinuousModeState extends State<_ContinuousMode>
}
return false;
}
@override
Future<Uint8List?> getImageByOffset(Offset offset) async {
String? imageKey;
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.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();
}
}
}
ImageProvider _createImageProviderFromKey(

View File

@@ -30,6 +30,7 @@ import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/clipboard_image.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/file_type.dart';
@@ -577,4 +578,6 @@ abstract interface class _ImageViewController {
/// Returns true if the event is handled.
bool handleOnTap(Offset location);
Future<Uint8List?> getImageByOffset(Offset offset);
}