kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'updating-zoom' into 'main'
feat: add 2x zoom on desktop See merge request soapbox-pub/soapbox!3328
commit
12d7e6da91
|
@ -12,6 +12,8 @@ interface IImageLoader {
|
|||
width?: number;
|
||||
height?: number;
|
||||
onClick?: React.MouseEventHandler;
|
||||
isMobile?: boolean;
|
||||
|
||||
}
|
||||
|
||||
class ImageLoader extends PureComponent<IImageLoader> {
|
||||
|
@ -135,7 +137,7 @@ class ImageLoader extends PureComponent<IImageLoader> {
|
|||
const { alt, src, width, height, onClick } = this.props;
|
||||
const { loading } = this.state;
|
||||
|
||||
const className = 'relative h-screen flex items-center justify-center flex-col';
|
||||
const className = clsx('relative flex h-screen w-full grow flex-col items-center justify-center');
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
|
@ -154,6 +156,7 @@ class ImageLoader extends PureComponent<IImageLoader> {
|
|||
alt={alt}
|
||||
src={src}
|
||||
onClick={onClick}
|
||||
isMobile={this.props.isMobile}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -24,6 +24,7 @@ import PlaceholderStatus from 'soapbox/features/placeholder/components/placehold
|
|||
import Thread from 'soapbox/features/status/components/thread.tsx';
|
||||
import Video from 'soapbox/features/video/index.tsx';
|
||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
||||
import { userTouching } from 'soapbox/is-mobile.ts';
|
||||
import { normalizeStatus } from 'soapbox/normalizers/index.ts';
|
||||
import { Status as StatusEntity, Attachment } from 'soapbox/schemas/index.ts';
|
||||
|
@ -72,6 +73,7 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const actualStatus = status ? getActualStatus(status) : undefined;
|
||||
|
||||
|
@ -143,6 +145,7 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
alt={attachment.description}
|
||||
key={attachment.url}
|
||||
onClick={toggleNavigation}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
);
|
||||
} else if (attachment.type === 'video') {
|
||||
|
@ -241,7 +244,7 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='media-modal pointer-events-auto fixed inset-0 z-[9999] flex size-full bg-gray-900/90'>
|
||||
<div className='pointer-events-auto fixed inset-0 z-[9999] flex size-full bg-gray-900/90'>
|
||||
<div
|
||||
className='absolute inset-0'
|
||||
role='presentation'
|
||||
|
@ -256,7 +259,7 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
}
|
||||
justifyContent='between'
|
||||
>
|
||||
<Stack className='relative h-full'>
|
||||
<Stack className='relative h-full items-center'>
|
||||
<HStack
|
||||
alignItems='center'
|
||||
justifyContent='between'
|
||||
|
@ -295,10 +298,10 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
|
||||
{/* Height based on height of top/bottom bars */}
|
||||
<div
|
||||
className='relative h-[calc(100vh-120px)] w-full grow'
|
||||
className='relative flex h-[calc(100vh-120px)] w-full grow items-center justify-center'
|
||||
>
|
||||
{hasMultipleImages && (
|
||||
<div className={clsx('absolute inset-y-0 left-5 z-10 flex items-center transition-opacity', navigationHiddenClassName)}>
|
||||
<div className={clsx('absolute left-5 z-10 flex items-center transition-opacity', navigationHiddenClassName)}>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='flex size-10 items-center justify-center rounded-full bg-gray-900 text-white'
|
||||
|
@ -310,23 +313,20 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className='size-full'>
|
||||
<ReactSwipeableViews
|
||||
style={swipeableViewsStyle}
|
||||
containerStyle={containerStyle}
|
||||
onChangeIndex={handleSwipe}
|
||||
className='flex items-center justify-center'
|
||||
index={getIndex()}
|
||||
>
|
||||
{content}
|
||||
</ReactSwipeableViews>
|
||||
</div>
|
||||
|
||||
{hasMultipleImages && (
|
||||
<div className={clsx('absolute inset-y-0 right-5 z-10 flex items-center transition-opacity', navigationHiddenClassName)}>
|
||||
<div className={clsx('absolute right-5 flex items-center transition-opacity', navigationHiddenClassName)}>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='flex size-10 items-center justify-center rounded-full bg-gray-900 text-white'
|
||||
className='z-10 flex size-10 items-center justify-center rounded-full bg-gray-900 text-white'
|
||||
onClick={handleNextClick}
|
||||
aria-label={intl.formatMessage(messages.next)}
|
||||
>
|
||||
|
@ -336,10 +336,10 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{actualStatus && (
|
||||
{isFullScreen && actualStatus && (
|
||||
<HStack
|
||||
justifyContent='center'
|
||||
className={clsx('absolute bottom-2 flex w-full transition-opacity', navigationHiddenClassName)}
|
||||
className={clsx('absolute bottom-2 flex transition-opacity', navigationHiddenClassName)}
|
||||
>
|
||||
<StatusActionBar
|
||||
status={normalizeStatus(actualStatus) as LegacyStatus}
|
||||
|
|
|
@ -20,6 +20,7 @@ interface IZoomableImage {
|
|||
alt?: string;
|
||||
src: string;
|
||||
onClick?: React.MouseEventHandler;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
class ZoomableImage extends PureComponent<IZoomableImage> {
|
||||
|
@ -32,11 +33,20 @@ class ZoomableImage extends PureComponent<IZoomableImage> {
|
|||
|
||||
state = {
|
||||
scale: MIN_SCALE,
|
||||
isDragging: false,
|
||||
};
|
||||
|
||||
container: HTMLDivElement | null = null;
|
||||
image: HTMLImageElement | null = null;
|
||||
lastDistance = 0;
|
||||
isDragging = false;
|
||||
isMouseDown = false;
|
||||
clickStartTime = 0;
|
||||
startX = 0;
|
||||
startY = 0;
|
||||
startScrollLeft = 0;
|
||||
startScrollTop = 0;
|
||||
isMobile = this.props.isMobile;
|
||||
|
||||
componentDidMount() {
|
||||
this.container?.addEventListener('touchstart', this.handleTouchStart);
|
||||
|
@ -47,7 +57,7 @@ class ZoomableImage extends PureComponent<IZoomableImage> {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.container?.removeEventListener('touchstart', this.handleTouchStart);
|
||||
this.container?.removeEventListener('touchend', this.handleTouchMove);
|
||||
this.container?.removeEventListener('touchmove', this.handleTouchMove);
|
||||
}
|
||||
|
||||
handleTouchStart = (e: TouchEvent) => {
|
||||
|
@ -81,11 +91,54 @@ class ZoomableImage extends PureComponent<IZoomableImage> {
|
|||
this.lastDistance = distance;
|
||||
};
|
||||
|
||||
handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.state.isDragging) {
|
||||
this.setState({ isDragging: false });
|
||||
}
|
||||
|
||||
if (this.state.scale === 1 || !this.container) return;
|
||||
|
||||
this.isDragging = true;
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
this.startScrollLeft = this.container.scrollLeft;
|
||||
this.startScrollTop = this.container.scrollTop;
|
||||
|
||||
window.addEventListener('mousemove', this.handleMouseMove);
|
||||
window.addEventListener('mouseup', this.handleMouseUp);
|
||||
};
|
||||
|
||||
handleMouseMove = (e: MouseEvent) => {
|
||||
if (!this.isDragging || !this.container) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const deltaX = this.startX - e.clientX;
|
||||
const deltaY = this.startY - e.clientY;
|
||||
|
||||
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) this.setState({ isDragging: true });
|
||||
|
||||
this.container.scrollLeft = this.startScrollLeft + deltaX;
|
||||
this.container.scrollTop = this.startScrollTop + deltaY;
|
||||
};
|
||||
|
||||
handleMouseUp = (e: MouseEvent) => {
|
||||
this.isDragging = false;
|
||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||
window.removeEventListener('mouseup', this.handleMouseUp);
|
||||
};
|
||||
|
||||
|
||||
|
||||
zoom(nextScale: number, midpoint: Point) {
|
||||
if (!this.container) return;
|
||||
if (!this.container || this.state.isDragging) return;
|
||||
|
||||
const { scale } = this.state;
|
||||
const { scrollLeft, scrollTop } = this.container;
|
||||
const { scrollLeft, scrollTop, clientWidth, clientHeight } = this.container;
|
||||
|
||||
// math memo:
|
||||
// x = (scrollLeft + midpoint.x) / scrollWidth
|
||||
|
@ -93,6 +146,9 @@ class ZoomableImage extends PureComponent<IZoomableImage> {
|
|||
// scrollWidth = clientWidth * scale
|
||||
// scrollWidth' = clientWidth * nextScale
|
||||
// Solve x = x' for nextScrollLeft
|
||||
const originX = (midpoint.x / clientWidth) * 100;
|
||||
const originY = (midpoint.y / clientHeight) * 100;
|
||||
|
||||
const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x;
|
||||
const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y;
|
||||
|
||||
|
@ -100,14 +156,30 @@ class ZoomableImage extends PureComponent<IZoomableImage> {
|
|||
if (!this.container) return;
|
||||
this.container.scrollLeft = nextScrollLeft;
|
||||
this.container.scrollTop = nextScrollTop;
|
||||
if (!this.isMobile) this.image!.style.transformOrigin = `${originX}% ${originY}%`;
|
||||
});
|
||||
}
|
||||
|
||||
handleClick: React.MouseEventHandler = e => {
|
||||
// don't propagate event to MediaModal
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.isMobile) {
|
||||
const handler = this.props.onClick;
|
||||
if (handler) handler(e);
|
||||
} else {
|
||||
if (this.state.scale !== 1) {
|
||||
this.zoom(1, { x: 0, y: 0 });
|
||||
} else {
|
||||
if (!this.image) return;
|
||||
|
||||
const rect = this.image.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const clickY = e.clientY - rect.top;
|
||||
|
||||
const midpoint: Point = { x: clickX, y: clickY };
|
||||
this.zoom(2, midpoint);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setContainerRef = (c: HTMLDivElement) => {
|
||||
|
@ -127,20 +199,21 @@ class ZoomableImage extends PureComponent<IZoomableImage> {
|
|||
<div
|
||||
className='relative flex size-full items-center justify-center'
|
||||
ref={this.setContainerRef}
|
||||
style={{ overflow }}
|
||||
style={{ overflow, cursor: scale > 1 ? 'grab' : 'default' }}
|
||||
>
|
||||
<img
|
||||
role='presentation'
|
||||
ref={this.setImageRef}
|
||||
alt={alt}
|
||||
className={clsx('size-auto max-h-[80%] max-w-full object-contain', { 'size-full max-h-full': scale !== 1 })}
|
||||
className={clsx('size-auto max-h-[80%] max-w-full object-contain', scale !== 1 ? 'size-full' : 'hover:cursor-pointer')}
|
||||
title={alt}
|
||||
src={src}
|
||||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: '0 0',
|
||||
transformOrigin: `${scale > 1 && !this.isMobile ? 'center' : '0 0'}`,
|
||||
}}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -534,7 +534,7 @@ const Video: React.FC<IVideo> = ({
|
|||
return (
|
||||
<div
|
||||
role='menuitem'
|
||||
className={clsx('relative box-border flex max-w-full overflow-hidden rounded-[10px] bg-black text-white focus:outline-0', { 'w-full h-full m-0': fullscreen })}
|
||||
className={clsx('relative box-border flex h-screen max-w-full items-center justify-center overflow-hidden rounded-[10px] bg-black text-white focus:outline-0', { 'w-full h-full m-0': fullscreen })}
|
||||
style={playerStyle}
|
||||
ref={player}
|
||||
onClick={handleClickRoot}
|
||||
|
|
Ładowanie…
Reference in New Issue