From 104daaac514e34ed8b60372da0492916d89c4f80 Mon Sep 17 00:00:00 2001 From: Alan Bedian Date: Mon, 18 Dec 2023 04:19:25 -0800 Subject: [PATCH 1/3] Added ImageViewer Control to display full screen Image with Zoom, Pan & paging --- package/lib/src/controls/create_control.dart | 6 + package/lib/src/controls/image_viewer.dart | 89 +++++++++ package/pubspec.yaml | 1 + .../flet-core/src/flet_core/__init__.py | 1 + .../flet-core/src/flet_core/image_viewer.py | 172 ++++++++++++++++++ 5 files changed, 269 insertions(+) create mode 100644 package/lib/src/controls/image_viewer.dart create mode 100644 sdk/python/packages/flet-core/src/flet_core/image_viewer.py diff --git a/package/lib/src/controls/create_control.dart b/package/lib/src/controls/create_control.dart index caf329e66..84873c2df 100644 --- a/package/lib/src/controls/create_control.dart +++ b/package/lib/src/controls/create_control.dart @@ -51,6 +51,7 @@ import 'haptic_feedback.dart'; import 'icon.dart'; import 'icon_button.dart'; import 'image.dart'; +import 'image_viewer.dart'; import 'linechart.dart'; import 'list_tile.dart'; import 'list_view.dart'; @@ -598,6 +599,11 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, children: controlView.children, parentDisabled: parentDisabled, nextChild: nextChild); + case "imageviewer": + return ImageViewerControl( + parent: parent, + control: controlView.control, + parentDisabled: parentDisabled); case "tabs": return TabsControl( key: key, diff --git a/package/lib/src/controls/image_viewer.dart b/package/lib/src/controls/image_viewer.dart new file mode 100644 index 000000000..116dff1dc --- /dev/null +++ b/package/lib/src/controls/image_viewer.dart @@ -0,0 +1,89 @@ +import 'package:easy_image_viewer/easy_image_viewer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; + +import '../models/app_state.dart'; +import '../models/page_args_model.dart'; +import '../models/control.dart'; +import '../utils/colors.dart'; +import '../utils/images.dart'; + +class ImageViewerControl extends StatefulWidget { + final Control? parent; + final Control control; + final bool parentDisabled; + + const ImageViewerControl( + {Key? key, + this.parent, + required this.control, + required this.parentDisabled}) + : super(key: key); + + @override + State createState() => _ImageViewerControlState(); +} + +class _ImageViewerControlState extends State { + bool _open = false; + + @override + Widget build(BuildContext context) { + debugPrint("ImageViewer build: ${widget.control.id}"); + + var src = widget.control.attrString("src", "")!; + bool swipeDismissible = widget.control.attrBool("swipeDismissible", true)!; + bool doubleTapZoomable = widget.control.attrBool("doubleTapZoomable", true)!; + String closeButtonTooltip = widget.control.attrString("closeButtonTooltip", "Close")!; + var backgroundColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("backgroundColor", "black")!); + var closeButtonColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("closeButtonColor", "white")!); + bool immersive = widget.control.attrBool("immersive", true)!; + var initialIndex = widget.control.attrInt("initialIndex", 0)!; + + return StoreConnector( + distinct: true, + converter: (store) => PageArgsModel.fromStore(store), + builder: (context, pageArgs) { + EasyImageProvider? imageProvider; + if (src.contains('|')) { + List> imageList = []; + for (String img in src.split('|')) { + var imageSrc = getAssetSrc(img, pageArgs.pageUri!, pageArgs.assetsDir); + imageList.add(imageSrc.isFile ? getFileImageProvider(imageSrc.path) : NetworkImage(imageSrc.path)); + } + imageProvider = MultiImageProvider(imageList, initialIndex: initialIndex); + } else { + var imageSrc = getAssetSrc(src, pageArgs.pageUri!, pageArgs.assetsDir); + imageProvider = SingleImageProvider(imageSrc.isFile ? getFileImageProvider(imageSrc.path) : NetworkImage(imageSrc.path)); + } + + debugPrint("ImageViewer StoreConnector build: ${widget.control.id}"); + + var open = widget.control.attrBool("open", false)!; + + debugPrint("Current open state: $_open"); + debugPrint("New open state: $open"); + + if (open && (open != _open)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showImageViewerPager( + context, + imageProvider!, + swipeDismissible: swipeDismissible, + doubleTapZoomable: doubleTapZoomable, + backgroundColor: backgroundColor!, + closeButtonColor: closeButtonColor!, + closeButtonTooltip: closeButtonTooltip, + immersive: immersive, + ); + }); + } + + _open = open; + + return const SizedBox.shrink(); + }); + } +} diff --git a/package/pubspec.yaml b/package/pubspec.yaml index 0f7e8d072..53095f4c1 100644 --- a/package/pubspec.yaml +++ b/package/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: window_to_front: ^0.0.3 audioplayers: ^5.2.1 shake: ^2.2.0 + easy_image_viewer: ^1.3.2 path: ^1.8.2 js: ^0.6.5 fl_chart: ^0.65.0 diff --git a/sdk/python/packages/flet-core/src/flet_core/__init__.py b/sdk/python/packages/flet-core/src/flet_core/__init__.py index 389c74d02..b3b60a071 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -119,6 +119,7 @@ from flet_core.icon import Icon from flet_core.icon_button import IconButton from flet_core.image import Image +from flet_core.image_viewer import ImageViewer from flet_core.list_tile import ListTile from flet_core.list_view import ListView from flet_core.margin import Margin diff --git a/sdk/python/packages/flet-core/src/flet_core/image_viewer.py b/sdk/python/packages/flet-core/src/flet_core/image_viewer.py new file mode 100644 index 000000000..609388b9b --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/image_viewer.py @@ -0,0 +1,172 @@ +from typing import Any, List, Dict, Optional, Union + +from flet_core.control import Control +from flet_core.ref import Ref +from flet_core.types import ( + MaterialState +) + +class ImageViewer(Control): + """ + A ImageViewer displays a full screen Image that allows Zoom, Pan, and paging through multipe images. + + The image src can be a single image path String or a List of image strings to page through. + Example: + ``` + import flet as ft + def main(page): + def show_image_viewer_click(e): + img = e.control.data + page.dialog = ft.ImageViewer(src=img, swipe_dismissable=False) + page.dialog.open = True + page.update() + images = ft.GridView(expand=1, runs_count=5) + for i in range(0, 60): + images.controls.append( + ft.GestureDetector( + content=ft.Image( + src=f"https://picsum.photos/150/150?{i}", + fit=ft.ImageFit.NONE, + ), + data=f"https://picsum.photos/150/150?{i}", + on_tap=show_image_viewer_click, + ) + ) + + page.add(images) + ft.app(target=main) + ``` + ----- + Online docs: https://flet.dev/docs/controls/ImageViewer + """ + + def __init__( + self, + ref: Optional[Ref] = None, + disabled: Optional[bool] = None, + visible: Optional[bool] = None, + data: Any = None, + # + # Specific + # + open: bool = False, + src: Union[str, List[str]] = None, + swipe_dismissible: Optional[bool] = True, + double_tap_zoomable: Optional[bool] = True, + background_color: Optional[str] = None, + close_button_color: Optional[str] = None, + close_button_tooltip: Optional[str] = "Close", + immersive: Optional[bool] = True, + initial_index: Optional[int] = 0, + ): + Control.__init__( + self, + ref=ref, + disabled=disabled, + visible=visible, + data=data, + ) + + self.open = open + self.src = src + self.swipe_dismissible = swipe_dismissible + self.double_tap_zoomable = double_tap_zoomable + self.background_color = background_color + self.close_button_color = close_button_color + self.close_button_tooltip = close_button_tooltip + self.immersive = immersive + self.initial_index = initial_index + + def _get_control_name(self): + return "imageviewer" + + def _before_build_command(self): + super()._before_build_command() + + # open + @property + def open(self) -> Optional[bool]: + return self._get_attr("open", data_type="bool", def_value=False) + + @open.setter + def open(self, value: Optional[bool]): + self._set_attr("open", value) + + # src + @property + def src(self) -> Union[str, List[str]]: + img = self._get_attr("src") + if "|" in img: + img = img.split("|") + return img + + @src.setter + def src(self, value: Union[str, List[str]]): + self.src = value + img = value + if isinstance(value, List): + img = "|".join(value) + self._set_attr("src", img) + + # swipe_dismissible + @property + def swipe_dismissible(self) -> Optional[bool]: + return self._get_attr("swipeDismissible", data_type="bool", def_value=True) + + @swipe_dismissible.setter + def swipe_dismissible(self, value: Optional[bool]): + self._set_attr("swipeDismissible", value) + + # double_tap_zoomable + @property + def double_tap_zoomable(self) -> Optional[bool]: + return self._get_attr("doubleTapZoomable", data_type="bool", def_value=True) + + @double_tap_zoomable.setter + def double_tap_zoomable(self, value: Optional[bool]): + self._set_attr("doubleTapZoomable", value) + + # close_button_color + @property + def background_color(self): + return self._get_attr("backgroundColor") + + @background_color.setter + def background_color(self, value): + self._set_attr("backgroundColor", value) + + # close_button_color + @property + def close_button_color(self): + return self._get_attr("closeButtonColor") + + @close_button_color.setter + def close_button_color(self, value): + self._set_attr("closeButtonColor", value) + + # close_button_tooltip + @property + def close_button_tooltip(self): + return self._get_attr("closeButtonTooltip", def_value="Close") + + @close_button_tooltip.setter + def close_button_tooltip(self, value): + self._set_attr("closeButtonTooltip", value) + + # immersive + @property + def immersive(self) -> Optional[bool]: + return self._get_attr("immersive", data_type="bool", def_value=True) + + @immersive.setter + def immersive(self, value: Optional[bool]): + self._set_attr("immersive", value) + + # initial_index + @property + def initial_index(self) -> Optional[int]: + return self._get_attr("initialIndex", def_value=0) + + @initial_index.setter + def divisions(self, value: Optional[int]): + self._set_attr("initialIndex", value) \ No newline at end of file From fae5eb3e34a6a8a10f18d1c3ca6da66201aebbd7 Mon Sep 17 00:00:00 2001 From: Alan Bedian Date: Fri, 12 Jan 2024 16:48:51 -0800 Subject: [PATCH 2/3] Got rid of shake requirement in pubspec --- package/pubspec.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package/pubspec.yaml b/package/pubspec.yaml index 624ada714..3a7d74dc1 100644 --- a/package/pubspec.yaml +++ b/package/pubspec.yaml @@ -37,7 +37,6 @@ dependencies: flutter_svg: ^2.0.9 window_to_front: ^0.0.3 audioplayers: ^5.2.1 - shake: ^2.2.0 sensors_plus: ^4.0.2 easy_image_viewer: ^1.3.2 path: ^1.8.2 @@ -56,4 +55,4 @@ flutter: # To add assets to your package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg \ No newline at end of file + # - images/a_dot_ham.jpeg From 279e39f15bc72ffd7f9f700b3d5edbf5d9845db7 Mon Sep 17 00:00:00 2001 From: Alan Bedian Date: Fri, 12 Jan 2024 16:50:40 -0800 Subject: [PATCH 3/3] Update ease_image_viewer to 1.4.0 --- package/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/pubspec.yaml b/package/pubspec.yaml index 3a7d74dc1..6b43a380c 100644 --- a/package/pubspec.yaml +++ b/package/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: window_to_front: ^0.0.3 audioplayers: ^5.2.1 sensors_plus: ^4.0.2 - easy_image_viewer: ^1.3.2 + easy_image_viewer: ^1.4.0 path: ^1.8.2 js: ^0.6.5 fl_chart: ^0.65.0