mirror of
https://github.com/immich-app/immich.git
synced 2026-07-10 22:22:29 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1989bf6b6f |
@@ -10,7 +10,6 @@ import 'package:immich_mobile/constants/aspect_ratios.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart';
|
import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/progressive_image_guard.dart';
|
|
||||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||||
import 'package:immich_mobile/theme/theme_data.dart';
|
import 'package:immich_mobile/theme/theme_data.dart';
|
||||||
import 'package:immich_mobile/utils/editor.utils.dart';
|
import 'package:immich_mobile/utils/editor.utils.dart';
|
||||||
@@ -118,9 +117,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
|||||||
bottom: false,
|
bottom: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(child: _EditorPreview(image: widget.image)),
|
||||||
child: ProgressiveImageGuard(child: _EditorPreview(image: widget.image)),
|
|
||||||
),
|
|
||||||
AnimatedSize(
|
AnimatedSize(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
@@ -430,9 +427,7 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
|||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
||||||
height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
||||||
child: ProgressiveImageGuard(
|
child: CropImage(controller: cropController, image: widget.image, gridColor: Colors.white),
|
||||||
child: CropImage(controller: cropController, image: widget.image, gridColor: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/progressive_image_guard.dart';
|
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/upload_profile_image.provider.dart';
|
import 'package:immich_mobile/providers/upload_profile_image.provider.dart';
|
||||||
@@ -171,9 +170,7 @@ class _ProfilePictureCropPageState extends ConsumerState<ProfilePictureCropPage>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
child: ProgressiveImageGuard(
|
child: CropImage(controller: _cropController, image: image, gridColor: Colors.white),
|
||||||
child: CropImage(controller: _cropController, image: image, gridColor: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
|||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/progressive_image_guard.dart';
|
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
|
||||||
@@ -302,8 +301,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (!_isVideoReady || widget.asset.isMotionPhoto || isCasting)
|
if (!_isVideoReady || widget.asset.isMotionPhoto || isCasting) Center(child: widget.image),
|
||||||
Center(child: ProgressiveImageGuard(child: widget.image)),
|
|
||||||
if (!isCasting) ...[
|
if (!isCasting) ...[
|
||||||
Visibility.maintain(
|
Visibility.maintain(
|
||||||
visible: _isVideoReady,
|
visible: _isVideoReady,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/progressive_image_guard.dart';
|
|
||||||
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
||||||
import 'package:octo_image/octo_image.dart';
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
|
||||||
@@ -22,20 +21,18 @@ class FullImage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final provider = getFullImageProvider(asset, size: size);
|
final provider = getFullImageProvider(asset, size: size);
|
||||||
return ProgressiveImageGuard(
|
return OctoImage(
|
||||||
child: OctoImage(
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
fadeInDuration: const Duration(milliseconds: 0),
|
fadeOutDuration: const Duration(milliseconds: 100),
|
||||||
fadeOutDuration: const Duration(milliseconds: 100),
|
placeholderBuilder: placeholder != null ? (_) => placeholder! : null,
|
||||||
placeholderBuilder: placeholder != null ? (_) => placeholder! : null,
|
image: provider,
|
||||||
image: provider,
|
width: size.width,
|
||||||
width: size.width,
|
height: size.height,
|
||||||
height: size.height,
|
fit: fit,
|
||||||
fit: fit,
|
errorBuilder: (context, error, stackTrace) {
|
||||||
errorBuilder: (context, error, stackTrace) {
|
provider.evict();
|
||||||
provider.evict();
|
return const Icon(Icons.image_not_supported_outlined, size: 32);
|
||||||
return const Icon(Icons.image_not_supported_outlined, size: 32);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
/// Keeps progressive image streams delivering frames when the platform
|
|
||||||
/// requests reduced animations.
|
|
||||||
///
|
|
||||||
/// The full-image providers emit multiple, increasingly higher-quality images
|
|
||||||
/// (thumbnail -> preview -> original) as successive frames of a single image
|
|
||||||
/// stream. Since Flutter 3.44, [Image] stops listening to its stream after the
|
|
||||||
/// first frame when [MediaQueryData.disableAnimations] is set (e.g. Android's
|
|
||||||
/// "Remove animations" accessibility setting or an animator duration scale of
|
|
||||||
/// zero), which would freeze these images at their low-res first frame.
|
|
||||||
/// Photos are not animations, so clear the flag for this subtree.
|
|
||||||
class ProgressiveImageGuard extends StatelessWidget {
|
|
||||||
const ProgressiveImageGuard({required this.child, super.key});
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final disableAnimations = MediaQuery.maybeDisableAnimationsOf(context) ?? false;
|
|
||||||
if (!disableAnimations) {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
return MediaQuery(data: MediaQuery.of(context).copyWith(disableAnimations: false), child: child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ import 'package:immich_mobile/domain/utils/event_stream.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/progressive_image_guard.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
@@ -405,28 +404,26 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
|
|||||||
if (_currentAsset != null)
|
if (_currentAsset != null)
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: _crossFadeAnimation.value,
|
opacity: _crossFadeAnimation.value,
|
||||||
child: ProgressiveImageGuard(
|
child: SizedBox(
|
||||||
child: SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Image(
|
||||||
child: Image(
|
alignment: Alignment.topRight,
|
||||||
alignment: Alignment.topRight,
|
image: getFullImageProvider(_currentAsset!),
|
||||||
image: getFullImageProvider(_currentAsset!),
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
if (wasSynchronouslyLoaded || frame != null) {
|
return child;
|
||||||
return child;
|
}
|
||||||
}
|
return Container();
|
||||||
return Container();
|
},
|
||||||
},
|
errorBuilder: (context, error, stackTrace) {
|
||||||
errorBuilder: (context, error, stackTrace) {
|
return SizedBox(
|
||||||
return SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -434,28 +431,26 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
|
|||||||
if (_nextAsset != null)
|
if (_nextAsset != null)
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: 1.0 - _crossFadeAnimation.value,
|
opacity: 1.0 - _crossFadeAnimation.value,
|
||||||
child: ProgressiveImageGuard(
|
child: SizedBox(
|
||||||
child: SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Image(
|
||||||
child: Image(
|
alignment: Alignment.topRight,
|
||||||
alignment: Alignment.topRight,
|
image: getFullImageProvider(_nextAsset!),
|
||||||
image: getFullImageProvider(_nextAsset!),
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
if (wasSynchronouslyLoaded || frame != null) {
|
return child;
|
||||||
return child;
|
}
|
||||||
}
|
return const SizedBox.shrink();
|
||||||
return const SizedBox.shrink();
|
},
|
||||||
},
|
errorBuilder: (context, error, stackTrace) {
|
||||||
errorBuilder: (context, error, stackTrace) {
|
return SizedBox(
|
||||||
return SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import 'package:immich_mobile/domain/utils/event_stream.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/progressive_image_guard.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
@@ -508,28 +507,26 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
|
|||||||
if (_currentAsset != null)
|
if (_currentAsset != null)
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: _crossFadeAnimation.value,
|
opacity: _crossFadeAnimation.value,
|
||||||
child: ProgressiveImageGuard(
|
child: SizedBox(
|
||||||
child: SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Image(
|
||||||
child: Image(
|
alignment: Alignment.topRight,
|
||||||
alignment: Alignment.topRight,
|
image: getFullImageProvider(_currentAsset!),
|
||||||
image: getFullImageProvider(_currentAsset!),
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
if (wasSynchronouslyLoaded || frame != null) {
|
return child;
|
||||||
return child;
|
}
|
||||||
}
|
return Container();
|
||||||
return Container();
|
},
|
||||||
},
|
errorBuilder: (context, error, stackTrace) {
|
||||||
errorBuilder: (context, error, stackTrace) {
|
return SizedBox(
|
||||||
return SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -537,28 +534,26 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
|
|||||||
if (_nextAsset != null)
|
if (_nextAsset != null)
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: 1.0 - _crossFadeAnimation.value,
|
opacity: 1.0 - _crossFadeAnimation.value,
|
||||||
child: ProgressiveImageGuard(
|
child: SizedBox(
|
||||||
child: SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Image(
|
||||||
child: Image(
|
alignment: Alignment.topRight,
|
||||||
alignment: Alignment.topRight,
|
image: getFullImageProvider(_nextAsset!),
|
||||||
image: getFullImageProvider(_nextAsset!),
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
if (wasSynchronouslyLoaded || frame != null) {
|
return child;
|
||||||
return child;
|
}
|
||||||
}
|
return const SizedBox.shrink();
|
||||||
return const SizedBox.shrink();
|
},
|
||||||
},
|
errorBuilder: (context, error, stackTrace) {
|
||||||
errorBuilder: (context, error, stackTrace) {
|
return SizedBox(
|
||||||
return SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/progressive_image_guard.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
@@ -484,28 +483,26 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
|
|||||||
if (_currentAsset != null)
|
if (_currentAsset != null)
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: _crossFadeAnimation.value,
|
opacity: _crossFadeAnimation.value,
|
||||||
child: ProgressiveImageGuard(
|
child: SizedBox(
|
||||||
child: SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Image(
|
||||||
child: Image(
|
alignment: Alignment.topRight,
|
||||||
alignment: Alignment.topRight,
|
image: getFullImageProvider(_currentAsset!),
|
||||||
image: getFullImageProvider(_currentAsset!),
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
if (wasSynchronouslyLoaded || frame != null) {
|
return child;
|
||||||
return child;
|
}
|
||||||
}
|
return Container();
|
||||||
return Container();
|
},
|
||||||
},
|
errorBuilder: (context, error, stackTrace) {
|
||||||
errorBuilder: (context, error, stackTrace) {
|
return SizedBox(
|
||||||
return SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -513,28 +510,26 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
|
|||||||
if (_nextAsset != null)
|
if (_nextAsset != null)
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: 1.0 - _crossFadeAnimation.value,
|
opacity: 1.0 - _crossFadeAnimation.value,
|
||||||
child: ProgressiveImageGuard(
|
child: SizedBox(
|
||||||
child: SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Image(
|
||||||
child: Image(
|
alignment: Alignment.topRight,
|
||||||
alignment: Alignment.topRight,
|
image: getFullImageProvider(_nextAsset!),
|
||||||
image: getFullImageProvider(_nextAsset!),
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
if (wasSynchronouslyLoaded || frame != null) {
|
return child;
|
||||||
return child;
|
}
|
||||||
}
|
return const SizedBox.shrink();
|
||||||
return const SizedBox.shrink();
|
},
|
||||||
},
|
errorBuilder: (context, error, stackTrace) {
|
||||||
errorBuilder: (context, error, stackTrace) {
|
return SizedBox(
|
||||||
return SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
height: double.infinity,
|
||||||
height: double.infinity,
|
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/progressive_image_guard.dart';
|
|
||||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart'
|
import 'package:immich_mobile/widgets/photo_view/photo_view.dart'
|
||||||
show
|
show
|
||||||
PhotoViewScaleState,
|
PhotoViewScaleState,
|
||||||
@@ -439,17 +438,15 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
height: scaleBoundaries.childSize.height * scale,
|
height: scaleBoundaries.childSize.height * scale,
|
||||||
child: widget.customChild!,
|
child: widget.customChild!,
|
||||||
)
|
)
|
||||||
: ProgressiveImageGuard(
|
: Image(
|
||||||
child: Image(
|
key: widget.heroAttributes?.tag != null ? ObjectKey(widget.heroAttributes!.tag) : null,
|
||||||
key: widget.heroAttributes?.tag != null ? ObjectKey(widget.heroAttributes!.tag) : null,
|
image: widget.imageProvider!,
|
||||||
image: widget.imageProvider!,
|
semanticLabel: widget.semanticLabel,
|
||||||
semanticLabel: widget.semanticLabel,
|
gaplessPlayback: widget.gaplessPlayback ?? false,
|
||||||
gaplessPlayback: widget.gaplessPlayback ?? false,
|
filterQuality: widget.filterQuality,
|
||||||
filterQuality: widget.filterQuality,
|
width: scaleBoundaries.childSize.width * scale,
|
||||||
width: scaleBoundaries.childSize.width * scale,
|
fit: BoxFit.contain,
|
||||||
fit: BoxFit.contain,
|
isAntiAlias: widget.filterQuality == FilterQuality.high,
|
||||||
isAntiAlias: widget.filterQuality == FilterQuality.high,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,296 +0,0 @@
|
|||||||
// End-to-end tests for progressive image loading (thumbnail -> preview)
|
|
||||||
// through the real provider/completer pipeline with a mocked platform
|
|
||||||
// image API.
|
|
||||||
//
|
|
||||||
// The "animations are disabled" cases are regression tests for
|
|
||||||
// https://github.com/immich-app/immich/issues/29727: since Flutter 3.44,
|
|
||||||
// a paused Image widget stops listening to its stream after the first
|
|
||||||
// frame, freezing progressive images at the low-res thumbnail.
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:ffi' hide Size;
|
|
||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
import 'package:drift/drift.dart' hide isNull;
|
|
||||||
import 'package:drift/native.dart';
|
|
||||||
import 'package:ffi/ffi.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
|
||||||
import 'package:immich_mobile/platform/remote_image_api.g.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/images/full_image.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
|
||||||
import 'package:immich_mobile/utils/cache/custom_image_cache.dart';
|
|
||||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
|
||||||
|
|
||||||
import '../../../test_utils.dart';
|
|
||||||
|
|
||||||
class _CustomCacheBinding extends AutomatedTestWidgetsFlutterBinding {
|
|
||||||
@override
|
|
||||||
ImageCache createImageCache() => CustomImageCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
const kThumbSize = 16;
|
|
||||||
const kPreviewSize = 64;
|
|
||||||
const kOriginalSize = 128;
|
|
||||||
|
|
||||||
late Uint8List thumbPng;
|
|
||||||
late Uint8List previewPng;
|
|
||||||
late Uint8List originalPng;
|
|
||||||
|
|
||||||
final requestedUrls = <String>[];
|
|
||||||
|
|
||||||
Future<Uint8List> _pngBytes(int size) async {
|
|
||||||
final image = await createTestImage(width: size, height: size);
|
|
||||||
final data = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
||||||
return data!.buffer.asUint8List();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _installRemoteImageApiMock() {
|
|
||||||
const channel = BasicMessageChannel<Object?>(
|
|
||||||
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage',
|
|
||||||
RemoteImageApi.pigeonChannelCodec,
|
|
||||||
);
|
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler<Object?>(channel, (
|
|
||||||
message,
|
|
||||||
) async {
|
|
||||||
final args = message as List<Object?>;
|
|
||||||
final url = args[0] as String;
|
|
||||||
requestedUrls.add(url);
|
|
||||||
|
|
||||||
final Uint8List bytes;
|
|
||||||
if (url.contains('size=thumbnail')) {
|
|
||||||
bytes = thumbPng;
|
|
||||||
} else if (url.contains('size=preview')) {
|
|
||||||
bytes = previewPng;
|
|
||||||
} else {
|
|
||||||
bytes = originalPng;
|
|
||||||
}
|
|
||||||
|
|
||||||
final pointer = malloc<Uint8>(bytes.length);
|
|
||||||
pointer.asTypedList(bytes.length).setAll(0, bytes);
|
|
||||||
return <Object?>[
|
|
||||||
{'pointer': pointer.address, 'length': bytes.length},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const cancelChannel = BasicMessageChannel<Object?>(
|
|
||||||
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest',
|
|
||||||
RemoteImageApi.pigeonChannelCodec,
|
|
||||||
);
|
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler<Object?>(
|
|
||||||
cancelChannel,
|
|
||||||
(message) async => <Object?>[null],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _precacheThumbnail(WidgetTester tester, dynamic asset) async {
|
|
||||||
await tester.runAsync(() async {
|
|
||||||
final provider = getThumbnailImageProvider(asset)!;
|
|
||||||
final completer = Completer<void>();
|
|
||||||
final stream = provider.resolve(ImageConfiguration.empty);
|
|
||||||
final listener = ImageStreamListener((info, _) {
|
|
||||||
info.dispose();
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete();
|
|
||||||
}
|
|
||||||
}, onError: (e, s) => completer.completeError(e, s));
|
|
||||||
stream.addListener(listener);
|
|
||||||
await completer.future;
|
|
||||||
stream.removeListener(listener);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _settle(WidgetTester tester) async {
|
|
||||||
for (int i = 0; i < 10; i++) {
|
|
||||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 50)));
|
|
||||||
await tester.pump();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int? _renderedImageWidth(WidgetTester tester) {
|
|
||||||
final rawImages = tester.widgetList<RawImage>(find.byType(RawImage)).toList();
|
|
||||||
return rawImages.isEmpty ? null : rawImages.first.image?.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
_CustomCacheBinding();
|
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
|
||||||
|
|
||||||
setUpAll(() async {
|
|
||||||
TestUtils.init();
|
|
||||||
final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
|
||||||
await StoreService.init(storeRepository: DriftStoreRepository(db), listenUpdates: false);
|
|
||||||
await StoreService.I.put(StoreKey.serverEndpoint, 'http://localhost:3000');
|
|
||||||
await SettingsRepository.ensureInitialized(db);
|
|
||||||
});
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
requestedUrls.clear();
|
|
||||||
_installRemoteImageApiMock();
|
|
||||||
imageCache.clear();
|
|
||||||
imageCache.clearLiveImages();
|
|
||||||
});
|
|
||||||
|
|
||||||
int assetCounter = 0;
|
|
||||||
|
|
||||||
Future<dynamic> setUpAsset(WidgetTester tester) async {
|
|
||||||
await tester.runAsync(() async {
|
|
||||||
thumbPng = await _pngBytes(kThumbSize);
|
|
||||||
previewPng = await _pngBytes(kPreviewSize);
|
|
||||||
originalPng = await _pngBytes(kOriginalSize);
|
|
||||||
});
|
|
||||||
return TestUtils.createRemoteAsset(id: 'asset-${++assetCounter}', width: 3000, height: 4000);
|
|
||||||
}
|
|
||||||
|
|
||||||
testWidgets('FullImage shows preview (thumbnail pre-cached by timeline)', (tester) async {
|
|
||||||
final asset = await setUpAsset(tester);
|
|
||||||
await _precacheThumbnail(tester, asset);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: Center(
|
|
||||||
child: SizedBox(width: 400, height: 800, child: FullImage(asset, size: const Size(400, 800))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _settle(tester);
|
|
||||||
expect(_renderedImageWidth(tester), kPreviewSize, reason: 'stuck on thumbnail. URLs: $requestedUrls');
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('FullImage shows preview (thumbnail not cached)', (tester) async {
|
|
||||||
final asset = await setUpAsset(tester);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: Center(
|
|
||||||
child: SizedBox(width: 400, height: 800, child: FullImage(asset, size: const Size(400, 800))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _settle(tester);
|
|
||||||
expect(_renderedImageWidth(tester), kPreviewSize, reason: 'stuck on thumbnail. URLs: $requestedUrls');
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('PhotoView shows preview (thumbnail pre-cached by timeline)', (tester) async {
|
|
||||||
final asset = await setUpAsset(tester);
|
|
||||||
await _precacheThumbnail(tester, asset);
|
|
||||||
|
|
||||||
final provider = getFullImageProvider(asset, size: const Size(400, 800));
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: PhotoView(
|
|
||||||
imageProvider: provider,
|
|
||||||
index: 0,
|
|
||||||
gaplessPlayback: true,
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
tightMode: true,
|
|
||||||
enablePanAlways: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _settle(tester);
|
|
||||||
expect(_renderedImageWidth(tester), kPreviewSize, reason: 'stuck on thumbnail. URLs: $requestedUrls');
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('PhotoView shows preview (thumbnail not cached)', (tester) async {
|
|
||||||
final asset = await setUpAsset(tester);
|
|
||||||
|
|
||||||
final provider = getFullImageProvider(asset, size: const Size(400, 800));
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: PhotoView(
|
|
||||||
imageProvider: provider,
|
|
||||||
index: 0,
|
|
||||||
gaplessPlayback: true,
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
tightMode: true,
|
|
||||||
enablePanAlways: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _settle(tester);
|
|
||||||
expect(_renderedImageWidth(tester), kPreviewSize, reason: 'stuck on thumbnail. URLs: $requestedUrls');
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('FullImage shows preview when animations are disabled (issue #29727)', (tester) async {
|
|
||||||
final asset = await setUpAsset(tester);
|
|
||||||
await _precacheThumbnail(tester, asset);
|
|
||||||
|
|
||||||
// Android "remove animations" / animator duration scale 0 sets
|
|
||||||
// MediaQuery.disableAnimations, which pauses Image stream listening
|
|
||||||
// after the first frame on Flutter 3.44+.
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: MediaQuery(
|
|
||||||
data: const MediaQueryData(disableAnimations: true),
|
|
||||||
child: Center(
|
|
||||||
child: SizedBox(width: 400, height: 800, child: FullImage(asset, size: const Size(400, 800))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _settle(tester);
|
|
||||||
expect(_renderedImageWidth(tester), kPreviewSize, reason: 'stuck on thumbnail. URLs: $requestedUrls');
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('PhotoView shows preview when animations are disabled (issue #29727)', (tester) async {
|
|
||||||
final asset = await setUpAsset(tester);
|
|
||||||
await _precacheThumbnail(tester, asset);
|
|
||||||
|
|
||||||
final provider = getFullImageProvider(asset, size: const Size(400, 800));
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: MediaQuery(
|
|
||||||
data: const MediaQueryData(disableAnimations: true),
|
|
||||||
child: PhotoView(
|
|
||||||
imageProvider: provider,
|
|
||||||
index: 0,
|
|
||||||
gaplessPlayback: true,
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
tightMode: true,
|
|
||||||
enablePanAlways: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _settle(tester);
|
|
||||||
expect(_renderedImageWidth(tester), kPreviewSize, reason: 'stuck on thumbnail. URLs: $requestedUrls');
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('memory page pattern: precacheImage then FullImage', (tester) async {
|
|
||||||
final asset = await setUpAsset(tester);
|
|
||||||
await _precacheThumbnail(tester, asset);
|
|
||||||
|
|
||||||
// The memory page precaches the full image provider before showing the card.
|
|
||||||
await tester.pumpWidget(MaterialApp(home: Builder(builder: (context) => const SizedBox())));
|
|
||||||
final context = tester.element(find.byType(SizedBox));
|
|
||||||
final precacheFuture = precacheImage(getFullImageProvider(asset, size: const Size(400, 800)), context);
|
|
||||||
await tester.pump();
|
|
||||||
await tester.runAsync(() => precacheFuture);
|
|
||||||
// Post-frame callback removes the precache listener.
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: Center(
|
|
||||||
child: SizedBox(width: 400, height: 800, child: FullImage(asset, size: const Size(400, 800))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _settle(tester);
|
|
||||||
expect(_renderedImageWidth(tester), kPreviewSize, reason: 'stuck on thumbnail. URLs: $requestedUrls');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -7,16 +7,6 @@ import { preferencesFactory } from '@test-data/factories/preferences-factory';
|
|||||||
import { userAdminFactory } from '@test-data/factories/user-factory';
|
import { userAdminFactory } from '@test-data/factories/user-factory';
|
||||||
import AssetViewerNavBar from './AssetViewerNavBar.svelte';
|
import AssetViewerNavBar from './AssetViewerNavBar.svelte';
|
||||||
|
|
||||||
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), function () {
|
|
||||||
return {
|
|
||||||
featureFlagsManager: {
|
|
||||||
init: vi.fn(),
|
|
||||||
loadFeatureFlags: vi.fn(),
|
|
||||||
value: { smartSearch: true, trash: true },
|
|
||||||
} as never,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('AssetViewerNavBar component', () => {
|
describe('AssetViewerNavBar component', () => {
|
||||||
const additionalProps = {
|
const additionalProps = {
|
||||||
preAction: () => {},
|
preAction: () => {},
|
||||||
@@ -34,6 +24,15 @@ describe('AssetViewerNavBar component', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
vi.stubGlobal('ResizeObserver', getResizeObserverMock());
|
vi.stubGlobal('ResizeObserver', getResizeObserverMock());
|
||||||
|
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), function () {
|
||||||
|
return {
|
||||||
|
featureFlagsManager: {
|
||||||
|
init: vi.fn(),
|
||||||
|
loadFeatureFlags: vi.fn(),
|
||||||
|
value: { smartSearch: true, trash: true },
|
||||||
|
} as never,
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ import { renderWithTooltips } from '$tests/helpers';
|
|||||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||||
import DeleteAction from './DeleteAction.svelte';
|
import DeleteAction from './DeleteAction.svelte';
|
||||||
|
|
||||||
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), () => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
return { featureFlagsManager: { init: vi.fn(), loadFeatureFlags: vi.fn(), value: { trash: true } } as any };
|
|
||||||
});
|
|
||||||
|
|
||||||
let asset: AssetResponseDto;
|
let asset: AssetResponseDto;
|
||||||
|
|
||||||
describe('DeleteAction component', () => {
|
describe('DeleteAction component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return { featureFlagsManager: { init: vi.fn(), loadFeatureFlags: vi.fn(), value: { trash: true } } as any };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('given an asset which is not trashed yet', () => {
|
describe('given an asset which is not trashed yet', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
asset = assetFactory.build({ isTrashed: false });
|
asset = assetFactory.build({ isTrashed: false });
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ import Thumbnail from '$lib/components/assets/thumbnail/Thumbnail.svelte';
|
|||||||
import { getTabbable } from '$lib/utils/focus-util';
|
import { getTabbable } from '$lib/utils/focus-util';
|
||||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||||
|
|
||||||
vi.mock('$lib/utils/navigation', () => ({
|
|
||||||
currentUrlReplaceAssetId: vi.fn(),
|
|
||||||
isSharedLinkRoute: vi.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.hoisted(() => {
|
vi.hoisted(() => {
|
||||||
Object.defineProperty(globalThis, 'matchMedia', {
|
Object.defineProperty(globalThis, 'matchMedia', {
|
||||||
writable: true,
|
writable: true,
|
||||||
@@ -31,6 +26,10 @@ vi.hoisted(() => {
|
|||||||
describe('Thumbnail component', () => {
|
describe('Thumbnail component', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock());
|
vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock());
|
||||||
|
vi.mock('$lib/utils/navigation', () => ({
|
||||||
|
currentUrlReplaceAssetId: vi.fn(),
|
||||||
|
isSharedLinkRoute: vi.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only contain a single tabbable element (the container)', () => {
|
it('should only contain a single tabbable element (the container)', () => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe('RecentAlbums component', () => {
|
|||||||
sdkMock.getAllAlbums.mockResolvedValueOnce([...albums]);
|
sdkMock.getAllAlbums.mockResolvedValueOnce([...albums]);
|
||||||
render(RecentAlbums);
|
render(RecentAlbums);
|
||||||
|
|
||||||
expect(sdkMock.getAllAlbums).toHaveBeenCalledOnce();
|
expect(sdkMock.getAllAlbums).toBeCalledTimes(1);
|
||||||
|
|
||||||
// wtf
|
// wtf
|
||||||
await tick();
|
await tick();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ describe('FormatMessage component', () => {
|
|||||||
|
|
||||||
it('throws an error when locale is empty', async () => {
|
it('throws an error when locale is empty', async () => {
|
||||||
await locale.set(undefined);
|
await locale.set(undefined);
|
||||||
expect(() => render(FormatMessage, { key: '' as Translations })).toThrow();
|
expect(() => render(FormatMessage, { key: '' as Translations })).toThrowError();
|
||||||
await locale.set('en');
|
await locale.set('en');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ describe('TimelineManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should load months in viewport', () => {
|
it('should load months in viewport', () => {
|
||||||
expect(sdkMock.getTimeBuckets).toHaveBeenCalledOnce();
|
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
|
||||||
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
|
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,13 +133,13 @@ describe('TimelineManager', () => {
|
|||||||
it('loads a month', async () => {
|
it('loads a month', async () => {
|
||||||
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
|
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(0);
|
||||||
await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
|
await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
|
||||||
expect(sdkMock.getTimeBucket).toHaveBeenCalledOnce();
|
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||||
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
|
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores invalid months', async () => {
|
it('ignores invalid months', async () => {
|
||||||
await timelineManager.loadTimelineMonth({ year: 2023, month: 1 });
|
await timelineManager.loadTimelineMonth({ year: 2023, month: 1 });
|
||||||
expect(sdkMock.getTimeBucket).not.toHaveBeenCalled();
|
expect(sdkMock.getTimeBucket).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cancels month loading', async () => {
|
it('cancels month loading', async () => {
|
||||||
@@ -147,7 +147,7 @@ describe('TimelineManager', () => {
|
|||||||
void timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
|
void timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
|
||||||
const abortSpy = vi.spyOn(month!.loader!.cancelToken!, 'abort');
|
const abortSpy = vi.spyOn(month!.loader!.cancelToken!, 'abort');
|
||||||
month?.cancel();
|
month?.cancel();
|
||||||
expect(abortSpy).toHaveBeenCalledOnce();
|
expect(abortSpy).toBeCalledTimes(1);
|
||||||
await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
|
await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
|
||||||
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
|
expect(getTimelineMonthByDate(timelineManager, { year: 2024, month: 1 })?.getAssets().length).toEqual(3);
|
||||||
});
|
});
|
||||||
@@ -157,10 +157,10 @@ describe('TimelineManager', () => {
|
|||||||
timelineManager.loadTimelineMonth({ year: 2024, month: 1 }),
|
timelineManager.loadTimelineMonth({ year: 2024, month: 1 }),
|
||||||
timelineManager.loadTimelineMonth({ year: 2024, month: 1 }),
|
timelineManager.loadTimelineMonth({ year: 2024, month: 1 }),
|
||||||
]);
|
]);
|
||||||
expect(sdkMock.getTimeBucket).toHaveBeenCalledOnce();
|
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||||
|
|
||||||
await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
|
await timelineManager.loadTimelineMonth({ year: 2024, month: 1 });
|
||||||
expect(sdkMock.getTimeBucket).toHaveBeenCalledOnce();
|
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows loading a canceled month', async () => {
|
it('allows loading a canceled month', async () => {
|
||||||
@@ -283,7 +283,7 @@ describe('TimelineManager', () => {
|
|||||||
const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build());
|
const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build());
|
||||||
timelineManager.upsertAssets([asset]);
|
timelineManager.upsertAssets([asset]);
|
||||||
|
|
||||||
expect(updateAssetsSpy).toHaveBeenCalledWith([asset]);
|
expect(updateAssetsSpy).toBeCalledWith([asset]);
|
||||||
expect(timelineManager.assetCount).toEqual(1);
|
expect(timelineManager.assetCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -642,8 +642,8 @@ describe('TimelineManager', () => {
|
|||||||
const previousMonthSpy = vi.spyOn(previousMonth!.loader!, 'execute');
|
const previousMonthSpy = vi.spyOn(previousMonth!.loader!, 'execute');
|
||||||
const previous = await timelineManager.getLaterAsset(a);
|
const previous = await timelineManager.getLaterAsset(a);
|
||||||
expect(previous).toEqual(b);
|
expect(previous).toEqual(b);
|
||||||
expect(loadTimelineMonthSpy).not.toHaveBeenCalled();
|
expect(loadTimelineMonthSpy).toBeCalledTimes(0);
|
||||||
expect(previousMonthSpy).not.toHaveBeenCalled();
|
expect(previousMonthSpy).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips removed assets', async () => {
|
it('skips removed assets', async () => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
const { trigger, selectedKey, onClose }: Props = $props();
|
const { trigger, selectedKey, onClose }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BasicModal title={$t('add_step')} {onClose} size="medium">
|
<BasicModal title={$t('add_step')} onClose={() => onClose()} size="medium">
|
||||||
{#await searchPluginMethods({ trigger })}
|
{#await searchPluginMethods({ trigger })}
|
||||||
<div class="flex w-full place-content-center place-items-center">
|
<div class="flex w-full place-content-center place-items-center">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ import type { ServerConfigDto } from '@immich/sdk';
|
|||||||
import { asUrl } from '$lib/services/shared-link.service';
|
import { asUrl } from '$lib/services/shared-link.service';
|
||||||
import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
|
import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
|
||||||
|
|
||||||
vi.mock(import('$lib/managers/server-config-manager.svelte'), () => ({
|
|
||||||
serverConfigManager: {
|
|
||||||
value: { externalDomain: 'http://localhost:2283' } as ServerConfigDto,
|
|
||||||
init: vi.fn(),
|
|
||||||
loadServerConfig: vi.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('SharedLinkService', () => {
|
describe('SharedLinkService', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.mock(import('$lib/managers/server-config-manager.svelte'), () => ({
|
||||||
|
serverConfigManager: {
|
||||||
|
value: { externalDomain: 'http://localhost:2283' } as ServerConfigDto,
|
||||||
|
init: vi.fn(),
|
||||||
|
loadServerConfig: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
describe('asUrl', () => {
|
describe('asUrl', () => {
|
||||||
it('should properly encode characters in slug', () => {
|
it('should properly encode characters in slug', () => {
|
||||||
expect(asUrl(sharedLinkFactory.build({ slug: 'foo/bar' }))).toBe('http://localhost:2283/s/foo%2Fbar');
|
expect(asUrl(sharedLinkFactory.build({ slug: 'foo/bar' }))).toBe('http://localhost:2283/s/foo%2Fbar');
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import { getAlbumDateRange, getShortDateRange } from './date-time';
|
import { getAlbumDateRange, getShortDateRange } from './date-time';
|
||||||
|
|
||||||
vitest.mock('$lib/stores/preferences.store', () => ({
|
|
||||||
locale: writable('en'),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('getShortDateRange', () => {
|
describe('getShortDateRange', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubEnv('TZ', 'UTC');
|
vi.stubEnv('TZ', 'UTC');
|
||||||
@@ -45,6 +41,10 @@ describe('getShortDateRange', () => {
|
|||||||
describe('getAlbumDate', () => {
|
describe('getAlbumDate', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
process.env.TZ = 'UTC';
|
process.env.TZ = 'UTC';
|
||||||
|
|
||||||
|
vitest.mock('$lib/stores/preferences.store', () => ({
|
||||||
|
locale: writable('en'),
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with only a start date', () => {
|
it('should work with only a start date', () => {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ describe('Executor Queue test', function () {
|
|||||||
// The last task will be executed after 200ms and will finish at 400ms
|
// The last task will be executed after 200ms and will finish at 400ms
|
||||||
void eq.addTask(() => timeoutPromiseBuilder(200, 'T4'));
|
void eq.addTask(() => timeoutPromiseBuilder(200, 'T4'));
|
||||||
|
|
||||||
expect(finished).not.toHaveBeenCalled();
|
expect(finished).not.toBeCalled();
|
||||||
expect(started).toHaveBeenCalledTimes(3);
|
expect(started).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
vi.advanceTimersByTime(100);
|
vi.advanceTimersByTime(100);
|
||||||
|
|||||||
Reference in New Issue
Block a user