1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

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
This commit is contained in:
Matthias Rupp 2022-08-08 02:43:09 +02:00 committed by GitHub
parent 46f4905259
commit b46e834220
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 159 additions and 81 deletions

View File

@ -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,

View File

@ -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,
),
);
}

View File

@ -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) =>

View File

@ -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;

View File

@ -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,

View File

@ -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(

View File

@ -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)

View File

@ -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,
),
);

View File

@ -76,6 +76,7 @@ class HomePage extends HookConsumerWidget {
imageGridGroup.add(
DailyTitleText(
key: Key('${dateGroup.toString()}title'),
isoDate: dateGroup,
assetGroup: immichAssetList,
),

View File

@ -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}';
}
}

View File

@ -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';
}

View File

@ -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:

View File

@ -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