From 424b11cf50a81e2870a72189d27ecc875838a53c Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey Date: Fri, 2 Dec 2022 21:55:10 +0100 Subject: [PATCH] feat(mobile): configure detail viewer asset loading (#1044) --- mobile/assets/i18n/en-US.json | 7 ++- .../asset_viewer/ui/remote_photo_view.dart | 28 +++++---- .../asset_viewer/views/gallery_viewer.dart | 14 +++-- .../asset_viewer/views/image_viewer_page.dart | 9 ++- .../modules/home/services/asset.service.dart | 18 +++--- .../services/app_settings.service.dart | 3 +- mobile/lib/modules/settings/ui/common.dart | 24 ++++++++ .../image_viewer_quality_setting.dart | 45 +++++++++++++-- .../three_stage_loading.dart | 57 ------------------- .../notification_setting.dart | 27 +-------- mobile/lib/routing/router.gr.dart | 18 ++++-- .../lib/shared/providers/asset.provider.dart | 19 ++++--- 12 files changed, 138 insertions(+), 131 deletions(-) create mode 100644 mobile/lib/modules/settings/ui/common.dart delete mode 100644 mobile/lib/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 5e072b84d4..b10b9b5e92 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -141,6 +141,11 @@ "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_single_progress_title": "Show background backup detail progress", "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", + "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", + "setting_image_viewer_preview_title": "Load preview image", + "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", + "setting_image_viewer_original_title": "Load original image", + "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", "setting_pages_app_bar_settings": "Settings", "share_add": "Add", "share_add_photos": "Add photos", @@ -165,8 +170,6 @@ "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", - "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", - "theme_setting_three_stage_loading_title": "Enable three-stage loading", "version_announcement_overlay_ack": "Acknowledge", "version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_text_1": "Hi friend, there is a new release of", 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 7ddbc3fcda..3f5f243897 100644 --- a/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart +++ b/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart @@ -128,7 +128,7 @@ class _RemotePhotoViewState extends State { }), ); - if (widget.threeStageLoading) { + if (widget.loadPreview) { _previewProvider = _authorizedImageProvider( getThumbnailUrl(widget.asset.remote!, type: ThumbnailFormat.JPEG), "${widget.asset.id}_previewStage", @@ -140,15 +140,17 @@ class _RemotePhotoViewState extends State { ); } - _fullProvider = _authorizedImageProvider( - getImageUrl(widget.asset.remote!), - "${widget.asset.id}_fullStage", - ); - _fullProvider.resolve(const ImageConfiguration()).addListener( - ImageStreamListener((ImageInfo imageInfo, _) { - _performStateTransition(_RemoteImageStatus.full, _fullProvider); - }), - ); + if (widget.loadOriginal) { + _fullProvider = _authorizedImageProvider( + getImageUrl(widget.asset.remote!), + "${widget.asset.id}_fullStage", + ); + _fullProvider.resolve(const ImageConfiguration()).addListener( + ImageStreamListener((ImageInfo imageInfo, _) { + _performStateTransition(_RemoteImageStatus.full, _fullProvider); + }), + ); + } } @override @@ -178,7 +180,8 @@ class RemotePhotoView extends StatefulWidget { Key? key, required this.asset, required this.authToken, - required this.threeStageLoading, + required this.loadPreview, + required this.loadOriginal, required this.isZoomedFunction, required this.isZoomedListener, required this.onSwipeDown, @@ -187,7 +190,8 @@ class RemotePhotoView extends StatefulWidget { final Asset asset; final String authToken; - final bool threeStageLoading; + final bool loadPreview; + final bool loadOriginal; final void Function() onSwipeDown; final void Function() onSwipeUp; final void Function() isZoomedFunction; diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 28e66a6543..74f53a2acc 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -31,8 +31,9 @@ class GalleryViewerPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final Box box = Hive.box(userInfoBox); - final appSettingService = ref.watch(appSettingsServiceProvider); - final threeStageLoading = useState(false); + final settings = ref.watch(appSettingsServiceProvider); + final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue); + final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue); final isZoomed = useState(false); final indexOfAsset = useState(assetList.indexOf(asset)); final isPlayingMotionVideo = useState(false); @@ -43,8 +44,10 @@ class GalleryViewerPage extends HookConsumerWidget { useEffect( () { - threeStageLoading.value = appSettingService - .getSetting(AppSettingsEnum.threeStageLoading); + isLoadPreview.value = + settings.getSetting(AppSettingsEnum.loadPreview); + isLoadOriginal.value = + settings.getSetting(AppSettingsEnum.loadOriginal); isPlayingMotionVideo.value = false; return null; }, @@ -140,7 +143,8 @@ class GalleryViewerPage extends HookConsumerWidget { isZoomedListener: isZoomedListener, asset: assetList[index], heroTag: assetList[index].id, - threeStageLoading: threeStageLoading.value, + loadPreview: isLoadPreview.value, + loadOriginal: isLoadOriginal.value, ); } } else { 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 59540386cf..e181ad4cd3 100644 --- a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart @@ -17,7 +17,8 @@ class ImageViewerPage extends HookConsumerWidget { final String authToken; final ValueNotifier isZoomedListener; final void Function() isZoomedFunction; - final bool threeStageLoading; + final bool loadPreview; + final bool loadOriginal; ImageViewerPage({ Key? key, @@ -26,7 +27,8 @@ class ImageViewerPage extends HookConsumerWidget { required this.authToken, required this.isZoomedFunction, required this.isZoomedListener, - required this.threeStageLoading, + required this.loadPreview, + required this.loadOriginal, }) : super(key: key); Asset? assetDetail; @@ -74,7 +76,8 @@ class ImageViewerPage extends HookConsumerWidget { child: RemotePhotoView( asset: asset, authToken: authToken, - threeStageLoading: threeStageLoading, + loadPreview: loadPreview, + loadOriginal: loadOriginal, isZoomedFunction: isZoomedFunction, isZoomedListener: isZoomedListener, onSwipeDown: () => AutoRouter.of(context).pop(), diff --git a/mobile/lib/modules/home/services/asset.service.dart b/mobile/lib/modules/home/services/asset.service.dart index 0599d0c547..9adad8b3dc 100644 --- a/mobile/lib/modules/home/services/asset.service.dart +++ b/mobile/lib/modules/home/services/asset.service.dart @@ -32,20 +32,20 @@ class AssetService { AssetService(this._apiService, this._backupService, this._backgroundService); /// Returns `null` if the server state did not change, else list of assets - Future?> getRemoteAssets({required bool hasCache}) async { + Future?, String?>> getRemoteAssets({String? etag}) async { try { - final Box box = Hive.box(userInfoBox); - final Pair, String?>? remote = await _apiService - .assetApi - .getAllAssetsWithETag(eTag: hasCache ? box.get(assetEtagKey) : null); + final Pair, String?>? remote = + await _apiService.assetApi.getAllAssetsWithETag(eTag: etag); if (remote == null) { - return null; + return const Pair(null, null); } - box.put(assetEtagKey, remote.second); - return remote.first.map(Asset.remote).toList(growable: false); + return Pair( + remote.first.map(Asset.remote).toList(growable: false), + remote.second, + ); } catch (e, stack) { log.severe('Error while getting remote assets', e, stack); - return null; + return const Pair(null, null); } } diff --git a/mobile/lib/modules/settings/services/app_settings.service.dart b/mobile/lib/modules/settings/services/app_settings.service.dart index 292c40c210..67bfedf774 100644 --- a/mobile/lib/modules/settings/services/app_settings.service.dart +++ b/mobile/lib/modules/settings/services/app_settings.service.dart @@ -2,7 +2,8 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:immich_mobile/constants/hive_box.dart'; enum AppSettingsEnum { - threeStageLoading("threeStageLoading", false), + loadPreview("loadPreview", true), + loadOriginal("loadOriginal", false), themeMode("themeMode", "system"), // "light","dark","system" tilesPerRow("tilesPerRow", 4), uploadErrorNotificationGracePeriod( diff --git a/mobile/lib/modules/settings/ui/common.dart b/mobile/lib/modules/settings/ui/common.dart new file mode 100644 index 0000000000..13e1d53e20 --- /dev/null +++ b/mobile/lib/modules/settings/ui/common.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; + +SwitchListTile buildSwitchListTile( + BuildContext context, + AppSettingsService appSettingService, + ValueNotifier valueNotifier, + AppSettingsEnum settingsEnum, { + required String title, + String? subtitle, +}) { + return SwitchListTile.adaptive( + key: Key(settingsEnum.name), + value: valueNotifier.value, + onChanged: (value) { + valueNotifier.value = value; + appSettingService.setSetting(settingsEnum, value); + }, + activeColor: Theme.of(context).primaryColor, + dense: true, + title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: subtitle != null ? Text(subtitle) : null, + ); +} diff --git a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart index 861f7efa9b..88a14d5ad3 100644 --- a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart +++ b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart @@ -1,14 +1,30 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/modules/settings/ui/common.dart'; -class ImageViewerQualitySetting extends StatelessWidget { +class ImageViewerQualitySetting extends HookConsumerWidget { const ImageViewerQualitySetting({ Key? key, }) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(appSettingsServiceProvider); + final isPreview = useState(AppSettingsEnum.loadPreview.defaultValue); + final isOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue); + + useEffect( + () { + isPreview.value = settings.getSetting(AppSettingsEnum.loadPreview); + isOriginal.value = settings.getSetting(AppSettingsEnum.loadOriginal); + return null; + }, + ); + return ExpansionTile( textColor: Theme.of(context).primaryColor, title: const Text( @@ -23,8 +39,27 @@ class ImageViewerQualitySetting extends StatelessWidget { fontSize: 13, ), ).tr(), - children: const [ - ThreeStageLoading(), + children: [ + ListTile( + title: const Text('setting_image_viewer_help').tr(), + dense: true, + ), + buildSwitchListTile( + context, + settings, + isPreview, + AppSettingsEnum.loadPreview, + title: "setting_image_viewer_preview_title".tr(), + subtitle: "setting_image_viewer_preview_subtitle".tr(), + ), + buildSwitchListTile( + context, + settings, + isOriginal, + AppSettingsEnum.loadOriginal, + title: "setting_image_viewer_original_title".tr(), + subtitle: "setting_image_viewer_original_subtitle".tr(), + ), ], ); } diff --git a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart deleted file mode 100644 index f10e663add..0000000000 --- a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; - -class ThreeStageLoading extends HookConsumerWidget { - const ThreeStageLoading({ - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - - final isEnable = useState(false); - - useEffect( - () { - var isThreeStageLoadingEnable = - appSettingService.getSetting(AppSettingsEnum.threeStageLoading); - - isEnable.value = isThreeStageLoadingEnable; - return null; - }, - [], - ); - - void onSwitchChanged(bool switchValue) { - appSettingService.setSetting( - AppSettingsEnum.threeStageLoading, - switchValue, - ); - isEnable.value = switchValue; - } - - return SwitchListTile.adaptive( - activeColor: Theme.of(context).primaryColor, - title: const Text( - "theme_setting_three_stage_loading_title", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ).tr(), - subtitle: const Text( - "theme_setting_three_stage_loading_subtitle", - style: TextStyle( - fontSize: 12, - ), - ).tr(), - value: isEnable.value, - onChanged: onSwitchChanged, - ); - } -} diff --git a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart index be988e01cb..ed996db35b 100644 --- a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart +++ b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/modules/settings/ui/common.dart'; class NotificationSetting extends HookConsumerWidget { const NotificationSetting({ @@ -50,7 +51,7 @@ class NotificationSetting extends HookConsumerWidget { ), ).tr(), children: [ - _buildSwitchListTile( + buildSwitchListTile( context, appSettingService, totalProgressValue, @@ -58,7 +59,7 @@ class NotificationSetting extends HookConsumerWidget { title: 'setting_notifications_total_progress_title'.tr(), subtitle: 'setting_notifications_total_progress_subtitle'.tr(), ), - _buildSwitchListTile( + buildSwitchListTile( context, appSettingService, singleProgressValue, @@ -91,28 +92,6 @@ class NotificationSetting extends HookConsumerWidget { } } -SwitchListTile _buildSwitchListTile( - BuildContext context, - AppSettingsService appSettingService, - ValueNotifier valueNotifier, - AppSettingsEnum settingsEnum, { - required String title, - String? subtitle, -}) { - return SwitchListTile( - key: Key(settingsEnum.name), - value: valueNotifier.value, - onChanged: (value) { - valueNotifier.value = value; - appSettingService.setSetting(settingsEnum, value); - }, - activeColor: Theme.of(context).primaryColor, - dense: true, - title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), - subtitle: subtitle != null ? Text(subtitle) : null, - ); -} - String _formatSliderValue(double v) { if (v == 0.0) { return 'setting_notifications_notify_immediately'.tr(); diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index bd266c00cd..d2100398f7 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -59,7 +59,8 @@ class _$AppRouter extends RootStackRouter { authToken: args.authToken, isZoomedFunction: args.isZoomedFunction, isZoomedListener: args.isZoomedListener, - threeStageLoading: args.threeStageLoading)); + loadPreview: args.loadPreview, + loadOriginal: args.loadOriginal)); }, VideoViewerRoute.name: (routeData) { final args = routeData.argsAs(); @@ -305,7 +306,8 @@ class ImageViewerRoute extends PageRouteInfo { required String authToken, required void Function() isZoomedFunction, required ValueNotifier isZoomedListener, - required bool threeStageLoading}) + required bool loadPreview, + required bool loadOriginal}) : super(ImageViewerRoute.name, path: '/image-viewer-page', args: ImageViewerRouteArgs( @@ -315,7 +317,8 @@ class ImageViewerRoute extends PageRouteInfo { authToken: authToken, isZoomedFunction: isZoomedFunction, isZoomedListener: isZoomedListener, - threeStageLoading: threeStageLoading)); + loadPreview: loadPreview, + loadOriginal: loadOriginal)); static const String name = 'ImageViewerRoute'; } @@ -328,7 +331,8 @@ class ImageViewerRouteArgs { required this.authToken, required this.isZoomedFunction, required this.isZoomedListener, - required this.threeStageLoading}); + required this.loadPreview, + required this.loadOriginal}); final Key? key; @@ -342,11 +346,13 @@ class ImageViewerRouteArgs { final ValueNotifier isZoomedListener; - final bool threeStageLoading; + final bool loadPreview; + + final bool loadOriginal; @override String toString() { - return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, threeStageLoading: $threeStageLoading}'; + return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, loadPreview: $loadPreview, loadOriginal: $loadOriginal}'; } } diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index 9250c33802..96c19147a2 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/home/services/asset_cache.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/services/device_info.service.dart'; import 'package:collection/collection.dart'; +import 'package:immich_mobile/utils/tuple.dart'; import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -37,8 +38,11 @@ class AssetNotifier extends StateNotifier> { _getAllAssetInProgress = true; final bool isCacheValid = await _assetCacheService.isValid(); stopwatch.start(); + final Box box = Hive.box(userInfoBox); final localTask = _assetService.getLocalAssets(urgent: !isCacheValid); - final remoteTask = _assetService.getRemoteAssets(hasCache: isCacheValid); + final remoteTask = _assetService.getRemoteAssets( + etag: isCacheValid ? box.get(assetEtagKey) : null, + ); if (isCacheValid && state.isEmpty) { state = await _assetCacheService.get(); log.info( @@ -50,7 +54,8 @@ class AssetNotifier extends StateNotifier> { int remoteBegin = state.indexWhere((a) => a.isRemote); remoteBegin = remoteBegin == -1 ? state.length : remoteBegin; final List currentLocal = state.slice(0, remoteBegin); - List? newRemote = await remoteTask; + final Pair?, String?> remoteResult = await remoteTask; + List? newRemote = remoteResult.first; List? newLocal = await localTask; log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms"); stopwatch.reset(); @@ -63,14 +68,14 @@ class AssetNotifier extends StateNotifier> { newLocal ??= []; state = _combineLocalAndRemoteAssets(local: newLocal, remote: newRemote); log.info("Combining assets: ${stopwatch.elapsedMilliseconds}ms"); + + stopwatch.reset(); + _cacheState(); + box.put(assetEtagKey, remoteResult.second); + log.info("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms"); } finally { _getAllAssetInProgress = false; } - log.info("setting new asset state"); - - stopwatch.reset(); - _cacheState(); - log.info("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms"); } List _combineLocalAndRemoteAssets({