diff --git a/app/javascript/gabsocial/components/media_gallery.js b/app/javascript/gabsocial/components/media_gallery.js index 56618462..5e624c6b 100644 --- a/app/javascript/gabsocial/components/media_gallery.js +++ b/app/javascript/gabsocial/components/media_gallery.js @@ -8,6 +8,7 @@ import { isIOS } from '../is_mobile'; import classNames from 'classnames'; import { autoPlayGif, displayMedia } from '../initial_state'; import { decode } from 'blurhash'; +import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio'; const messages = defineMessages({ toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, @@ -23,6 +24,7 @@ class Item extends React.PureComponent { onClick: PropTypes.func.isRequired, displayWidth: PropTypes.number, visible: PropTypes.bool.isRequired, + dimensions: PropTypes.object, }; static defaultProps = { @@ -101,62 +103,35 @@ class Item extends React.PureComponent { } render () { - const { attachment, index, size, standalone, displayWidth, visible } = this.props; + const { attachment, index, size, standalone, displayWidth, visible, dimensions } = this.props; - let width = 50; - let height = 100; + const ar = attachment.getIn(['meta', 'small', 'aspect']); + + let width = 100; + let height = '100%'; let top = 'auto'; let left = 'auto'; let bottom = 'auto'; let right = 'auto'; + let float = 'left'; + let position = 'relative'; - if (size === 1) { - width = 100; - } - - if (size === 4 || (size === 3 && index > 0)) { - height = 50; - } - - if (size === 2) { - if (index === 0) { - right = '2px'; - } else { - left = '2px'; - } - } else if (size === 3) { - if (index === 0) { - right = '2px'; - } else if (index > 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } + if (dimensions) { + width = dimensions.w; + height = dimensions.h; + top = dimensions.t || 'auto'; + right = dimensions.r || 'auto'; + bottom = dimensions.b || 'auto'; + left = dimensions.l || 'auto'; + float = dimensions.float || 'left'; + position = dimensions.pos || 'relative'; } let thumbnail = ''; if (attachment.get('type') === 'unknown') { return ( -
+
@@ -222,7 +197,7 @@ class Item extends React.PureComponent { } return ( -
+
{visible && thumbnail}
@@ -278,7 +253,7 @@ class MediaGallery extends React.PureComponent { } handleRef = (node) => { - if (node /*&& this.isStandaloneEligible()*/) { + if (node) { // offsetWidth triggers a layout, so only calculate when we need to if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth); @@ -288,11 +263,6 @@ class MediaGallery extends React.PureComponent { } } - isStandaloneEligible() { - const { media, standalone } = this.props; - return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']); - } - render () { const { media, intl, sensitive, height, defaultWidth } = this.props; const { visible } = this.state; @@ -302,24 +272,221 @@ class MediaGallery extends React.PureComponent { let children, spoilerButton; const style = {}; + const size = media.take(4).size; - if (this.isStandaloneEligible()) { - if (width) { - style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); + const standard169 = width / (16 / 9); + const standard169_percent = 100 / (16 / 9); + const standard169_px = `${standard169}px`; + const panoSize = Math.floor(width / maximumAspectRatio); + const panoSize_px = `${Math.floor(width / maximumAspectRatio)}px`; + let itemsDimensions = []; + + if (size == 1 && width) { + const aspectRatio = media.getIn([0, 'meta', 'small', 'aspect']); + + if (isPanoramic(aspectRatio)) { + style.height = Math.floor(width / maximumAspectRatio); + } else if (isPortrait(aspectRatio)) { + style.height = Math.floor(width / minimumAspectRatio); + } else { + style.height = Math.floor(width / aspectRatio); + } + } else if (size > 1 && width) { + const ar1 = media.getIn([0, 'meta', 'small', 'aspect']); + const ar2 = media.getIn([1, 'meta', 'small', 'aspect']); + const ar3 = media.getIn([2, 'meta', 'small', 'aspect']); + const ar4 = media.getIn([3, 'meta', 'small', 'aspect']); + + if (size == 2) { + if (isPortrait(ar1) && isPortrait(ar2)) { + style.height = width - (width / maximumAspectRatio); + } else if (isPanoramic(ar1) && isPanoramic(ar2)) { + style.height = panoSize * 2; + } else if ( + (isPanoramic(ar1) && isPortrait(ar2)) || + (isPortrait(ar1) && isPanoramic(ar2)) || + (isPanoramic(ar1) && isNonConformingRatio(ar2)) || + (isNonConformingRatio(ar1) && isPanoramic(ar2)) + ) { + style.height = (width * 0.6) + (width / maximumAspectRatio); + } else { + style.height = width / 2; + } + + // + + if (isPortrait(ar1) && isPortrait(ar2)) { + itemsDimensions = [ + { w: 50, h: '100%', r: '2px' }, + { w: 50, h: '100%', l: '2px' } + ]; + } else if (isPanoramic(ar1) && isPanoramic(ar2)) { + itemsDimensions = [ + { w: 100, h: panoSize_px, b: '2px' }, + { w: 100, h: panoSize_px, t: '2px' } + ]; + } else if ( + (isPanoramic(ar1) && isPortrait(ar2)) || + (isPanoramic(ar1) && isNonConformingRatio(ar2)) + ) { + itemsDimensions = [ + { w: 100, h: `${(width / maximumAspectRatio)}px`, b: '2px' }, + { w: 100, h: `${(width * 0.6)}px`, t: '2px' }, + ]; + } else if ( + (isPortrait(ar1) && isPanoramic(ar2)) || + (isNonConformingRatio(ar1) && isPanoramic(ar2)) + ) { + itemsDimensions = [ + { w: 100, h: `${(width * 0.6)}px`, b: '2px' }, + { w: 100, h: `${(width / maximumAspectRatio)}px`, t: '2px' }, + ]; + } else { + itemsDimensions = [ + { w: 50, h: '100%', r: '2px' }, + { w: 50, h: '100%', l: '2px' } + ]; + } + } else if (size == 3) { + if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) { + style.height = panoSize * 3; + } else if (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) { + style.height = Math.floor(width / minimumAspectRatio); + } else { + style.height = width; + } + + // + + if (isPanoramic(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) { + itemsDimensions = [ + { w: 100, h: `50%`, b: '2px' }, + { w: 50, h: '50%', t: '2px', r: '2px' }, + { w: 50, h: '50%', t: '2px', l: '2px' } + ]; + } else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) { + itemsDimensions = [ + { w: 100, h: panoSize_px, b: '4px' }, + { w: 100, h: panoSize_px }, + { w: 100, h: panoSize_px, t: '4px' } + ]; + } else if (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) { + itemsDimensions = [ + { w: 50, h: `100%`, r: '2px' }, + { w: 50, h: '50%', b: '2px', l: '2px' }, + { w: 50, h: '50%', t: '2px', l: '2px' }, + ]; + } else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3)) { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '50%', l: '-2px', b: '-2px', pos: 'absolute', float: 'none' }, + { w: 50, h: `100%`, r: '-2px', t: '0px', b: '0px', pos: 'absolute', float: 'none' } + ]; + } else if ( + (isNonConformingRatio(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3)) || + (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) + ) { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: `100%`, l: '2px', float: 'right' }, + { w: 50, h: '50%', t: '2px', r: '2px' } + ]; + } else if ( + (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3)) || + (isPanoramic(ar1) && isPanoramic(ar2) && isPortrait(ar3)) + ) { + itemsDimensions = [ + { w: 50, h: panoSize_px, b: '2px', r: '2px' }, + { w: 50, h: panoSize_px, b: '2px', l: '2px' }, + { w: 100, h: `${width - panoSize}px`, t: '2px' } + ]; + } else if ( + (isNonConformingRatio(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) || + (isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) + ) { + itemsDimensions = [ + { w: 100, h: `${width - panoSize}px`, b: '2px' }, + { w: 50, h: panoSize_px, t: '2px', r: '2px' }, + { w: 50, h: panoSize_px, t: '2px', l: '2px' }, + ]; + } else { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '50%', b: '2px', l: '2px' }, + { w: 100, h: `50%`, t: '2px' } + ]; + } + } else if (size == 4) { + if ( + (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) || + (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isNonConformingRatio(ar4)) || + (isPortrait(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3) && isPortrait(ar4)) || + (isPortrait(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3) && isPortrait(ar4)) || + (isNonConformingRatio(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) + ) { + style.height = Math.floor(width / minimumAspectRatio); + } else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) { + style.height = panoSize * 2; + } else if ( + (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) || + (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) + ) { + style.height = panoSize + (width / 2); + } else { + style.height = width; + } + + // + + if (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) { + itemsDimensions = [ + { w: 50, h: panoSize_px, b: '2px', r: '2px' }, + { w: 50, h: panoSize_px, b: '2px', l: '2px' }, + { w: 50, h: `${(width / 2)}px`, t: '2px', r: '2px' }, + { w: 50, h: `${(width / 2)}px`, t: '2px', l: '2px' }, + ]; + } else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) { + itemsDimensions = [ + { w: 50, h: `${(width / 2)}px`, b: '2px', r: '2px' }, + { w: 50, h: `${(width / 2)}px`, b: '2px', l: '2px' }, + { w: 50, h: panoSize_px, t: '2px', r: '2px' }, + { w: 50, h: panoSize_px, t: '2px', l: '2px' }, + ]; + } else if ( + (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) || + (isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) + ) { + itemsDimensions = [ + { w: 67, h: '100%', r: '2px' }, + { w: 33, h: '33%', b: '4px', l: '2px' }, + { w: 33, h: '33%', l: '2px' }, + { w: 33, h: '33%', t: '4px', l: '2px' } + ]; + } else { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '50%', b: '2px', l: '2px' }, + { w: 50, h: '50%', t: '2px', r: '2px' }, + { w: 50, h: '50%', t: '2px', l: '2px' }, + ]; + } } - } else if (width) { - style.height = width / (16/9); } else { style.height = height; } - const size = media.take(4).size; - - if (this.isStandaloneEligible()) { - children = ; - } else { - children = media.take(4).map((attachment, i) => ); - } + children = media.take(4).map((attachment, i) => ( + + )); if (visible) { spoilerButton = ; diff --git a/app/javascript/gabsocial/components/status.js b/app/javascript/gabsocial/components/status.js index 58389e1a..091ad617 100644 --- a/app/javascript/gabsocial/components/status.js +++ b/app/javascript/gabsocial/components/status.js @@ -344,6 +344,7 @@ class Status extends ImmutablePureComponent { blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} + aspectRatio={video.getIn(['meta', 'small', 'aspect'])} width={this.props.cachedMediaWidth} height={110} inline diff --git a/app/javascript/gabsocial/features/report/components/status_check_box.js b/app/javascript/gabsocial/features/report/components/status_check_box.js index c29e517d..9991216d 100644 --- a/app/javascript/gabsocial/features/report/components/status_check_box.js +++ b/app/javascript/gabsocial/features/report/components/status_check_box.js @@ -38,6 +38,7 @@ export default class StatusCheckBox extends React.PureComponent { blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} + aspectRatio={video.getIn(['meta', 'small', 'aspect'])} width={239} height={110} inline diff --git a/app/javascript/gabsocial/features/status/components/detailed_status.js b/app/javascript/gabsocial/features/status/components/detailed_status.js index b8f2f773..7b87b54b 100644 --- a/app/javascript/gabsocial/features/status/components/detailed_status.js +++ b/app/javascript/gabsocial/features/status/components/detailed_status.js @@ -111,6 +111,7 @@ export default class DetailedStatus extends ImmutablePureComponent { blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} + aspectRatio={video.getIn(['meta', 'small', 'aspect'])} width={300} height={150} inline diff --git a/app/javascript/gabsocial/features/video/index.js b/app/javascript/gabsocial/features/video/index.js index 64a24cbe..17deea6a 100644 --- a/app/javascript/gabsocial/features/video/index.js +++ b/app/javascript/gabsocial/features/video/index.js @@ -8,6 +8,7 @@ import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/full import { displayMedia } from '../../initial_state'; import Icon from 'gabsocial/components/icon'; import { decode } from 'blurhash'; +import { isPanoramic, isPortrait, minimumAspectRatio, maximumAspectRatio } from '../../utils/media_aspect_ratio'; const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, @@ -107,6 +108,7 @@ class Video extends React.PureComponent { intl: PropTypes.object.isRequired, blurhash: PropTypes.string, link: PropTypes.node, + aspectRatio: PropTypes.number, }; state = { @@ -373,7 +375,7 @@ class Video extends React.PureComponent { } render () { - const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link } = this.props; + const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, aspectRatio } = this.props; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = (currentTime / duration) * 100; @@ -384,8 +386,16 @@ class Video extends React.PureComponent { let { width, height } = this.props; if (inline && containerWidth) { - width = containerWidth; - height = containerWidth / (16/9); + width = containerWidth; + const minSize = containerWidth / (16/9); + + if (isPanoramic(aspectRatio)) { + height = Math.max(Math.floor(containerWidth / maximumAspectRatio), minSize); + } else if (isPortrait(aspectRatio)) { + height = Math.max(Math.floor(containerWidth / minimumAspectRatio), minSize); + } else { + height = Math.floor(containerWidth / aspectRatio); + } playerStyle.height = height; } diff --git a/app/javascript/gabsocial/utils/media_aspect_ratio.js b/app/javascript/gabsocial/utils/media_aspect_ratio.js new file mode 100644 index 00000000..0a7d740a --- /dev/null +++ b/app/javascript/gabsocial/utils/media_aspect_ratio.js @@ -0,0 +1,17 @@ +export const minimumAspectRatio = .8; +export const maximumAspectRatio = 2.8; + +export const isPanoramic = ar => { + if (isNaN(ar)) return false; + return ar >= maximumAspectRatio; +} + +export const isPortrait = ar => { + if (isNaN(ar)) return false; + return ar <= minimumAspectRatio; +} + +export const isNonConformingRatio = ar => { + if (isNaN(ar)) return false; + return !isPanoramic(ar) && !isPortrait(ar); +} diff --git a/app/javascript/styles/gabsocial/components.scss b/app/javascript/styles/gabsocial/components.scss index 9d1c7899..01fabb5e 100644 --- a/app/javascript/styles/gabsocial/components.scss +++ b/app/javascript/styles/gabsocial/components.scss @@ -3580,13 +3580,6 @@ a.status-card.compact:hover { position: relative; border-radius: 4px; overflow: hidden; - - &.standalone { - .media-gallery__item-gifv-thumbnail { - transform: none; - top: 0; - } - } } .media-gallery__item-thumbnail { @@ -3636,10 +3629,10 @@ a.status-card.compact:hover { height: 100%; object-fit: cover; position: relative; - top: 50%; - transform: translateY(-50%); width: 100%; z-index: 1; + transform: none; + top: 0; } .media-gallery__item-thumbnail-label {