From 6d2c445cd4a0bab732755f21c724b9f7c158d759 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Tue, 29 Oct 2024 10:01:56 +0000 Subject: [PATCH] Add support to copy image and url in viewer --- lib/provider/dio_image_provider.dart | 15 +++++++-- lib/utils/clipboard.dart | 14 ++++++++ lib/viewer/single.dart | 48 +++++++++++++++++++++++++--- pubspec.lock | 8 +++++ pubspec.yaml | 1 + 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/lib/provider/dio_image_provider.dart b/lib/provider/dio_image_provider.dart index f7b2b3a..cc10f61 100644 --- a/lib/provider/dio_image_provider.dart +++ b/lib/provider/dio_image_provider.dart @@ -28,7 +28,8 @@ class DioImage extends ImageProvider { /// /// The arguments [url] and [scale] must not be null. /// [dio] will be the default [Dio] if not set. - DioImage.string(String url, {this.scale = 1.0, this.headers, Dio? dio}) + DioImage.string(String url, + {this.scale = 1.0, this.headers, this.onData, Dio? dio}) : dio = dio ?? defaultDio, url = Uri.parse(url); @@ -36,7 +37,7 @@ class DioImage extends ImageProvider { /// /// The arguments [url] and [scale] must not be null. /// [dio] will be the default [Dio] if not set. - DioImage(this.url, {this.scale = 1.0, this.headers, Dio? dio}) + DioImage(this.url, {this.scale = 1.0, this.headers, this.onData, Dio? dio}) : dio = dio ?? defaultDio; /// The URL from which the image will be fetched. @@ -53,6 +54,8 @@ class DioImage extends ImageProvider { /// [dio] will be the default [Dio] if not set. final Dio dio; + final void Function(Uint8List, Headers, String)? onData; + @override Future obtainKey(ImageConfiguration configuration) { return SynchronousFuture(this); @@ -89,6 +92,10 @@ class DioImage extends ImageProvider { try { final cache = await imageCaches.getCache(url.toString()); if (cache != null) { + if (onData != null) { + onData!(cache!.$1, Headers.fromMap(cache!.$2), + cache!.$3 ?? url.toString()); + } final buffer = await ui.ImmutableBuffer.fromUint8List(cache!.$1); return decode(buffer); } @@ -124,6 +131,10 @@ class DioImage extends ImageProvider { ); } + if (onData != null) { + onData!(bytes, response.headers, response.realUri.toString()); + } + if (isImageCacheEnabled) { try { await imageCaches.putCache(url.toString(), bytes, diff --git a/lib/utils/clipboard.dart b/lib/utils/clipboard.dart index eb93e38..b6b99ac 100644 --- a/lib/utils/clipboard.dart +++ b/lib/utils/clipboard.dart @@ -20,6 +20,20 @@ enum ImageFmt { return "image/gif"; } } + + static ImageFmt? fromMimeType(String? mime) { + if (mime == null) return null; + switch (mime) { + case "image/jpeg": + return ImageFmt.jpg; + case "image/png": + return ImageFmt.png; + case "image/gif": + return ImageFmt.gif; + default: + return null; + } + } } Future copyImageToClipboard(Uint8List data, ImageFmt fmt) async { diff --git a/lib/viewer/single.dart b/lib/viewer/single.dart index 044c3b7..82820ac 100644 --- a/lib/viewer/single.dart +++ b/lib/viewer/single.dart @@ -10,12 +10,15 @@ import 'package:keymap/keymap.dart'; import 'package:logging/logging.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; +import 'package:quiver/collection.dart'; +import 'package:super_context_menu/super_context_menu.dart'; import '../api/file.dart'; import '../api/gallery.dart'; import '../components/fit_text.dart'; import '../globals.dart'; import '../platform/media_query.dart'; import '../provider/dio_image_provider.dart'; +import '../utils/clipboard.dart'; final _log = Logger("SinglePageViewer"); @@ -57,6 +60,8 @@ class _SinglePageViewer extends State bool _inited = false; bool _showMenu = false; late PhotoViewController _photoViewController; + final LruMap _imgData = + LruMap(maximumSize: 20); void _updatePages() { if (_data == null) return; final displayAd = prefs.getBool("displayAd") ?? false; @@ -138,10 +143,10 @@ class _SinglePageViewer extends State _photoViewController.reset(); } return PhotoViewGalleryPageOptions( - imageProvider: DioImage.string( - api.getFileUrl(f.id), - dio: dio, - ), + imageProvider: DioImage.string(api.getFileUrl(f.id), dio: dio, + onData: (data, headers, url) { + _imgData[index] = (data, headers.value("content-type"), url); + }), initialScale: PhotoViewComputedScale.contained, heroAttributes: PhotoViewHeroAttributes( tag: data.token, @@ -216,11 +221,44 @@ class _SinglePageViewer extends State ); } + Widget _buildWithContextMenu(BuildContext context, {required Widget child}) { + final i18n = AppLocalizations.of(context)!; + return ContextMenuWidget( + menuProvider: (_) { + var list = []; + final url = _imgData[_index]?.$3; + if (url != null) { + list.add(MenuAction( + title: i18n.copyImgUrl, + callback: () { + copyTextToClipboard(url!).catchError((err) { + _log.warning("Failed to copy image to clipboard:", err); + }); + })); + } + final data = _imgData[_index]?.$1; + if (data != null) { + final fmt = + ImageFmt.fromMimeType(_imgData[_index]?.$2) ?? ImageFmt.jpg; + list.add(MenuAction( + title: i18n.copyImage, + callback: () { + copyImageToClipboard(data!, fmt).catchError((err) { + _log.warning("Failed to copy image to clipboard:", err); + }); + })); + } + return Menu(children: list); + }, + child: child); + } + Widget _buildViewer(BuildContext context) { return _buildWithTap(context, child: _buildWithKeyboardSupport(context, child: _buildWithScrollSupport(context, - child: _buildGallery(context)))); + child: _buildWithContextMenu(context, + child: _buildGallery(context))))); } Widget _buildTopAppBar(BuildContext context) { diff --git a/pubspec.lock b/pubspec.lock index 695f37c..23a6be6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -755,6 +755,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + quiver: + dependency: "direct main" + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" retrofit: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 639f1d2..3d8bbce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: path_provider: ^2.1.0 percent_indicator: ^4.2.3 photo_view: ^0.15.0 + quiver: ^3.2.2 retrofit: ^4.0.1 shared_preferences: ^2.2.0 sqflite_common_ffi: ^2.3.3