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

This commit is contained in:
Fynn Petersen-Frey 2022-12-02 21:55:10 +01:00 committed by GitHub
parent da87b1256c
commit 424b11cf50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 138 additions and 131 deletions

View file

@ -141,6 +141,11 @@
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "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_title": "Show background backup detail progress",
"setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", "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", "setting_pages_app_bar_settings": "Settings",
"share_add": "Add", "share_add": "Add",
"share_add_photos": "Add photos", "share_add_photos": "Add photos",
@ -165,8 +170,6 @@
"theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_system_theme_switch": "Automatic (Follow system setting)",
"theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_theme_title": "Theme", "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_ack": "Acknowledge",
"version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_release_notes": "release notes",
"version_announcement_overlay_text_1": "Hi friend, there is a new release of", "version_announcement_overlay_text_1": "Hi friend, there is a new release of",

View file

@ -128,7 +128,7 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}), }),
); );
if (widget.threeStageLoading) { if (widget.loadPreview) {
_previewProvider = _authorizedImageProvider( _previewProvider = _authorizedImageProvider(
getThumbnailUrl(widget.asset.remote!, type: ThumbnailFormat.JPEG), getThumbnailUrl(widget.asset.remote!, type: ThumbnailFormat.JPEG),
"${widget.asset.id}_previewStage", "${widget.asset.id}_previewStage",
@ -140,6 +140,7 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
); );
} }
if (widget.loadOriginal) {
_fullProvider = _authorizedImageProvider( _fullProvider = _authorizedImageProvider(
getImageUrl(widget.asset.remote!), getImageUrl(widget.asset.remote!),
"${widget.asset.id}_fullStage", "${widget.asset.id}_fullStage",
@ -150,6 +151,7 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}), }),
); );
} }
}
@override @override
void initState() { void initState() {
@ -178,7 +180,8 @@ class RemotePhotoView extends StatefulWidget {
Key? key, Key? key,
required this.asset, required this.asset,
required this.authToken, required this.authToken,
required this.threeStageLoading, required this.loadPreview,
required this.loadOriginal,
required this.isZoomedFunction, required this.isZoomedFunction,
required this.isZoomedListener, required this.isZoomedListener,
required this.onSwipeDown, required this.onSwipeDown,
@ -187,7 +190,8 @@ class RemotePhotoView extends StatefulWidget {
final Asset asset; final Asset asset;
final String authToken; final String authToken;
final bool threeStageLoading; final bool loadPreview;
final bool loadOriginal;
final void Function() onSwipeDown; final void Function() onSwipeDown;
final void Function() onSwipeUp; final void Function() onSwipeUp;
final void Function() isZoomedFunction; final void Function() isZoomedFunction;

View file

@ -31,8 +31,9 @@ class GalleryViewerPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final Box<dynamic> box = Hive.box(userInfoBox); final Box<dynamic> box = Hive.box(userInfoBox);
final appSettingService = ref.watch(appSettingsServiceProvider); final settings = ref.watch(appSettingsServiceProvider);
final threeStageLoading = useState(false); final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
final isZoomed = useState<bool>(false); final isZoomed = useState<bool>(false);
final indexOfAsset = useState(assetList.indexOf(asset)); final indexOfAsset = useState(assetList.indexOf(asset));
final isPlayingMotionVideo = useState(false); final isPlayingMotionVideo = useState(false);
@ -43,8 +44,10 @@ class GalleryViewerPage extends HookConsumerWidget {
useEffect( useEffect(
() { () {
threeStageLoading.value = appSettingService isLoadPreview.value =
.getSetting<bool>(AppSettingsEnum.threeStageLoading); settings.getSetting<bool>(AppSettingsEnum.loadPreview);
isLoadOriginal.value =
settings.getSetting<bool>(AppSettingsEnum.loadOriginal);
isPlayingMotionVideo.value = false; isPlayingMotionVideo.value = false;
return null; return null;
}, },
@ -140,7 +143,8 @@ class GalleryViewerPage extends HookConsumerWidget {
isZoomedListener: isZoomedListener, isZoomedListener: isZoomedListener,
asset: assetList[index], asset: assetList[index],
heroTag: assetList[index].id, heroTag: assetList[index].id,
threeStageLoading: threeStageLoading.value, loadPreview: isLoadPreview.value,
loadOriginal: isLoadOriginal.value,
); );
} }
} else { } else {

View file

@ -17,7 +17,8 @@ class ImageViewerPage extends HookConsumerWidget {
final String authToken; final String authToken;
final ValueNotifier<bool> isZoomedListener; final ValueNotifier<bool> isZoomedListener;
final void Function() isZoomedFunction; final void Function() isZoomedFunction;
final bool threeStageLoading; final bool loadPreview;
final bool loadOriginal;
ImageViewerPage({ ImageViewerPage({
Key? key, Key? key,
@ -26,7 +27,8 @@ class ImageViewerPage extends HookConsumerWidget {
required this.authToken, required this.authToken,
required this.isZoomedFunction, required this.isZoomedFunction,
required this.isZoomedListener, required this.isZoomedListener,
required this.threeStageLoading, required this.loadPreview,
required this.loadOriginal,
}) : super(key: key); }) : super(key: key);
Asset? assetDetail; Asset? assetDetail;
@ -74,7 +76,8 @@ class ImageViewerPage extends HookConsumerWidget {
child: RemotePhotoView( child: RemotePhotoView(
asset: asset, asset: asset,
authToken: authToken, authToken: authToken,
threeStageLoading: threeStageLoading, loadPreview: loadPreview,
loadOriginal: loadOriginal,
isZoomedFunction: isZoomedFunction, isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener, isZoomedListener: isZoomedListener,
onSwipeDown: () => AutoRouter.of(context).pop(), onSwipeDown: () => AutoRouter.of(context).pop(),

View file

@ -32,20 +32,20 @@ class AssetService {
AssetService(this._apiService, this._backupService, this._backgroundService); AssetService(this._apiService, this._backupService, this._backgroundService);
/// Returns `null` if the server state did not change, else list of assets /// 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 { try {
final Box box = Hive.box(userInfoBox); final Pair<List<AssetResponseDto>, String?>? remote =
final Pair<List<AssetResponseDto>, String?>? remote = await _apiService await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
.assetApi
.getAllAssetsWithETag(eTag: hasCache ? box.get(assetEtagKey) : null);
if (remote == null) { if (remote == null) {
return null; return const Pair(null, null);
} }
box.put(assetEtagKey, remote.second); return Pair(
return remote.first.map(Asset.remote).toList(growable: false); remote.first.map(Asset.remote).toList(growable: false),
remote.second,
);
} catch (e, stack) { } catch (e, stack) {
log.severe('Error while getting remote assets', e, stack); log.severe('Error while getting remote assets', e, stack);
return null; return const Pair(null, null);
} }
} }

View file

@ -2,7 +2,8 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
enum AppSettingsEnum<T> { enum AppSettingsEnum<T> {
threeStageLoading<bool>("threeStageLoading", false), loadPreview<bool>("loadPreview", true),
loadOriginal<bool>("loadOriginal", false),
themeMode<String>("themeMode", "system"), // "light","dark","system" themeMode<String>("themeMode", "system"), // "light","dark","system"
tilesPerRow<int>("tilesPerRow", 4), tilesPerRow<int>("tilesPerRow", 4),
uploadErrorNotificationGracePeriod<int>( uploadErrorNotificationGracePeriod<int>(

View file

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

View file

@ -1,14 +1,30 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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({ const ImageViewerQualitySetting({
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @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( return ExpansionTile(
textColor: Theme.of(context).primaryColor, textColor: Theme.of(context).primaryColor,
title: const Text( title: const Text(
@ -23,8 +39,27 @@ class ImageViewerQualitySetting extends StatelessWidget {
fontSize: 13, fontSize: 13,
), ),
).tr(), ).tr(),
children: const [ children: [
ThreeStageLoading(), 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(),
),
], ],
); );
} }

View file

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

View file

@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.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 { class NotificationSetting extends HookConsumerWidget {
const NotificationSetting({ const NotificationSetting({
@ -50,7 +51,7 @@ class NotificationSetting extends HookConsumerWidget {
), ),
).tr(), ).tr(),
children: [ children: [
_buildSwitchListTile( buildSwitchListTile(
context, context,
appSettingService, appSettingService,
totalProgressValue, totalProgressValue,
@ -58,7 +59,7 @@ class NotificationSetting extends HookConsumerWidget {
title: 'setting_notifications_total_progress_title'.tr(), title: 'setting_notifications_total_progress_title'.tr(),
subtitle: 'setting_notifications_total_progress_subtitle'.tr(), subtitle: 'setting_notifications_total_progress_subtitle'.tr(),
), ),
_buildSwitchListTile( buildSwitchListTile(
context, context,
appSettingService, appSettingService,
singleProgressValue, 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) { String _formatSliderValue(double v) {
if (v == 0.0) { if (v == 0.0) {
return 'setting_notifications_notify_immediately'.tr(); return 'setting_notifications_notify_immediately'.tr();

View file

@ -59,7 +59,8 @@ class _$AppRouter extends RootStackRouter {
authToken: args.authToken, authToken: args.authToken,
isZoomedFunction: args.isZoomedFunction, isZoomedFunction: args.isZoomedFunction,
isZoomedListener: args.isZoomedListener, isZoomedListener: args.isZoomedListener,
threeStageLoading: args.threeStageLoading)); loadPreview: args.loadPreview,
loadOriginal: args.loadOriginal));
}, },
VideoViewerRoute.name: (routeData) { VideoViewerRoute.name: (routeData) {
final args = routeData.argsAs<VideoViewerRouteArgs>(); final args = routeData.argsAs<VideoViewerRouteArgs>();
@ -305,7 +306,8 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
required String authToken, required String authToken,
required void Function() isZoomedFunction, required void Function() isZoomedFunction,
required ValueNotifier<bool> isZoomedListener, required ValueNotifier<bool> isZoomedListener,
required bool threeStageLoading}) required bool loadPreview,
required bool loadOriginal})
: super(ImageViewerRoute.name, : super(ImageViewerRoute.name,
path: '/image-viewer-page', path: '/image-viewer-page',
args: ImageViewerRouteArgs( args: ImageViewerRouteArgs(
@ -315,7 +317,8 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
authToken: authToken, authToken: authToken,
isZoomedFunction: isZoomedFunction, isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener, isZoomedListener: isZoomedListener,
threeStageLoading: threeStageLoading)); loadPreview: loadPreview,
loadOriginal: loadOriginal));
static const String name = 'ImageViewerRoute'; static const String name = 'ImageViewerRoute';
} }
@ -328,7 +331,8 @@ class ImageViewerRouteArgs {
required this.authToken, required this.authToken,
required this.isZoomedFunction, required this.isZoomedFunction,
required this.isZoomedListener, required this.isZoomedListener,
required this.threeStageLoading}); required this.loadPreview,
required this.loadOriginal});
final Key? key; final Key? key;
@ -342,11 +346,13 @@ class ImageViewerRouteArgs {
final ValueNotifier<bool> isZoomedListener; final ValueNotifier<bool> isZoomedListener;
final bool threeStageLoading; final bool loadPreview;
final bool loadOriginal;
@override @override
String toString() { 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}';
} }
} }

View file

@ -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/models/asset.dart';
import 'package:immich_mobile/shared/services/device_info.service.dart'; import 'package:immich_mobile/shared/services/device_info.service.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:immich_mobile/utils/tuple.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -37,8 +38,11 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
_getAllAssetInProgress = true; _getAllAssetInProgress = true;
final bool isCacheValid = await _assetCacheService.isValid(); final bool isCacheValid = await _assetCacheService.isValid();
stopwatch.start(); stopwatch.start();
final Box box = Hive.box(userInfoBox);
final localTask = _assetService.getLocalAssets(urgent: !isCacheValid); 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) { if (isCacheValid && state.isEmpty) {
state = await _assetCacheService.get(); state = await _assetCacheService.get();
log.info( log.info(
@ -50,7 +54,8 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
int remoteBegin = state.indexWhere((a) => a.isRemote); int remoteBegin = state.indexWhere((a) => a.isRemote);
remoteBegin = remoteBegin == -1 ? state.length : remoteBegin; remoteBegin = remoteBegin == -1 ? state.length : remoteBegin;
final List<Asset> currentLocal = state.slice(0, 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; List<Asset>? newLocal = await localTask;
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms"); log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
stopwatch.reset(); stopwatch.reset();
@ -63,14 +68,14 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
newLocal ??= []; newLocal ??= [];
state = _combineLocalAndRemoteAssets(local: newLocal, remote: newRemote); state = _combineLocalAndRemoteAssets(local: newLocal, remote: newRemote);
log.info("Combining assets: ${stopwatch.elapsedMilliseconds}ms"); log.info("Combining assets: ${stopwatch.elapsedMilliseconds}ms");
} finally {
_getAllAssetInProgress = false;
}
log.info("setting new asset state");
stopwatch.reset(); stopwatch.reset();
_cacheState(); _cacheState();
box.put(assetEtagKey, remoteResult.second);
log.info("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms"); log.info("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
} finally {
_getAllAssetInProgress = false;
}
} }
List<Asset> _combineLocalAndRemoteAssets({ List<Asset> _combineLocalAndRemoteAssets({