From b46e8342202c546efdec6b037acd1fc6afdd87f3 Mon Sep 17 00:00:00 2001 From: Matthias Rupp <matthias.rupp@posteo.de> Date: Mon, 8 Aug 2022 02:43:09 +0200 Subject: [PATCH] Mobile performance improvements (#417) * First performance tweaks (caching and rendering improvemetns) * Revert asset response caching * 3-step image loading in asset viewer * Prevent panning and zooming until full-scale version is loaded * Loading indicator * Adapt to gallery PR * Cleanup * Dart format * Fix exif sheet * Disable three stage loading until settings are available --- mobile/lib/main.dart | 6 ++ .../album/ui/album_viewer_thumbnail.dart | 5 +- .../ui/shared_album_thumbnail_image.dart | 5 +- .../asset_viewer/ui/remote_photo_view.dart | 78 ++++++++++++++----- .../asset_viewer/ui/top_control_app_bar.dart | 10 +++ .../asset_viewer/views/gallery_viewer.dart | 13 ++-- .../asset_viewer/views/image_viewer_page.dart | 31 +++++--- .../lib/modules/home/ui/thumbnail_image.dart | 5 +- mobile/lib/modules/home/views/home_page.dart | 1 + mobile/lib/routing/router.gr.dart | 62 +++++++-------- mobile/lib/utils/image_url_builder.dart | 16 ++++ mobile/pubspec.lock | 7 ++ mobile/pubspec.yaml | 1 + 13 files changed, 159 insertions(+), 81 deletions(-) create mode 100644 mobile/lib/utils/image_url_builder.dart diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index fdc1a43b8e..94d581c57a 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,6 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/immich_colors.dart'; @@ -49,6 +51,10 @@ void main() async { Locale('it', 'IT'), ]; + if (kReleaseMode) { + await FlutterDisplayMode.setHighRefreshRate(); + } + runApp( EasyLocalization( supportedLocales: locales, diff --git a/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart b/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart index 17686afeb2..c5441fe9d7 100644 --- a/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart +++ b/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:openapi/api.dart'; class AlbumViewerThumbnail extends HookConsumerWidget { @@ -24,8 +25,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final cacheKey = useState(1); var box = Hive.box(userInfoBox); - var thumbnailRequestUrl = - '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}'; + var thumbnailRequestUrl = getThumbnailUrl(asset); var deviceId = ref.watch(authenticationProvider).deviceId; final selectedAssetsInAlbumViewer = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer; @@ -37,7 +37,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget { GalleryViewerRoute( asset: asset, assetList: assetList, - thumbnailRequestUrl: thumbnailRequestUrl, ), ); } diff --git a/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart b/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart index d62f1e097b..4f90a8cf36 100644 --- a/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart +++ b/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:openapi/api.dart'; class SharedAlbumThumbnailImage extends HookConsumerWidget { @@ -17,8 +18,6 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget { final cacheKey = useState(1); var box = Hive.box(userInfoBox); - var thumbnailRequestUrl = - '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}'; return GestureDetector( onTap: () { @@ -32,7 +31,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget { height: 500, memCacheHeight: 500, fit: BoxFit.cover, - imageUrl: thumbnailRequestUrl, + imageUrl: getThumbnailUrl(asset), httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, fadeInDuration: const Duration(milliseconds: 250), progressIndicatorBuilder: (context, url, downloadProgress) => diff --git a/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart b/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart index cb930bc621..862de5b501 100644 --- a/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart +++ b/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart @@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; -enum _RemoteImageStatus { empty, thumbnail, full } +enum _RemoteImageStatus { empty, thumbnail, preview, full } class _RemotePhotoViewState extends State<RemotePhotoView> { late CachedNetworkImageProvider _imageProvider; @@ -15,13 +15,16 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { @override Widget build(BuildContext context) { bool allowMoving = _status == _RemoteImageStatus.full; - return PhotoView( - imageProvider: _imageProvider, - minScale: PhotoViewComputedScale.contained, - maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained, - enablePanAlways: true, - scaleStateChangedCallback: _scaleStateChanged, - onScaleEnd: _onScaleListener, + + return IgnorePointer( + ignoring: !allowMoving, + child: PhotoView( + imageProvider: _imageProvider, + minScale: PhotoViewComputedScale.contained, + enablePanAlways: true, + scaleStateChangedCallback: _scaleStateChanged, + onScaleEnd: _onScaleListener, + ), ); } @@ -52,6 +55,14 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { widget.isZoomedFunction(); } + void _fireStartLoadingEvent() { + if (widget.onLoadingStart != null) widget.onLoadingStart!(); + } + + void _fireFinishedLoadingEvent() { + if (widget.onLoadingCompleted != null) widget.onLoadingCompleted!(); + } + CachedNetworkImageProvider _authorizedImageProvider(String url) { return CachedNetworkImageProvider( url, @@ -64,14 +75,25 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { _RemoteImageStatus newStatus, CachedNetworkImageProvider provider, ) { - // Transition to same status is forbidden if (_status == newStatus) return; - // Transition full -> thumbnail is forbidden + if (_status == _RemoteImageStatus.full && newStatus == _RemoteImageStatus.thumbnail) return; + if (_status == _RemoteImageStatus.preview && + newStatus == _RemoteImageStatus.thumbnail) return; + + if (_status == _RemoteImageStatus.full && + newStatus == _RemoteImageStatus.preview) return; + if (!mounted) return; + if (newStatus != _RemoteImageStatus.full) { + _fireStartLoadingEvent(); + } else { + _fireFinishedLoadingEvent(); + } + setState(() { _status = newStatus; _imageProvider = provider; @@ -92,6 +114,16 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { }), ); + if (widget.previewUrl != null) { + CachedNetworkImageProvider previewProvider = + _authorizedImageProvider(widget.previewUrl!); + previewProvider.resolve(const ImageConfiguration()).addListener( + ImageStreamListener((ImageInfo imageInfo, _) { + _performStateTransition(_RemoteImageStatus.preview, previewProvider); + }), + ); + } + CachedNetworkImageProvider fullProvider = _authorizedImageProvider(widget.imageUrl); fullProvider.resolve(const ImageConfiguration()).addListener( @@ -109,20 +141,26 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { } class RemotePhotoView extends StatefulWidget { - const RemotePhotoView({ - Key? key, - required this.thumbnailUrl, - required this.imageUrl, - required this.authToken, - required this.isZoomedFunction, - required this.isZoomedListener, - required this.onSwipeDown, - required this.onSwipeUp, - }) : super(key: key); + const RemotePhotoView( + {Key? key, + required this.thumbnailUrl, + required this.imageUrl, + required this.authToken, + required this.isZoomedFunction, + required this.isZoomedListener, + required this.onSwipeDown, + required this.onSwipeUp, + this.previewUrl, + this.onLoadingCompleted, + this.onLoadingStart}) + : super(key: key); final String thumbnailUrl; final String imageUrl; final String authToken; + final String? previewUrl; + final Function? onLoadingCompleted; + final Function? onLoadingStart; final void Function() onSwipeDown; final void Function() onSwipeUp; diff --git a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart index eba66a2691..3c298568b2 100644 --- a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart +++ b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart @@ -11,11 +11,13 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget { required this.asset, required this.onMoreInfoPressed, required this.onDownloadPressed, + this.loading = false }) : super(key: key); final AssetResponseDto asset; final Function onMoreInfoPressed; final Function onDownloadPressed; + final bool loading; @override Widget build(BuildContext context, WidgetRef ref) { @@ -35,6 +37,14 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget { ), ), actions: [ + if (loading) Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 15.0), + width: iconSize, + height: iconSize, + child: const CircularProgressIndicator(strokeWidth: 2.0), + ), + ) , IconButton( iconSize: iconSize, splashRadius: iconSize, diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 7e0ecc9357..c9d46e6c61 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -17,13 +17,13 @@ import 'package:openapi/api.dart'; class GalleryViewerPage extends HookConsumerWidget { late List<AssetResponseDto> assetList; final AssetResponseDto asset; - final String thumbnailRequestUrl; + + static const _threeStageLoading = false; GalleryViewerPage({ Key? key, required this.assetList, required this.asset, - required this.thumbnailRequestUrl, }) : super(key: key); AssetResponseDto? assetDetail; @@ -32,6 +32,7 @@ class GalleryViewerPage extends HookConsumerWidget { final Box<dynamic> box = Hive.box(userInfoBox); int indexOfAsset = assetList.indexOf(asset); + final loading = useState(false); @override void initState(int index) { @@ -74,6 +75,7 @@ class GalleryViewerPage extends HookConsumerWidget { return Scaffold( backgroundColor: Colors.black, appBar: TopControlAppBar( + loading: loading.value, asset: assetList[indexOfAsset], onMoreInfoPressed: () { showInfo(); @@ -98,15 +100,14 @@ class GalleryViewerPage extends HookConsumerWidget { getAssetExif(); if (assetList[index].type == AssetTypeEnum.IMAGE) { return ImageViewerPage( - thumbnailUrl: - '${box.get(serverEndpointKey)}/asset/thumbnail/${assetList[index].id}', - imageUrl: - '${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}&isThumb=false', authToken: 'Bearer ${box.get(accessTokenKey)}', isZoomedFunction: isZoomedMethod, isZoomedListener: isZoomedListener, + onLoadingCompleted: () => loading.value = false, + onLoadingStart: () => loading.value = _threeStageLoading, asset: assetList[index], heroTag: assetList[index].id, + threeStageLoading: _threeStageLoading ); } else { return SwipeDetector( diff --git a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart index f1130cdf6c..a4817ee9dc 100644 --- a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart @@ -8,27 +8,30 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:openapi/api.dart'; // ignore: must_be_immutable class ImageViewerPage extends HookConsumerWidget { - final String imageUrl; final String heroTag; - final String thumbnailUrl; final AssetResponseDto asset; final String authToken; final ValueNotifier<bool> isZoomedListener; final void Function() isZoomedFunction; + final void Function() onLoadingCompleted; + final void Function() onLoadingStart; + final bool threeStageLoading; ImageViewerPage({ Key? key, - required this.imageUrl, required this.heroTag, - required this.thumbnailUrl, required this.asset, required this.authToken, required this.isZoomedFunction, required this.isZoomedListener, + required this.onLoadingCompleted, + required this.onLoadingStart, + required this.threeStageLoading, }) : super(key: key); AssetResponseDto? assetDetail; @@ -68,14 +71,18 @@ class ImageViewerPage extends HookConsumerWidget { child: Hero( tag: heroTag, child: RemotePhotoView( - thumbnailUrl: thumbnailUrl, - imageUrl: imageUrl, - authToken: authToken, - isZoomedFunction: isZoomedFunction, - isZoomedListener: isZoomedListener, - onSwipeDown: () => AutoRouter.of(context).pop(), - onSwipeUp: () => showInfo(), - ), + thumbnailUrl: getThumbnailUrl(asset), + imageUrl: getImageUrl(asset), + previewUrl: threeStageLoading + ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG) + : null, + authToken: authToken, + isZoomedFunction: isZoomedFunction, + isZoomedListener: isZoomedListener, + onSwipeDown: () => AutoRouter.of(context).pop(), + onSwipeUp: () => showInfo(), + onLoadingCompleted: onLoadingCompleted, + onLoadingStart: onLoadingStart), ), ), if (downloadAssetStatus == DownloadAssetStatus.loading) diff --git a/mobile/lib/modules/home/ui/thumbnail_image.dart b/mobile/lib/modules/home/ui/thumbnail_image.dart index c2b07c7621..683ec2d470 100644 --- a/mobile/lib/modules/home/ui/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/thumbnail_image.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:openapi/api.dart'; class ThumbnailImage extends HookConsumerWidget { @@ -23,8 +24,7 @@ class ThumbnailImage extends HookConsumerWidget { final cacheKey = useState(1); var box = Hive.box(userInfoBox); - var thumbnailRequestUrl = - '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}'; + var thumbnailRequestUrl = getThumbnailUrl(asset); var selectedAsset = ref.watch(homePageStateProvider).selectedItems; var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable; @@ -65,7 +65,6 @@ class ThumbnailImage extends HookConsumerWidget { AutoRouter.of(context).push( GalleryViewerRoute( assetList: assetList, - thumbnailRequestUrl: thumbnailRequestUrl, asset: asset, ), ); diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index e63acf1abd..ff259a2fec 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -76,6 +76,7 @@ class HomePage extends HookConsumerWidget { imageGridGroup.add( DailyTitleText( + key: Key('${dateGroup.toString()}title'), isoDate: dateGroup, assetGroup: immichAssetList, ), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index a367930af9..c047be73fc 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -46,10 +46,7 @@ class _$AppRouter extends RootStackRouter { return MaterialPageX<dynamic>( routeData: routeData, child: GalleryViewerPage( - key: args.key, - assetList: args.assetList, - asset: args.asset, - thumbnailRequestUrl: args.thumbnailRequestUrl)); + key: args.key, assetList: args.assetList, asset: args.asset)); }, ImageViewerRoute.name: (routeData) { final args = routeData.argsAs<ImageViewerRouteArgs>(); @@ -57,13 +54,14 @@ class _$AppRouter extends RootStackRouter { routeData: routeData, child: ImageViewerPage( key: args.key, - imageUrl: args.imageUrl, heroTag: args.heroTag, - thumbnailUrl: args.thumbnailUrl, asset: args.asset, authToken: args.authToken, isZoomedFunction: args.isZoomedFunction, - isZoomedListener: args.isZoomedListener)); + isZoomedListener: args.isZoomedListener, + onLoadingCompleted: args.onLoadingCompleted, + onLoadingStart: args.onLoadingStart, + threeStageLoading: args.threeStageLoading)); }, VideoViewerRoute.name: (routeData) { final args = routeData.argsAs<VideoViewerRouteArgs>(); @@ -258,25 +256,18 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> { GalleryViewerRoute( {Key? key, required List<AssetResponseDto> assetList, - required AssetResponseDto asset, - required String thumbnailRequestUrl}) + required AssetResponseDto asset}) : super(GalleryViewerRoute.name, path: '/gallery-viewer-page', args: GalleryViewerRouteArgs( - key: key, - assetList: assetList, - asset: asset, - thumbnailRequestUrl: thumbnailRequestUrl)); + key: key, assetList: assetList, asset: asset)); static const String name = 'GalleryViewerRoute'; } class GalleryViewerRouteArgs { const GalleryViewerRouteArgs( - {this.key, - required this.assetList, - required this.asset, - required this.thumbnailRequestUrl}); + {this.key, required this.assetList, required this.asset}); final Key? key; @@ -284,11 +275,9 @@ class GalleryViewerRouteArgs { final AssetResponseDto asset; - final String thumbnailRequestUrl; - @override String toString() { - return 'GalleryViewerRouteArgs{key: $key, assetList: $assetList, asset: $asset, thumbnailRequestUrl: $thumbnailRequestUrl}'; + return 'GalleryViewerRouteArgs{key: $key, assetList: $assetList, asset: $asset}'; } } @@ -297,24 +286,26 @@ class GalleryViewerRouteArgs { class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> { ImageViewerRoute( {Key? key, - required String imageUrl, required String heroTag, - required String thumbnailUrl, required AssetResponseDto asset, required String authToken, required void Function() isZoomedFunction, - required ValueNotifier<bool> isZoomedListener}) + required ValueNotifier<bool> isZoomedListener, + required void Function() onLoadingCompleted, + required void Function() onLoadingStart, + required bool threeStageLoading}) : super(ImageViewerRoute.name, path: '/image-viewer-page', args: ImageViewerRouteArgs( key: key, - imageUrl: imageUrl, heroTag: heroTag, - thumbnailUrl: thumbnailUrl, asset: asset, authToken: authToken, isZoomedFunction: isZoomedFunction, - isZoomedListener: isZoomedListener)); + isZoomedListener: isZoomedListener, + onLoadingCompleted: onLoadingCompleted, + onLoadingStart: onLoadingStart, + threeStageLoading: threeStageLoading)); static const String name = 'ImageViewerRoute'; } @@ -322,22 +313,19 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> { class ImageViewerRouteArgs { const ImageViewerRouteArgs( {this.key, - required this.imageUrl, required this.heroTag, - required this.thumbnailUrl, required this.asset, required this.authToken, required this.isZoomedFunction, - required this.isZoomedListener}); + required this.isZoomedListener, + required this.onLoadingCompleted, + required this.onLoadingStart, + required this.threeStageLoading}); final Key? key; - final String imageUrl; - final String heroTag; - final String thumbnailUrl; - final AssetResponseDto asset; final String authToken; @@ -346,9 +334,15 @@ class ImageViewerRouteArgs { final ValueNotifier<bool> isZoomedListener; + final void Function() onLoadingCompleted; + + final void Function() onLoadingStart; + + final bool threeStageLoading; + @override String toString() { - return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener}'; + return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, onLoadingCompleted: $onLoadingCompleted, onLoadingStart: $onLoadingStart, threeStageLoading: $threeStageLoading}'; } } diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart new file mode 100644 index 0000000000..0a22b54dd8 --- /dev/null +++ b/mobile/lib/utils/image_url_builder.dart @@ -0,0 +1,16 @@ +import 'package:hive/hive.dart'; +import 'package:openapi/api.dart'; + +import '../constants/hive_box.dart'; + +String getThumbnailUrl(final AssetResponseDto asset, + {ThumbnailFormat type = ThumbnailFormat.WEBP}) { + final box = Hive.box(userInfoBox); + + return '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}?format=${type.value}'; +} + +String getImageUrl(final AssetResponseDto asset) { + final box = Hive.box(userInfoBox); + return '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false'; +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index ec0701fafe..6a756d003f 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -328,6 +328,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.3.0" + flutter_displaymode: + dependency: "direct main" + description: + name: flutter_displaymode + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" flutter_hooks: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index dbe84f23af..e7d495a761 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: http: 0.13.4 cancellation_token_http: ^1.1.0 easy_localization: ^3.0.1 + flutter_displaymode: ^0.4.0 path: ^1.8.1 path_provider: ^2.0.11