[Glitch] Change zoom icon in web UI

Port e7fd0985c9 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Eugen Rochko 2024-09-20 11:42:02 +02:00 committed by Claire
parent 03829d8e1d
commit a969c6a6a6
5 changed files with 106 additions and 143 deletions

View file

@ -153,7 +153,7 @@ class ModalRoot extends PureComponent {
return (
<div className='modal-root' ref={this.setRef}>
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.7)` : null }} />
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.9)` : null }} />
<div role='dialog' className='modal-root__container'>{children}</div>
</div>
</div>

View file

@ -17,7 +17,7 @@ export default class ImageLoader extends PureComponent {
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool,
zoomedIn: PropTypes.bool,
};
static defaultProps = {
@ -134,7 +134,7 @@ export default class ImageLoader extends PureComponent {
};
render () {
const { alt, lang, src, width, height, onClick } = this.props;
const { alt, lang, src, width, height, onClick, zoomedIn } = this.props;
const { loading } = this.state;
const className = classNames('image-loader', {
@ -149,6 +149,7 @@ export default class ImageLoader extends PureComponent {
<div className='loading-bar__container' style={{ width: this.state.width || width }}>
<LoadingBar className='loading-bar' loading={1} />
</div>
<canvas
className='image-loader__preview-canvas'
ref={this.setCanvasRef}
@ -164,7 +165,7 @@ export default class ImageLoader extends PureComponent {
onClick={onClick}
width={width}
height={height}
zoomButtonHidden={this.props.zoomButtonHidden}
zoomedIn={zoomedIn}
/>
)}
</div>

View file

@ -12,6 +12,8 @@ import ReactSwipeableViews from 'react-swipeable-views';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import FitScreenIcon from '@/material-icons/400-24px/fit_screen.svg?react';
import ActualSizeIcon from '@/svg-icons/actual_size.svg?react';
import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
import { GIFV } from 'flavours/glitch/components/gifv';
import { Icon } from 'flavours/glitch/components/icon';
@ -26,6 +28,8 @@ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
zoomIn: { id: 'lightbox.zoom_in', defaultMessage: 'Zoom to actual size' },
zoomOut: { id: 'lightbox.zoom_out', defaultMessage: 'Zoom to fit' },
});
class MediaModal extends ImmutablePureComponent {
@ -46,30 +50,39 @@ class MediaModal extends ImmutablePureComponent {
state = {
index: null,
navigationHidden: false,
zoomButtonHidden: false,
zoomedIn: false,
};
handleZoomClick = () => {
this.setState(prevState => ({
zoomedIn: !prevState.zoomedIn,
}));
};
handleSwipe = (index) => {
this.setState({ index: index % this.props.media.size });
this.setState({
index: index % this.props.media.size,
zoomedIn: false,
});
};
handleTransitionEnd = () => {
this.setState({
zoomButtonHidden: false,
zoomedIn: false,
});
};
handleNextClick = () => {
this.setState({
index: (this.getIndex() + 1) % this.props.media.size,
zoomButtonHidden: true,
zoomedIn: false,
});
};
handlePrevClick = () => {
this.setState({
index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size,
zoomButtonHidden: true,
zoomedIn: false,
});
};
@ -78,7 +91,7 @@ class MediaModal extends ImmutablePureComponent {
this.setState({
index: index % this.props.media.size,
zoomButtonHidden: true,
zoomedIn: false,
});
};
@ -130,15 +143,22 @@ class MediaModal extends ImmutablePureComponent {
return this.state.index !== null ? this.state.index : this.props.index;
}
toggleNavigation = () => {
handleToggleNavigation = () => {
this.setState(prevState => ({
navigationHidden: !prevState.navigationHidden,
}));
};
setRef = c => {
this.setState({
viewportWidth: c?.clientWidth,
viewportHeight: c?.clientHeight,
});
};
render () {
const { media, statusId, lang, intl, onClose } = this.props;
const { navigationHidden } = this.state;
const { navigationHidden, zoomedIn, viewportWidth, viewportHeight } = this.state;
const index = this.getIndex();
@ -160,8 +180,8 @@ class MediaModal extends ImmutablePureComponent {
alt={description}
lang={lang}
key={image.get('url')}
onClick={this.toggleNavigation}
zoomButtonHidden={this.state.zoomButtonHidden}
onClick={this.handleToggleNavigation}
zoomedIn={zoomedIn}
/>
);
} else if (image.get('type') === 'video') {
@ -229,9 +249,12 @@ class MediaModal extends ImmutablePureComponent {
));
}
const currentMedia = media.get(index);
const zoomable = currentMedia.get('type') === 'image' && (currentMedia.getIn(['meta', 'original', 'width']) > viewportWidth || currentMedia.getIn(['meta', 'original', 'height']) > viewportHeight);
return (
<div className='modal-root__modal media-modal'>
<div className='media-modal__closer' role='presentation' onClick={onClose} >
<div className='modal-root__modal media-modal' ref={this.setRef}>
<div className='media-modal__closer' role='presentation' onClick={onClose}>
<ReactSwipeableViews
style={swipeableViewsStyle}
containerStyle={containerStyle}
@ -245,7 +268,10 @@ class MediaModal extends ImmutablePureComponent {
</div>
<div className={navigationClassName}>
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={40} />
<div className='media-modal__buttons'>
{zoomable && <IconButton title={intl.formatMessage(zoomedIn ? messages.zoomOut : messages.zoomIn)} iconComponent={zoomedIn ? FitScreenIcon : ActualSizeIcon} onClick={this.handleZoomClick} />}
<IconButton title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} />
</div>
{leftNav}
{rightNav}

View file

@ -1,17 +1,6 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import FullscreenExitIcon from '@/material-icons/400-24px/fullscreen_exit.svg?react';
import RectangleIcon from '@/material-icons/400-24px/rectangle.svg?react';
import { IconButton } from 'flavours/glitch/components/icon_button';
const messages = defineMessages({
compress: { id: 'lightbox.compress', defaultMessage: 'Compress image view box' },
expand: { id: 'lightbox.expand', defaultMessage: 'Expand image view box' },
});
const MIN_SCALE = 1;
const MAX_SCALE = 4;
const NAV_BAR_HEIGHT = 66;
@ -104,8 +93,7 @@ class ZoomableImage extends PureComponent {
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool,
intl: PropTypes.object.isRequired,
zoomedIn: PropTypes.bool,
};
static defaultProps = {
@ -131,8 +119,6 @@ class ZoomableImage extends PureComponent {
translateX: null,
translateY: null,
},
zoomState: 'expand', // 'expand' 'compress'
navigationHidden: false,
dragPosition: { top: 0, left: 0, x: 0, y: 0 },
dragged: false,
lockScroll: { x: 0, y: 0 },
@ -169,35 +155,20 @@ class ZoomableImage extends PureComponent {
this.container.addEventListener('DOMMouseScroll', handler);
this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler));
this.initZoomMatrix();
this._initZoomMatrix();
}
componentWillUnmount () {
this.removeEventListeners();
this._removeEventListeners();
}
componentDidUpdate () {
this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' });
if (this.state.scale === MIN_SCALE) {
this.container.style.removeProperty('cursor');
componentDidUpdate (prevProps) {
if (prevProps.zoomedIn !== this.props.zoomedIn) {
this._toggleZoom();
}
}
UNSAFE_componentWillReceiveProps () {
// reset when slide to next image
if (this.props.zoomButtonHidden) {
this.setState({
scale: MIN_SCALE,
lockTranslate: { x: 0, y: 0 },
}, () => {
this.container.scrollLeft = 0;
this.container.scrollTop = 0;
});
}
}
removeEventListeners () {
_removeEventListeners () {
this.removers.forEach(listeners => listeners());
this.removers = [];
}
@ -220,9 +191,6 @@ class ZoomableImage extends PureComponent {
};
mouseDownHandler = e => {
this.container.style.cursor = 'grabbing';
this.container.style.userSelect = 'none';
this.setState({ dragPosition: {
left: this.container.scrollLeft,
top: this.container.scrollTop,
@ -246,9 +214,6 @@ class ZoomableImage extends PureComponent {
};
mouseUpHandler = () => {
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
this.image.removeEventListener('mousemove', this.mouseMoveHandler);
this.image.removeEventListener('mouseup', this.mouseUpHandler);
};
@ -276,13 +241,13 @@ class ZoomableImage extends PureComponent {
const _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate);
const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance);
this.zoom(scale, midpoint);
this._zoom(scale, midpoint);
this.lastMidpoint = midpoint;
this.lastDistance = distance;
};
zoom(nextScale, midpoint) {
_zoom(nextScale, midpoint) {
const { scale, zoomMatrix } = this.state;
const { scrollLeft, scrollTop } = this.container;
@ -318,14 +283,13 @@ class ZoomableImage extends PureComponent {
if (dragged) return;
const handler = this.props.onClick;
if (handler) handler();
this.setState({ navigationHidden: !this.state.navigationHidden });
};
handleMouseDown = e => {
e.preventDefault();
};
initZoomMatrix = () => {
_initZoomMatrix = () => {
const { width, height } = this.props;
const { clientWidth, clientHeight } = this.container;
const { offsetWidth, offsetHeight } = this.image;
@ -357,10 +321,7 @@ class ZoomableImage extends PureComponent {
});
};
handleZoomClick = e => {
e.preventDefault();
e.stopPropagation();
_toggleZoom () {
const { scale, zoomMatrix } = this.state;
if ( scale >= zoomMatrix.rate ) {
@ -394,10 +355,7 @@ class ZoomableImage extends PureComponent {
this.container.scrollTop = zoomMatrix.scrollTop;
});
}
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
};
}
setContainerRef = c => {
this.container = c;
@ -408,52 +366,37 @@ class ZoomableImage extends PureComponent {
};
render () {
const { alt, lang, src, width, height, intl } = this.props;
const { scale, lockTranslate } = this.state;
const { alt, lang, src, width, height } = this.props;
const { scale, lockTranslate, dragged } = this.state;
const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : '';
const zoomButtonTitle = this.state.zoomState === 'compress' ? intl.formatMessage(messages.compress) : intl.formatMessage(messages.expand);
const cursor = scale === MIN_SCALE ? null : (dragged ? 'grabbing' : 'grab');
return (
<>
<IconButton
className={`media-modal__zoom-button ${zoomButtonShouldHide}`}
title={zoomButtonTitle}
icon={this.state.zoomState}
iconComponent={this.state.zoomState === 'compress' ? FullscreenExitIcon : RectangleIcon}
onClick={this.handleZoomClick}
size={40}
<div
className='zoomable-image'
ref={this.setContainerRef}
style={{ overflow, cursor, userSelect: 'none' }}
>
<img
role='presentation'
ref={this.setImageRef}
alt={alt}
title={alt}
lang={lang}
src={src}
width={width}
height={height}
style={{
fontSize: '30px', /* Fontawesome's fa-compress fa-expand is larger than fa-close */
transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
transformOrigin: '0 0',
}}
draggable={false}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
/>
<div
className='zoomable-image'
ref={this.setContainerRef}
style={{ overflow }}
>
<img
role='presentation'
ref={this.setImageRef}
alt={alt}
title={alt}
lang={lang}
src={src}
width={width}
height={height}
style={{
transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
transformOrigin: '0 0',
}}
draggable={false}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
/>
</div>
</>
</div>
);
}
}
export default injectIntl(ZoomableImage);
export default ZoomableImage;

View file

@ -6212,19 +6212,34 @@ a.status-card {
height: 100%;
position: relative;
&__close,
&__zoom-button {
color: rgba($white, 0.7);
&__buttons {
position: absolute;
inset-inline-end: 8px;
top: 8px;
z-index: 100;
display: flex;
gap: 8px;
align-items: center;
&:hover,
&:focus,
&:active {
color: $white;
background-color: rgba($white, 0.15);
}
.icon-button {
color: rgba($white, 0.7);
padding: 8px;
&:focus {
background-color: rgba($white, 0.3);
.icon {
width: 24px;
height: 24px;
}
&:hover,
&:focus,
&:active {
color: $white;
background-color: rgba($white, 0.15);
}
&:focus {
background-color: rgba($white, 0.3);
}
}
}
}
@ -6385,28 +6400,6 @@ a.status-card {
}
}
.media-modal__close {
position: absolute;
inset-inline-end: 8px;
top: 8px;
z-index: 100;
}
.media-modal__zoom-button {
position: absolute;
inset-inline-end: 64px;
top: 8px;
z-index: 100;
pointer-events: auto;
transition: opacity 0.3s linear;
will-change: opacity;
}
.media-modal__zoom-button--hidden {
pointer-events: none;
opacity: 0;
}
.onboarding-modal,
.error-modal,
.embed-modal {