Forráskód Böngészése

feat(mobile): configure detail viewer asset loading (#1044)

Fynn Petersen-Frey 2 éve
szülő
commit
424b11cf50

+ 5 - 2
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",

+ 16 - 12
mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart

@@ -128,7 +128,7 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
       }),
     );
 
-    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<RemotePhotoView> {
       );
     }
 
-    _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;

+ 9 - 5
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<dynamic> 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<bool>(false);
     final indexOfAsset = useState(assetList.indexOf(asset));
     final isPlayingMotionVideo = useState(false);
@@ -43,8 +44,10 @@ class GalleryViewerPage extends HookConsumerWidget {
 
     useEffect(
       () {
-        threeStageLoading.value = appSettingService
-            .getSetting<bool>(AppSettingsEnum.threeStageLoading);
+        isLoadPreview.value =
+            settings.getSetting<bool>(AppSettingsEnum.loadPreview);
+        isLoadOriginal.value =
+            settings.getSetting<bool>(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 {

+ 6 - 3
mobile/lib/modules/asset_viewer/views/image_viewer_page.dart

@@ -17,7 +17,8 @@ class ImageViewerPage extends HookConsumerWidget {
   final String authToken;
   final ValueNotifier<bool> 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(),

+ 9 - 9
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<List<Asset>?> getRemoteAssets({required bool hasCache}) async {
+  Future<Pair<List<Asset>?, String?>> getRemoteAssets({String? etag}) async {
     try {
-      final Box box = Hive.box(userInfoBox);
-      final Pair<List<AssetResponseDto>, String?>? remote = await _apiService
-          .assetApi
-          .getAllAssetsWithETag(eTag: hasCache ? box.get(assetEtagKey) : null);
+      final Pair<List<AssetResponseDto>, 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);
     }
   }
 

+ 2 - 1
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<T> {
-  threeStageLoading<bool>("threeStageLoading", false),
+  loadPreview<bool>("loadPreview", true),
+  loadOriginal<bool>("loadOriginal", false),
   themeMode<String>("themeMode", "system"), // "light","dark","system"
   tilesPerRow<int>("tilesPerRow", 4),
   uploadErrorNotificationGracePeriod<int>(

+ 24 - 0
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<bool> 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,
+  );
+}

+ 40 - 5
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(),
+        ),
       ],
     );
   }

+ 0 - 57
mobile/lib/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart

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

+ 3 - 24
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<bool> 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();

+ 12 - 6
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<VideoViewerRouteArgs>();
@@ -305,7 +306,8 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
       required String authToken,
       required void Function() isZoomedFunction,
       required ValueNotifier<bool> 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<ImageViewerRouteArgs> {
                 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<bool> 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}';
   }
 }
 

+ 12 - 7
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<List<Asset>> {
       _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<List<Asset>> {
       int remoteBegin = state.indexWhere((a) => a.isRemote);
       remoteBegin = remoteBegin == -1 ? state.length : remoteBegin;
       final List<Asset> currentLocal = state.slice(0, remoteBegin);
-      List<Asset>? newRemote = await remoteTask;
+      final Pair<List<Asset>?, String?> remoteResult = await remoteTask;
+      List<Asset>? newRemote = remoteResult.first;
       List<Asset>? newLocal = await localTask;
       log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
       stopwatch.reset();
@@ -63,14 +68,14 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
       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<Asset> _combineLocalAndRemoteAssets({