view transitions
parent
ea1ef3df13
commit
f7b08fcd95
|
|
@ -74,6 +74,9 @@
|
|||
--immich-dark-bg: 10 10 10;
|
||||
--immich-dark-fg: 229 231 235;
|
||||
--immich-dark-gray: 33 33 33;
|
||||
|
||||
/* transitions */
|
||||
--immich-split-viewer-nav: enabled;
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
|
|
|
|||
|
|
@ -35,10 +35,6 @@ interface SwipeAnimations {
|
|||
export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions) => {
|
||||
// Animation configuration
|
||||
const ANIMATION_DURATION_MS = 300;
|
||||
// Drag sensitivity: pixels needed to reach 100% animation progress
|
||||
// Higher value = less sensitive (need to drag further)
|
||||
// Lower value = more sensitive (animation advances quickly)
|
||||
const DRAG_DISTANCE_FOR_FULL_ANIMATION = 400;
|
||||
// Enable/disable scaling effect during animation
|
||||
const ENABLE_SCALE_ANIMATION = false;
|
||||
|
||||
|
|
@ -103,7 +99,7 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
}
|
||||
|
||||
const duration = ANIMATION_DURATION_MS;
|
||||
const easing = 'cubic-bezier(0.33, 1, 0.68, 1)'; // Match Month.svelte:156
|
||||
const easing = 'linear'; // Linear easing to match drag rate
|
||||
|
||||
// Set transform origin to center for proper scaling
|
||||
imgElement.style.transformOrigin = 'center';
|
||||
|
|
@ -118,17 +114,11 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
? [
|
||||
// flyOutLeft - Month.svelte:280-289
|
||||
{ transform: `translateX(0)${scale(1)}`, opacity: '1', offset: 0 },
|
||||
{ transform: `translateX(-20vw)${scale(0.8)}`, opacity: '1', offset: 0.2 },
|
||||
{ transform: `translateX(-50vw)${scale(0.5)}`, opacity: '0.5', offset: 0.5 },
|
||||
{ transform: `translateX(-80vw)${scale(0.2)}`, opacity: '0', offset: 0.8 },
|
||||
{ transform: `translateX(-100vw)${scale(0)}`, opacity: '0', offset: 1 },
|
||||
]
|
||||
: [
|
||||
// flyOutRight - Month.svelte:303-312
|
||||
{ transform: `translateX(0)${scale(1)}`, opacity: '1', offset: 0 },
|
||||
{ transform: `translateX(20vw)${scale(0.8)}`, opacity: '1', offset: 0.2 },
|
||||
{ transform: `translateX(50vw)${scale(0.5)}`, opacity: '0.5', offset: 0.5 },
|
||||
{ transform: `translateX(80vw)${scale(0.2)}`, opacity: '0', offset: 0.8 },
|
||||
{ transform: `translateX(100vw)${scale(0)}`, opacity: '0', offset: 1 },
|
||||
],
|
||||
{
|
||||
|
|
@ -252,126 +242,134 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates animation progress (0-1) based on drag distance.
|
||||
* Maps pixel drag distance to viewport width to match the animation's vw-based transforms.
|
||||
* @param dragPixels - Absolute drag distance in pixels
|
||||
* @returns Progress value between 0 and 1
|
||||
*/
|
||||
const calculateAnimationProgress = (dragPixels: number): number => {
|
||||
// The animation moves from 0 to 100vw, so map pixel drag to viewport width
|
||||
const viewportWidth = window.innerWidth;
|
||||
const dragInViewportUnits = dragPixels / viewportWidth;
|
||||
return Math.min(dragInViewportUnits, 1);
|
||||
};
|
||||
|
||||
const pointerDown = (event: PointerEvent) => {
|
||||
if (options?.disabled || !imgElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle single pointer (mouse or single touch)
|
||||
if (event.isPrimary) {
|
||||
isDragging = true;
|
||||
startX = event.clientX;
|
||||
|
||||
// Change cursor to grabbing
|
||||
node.style.cursor = 'grabbing';
|
||||
// Capture pointer so we continue to receive events even if mouse moves outside element
|
||||
node.setPointerCapture(event.pointerId);
|
||||
dragStartTime = new Date();
|
||||
|
||||
// Also add document listeners as fallback
|
||||
document.addEventListener('pointerup', pointerUp);
|
||||
document.addEventListener('pointercancel', pointerUp);
|
||||
|
||||
// Ensure preview containers are created and positioned
|
||||
ensurePreviewsCreated();
|
||||
updatePreviewPositions();
|
||||
|
||||
// Note: We don't create animations here - they're lazy-created in pointerMove
|
||||
// when we know which direction the user is swiping
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const pointerMove = (event: PointerEvent) => {
|
||||
if (options?.disabled || !imgElement) {
|
||||
// Only handle primary mouse button (0) and single touch (isPrimary)
|
||||
if (!event.isPrimary || (event.pointerType === 'mouse' && event.button !== 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
currentOffsetX = event.clientX - startX;
|
||||
swipeAmount = currentOffsetX;
|
||||
isDragging = true;
|
||||
startX = event.clientX;
|
||||
swipeAmount = 0;
|
||||
|
||||
// Determine which direction we're swiping
|
||||
const isSwipingLeft = currentOffsetX < 0;
|
||||
const isSwipingRight = currentOffsetX > 0;
|
||||
// Change cursor to grabbing
|
||||
node.style.cursor = 'grabbing';
|
||||
// Capture pointer so we continue to receive events even if mouse moves outside element
|
||||
node.setPointerCapture(event.pointerId);
|
||||
dragStartTime = new Date();
|
||||
|
||||
// Lazy create animations when first needed
|
||||
if (isSwipingLeft && !leftAnimations) {
|
||||
leftAnimations = createSwipeAnimations('left');
|
||||
// Ensure the right preview container is visible
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.display = 'block';
|
||||
rightPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
} else if (isSwipingRight && !rightAnimations) {
|
||||
rightAnimations = createSwipeAnimations('right');
|
||||
// Ensure the left preview container is visible
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.display = 'block';
|
||||
leftPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
}
|
||||
// Also add document listeners as fallback
|
||||
document.addEventListener('pointerup', pointerUp);
|
||||
document.addEventListener('pointercancel', pointerUp);
|
||||
|
||||
// Calculate progress based on absolute drag distance
|
||||
// Using a threshold distance to map to full animation (0-1)
|
||||
const progress = Math.min(Math.abs(currentOffsetX) / DRAG_DISTANCE_FOR_FULL_ANIMATION, 1);
|
||||
// Ensure preview containers are created and positioned
|
||||
ensurePreviewsCreated();
|
||||
updatePreviewPositions();
|
||||
|
||||
// Map progress to animation time
|
||||
const animationTime = progress * ANIMATION_DURATION_MS;
|
||||
console.log(
|
||||
`Animation progress: ${(progress * 100).toFixed(1)}% | Time: ${animationTime.toFixed(1)}ms | Offset: ${currentOffsetX.toFixed(1)}px`,
|
||||
);
|
||||
// Note: We don't create animations here - they're lazy-created in pointerMove
|
||||
// when we know which direction the user is swiping
|
||||
|
||||
if (isSwipingLeft && leftAnimations) {
|
||||
// Ensure the right preview container is visible
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.display = 'block';
|
||||
rightPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
// Scrub left animations forward
|
||||
leftAnimations.currentImageAnimation.currentTime = animationTime;
|
||||
if (leftAnimations.previewAnimation) {
|
||||
leftAnimations.previewAnimation.currentTime = animationTime;
|
||||
}
|
||||
// Cancel and recreate right animations to prevent conflicts on imgElement
|
||||
if (rightAnimations) {
|
||||
rightAnimations.currentImageAnimation.cancel();
|
||||
if (rightAnimations.previewAnimation) {
|
||||
rightAnimations.previewAnimation.cancel();
|
||||
}
|
||||
rightAnimations = null;
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} else if (isSwipingRight && rightAnimations) {
|
||||
// Ensure the left preview container is visible
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.display = 'block';
|
||||
leftPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
// Scrub right animations forward
|
||||
rightAnimations.currentImageAnimation.currentTime = animationTime;
|
||||
if (rightAnimations.previewAnimation) {
|
||||
rightAnimations.previewAnimation.currentTime = animationTime;
|
||||
}
|
||||
// Cancel and recreate left animations to prevent conflicts on imgElement
|
||||
if (leftAnimations) {
|
||||
leftAnimations.currentImageAnimation.cancel();
|
||||
if (leftAnimations.previewAnimation) {
|
||||
leftAnimations.previewAnimation.cancel();
|
||||
}
|
||||
leftAnimations = null;
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
// Notify about swipe movement
|
||||
options?.onSwipeMove?.(currentOffsetX);
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const pointerMove = (event: PointerEvent) => {
|
||||
if (options?.disabled || !imgElement || !isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentOffsetX = event.clientX - startX;
|
||||
swipeAmount = currentOffsetX;
|
||||
|
||||
// Determine which direction we're swiping
|
||||
const isSwipingLeft = currentOffsetX < 0;
|
||||
const isSwipingRight = currentOffsetX > 0;
|
||||
|
||||
// Lazy create animations when first needed
|
||||
if (isSwipingLeft && !leftAnimations) {
|
||||
leftAnimations = createSwipeAnimations('left');
|
||||
// Ensure the right preview container is visible
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.display = 'block';
|
||||
rightPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
} else if (isSwipingRight && !rightAnimations) {
|
||||
rightAnimations = createSwipeAnimations('right');
|
||||
// Ensure the left preview container is visible
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.display = 'block';
|
||||
leftPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate animation progress based on drag distance
|
||||
const progress = calculateAnimationProgress(Math.abs(currentOffsetX));
|
||||
const animationTime = progress * ANIMATION_DURATION_MS;
|
||||
|
||||
if (isSwipingLeft && leftAnimations) {
|
||||
// Ensure the right preview container is visible
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.display = 'block';
|
||||
rightPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
// Scrub left animations forward
|
||||
leftAnimations.currentImageAnimation.currentTime = animationTime;
|
||||
if (leftAnimations.previewAnimation) {
|
||||
leftAnimations.previewAnimation.currentTime = animationTime;
|
||||
}
|
||||
// Cancel and recreate right animations to prevent conflicts on imgElement
|
||||
if (rightAnimations) {
|
||||
rightAnimations.currentImageAnimation.cancel();
|
||||
if (rightAnimations.previewAnimation) {
|
||||
rightAnimations.previewAnimation.cancel();
|
||||
}
|
||||
rightAnimations = null;
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} else if (isSwipingRight && rightAnimations) {
|
||||
// Ensure the left preview container is visible
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.display = 'block';
|
||||
leftPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
// Scrub right animations forward
|
||||
rightAnimations.currentImageAnimation.currentTime = animationTime;
|
||||
if (rightAnimations.previewAnimation) {
|
||||
rightAnimations.previewAnimation.currentTime = animationTime;
|
||||
}
|
||||
// Cancel and recreate left animations to prevent conflicts on imgElement
|
||||
if (leftAnimations) {
|
||||
leftAnimations.currentImageAnimation.cancel();
|
||||
if (leftAnimations.previewAnimation) {
|
||||
leftAnimations.previewAnimation.cancel();
|
||||
}
|
||||
leftAnimations = null;
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
// Notify about swipe movement
|
||||
options?.onSwipeMove?.(currentOffsetX);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const resetPosition = () => {
|
||||
|
|
@ -383,33 +381,36 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
const activeAnimations = currentOffsetX < 0 ? leftAnimations : rightAnimations;
|
||||
const activePreviewContainer = currentOffsetX < 0 ? rightPreviewContainer : leftPreviewContainer;
|
||||
|
||||
if (activeAnimations) {
|
||||
// Reverse the animation back to 0
|
||||
activeAnimations.currentImageAnimation.playbackRate = -1;
|
||||
if (activeAnimations.previewAnimation) {
|
||||
activeAnimations.previewAnimation.playbackRate = -1;
|
||||
}
|
||||
|
||||
// Play from current position back to start
|
||||
activeAnimations.currentImageAnimation.play();
|
||||
activeAnimations.previewAnimation?.play();
|
||||
|
||||
// Listen for finish event to clean up
|
||||
const handleFinish = () => {
|
||||
activeAnimations.currentImageAnimation.removeEventListener('finish', handleFinish);
|
||||
// Reset to original state
|
||||
activeAnimations.currentImageAnimation.cancel();
|
||||
activeAnimations.previewAnimation?.cancel();
|
||||
|
||||
// Hide the preview container after animation completes
|
||||
if (activePreviewContainer) {
|
||||
activePreviewContainer.style.display = 'none';
|
||||
activePreviewContainer.style.zIndex = '-1';
|
||||
}
|
||||
};
|
||||
activeAnimations.currentImageAnimation.addEventListener('finish', handleFinish, { once: true });
|
||||
if (!activeAnimations) {
|
||||
currentOffsetX = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Reverse the animation back to 0
|
||||
activeAnimations.currentImageAnimation.playbackRate = -1;
|
||||
if (activeAnimations.previewAnimation) {
|
||||
activeAnimations.previewAnimation.playbackRate = -1;
|
||||
}
|
||||
|
||||
// Play from current position back to start
|
||||
activeAnimations.currentImageAnimation.play();
|
||||
activeAnimations.previewAnimation?.play();
|
||||
|
||||
// Listen for finish event to clean up
|
||||
const handleFinish = () => {
|
||||
activeAnimations.currentImageAnimation.removeEventListener('finish', handleFinish);
|
||||
// Reset to original state
|
||||
activeAnimations.currentImageAnimation.cancel();
|
||||
activeAnimations.previewAnimation?.cancel();
|
||||
|
||||
// Hide the preview container after animation completes
|
||||
if (activePreviewContainer) {
|
||||
activePreviewContainer.style.display = 'none';
|
||||
activePreviewContainer.style.zIndex = '-1';
|
||||
}
|
||||
};
|
||||
activeAnimations.currentImageAnimation.addEventListener('finish', handleFinish, { once: true });
|
||||
|
||||
currentOffsetX = 0;
|
||||
};
|
||||
|
||||
|
|
@ -430,110 +431,116 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
// Get the active animations
|
||||
const activeAnimations = direction === 'left' ? leftAnimations : rightAnimations;
|
||||
|
||||
if (activeAnimations) {
|
||||
// Get current time before modifying animation
|
||||
const currentTime = Number(activeAnimations.currentImageAnimation.currentTime) || 0;
|
||||
console.log(`Committing transition from ${currentTime}ms / ${ANIMATION_DURATION_MS}ms`);
|
||||
|
||||
// If animation is already at or near the end, skip to finish immediately
|
||||
if (currentTime >= ANIMATION_DURATION_MS - 5) {
|
||||
console.log('Animation already complete, finishing immediately');
|
||||
|
||||
// Keep the preview visible by hiding the main image but showing the preview
|
||||
imgElement.style.opacity = '0';
|
||||
|
||||
// Show the preview that's now in the center
|
||||
const activePreview = direction === 'right' ? leftPreviewContainer : rightPreviewContainer;
|
||||
|
||||
if (activePreview) {
|
||||
activePreview.style.zIndex = '1'; // Bring to front
|
||||
}
|
||||
|
||||
// Trigger navigation (dimensions were already passed in onPreCommit)
|
||||
options?.onSwipeCommit?.(direction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure playback rate is forward (in case it was reversed)
|
||||
activeAnimations.currentImageAnimation.playbackRate = 1;
|
||||
if (activeAnimations.previewAnimation) {
|
||||
activeAnimations.previewAnimation.playbackRate = 1;
|
||||
}
|
||||
|
||||
// Play the animation to completion from current position
|
||||
activeAnimations.currentImageAnimation.play();
|
||||
activeAnimations.previewAnimation?.play();
|
||||
|
||||
// Listen for animation finish
|
||||
const handleFinish = () => {
|
||||
if (!imgElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeAnimations.currentImageAnimation.removeEventListener('finish', handleFinish);
|
||||
|
||||
// Keep the preview visible by hiding the main image but showing the preview
|
||||
// The preview is now centered, and we want it to stay visible while the new component loads
|
||||
imgElement.style.opacity = '0';
|
||||
|
||||
// Show the preview that's now in the center
|
||||
const activePreview = direction === 'right' ? leftPreviewContainer : rightPreviewContainer;
|
||||
|
||||
if (activePreview) {
|
||||
activePreview.style.zIndex = '1'; // Bring to front
|
||||
}
|
||||
|
||||
// Trigger navigation (dimensions were already passed in onPreCommit)
|
||||
options?.onSwipeCommit?.(direction);
|
||||
};
|
||||
|
||||
activeAnimations.currentImageAnimation.addEventListener('finish', handleFinish, { once: true });
|
||||
if (!activeAnimations) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const pointerUp = (event: PointerEvent) => {
|
||||
if (isDragging) {
|
||||
// Get current time before modifying animation
|
||||
const currentTime = Number(activeAnimations.currentImageAnimation.currentTime) || 0;
|
||||
console.log(`Committing transition from ${currentTime}ms / ${ANIMATION_DURATION_MS}ms`);
|
||||
|
||||
// If animation is already at or near the end, skip to finish immediately
|
||||
if (currentTime >= ANIMATION_DURATION_MS - 5) {
|
||||
console.log('Animation already complete, finishing immediately');
|
||||
|
||||
// Keep the preview visible by hiding the main image but showing the preview
|
||||
imgElement.style.opacity = '0';
|
||||
|
||||
// Show the preview that's now in the center
|
||||
const activePreview = direction === 'right' ? leftPreviewContainer : rightPreviewContainer;
|
||||
|
||||
if (activePreview) {
|
||||
activePreview.style.zIndex = '1'; // Bring to front
|
||||
}
|
||||
|
||||
// Trigger navigation (dimensions were already passed in onPreCommit)
|
||||
options?.onSwipeCommit?.(direction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure playback rate is forward (in case it was reversed)
|
||||
activeAnimations.currentImageAnimation.playbackRate = 1;
|
||||
if (activeAnimations.previewAnimation) {
|
||||
activeAnimations.previewAnimation.playbackRate = 1;
|
||||
}
|
||||
|
||||
// Play the animation to completion from current position
|
||||
activeAnimations.currentImageAnimation.play();
|
||||
activeAnimations.previewAnimation?.play();
|
||||
|
||||
// Listen for animation finish
|
||||
const handleFinish = () => {
|
||||
if (!imgElement) {
|
||||
return;
|
||||
}
|
||||
isDragging = false;
|
||||
// Reset cursor
|
||||
node.style.cursor = 'grab';
|
||||
// Release pointer capture
|
||||
if (node.hasPointerCapture(event.pointerId)) {
|
||||
node.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
// Remove document listeners
|
||||
document.removeEventListener('pointerup', pointerUp);
|
||||
document.removeEventListener('pointercancel', pointerUp);
|
||||
|
||||
const threshold = options?.swipeThreshold ?? 45;
|
||||
activeAnimations.currentImageAnimation.removeEventListener('finish', handleFinish);
|
||||
|
||||
const timeTaken = Date.now() - (dragStartTime?.getTime() ?? 0);
|
||||
const velocity = Math.abs(swipeAmount) / timeTaken;
|
||||
// Keep the preview visible by hiding the main image but showing the preview
|
||||
// The preview is now centered, and we want it to stay visible while the new component loads
|
||||
imgElement.style.opacity = '0';
|
||||
|
||||
// Calculate animation progress (same calculation as in pointerMove)
|
||||
const progress = Math.min(Math.abs(currentOffsetX) / DRAG_DISTANCE_FOR_FULL_ANIMATION, 1);
|
||||
// Show the preview that's now in the center
|
||||
const activePreview = direction === 'right' ? leftPreviewContainer : rightPreviewContainer;
|
||||
|
||||
// Commit if EITHER:
|
||||
// 1. High velocity (fast swipe) OR
|
||||
// 2. Animation progress is over 25%
|
||||
const hasEnoughVelocity = velocity >= 0.11;
|
||||
const hasEnoughProgress = progress > 0.25;
|
||||
|
||||
if (Math.abs(swipeAmount) < threshold || (!hasEnoughVelocity && !hasEnoughProgress)) {
|
||||
resetPosition();
|
||||
return;
|
||||
if (activePreview) {
|
||||
activePreview.style.zIndex = '1'; // Bring to front
|
||||
}
|
||||
|
||||
const commitDirection = currentOffsetX > 0 ? 'right' : 'left';
|
||||
// Trigger navigation (dimensions were already passed in onPreCommit)
|
||||
options?.onSwipeCommit?.(direction);
|
||||
};
|
||||
|
||||
// Call onSwipeEnd callback
|
||||
options?.onSwipeEnd?.(currentOffsetX);
|
||||
activeAnimations.currentImageAnimation.addEventListener('finish', handleFinish, { once: true });
|
||||
};
|
||||
|
||||
// complete the transition animation
|
||||
completeTransition(commitDirection);
|
||||
const pointerUp = (event: PointerEvent) => {
|
||||
console.log('up', event);
|
||||
if (!isDragging || !event.isPrimary || (event.pointerType === 'mouse' && event.button !== 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!imgElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
// Reset cursor
|
||||
node.style.cursor = 'grab';
|
||||
// Release pointer capture
|
||||
if (node.hasPointerCapture(event.pointerId)) {
|
||||
node.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
// Remove document listeners
|
||||
document.removeEventListener('pointerup', pointerUp);
|
||||
document.removeEventListener('pointercancel', pointerUp);
|
||||
|
||||
const threshold = options?.swipeThreshold ?? 45;
|
||||
|
||||
const timeTaken = Date.now() - (dragStartTime?.getTime() ?? 0);
|
||||
const velocity = Math.abs(swipeAmount) / timeTaken;
|
||||
|
||||
// Calculate animation progress (same calculation as in pointerMove)
|
||||
const progress = calculateAnimationProgress(Math.abs(currentOffsetX));
|
||||
|
||||
// Commit if EITHER:
|
||||
// 1. High velocity (fast swipe) OR
|
||||
// 2. Animation progress is over 25%
|
||||
const hasEnoughVelocity = velocity >= 0.11;
|
||||
const hasEnoughProgress = progress > 0.25;
|
||||
|
||||
if (Math.abs(swipeAmount) < threshold || (!hasEnoughVelocity && !hasEnoughProgress)) {
|
||||
resetPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
const commitDirection = currentOffsetX > 0 ? 'right' : 'left';
|
||||
|
||||
// Call onSwipeEnd callback
|
||||
options?.onSwipeEnd?.(currentOffsetX);
|
||||
|
||||
// complete the transition animation
|
||||
completeTransition(commitDirection);
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
type StackResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { toastManager } from '@immich/ui';
|
||||
import { onDestroy, onMount, untrack } from 'svelte';
|
||||
import { onDestroy, onMount, tick, untrack } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fly, slide } from 'svelte/transition';
|
||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||
|
|
@ -167,6 +167,7 @@
|
|||
|
||||
if (viewTransitionManager.activeViewTransition) {
|
||||
transitionName = 'hero';
|
||||
console.log('setting name initial');
|
||||
equirectangularTransitionName = 'hero';
|
||||
}
|
||||
let addInfoTransition;
|
||||
|
|
@ -180,6 +181,7 @@
|
|||
finished = () => {
|
||||
detailPanelTransitionName = null;
|
||||
transitionName = null;
|
||||
console.log('setting null');
|
||||
};
|
||||
eventManager.on('Finished', finished);
|
||||
// eventManager.emit('AssetViewerLoaded');
|
||||
|
|
@ -264,19 +266,23 @@
|
|||
});
|
||||
};
|
||||
|
||||
const startTransition = (targetTransition: string | null, targetAsset?: AssetResponseDto) => {
|
||||
transitionName = targetTransition;
|
||||
equirectangularTransitionName = targetTransition;
|
||||
detailPanelTransitionName = 'onTop';
|
||||
|
||||
const startTransition = async (targetTransition: string | null, targetAsset?: AssetResponseDto) => {
|
||||
transitionName = viewTransitionManager.getTransitionName('old', targetTransition);
|
||||
console.log('transitionName', transitionName);
|
||||
equirectangularTransitionName = viewTransitionManager.getTransitionName('old', targetTransition);
|
||||
detailPanelTransitionName = 'detail-panel';
|
||||
await tick();
|
||||
debugger;
|
||||
viewTransitionManager.startTransition(
|
||||
new Promise<void>((resolve) => {
|
||||
eventManager.once('StartViewTransition', () => {
|
||||
transitionName = viewTransitionManager.getTransitionName('new', targetTransition);
|
||||
console.log(transitionName);
|
||||
if (targetAsset && isEquirectangular(asset) && !isEquirectangular(targetAsset)) {
|
||||
equirectangularTransitionName = null;
|
||||
}
|
||||
});
|
||||
eventManager.once('AssetViewerFree', () => resolve());
|
||||
eventManager.once('AssetViewerFree', () => tick().then(resolve()));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -297,11 +303,18 @@
|
|||
}
|
||||
|
||||
void tracker.invoke(async () => {
|
||||
let hasNext = false;
|
||||
let skipped = false;
|
||||
if (viewTransitionManager.skipTransitions()) {
|
||||
await tick();
|
||||
skipped = true;
|
||||
console.log('was skipped');
|
||||
}
|
||||
|
||||
let hasNext = false;
|
||||
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) {
|
||||
console.log('$slideshowState', $slideshowState, skipTransition);
|
||||
if (!skipTransition) {
|
||||
startTransition(null, undefined);
|
||||
await startTransition('slideshow', undefined);
|
||||
}
|
||||
hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next();
|
||||
if (!hasNext) {
|
||||
|
|
@ -314,12 +327,19 @@
|
|||
} else if (onNavigateToAsset) {
|
||||
// only transition if the target is already preloaded, and is in a secure context
|
||||
const targetAsset = order === 'previous' ? previousAsset : nextAsset;
|
||||
if (!skipTransition && !!targetAsset && globalThis.isSecureContext && preloadManager.isPreloaded(targetAsset)) {
|
||||
const preloaded = await preloadManager.isPreloaded(targetAsset);
|
||||
|
||||
if (!skipTransition && !!targetAsset && globalThis.isSecureContext && preloaded) {
|
||||
const targetTransition = $slideshowState === SlideshowState.PlaySlideshow ? null : order;
|
||||
startTransition(targetTransition, targetAsset);
|
||||
console.log('sta', $slideshowState);
|
||||
await startTransition(targetTransition, targetAsset);
|
||||
} else {
|
||||
console.log('not');
|
||||
}
|
||||
resetZoomState();
|
||||
console.log('about to');
|
||||
hasNext = order === 'previous' ? await onNavigateToAsset(previousAsset) : await onNavigateToAsset(nextAsset);
|
||||
console.log('done to');
|
||||
} else {
|
||||
hasNext = false;
|
||||
}
|
||||
|
|
@ -435,7 +455,6 @@
|
|||
};
|
||||
|
||||
const handleAboutToNavigate = (target: { direction: 'left' | 'right'; nextWidth: number; nextHeight: number }) => {
|
||||
debugger;
|
||||
nextSizeHint = {
|
||||
width: target.nextWidth,
|
||||
height: target.nextHeight,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
transitionName?: string | null;
|
||||
transitionName?: string | null | undefined;
|
||||
asset: AssetResponseDto;
|
||||
previousAsset?: AssetResponseDto;
|
||||
nextAsset?: AssetResponseDto;
|
||||
|
|
@ -85,6 +85,7 @@
|
|||
onDestroy(() => {
|
||||
$boundingBoxesArray = [];
|
||||
});
|
||||
$inspect(transitionName).with(console.log.bind(null, 'transit'));
|
||||
|
||||
const box = $derived.by(() => {
|
||||
const { width, height } = scaleToFit(naturalWidth, naturalHeight, containerWidth, containerHeight);
|
||||
|
|
@ -107,15 +108,24 @@
|
|||
const handlePreCommit = (direction: 'left' | 'right', nextWidth: number, nextHeight: number) => {
|
||||
// Scale the preview dimensions to fit within the viewport (like object-fit: contain)
|
||||
// This prevents flashing when small images are scaled up by scaleToFit
|
||||
const { width: scaledWidth, height: scaledHeight } = scaleToFit(
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
);
|
||||
console.log('nextSize', nextWidth, nextHeight, scaledWidth, scaledHeight);
|
||||
|
||||
let width = nextWidth;
|
||||
let height = nextHeight;
|
||||
if (direction === 'right' && nextAsset?.exifInfo?.exifImageWidth && nextAsset?.exifInfo?.exifImageHeight) {
|
||||
width = nextAsset.exifInfo.exifImageWidth;
|
||||
height = nextAsset.exifInfo.exifImageHeight;
|
||||
} else if (
|
||||
direction === 'left' &&
|
||||
previousAsset?.exifInfo?.exifImageWidth &&
|
||||
previousAsset?.exifInfo?.exifImageHeight
|
||||
) {
|
||||
width = previousAsset.exifInfo.exifImageWidth;
|
||||
height = previousAsset.exifInfo.exifImageHeight;
|
||||
}
|
||||
const box = scaleToFit(width, height, containerWidth, containerHeight);
|
||||
console.log('nextSize', nextWidth, nextHeight, box);
|
||||
// onAboutToNavigate?.({ direction, nextWidth: scaledWidth, nextHeight: scaledHeight });
|
||||
onAboutToNavigate?.({ direction, nextWidth, nextHeight });
|
||||
onAboutToNavigate?.({ direction, nextWidth: box.width, nextHeight: box.height });
|
||||
};
|
||||
|
||||
const handleSwipeCommit = (direction: 'left' | 'right') => {
|
||||
|
|
@ -225,8 +235,6 @@
|
|||
let naturalWidth = $derived(nextSizeHint?.width ?? 1);
|
||||
let naturalHeight = $derived(nextSizeHint?.height ?? 1);
|
||||
|
||||
$inspect(naturalWidth).with(console.log.bind(null, 'natW'));
|
||||
$inspect(naturalHeight).with(console.log.bind(null, 'natH'));
|
||||
let lastUrl: string | undefined | null;
|
||||
let lastPreviousUrl: string | undefined | null;
|
||||
let lastNextUrl: string | undefined | null;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import { fromTimelinePlainDate, getDateLocaleString } from '$lib/utils/timeline-util';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
||||
import { onDestroy, type Snippet } from 'svelte';
|
||||
import { onDestroy, tick, type Snippet } from 'svelte';
|
||||
|
||||
type Props = {
|
||||
toAssetViewerTransitionId?: string | null;
|
||||
|
|
@ -67,8 +67,9 @@
|
|||
|
||||
viewTransitionManager.startTransition(
|
||||
new Promise<void>((resolve) => {
|
||||
eventManager.once('TimelineLoaded', ({ id }) => {
|
||||
eventManager.once('TimelineLoaded', async ({ id }) => {
|
||||
animationTargetAssetId = id;
|
||||
await tick();
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
|
|
@ -153,47 +154,47 @@
|
|||
:global(::view-transition-new(*)) {
|
||||
mix-blend-mode: normal;
|
||||
animation-duration: inherit;
|
||||
animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
|
||||
}
|
||||
|
||||
:global(::view-transition-old(*)) {
|
||||
animation-name: fadeOut forwards;
|
||||
animation-name: fadeOut;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
:global(::view-transition-new(*)) {
|
||||
animation-name: fadeIn forwards;
|
||||
}
|
||||
|
||||
:global(::view-transition-old(slideshow)) {
|
||||
animation: 500ms 0s fadeOut forwards;
|
||||
}
|
||||
|
||||
:global(::view-transition-new(slideshow)) {
|
||||
animation: 500ms 0s fadeIn forwards;
|
||||
animation-name: fadeIn;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
:global(::view-transition-old(root)) {
|
||||
animation: 500ms 0s fadeOut forwards;
|
||||
animation-timing-function: inherit;
|
||||
}
|
||||
:global(::view-transition-new(root)) {
|
||||
animation: 500ms 0s fadeIn forwards;
|
||||
animation-timing-function: inherit;
|
||||
}
|
||||
|
||||
:global(::view-transition-old(info)) {
|
||||
animation: 250ms 0s flyOutRight forwards;
|
||||
animation-timing-function: inherit;
|
||||
}
|
||||
:global(::view-transition-new(info)) {
|
||||
animation: 250ms 0s flyInRight forwards;
|
||||
animation-timing-function: inherit;
|
||||
}
|
||||
|
||||
:global(::view-transition-old(onTop)),
|
||||
:global(::view-transition-new(onTop)) {
|
||||
z-index: 100;
|
||||
:global(::view-transition-old(detail-panel)),
|
||||
:global(::view-transition-new(detail-panel)) {
|
||||
z-index: 3;
|
||||
animation: none;
|
||||
}
|
||||
:global(::view-transition-group(exclude)) {
|
||||
animation: none;
|
||||
z-index: 2;
|
||||
}
|
||||
:global(::view-transition-old(exclude)) {
|
||||
visibility: hidden;
|
||||
}
|
||||
:global(::view-transition-new(exclude)) {
|
||||
animation: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
:global(::view-transition-old(hero)) {
|
||||
animation: 350ms fadeOut forwards;
|
||||
|
|
@ -203,98 +204,111 @@
|
|||
animation: 350ms fadeIn forwards;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
:global(::view-transition-new(exclude)) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
:global(::view-transition-old(next)) {
|
||||
:global(::view-transition-old(next)),
|
||||
:global(::view-transition-old(next-old)) {
|
||||
animation: 250ms flyOutLeft forwards;
|
||||
transform-origin: center;
|
||||
/* transform-origin: center; */
|
||||
height: 100%;
|
||||
/* display: flex; */
|
||||
object-fit: contain;
|
||||
/* margin: auto; */
|
||||
/* transform: translateY(50%); */
|
||||
/* width: auto;
|
||||
left: 50dvw;
|
||||
top: 50dvh;
|
||||
transform: translate3d(-50%, -50%, 0); */
|
||||
/* object-fit: contain;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
z-index: 10; */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(::view-transition-new(next)) {
|
||||
animation: 250ms flyInRight forwards;
|
||||
transform-origin: center;
|
||||
:global(::view-transition-new(next)),
|
||||
:global(::view-transition-new(next-new)) {
|
||||
animation: 250ms fadeIn forwards;
|
||||
height: 100%;
|
||||
/* display: flex; */
|
||||
object-fit: contain;
|
||||
/* transform-origin: center;
|
||||
width: auto;
|
||||
left: 50dvw;
|
||||
top: 50dvh;
|
||||
transform: translate3d(-50%, -50%, 0); */
|
||||
/* height: 100%;
|
||||
object-fit: contain;
|
||||
height: 100vh;
|
||||
width: 100vw; */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(::view-transition-old(previous)) {
|
||||
animation: 1s flyOutRight forwards;
|
||||
}
|
||||
:global(::view-transition-old(previous-old)) {
|
||||
animation: 250ms flyOutRight forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
/* transform-origin: center; */
|
||||
/* height: 100%;
|
||||
object-fit: contain;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
z-index: 10; */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(::view-transition-new(previous)) {
|
||||
animation: 250ms flyInLeft forwards;
|
||||
transform-origin: center;
|
||||
animation: 1s flyOutRight forwards;
|
||||
}
|
||||
|
||||
:global(::view-transition-new(previous-new)) {
|
||||
animation: 250ms fadeIn forwards;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
:global(::view-transition-new(navbar)) {
|
||||
z-index: 100;
|
||||
animation: none;
|
||||
/* transform-origin: center; */
|
||||
/* height: 100%;
|
||||
object-fit: contain;
|
||||
height: 100vh;
|
||||
width: 100vw; */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
:global(::view-transition-group(previous)),
|
||||
:global(::view-transition-group(next)) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
:global(::view-transition-old(previous)),
|
||||
:global(::view-transition-old(next)) {
|
||||
animation: 250ms fadeOut forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
:global(::view-transition-new(previous)),
|
||||
:global(::view-transition-new(next)) {
|
||||
animation: 250ms fadeIn forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
@keyframes -global-flyInLeft {
|
||||
from {
|
||||
transform: translateX(-100vw);
|
||||
/* transform: translateX(-50dvw); */
|
||||
object-position: -25dvw;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
/* transform: translateX(0); */
|
||||
object-position: 0px 0px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes -global-flyOutLeft {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
/* transform: translateX(0); */
|
||||
object-position: 0px;
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(-100vw);
|
||||
/* transform: translateX(-50dvw); */
|
||||
object-position: -25dvw;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes -global-flyInRight {
|
||||
from {
|
||||
transform: translateX(100vw);
|
||||
/* transform: translateX(50dvw); */
|
||||
object-position: 25dvw;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
/* transform: translateX(0); */
|
||||
object-position: 0px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -302,11 +316,13 @@
|
|||
/* Fly out to right */
|
||||
@keyframes -global-flyOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
/* transform: translateX(0); */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(100vw);
|
||||
/* transform: translateX(50dvw); */
|
||||
object-position: 50dvw 0px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -327,4 +343,32 @@
|
|||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
:global(::view-transition-group(previous)),
|
||||
:global(::view-transition-group(next)) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
:global(::view-transition-old(previous)),
|
||||
:global(::view-transition-old(next)) {
|
||||
animation: 250ms fadeOut forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(::view-transition-new(previous)),
|
||||
:global(::view-transition-new(next)) {
|
||||
animation: 250ms fadeIn forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@
|
|||
// and a new route is being navigated to. It will never be called on direct
|
||||
// navigations by the browser.
|
||||
beforeNavigate(({ from, to }) => {
|
||||
console.log('BEFORE NAV');
|
||||
timelineManager.suspendTransitions = true;
|
||||
const isNavigatingToAssetViewer = isAssetViewerRoute(to);
|
||||
const isNavigatingFromAssetViewer = isAssetViewerRoute(from);
|
||||
|
|
@ -256,6 +257,7 @@
|
|||
// after successful navigation.
|
||||
afterNavigate(({ complete }) => {
|
||||
void complete.finally(async () => {
|
||||
console.log('AFTER nav');
|
||||
const isAssetViewerPage = isAssetViewerRoute(page);
|
||||
|
||||
// Set initial load state only once - if initialLoadWasAssetViewer is null, then
|
||||
|
|
@ -732,8 +734,9 @@
|
|||
|
||||
viewTransitionManager.startTransition(
|
||||
new Promise((resolve) =>
|
||||
eventManager.once('AssetViewerFree', () => {
|
||||
eventManager.once('AssetViewerFree', async () => {
|
||||
toAssetViewerTransitionId = null;
|
||||
await tick();
|
||||
eventManager.emit('TransitionToAssetViewer');
|
||||
resolve();
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -87,11 +87,12 @@
|
|||
if (!targetAsset) {
|
||||
return false;
|
||||
}
|
||||
let waitForAssetViewerFree = new Promise<void>((resolve) => {
|
||||
eventManager.once('AssetViewerFree', () => resolve());
|
||||
});
|
||||
// let waitForAssetViewerFree = new Promise<void>((resolve) => {
|
||||
// eventManager.once('AssetViewerFree', () => resolve());
|
||||
// });
|
||||
await navigate({ targetRoute: 'current', assetId: targetAsset.id });
|
||||
await waitForAssetViewerFree;
|
||||
|
||||
// await waitForAssetViewerFree;
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ class PreloadManager {
|
|||
return false;
|
||||
}
|
||||
if (globalThis.isSecureContext) {
|
||||
return isImageUrlCached(getAssetUrl({ asset }));
|
||||
const img = getAssetUrl({ asset });
|
||||
|
||||
return isImageUrlCached(img);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,45 @@ import { eventManager } from '$lib/managers/event-manager.svelte';
|
|||
|
||||
class ViewTransitionManager {
|
||||
#activeViewTransition = $state<ViewTransition | null>(null);
|
||||
#finishedCallbacks: (() => void)[] = [];
|
||||
|
||||
#splitViewerNavTransitionNames = true;
|
||||
|
||||
constructor() {
|
||||
const root = document.documentElement;
|
||||
const value = getComputedStyle(root).getPropertyValue('--immich-split-viewer-nav').trim();
|
||||
this.#splitViewerNavTransitionNames = value === 'enabled';
|
||||
}
|
||||
|
||||
getTransitionName = (kind: 'old' | 'new', name: string | null | undefined) => {
|
||||
if (name === 'previous' || name === 'next') {
|
||||
return this.#splitViewerNavTransitionNames ? name + '-' + kind : name;
|
||||
} else if (name) {
|
||||
return name;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
get activeViewTransition() {
|
||||
return this.#activeViewTransition;
|
||||
}
|
||||
|
||||
skipTransitions() {
|
||||
const skippedTransitions = !!this.#activeViewTransition;
|
||||
if (skippedTransitions) {
|
||||
console.log('skipped!');
|
||||
}
|
||||
this.#activeViewTransition?.skipTransition();
|
||||
this.#notifyFinished();
|
||||
return skippedTransitions;
|
||||
}
|
||||
|
||||
startTransition(domUpdateComplete: Promise<void>, finishedCallback?: () => void) {
|
||||
if (this.#activeViewTransition) {
|
||||
console.error('Can not start transition - one already active');
|
||||
return;
|
||||
}
|
||||
|
||||
// good time to add view-transition-name styles (if needed)
|
||||
eventManager.emit('BeforeStartViewTransition');
|
||||
// next call will create the 'old' view snapshot
|
||||
|
|
@ -23,28 +56,47 @@ class ViewTransitionManager {
|
|||
}
|
||||
});
|
||||
this.#activeViewTransition = transition;
|
||||
this.#finishedCallbacks.push(() => {
|
||||
this.#activeViewTransition = null;
|
||||
});
|
||||
if (finishedCallback) {
|
||||
this.#finishedCallbacks.push(finishedCallback);
|
||||
}
|
||||
// UpdateCallbackDone is a good time to add any view-transition-name styles
|
||||
// to the new DOM state, before the 'new' view snapshot is creatd
|
||||
// eslint-disable-next-line tscompat/tscompat
|
||||
transition.updateCallbackDone
|
||||
.then(() => eventManager.emit('UpdateCallbackDone'))
|
||||
.catch((error: unknown) => console.log('exception', error));
|
||||
.then(() => {
|
||||
console.log('update done');
|
||||
eventManager.emit('UpdateCallbackDone');
|
||||
})
|
||||
.catch((error: unknown) => console.log('exception in update', error));
|
||||
// Both old/new snapshots are taken - pseudo elements are created, transition is
|
||||
// about to start
|
||||
// eslint-disable-next-line tscompat/tscompat
|
||||
transition.ready
|
||||
.then(() => eventManager.emit('Ready'))
|
||||
.catch((error: unknown) => console.log('exception in ready', error));
|
||||
.catch((error: unknown) => {
|
||||
this.#notifyFinished();
|
||||
console.log('exception in ready', error);
|
||||
});
|
||||
// Transition is complete
|
||||
// eslint-disable-next-line tscompat/tscompat
|
||||
transition.finished
|
||||
.then(() => eventManager.emit('Finished'))
|
||||
.then(() => {
|
||||
eventManager.emit('Finished');
|
||||
console.log('finished');
|
||||
})
|
||||
.catch((error: unknown) => console.log('exception in finished', error));
|
||||
// eslint-disable-next-line tscompat/tscompat
|
||||
void transition.finished.then(() => {
|
||||
finishedCallback?.();
|
||||
this.#activeViewTransition = null;
|
||||
});
|
||||
void transition.finished.then(() => this.#notifyFinished());
|
||||
}
|
||||
#notifyFinished() {
|
||||
console.log('finishedCallbacks len', this.#finishedCallbacks.length);
|
||||
for (const callback of this.#finishedCallbacks) {
|
||||
callback();
|
||||
}
|
||||
this.#finishedCallbacks = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ export const handleCancel = (url: URL) => {
|
|||
|
||||
export const handleIsUrlCached = async (url: URL) => {
|
||||
const cacheKey = getCacheKey(url);
|
||||
|
||||
const isImageUrlCached = !!(await get(cacheKey));
|
||||
console.log('cacheKey', cacheKey, isImageUrlCached);
|
||||
replyIsImageUrlCached(url.pathname + url.search + url.hash, isImageUrlCached);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue