diff --git a/lib/shared/image_viewer.dart b/lib/shared/image_viewer.dart index b90b7977b..73573435f 100644 --- a/lib/shared/image_viewer.dart +++ b/lib/shared/image_viewer.dart @@ -1,9 +1,20 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:extended_image/extended_image.dart'; import 'package:thunder/shared/hero.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; + +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart'; + +import 'package:permission_handler/permission_handler.dart'; + class ImageViewer extends StatefulWidget { final String url; @@ -14,51 +25,90 @@ class ImageViewer extends StatefulWidget { } class _ImageViewerState extends State { - GlobalKey slidePagekey = GlobalKey(); + GlobalKey slidePagekey = + GlobalKey(); + + _requestPermission() async { + Map statuses = await [ + Permission.photos, + ].request(); + + final info2 = statuses[Permission.photos].toString(); + print(info2); + } @override Widget build(BuildContext context) { + final theme = Theme.of(context); return Scaffold( backgroundColor: Colors.black87, - body: Center( - child: ExtendedImageSlidePage( - key: slidePagekey, - slideAxis: SlideAxis.both, - slideType: SlideType.onlyImage, - child: GestureDetector( - child: HeroWidget( - tag: widget.url, + body: Stack( + children: [ + Positioned( + top: 0, + left: 0, + child: IconButton( + color: theme.textTheme.titleLarge?.color, + onPressed: () async { + File file = await DefaultCacheManager().getSingleFile(widget.url); + if ((Platform.isAndroid || Platform.isIOS) && + await Permission.contacts.request().isGranted) { + final result = await ImageGallerySaver.saveFile(file.path); + } else if (Platform.isLinux || Platform.isWindows) { + final filePath = + '${(await getApplicationDocumentsDirectory()).path}/ThunderImages/${basename(file.path)}'; + File(filePath) + ..createSync(recursive: true) + ..writeAsBytesSync(file.readAsBytesSync()); + } + }, + icon: const Icon( + Icons.download, + semanticLabel: 'Download', + ), + ), + ), + Center( + child: ExtendedImageSlidePage( + key: slidePagekey, + slideAxis: SlideAxis.both, slideType: SlideType.onlyImage, - slidePagekey: slidePagekey, - child: ExtendedImage.network( - widget.url, - enableSlideOutPage: true, - mode: ExtendedImageMode.gesture, - cache: true, - clearMemoryCacheWhenDispose: true, - initGestureConfigHandler: (ExtendedImageState state) { - return GestureConfig( - minScale: 0.9, - animationMinScale: 0.7, - maxScale: 4.0, - animationMaxScale: 4.5, - speed: 1.0, - inertialSpeed: 100.0, - initialScale: 1.0, - inPageView: false, - initialAlignment: InitialAlignment.center, - reverseMousePointerScrollDirection: true, - gestureDetailsIsChanged: (GestureDetails? details) {}, - ); + child: GestureDetector( + child: HeroWidget( + tag: widget.url, + slideType: SlideType.onlyImage, + slidePagekey: slidePagekey, + child: ExtendedImage.network( + widget.url, + enableSlideOutPage: true, + mode: ExtendedImageMode.gesture, + cache: true, + clearMemoryCacheWhenDispose: true, + initGestureConfigHandler: (ExtendedImageState state) { + return GestureConfig( + minScale: 0.9, + animationMinScale: 0.7, + maxScale: 4.0, + animationMaxScale: 4.5, + speed: 1.0, + inertialSpeed: 100.0, + initialScale: 1.0, + inPageView: false, + initialAlignment: InitialAlignment.center, + reverseMousePointerScrollDirection: true, + gestureDetailsIsChanged: (GestureDetails? details) {}, + ); + }, + ), + ), + onTap: () { + slidePagekey.currentState!.popPage(); + Navigator.pop(context); }, ), ), - onTap: () { - slidePagekey.currentState!.popPage(); - Navigator.pop(context); - }, - ), - ), + ) + ], ), ); } diff --git a/pubspec.lock b/pubspec.lock index fe48bdff9..26664fb34 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -288,7 +288,7 @@ packages: source: hosted version: "0.7.0" flutter_cache_manager: - dependency: transitive + dependency: "direct main" description: name: flutter_cache_manager sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" @@ -401,6 +401,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.17" + image_gallery_saver: + dependency: "direct main" + description: + name: image_gallery_saver + sha256: "467de169167b5c4e1ddde65395e4653c336a82f760b6700ea295085b7f2dd248" + url: "https://pub.dev" + source: hosted + version: "2.0.2" js: dependency: transitive description: @@ -463,10 +471,10 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: @@ -540,7 +548,7 @@ packages: source: hosted version: "1.8.3" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" @@ -595,6 +603,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "1b6b3e73f0bcbc856548bbdfb1c33084a401c4f143e220629a9055233d76c331" + url: "https://pub.dev" + source: hosted + version: "10.3.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8f6a95ccbca13766882f95d32684d7c9bfe6c45650c32bedba948ef1c6a4ddf7" + url: "https://pub.dev" + source: hosted + version: "10.2.3" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "08dcb6ce628ac0b257e429944b4c652c2a4e6af725bdf12b498daa2c6b2b1edb" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: de20a5c3269229c1ae2e5a6b822f6cb59578b23e8255c93fbeebfc82116e6b11 + url: "https://pub.dev" + source: hosted + version: "3.10.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + url: "https://pub.dev" + source: hosted + version: "0.1.2" petitparser: dependency: transitive description: @@ -852,10 +900,10 @@ packages: dependency: transitive description: name: test_api - sha256: daadc9baabec998b062c9091525aa95786508b1c48e9c30f1f891b8bf6ff2e64 + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.2" + version: "0.6.0" translator: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index adf5f948c..21a22fb8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,10 @@ dependencies: share_plus: ^7.0.2 flex_color_scheme: ^7.1.2 dynamic_color: ^1.6.5 + image_gallery_saver: ^2.0.2 + flutter_cache_manager: ^3.3.0 + permission_handler: ^10.3.0 + path_provider: ^2.0.15 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3d1dcc5b2..d1b636dcc 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -14,6 +15,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SentryFlutterPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SentryFlutterPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f9947fb2b..650e78995 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color + permission_handler_windows sentry_flutter share_plus url_launcher_windows diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b25e363ef..955ee3038 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() { this->Show(); }); + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; }