Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 34 additions & 23 deletions mobile/lib/pages/photos/memory.page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class MemoryPage extends HookConsumerWidget {
final assetProgress = useState("${currentAssetPage.value + 1}|${currentMemory.value.assets.length}");
const bgColor = Colors.black;
final currentAsset = useState<Asset?>(null);
final isZoomed = useState(false);

/// The list of all of the asset page controllers
final memoryAssetPageControllers = List.generate(memories.length, (i) => usePageController());
Expand Down Expand Up @@ -156,6 +157,7 @@ class MemoryPage extends HookConsumerWidget {
Future<void> onAssetChanged(int otherIndex) async {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
currentAssetPage.value = otherIndex;
isZoomed.value = false; // Reset zoom state when changing assets
updateProgressText();

// Wait for page change animation to finish
Expand Down Expand Up @@ -258,33 +260,42 @@ class MemoryPage extends HookConsumerWidget {
children: [
Container(
color: Colors.black,
child: MemoryCard(asset: asset, title: memories[mIndex].title, showTitle: index == 0),
child: MemoryCard(
asset: asset,
title: memories[mIndex].title,
showTitle: index == 0,
onZoomChanged: (zoomed) {
isZoomed.value = zoomed;
},
),
),
Positioned.fill(
child: Row(
children: [
// Left side of the screen
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
toPreviousAsset(index);
},
// Only show navigation overlays when not zoomed
if (!isZoomed.value)
Positioned.fill(
child: Row(
children: [
// Left side of the screen
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
toPreviousAsset(index);
},
),
),
),

// Right side of the screen
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
toNextAsset(index);
},

// Right side of the screen
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
toNextAsset(index);
},
),
),
),
],
],
),
),
),
],
);
},
Expand Down
57 changes: 34 additions & 23 deletions mobile/lib/presentation/pages/drift_memory.page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class DriftMemoryPage extends HookConsumerWidget {
final assetProgress = useState("${currentAssetPage.value + 1}|${currentMemory.value.assets.length}");
const bgColor = Colors.black;
final currentAsset = useState<RemoteAsset?>(null);
final isZoomed = useState(false);

/// The list of all of the asset page controllers
final memoryAssetPageControllers = List.generate(memories.length, (i) => usePageController());
Expand Down Expand Up @@ -163,6 +164,7 @@ class DriftMemoryPage extends HookConsumerWidget {
Future<void> onAssetChanged(int otherIndex) async {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
currentAssetPage.value = otherIndex;
isZoomed.value = false; // Reset zoom state when changing assets
updateProgressText();

// Wait for page change animation to finish
Expand Down Expand Up @@ -273,33 +275,42 @@ class DriftMemoryPage extends HookConsumerWidget {
children: [
Container(
color: Colors.black,
child: DriftMemoryCard(asset: asset, title: title, showTitle: index == 0),
child: DriftMemoryCard(
asset: asset,
title: title,
showTitle: index == 0,
onZoomChanged: (zoomed) {
isZoomed.value = zoomed;
},
),
),
Positioned.fill(
child: Row(
children: [
// Left side of the screen
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
toPreviousAsset(index);
},
// Only show navigation overlays when not zoomed
if (!isZoomed.value)
Positioned.fill(
child: Row(
children: [
// Left side of the screen
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
toPreviousAsset(index);
},
),
),
),

// Right side of the screen
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
toNextAsset(index);
},

// Right side of the screen
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
toNextAsset(index);
},
),
),
),
],
],
),
),
),
],
);
},
Expand Down
31 changes: 18 additions & 13 deletions mobile/lib/presentation/widgets/memory/memory_card.widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.wid
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/hooks/blurhash_hook.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';

class DriftMemoryCard extends StatelessWidget {
final RemoteAsset asset;
final String title;
final bool showTitle;
final Function()? onVideoEnded;
final ValueChanged<bool>? onZoomChanged;

const DriftMemoryCard({
required this.asset,
required this.title,
required this.showTitle,
this.onVideoEnded,
this.onZoomChanged,
super.key,
});

Expand All @@ -37,20 +40,22 @@ class DriftMemoryCard extends StatelessWidget {
SizedBox.expand(child: _BlurredBackdrop(asset: asset)),
LayoutBuilder(
builder: (context, constraints) {
// Determine the fit using the aspect ratio
BoxFit fit = BoxFit.contain;
if (asset.width != null && asset.height != null) {
final aspectRatio = asset.width! / asset.height!;
final phoneAspectRatio = constraints.maxWidth / constraints.maxHeight;
// Look for a 25% difference in either direction
if (phoneAspectRatio * .75 < aspectRatio && phoneAspectRatio * 1.25 > aspectRatio) {
// Cover to look nice if we have nearly the same aspect ratio
fit = BoxFit.cover;
}
}

if (asset.isImage) {
return FullImage(asset, fit: fit, size: const Size(double.infinity, double.infinity));
final size = Size(constraints.maxWidth, constraints.maxHeight);
return PhotoView(
key: ValueKey('photo-${asset.id}'),
imageProvider: getFullImageProvider(asset, size: size),
index: 0,
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 5,
initialScale: PhotoViewComputedScale.contained,
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
filterQuality: FilterQuality.high,
scaleStateChangedCallback: (scaleState) {
final isZoomed = scaleState != PhotoViewScaleState.initial;
onZoomChanged?.call(isZoomed);
},
);
} else {
return SizedBox(
width: context.width,
Expand Down
42 changes: 28 additions & 14 deletions mobile/lib/widgets/memories/memory_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';

class MemoryCard extends StatelessWidget {
final Asset asset;
final String title;
final bool showTitle;
final Function()? onVideoEnded;
final ValueChanged<bool>? onZoomChanged;

const MemoryCard({required this.asset, required this.title, required this.showTitle, this.onVideoEnded, super.key});
const MemoryCard({
required this.asset,
required this.title,
required this.showTitle,
this.onVideoEnded,
this.onZoomChanged,
super.key,
});

@override
Widget build(BuildContext context) {
Expand All @@ -30,22 +39,27 @@ class MemoryCard extends StatelessWidget {
SizedBox.expand(child: _BlurredBackdrop(asset: asset)),
LayoutBuilder(
builder: (context, constraints) {
// Determine the fit using the aspect ratio
BoxFit fit = BoxFit.contain;
if (asset.width != null && asset.height != null) {
final aspectRatio = asset.width! / asset.height!;
final phoneAspectRatio = constraints.maxWidth / constraints.maxHeight;
// Look for a 25% difference in either direction
if (phoneAspectRatio * .75 < aspectRatio && phoneAspectRatio * 1.25 > aspectRatio) {
// Cover to look nice if we have nearly the same aspect ratio
fit = BoxFit.cover;
}
}

if (asset.isImage) {
return Hero(
tag: 'memory-${asset.id}',
child: ImmichImage(asset, fit: fit, height: double.infinity, width: double.infinity),
child: PhotoView(
key: ValueKey('photo-${asset.id}'),
imageProvider: ImmichImage.imageProvider(
asset: asset,
width: constraints.maxWidth,
height: constraints.maxHeight,
),
index: 0,
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 5,
initialScale: PhotoViewComputedScale.contained,
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
filterQuality: FilterQuality.high,
scaleStateChangedCallback: (scaleState) {
final isZoomed = scaleState != PhotoViewScaleState.initial;
onZoomChanged?.call(isZoomed);
},
),
);
} else {
return Hero(
Expand Down
22 changes: 20 additions & 2 deletions web/src/lib/components/memory-page/memory-photo-viewer.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script lang="ts">
import { zoomImageAction } from '$lib/actions/zoom-image';
import { assetViewerFadeDuration } from '$lib/constants';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { photoZoomState } from '$lib/stores/zoom-image.store';
import { getAssetThumbnailUrl } from '$lib/utils';
import { getAltText } from '$lib/utils/thumbnail-util';
import { AssetMediaSize } from '@immich/sdk';
Expand All @@ -11,14 +13,30 @@
interface Props {
asset: TimelineAsset;
onImageLoad: () => void;
onZoomChange?: (isZoomed: boolean) => void;
}
const { asset, onImageLoad }: Props = $props();
const { asset, onImageLoad, onZoomChange }: Props = $props();
let assetFileUrl: string = $state('');
let imageLoaded: boolean = $state(false);
let loader = $state<HTMLImageElement>();
// Reset zoom state when component mounts (new asset)
photoZoomState.set({
currentRotation: 0,
currentZoom: 1,
enable: true,
currentPositionX: 0,
currentPositionY: 0,
});
Comment on lines +25 to +32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd want that in an onMount instead.

Suggested change
// Reset zoom state when component mounts (new asset)
photoZoomState.set({
currentRotation: 0,
currentZoom: 1,
enable: true,
currentPositionX: 0,
currentPositionY: 0,
});
onMount(() => {
photoZoomState.set({
currentRotation: 0,
currentZoom: 1,
enable: true,
currentPositionX: 0,
currentPositionY: 0,
});
});

// Notify parent when zoom state changes
$effect(() => {
const isZoomed = $photoZoomState?.currentZoom > 1;
onZoomChange?.(isZoomed);
});
const onLoadCallback = () => {
imageLoaded = true;
assetFileUrl = imageLoaderUrl;
Expand Down Expand Up @@ -48,7 +66,7 @@
<LoadingSpinner />
</div>
{:else if imageLoaded}
<div transition:fade={{ duration: assetViewerFadeDuration }} class="h-full w-full">
<div use:zoomImageAction transition:fade={{ duration: assetViewerFadeDuration }} class="h-full w-full">
<img
class="h-full w-full rounded-2xl object-contain transition-all"
src={assetFileUrl}
Expand Down
15 changes: 13 additions & 2 deletions web/src/lib/components/memory-page/memory-viewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
let galleryFirstLoad = $state(true);
let playerInitialized = $state(false);
let paused = $state(false);
let isZoomed = $state(false);
let current = $state<MemoryAsset | undefined>(undefined);
let currentMemoryAssetFull = $derived.by(async () =>
current?.asset ? await getAssetInfo({ ...authManager.params, id: current.asset.id }) : undefined,
Expand Down Expand Up @@ -225,12 +226,22 @@
galleryInView = false;
// only call play after the first page load. When page first loads the gallery will not be visible
// and calling play here will result in duplicate invocation.
if (!galleryFirstLoad) {
// Also don't auto-play if user is zoomed in
if (!galleryFirstLoad && !isZoomed) {
handlePromiseError(handleAction('galleryOutOfView', 'play'));
}
galleryFirstLoad = false;
};

const handleZoomChange = (zoomed: boolean) => {
isZoomed = zoomed;
if (zoomed) {
handlePromiseError(handleAction('zoomIn', 'pause'));
} else if (!galleryInView && !paused) {
handlePromiseError(handleAction('zoomOut', 'play'));
}
};

const loadFromParams = (page: Page | NavigationTarget | null) => {
const assetId = page?.params?.assetId ?? page?.url.searchParams.get(QueryParameter.ID) ?? undefined;
return memoryStore.getMemoryAsset(assetId);
Expand Down Expand Up @@ -485,7 +496,7 @@
videoViewerVolume={$videoViewerVolume}
/>
{:else}
<MemoryPhotoViewer asset={current.asset} onImageLoad={resetAndPlay} />
<MemoryPhotoViewer asset={current.asset} onImageLoad={resetAndPlay} onZoomChange={handleZoomChange} />
{/if}
{/key}

Expand Down
Loading