[Glitch] Change zoom icon in web UI

Port e7fd0985c9cd14437443345adfe725ecd3b038a6 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 ( return (
<div className='modal-root' ref={this.setRef}> <div className='modal-root' ref={this.setRef}>
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}> <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 role='dialog' className='modal-root__container'>{children}</div>
</div> </div>
</div> </div>

View File

@ -17,7 +17,7 @@ export default class ImageLoader extends PureComponent {
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
onClick: PropTypes.func, onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool, zoomedIn: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -134,7 +134,7 @@ export default class ImageLoader extends PureComponent {
}; };
render () { 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 { loading } = this.state;
const className = classNames('image-loader', { 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 }}> <div className='loading-bar__container' style={{ width: this.state.width || width }}>
<LoadingBar className='loading-bar' loading={1} /> <LoadingBar className='loading-bar' loading={1} />
</div> </div>
<canvas <canvas
className='image-loader__preview-canvas' className='image-loader__preview-canvas'
ref={this.setCanvasRef} ref={this.setCanvasRef}
@ -164,7 +165,7 @@ export default class ImageLoader extends PureComponent {
onClick={onClick} onClick={onClick}
width={width} width={width}
height={height} height={height}
zoomButtonHidden={this.props.zoomButtonHidden} zoomedIn={zoomedIn}
/> />
)} )}
</div> </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 ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.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 { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
import { GIFV } from 'flavours/glitch/components/gifv'; import { GIFV } from 'flavours/glitch/components/gifv';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
@ -26,6 +28,8 @@ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' }, 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 { class MediaModal extends ImmutablePureComponent {
@ -46,30 +50,39 @@ class MediaModal extends ImmutablePureComponent {
state = { state = {
index: null, index: null,
navigationHidden: false, navigationHidden: false,
zoomButtonHidden: false, zoomedIn: false,
};
handleZoomClick = () => {
this.setState(prevState => ({
zoomedIn: !prevState.zoomedIn,
}));
}; };
handleSwipe = (index) => { handleSwipe = (index) => {
this.setState({ index: index % this.props.media.size }); this.setState({
index: index % this.props.media.size,
zoomedIn: false,
});
}; };
handleTransitionEnd = () => { handleTransitionEnd = () => {
this.setState({ this.setState({
zoomButtonHidden: false, zoomedIn: false,
}); });
}; };
handleNextClick = () => { handleNextClick = () => {
this.setState({ this.setState({
index: (this.getIndex() + 1) % this.props.media.size, index: (this.getIndex() + 1) % this.props.media.size,
zoomButtonHidden: true, zoomedIn: false,
}); });
}; };
handlePrevClick = () => { handlePrevClick = () => {
this.setState({ this.setState({
index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size, 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({ this.setState({
index: index % this.props.media.size, 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; return this.state.index !== null ? this.state.index : this.props.index;
} }
toggleNavigation = () => { handleToggleNavigation = () => {
this.setState(prevState => ({ this.setState(prevState => ({
navigationHidden: !prevState.navigationHidden, navigationHidden: !prevState.navigationHidden,
})); }));
}; };
setRef = c => {
this.setState({
viewportWidth: c?.clientWidth,
viewportHeight: c?.clientHeight,
});
};
render () { render () {
const { media, statusId, lang, intl, onClose } = this.props; const { media, statusId, lang, intl, onClose } = this.props;
const { navigationHidden } = this.state; const { navigationHidden, zoomedIn, viewportWidth, viewportHeight } = this.state;
const index = this.getIndex(); const index = this.getIndex();
@ -160,8 +180,8 @@ class MediaModal extends ImmutablePureComponent {
alt={description} alt={description}
lang={lang} lang={lang}
key={image.get('url')} key={image.get('url')}
onClick={this.toggleNavigation} onClick={this.handleToggleNavigation}
zoomButtonHidden={this.state.zoomButtonHidden} zoomedIn={zoomedIn}
/> />
); );
} else if (image.get('type') === 'video') { } else if (image.get('type') === 'video') {
@ -229,8 +249,11 @@ 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 ( return (
<div className='modal-root__modal media-modal'> <div className='modal-root__modal media-modal' ref={this.setRef}>
<div className='media-modal__closer' role='presentation' onClick={onClose}> <div className='media-modal__closer' role='presentation' onClick={onClose}>
<ReactSwipeableViews <ReactSwipeableViews
style={swipeableViewsStyle} style={swipeableViewsStyle}
@ -245,7 +268,10 @@ class MediaModal extends ImmutablePureComponent {
</div> </div>
<div className={navigationClassName}> <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} {leftNav}
{rightNav} {rightNav}

View File

@ -1,17 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; 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 MIN_SCALE = 1;
const MAX_SCALE = 4; const MAX_SCALE = 4;
const NAV_BAR_HEIGHT = 66; const NAV_BAR_HEIGHT = 66;
@ -104,8 +93,7 @@ class ZoomableImage extends PureComponent {
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
onClick: PropTypes.func, onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool, zoomedIn: PropTypes.bool,
intl: PropTypes.object.isRequired,
}; };
static defaultProps = { static defaultProps = {
@ -131,8 +119,6 @@ class ZoomableImage extends PureComponent {
translateX: null, translateX: null,
translateY: null, translateY: null,
}, },
zoomState: 'expand', // 'expand' 'compress'
navigationHidden: false,
dragPosition: { top: 0, left: 0, x: 0, y: 0 }, dragPosition: { top: 0, left: 0, x: 0, y: 0 },
dragged: false, dragged: false,
lockScroll: { x: 0, y: 0 }, lockScroll: { x: 0, y: 0 },
@ -169,35 +155,20 @@ class ZoomableImage extends PureComponent {
this.container.addEventListener('DOMMouseScroll', handler); this.container.addEventListener('DOMMouseScroll', handler);
this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler)); this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler));
this.initZoomMatrix(); this._initZoomMatrix();
} }
componentWillUnmount () { componentWillUnmount () {
this.removeEventListeners(); this._removeEventListeners();
} }
componentDidUpdate () { componentDidUpdate (prevProps) {
this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' }); if (prevProps.zoomedIn !== this.props.zoomedIn) {
this._toggleZoom();
if (this.state.scale === MIN_SCALE) {
this.container.style.removeProperty('cursor');
} }
} }
UNSAFE_componentWillReceiveProps () { _removeEventListeners () {
// 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 () {
this.removers.forEach(listeners => listeners()); this.removers.forEach(listeners => listeners());
this.removers = []; this.removers = [];
} }
@ -220,9 +191,6 @@ class ZoomableImage extends PureComponent {
}; };
mouseDownHandler = e => { mouseDownHandler = e => {
this.container.style.cursor = 'grabbing';
this.container.style.userSelect = 'none';
this.setState({ dragPosition: { this.setState({ dragPosition: {
left: this.container.scrollLeft, left: this.container.scrollLeft,
top: this.container.scrollTop, top: this.container.scrollTop,
@ -246,9 +214,6 @@ class ZoomableImage extends PureComponent {
}; };
mouseUpHandler = () => { mouseUpHandler = () => {
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
this.image.removeEventListener('mousemove', this.mouseMoveHandler); this.image.removeEventListener('mousemove', this.mouseMoveHandler);
this.image.removeEventListener('mouseup', this.mouseUpHandler); 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 _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate);
const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance); 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.lastMidpoint = midpoint;
this.lastDistance = distance; this.lastDistance = distance;
}; };
zoom(nextScale, midpoint) { _zoom(nextScale, midpoint) {
const { scale, zoomMatrix } = this.state; const { scale, zoomMatrix } = this.state;
const { scrollLeft, scrollTop } = this.container; const { scrollLeft, scrollTop } = this.container;
@ -318,14 +283,13 @@ class ZoomableImage extends PureComponent {
if (dragged) return; if (dragged) return;
const handler = this.props.onClick; const handler = this.props.onClick;
if (handler) handler(); if (handler) handler();
this.setState({ navigationHidden: !this.state.navigationHidden });
}; };
handleMouseDown = e => { handleMouseDown = e => {
e.preventDefault(); e.preventDefault();
}; };
initZoomMatrix = () => { _initZoomMatrix = () => {
const { width, height } = this.props; const { width, height } = this.props;
const { clientWidth, clientHeight } = this.container; const { clientWidth, clientHeight } = this.container;
const { offsetWidth, offsetHeight } = this.image; const { offsetWidth, offsetHeight } = this.image;
@ -357,10 +321,7 @@ class ZoomableImage extends PureComponent {
}); });
}; };
handleZoomClick = e => { _toggleZoom () {
e.preventDefault();
e.stopPropagation();
const { scale, zoomMatrix } = this.state; const { scale, zoomMatrix } = this.state;
if ( scale >= zoomMatrix.rate ) { if ( scale >= zoomMatrix.rate ) {
@ -394,10 +355,7 @@ class ZoomableImage extends PureComponent {
this.container.scrollTop = zoomMatrix.scrollTop; this.container.scrollTop = zoomMatrix.scrollTop;
}); });
} }
}
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
};
setContainerRef = c => { setContainerRef = c => {
this.container = c; this.container = c;
@ -408,29 +366,16 @@ class ZoomableImage extends PureComponent {
}; };
render () { render () {
const { alt, lang, src, width, height, intl } = this.props; const { alt, lang, src, width, height } = this.props;
const { scale, lockTranslate } = this.state; const { scale, lockTranslate, dragged } = this.state;
const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll'; 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 cursor = scale === MIN_SCALE ? null : (dragged ? 'grabbing' : 'grab');
const zoomButtonTitle = this.state.zoomState === 'compress' ? intl.formatMessage(messages.compress) : intl.formatMessage(messages.expand);
return ( 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}
style={{
fontSize: '30px', /* Fontawesome's fa-compress fa-expand is larger than fa-close */
}}
/>
<div <div
className='zoomable-image' className='zoomable-image'
ref={this.setContainerRef} ref={this.setContainerRef}
style={{ overflow }} style={{ overflow, cursor, userSelect: 'none' }}
> >
<img <img
role='presentation' role='presentation'
@ -450,10 +395,8 @@ class ZoomableImage extends PureComponent {
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
/> />
</div> </div>
</>
); );
} }
} }
export default injectIntl(ZoomableImage); export default ZoomableImage;

View File

@ -6212,9 +6212,23 @@ a.status-card {
height: 100%; height: 100%;
position: relative; position: relative;
&__close, &__buttons {
&__zoom-button { position: absolute;
inset-inline-end: 8px;
top: 8px;
z-index: 100;
display: flex;
gap: 8px;
align-items: center;
.icon-button {
color: rgba($white, 0.7); color: rgba($white, 0.7);
padding: 8px;
.icon {
width: 24px;
height: 24px;
}
&:hover, &:hover,
&:focus, &:focus,
@ -6228,6 +6242,7 @@ a.status-card {
} }
} }
} }
}
.media-modal__closer { .media-modal__closer {
position: absolute; position: absolute;
@ -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, .onboarding-modal,
.error-modal, .error-modal,
.embed-modal { .embed-modal {