1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

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_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",

View file

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

View file

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

View file

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

View file

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

View file

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

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: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(),
),
],
);
}

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: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();

View file

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

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/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({