@ -1,158 +0,0 @@
|
||||
import PropTypes from 'prop-types'; |
||||
|
||||
import classNames from 'classnames'; |
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'; |
||||
import ImmutablePureComponent from 'react-immutable-pure-component'; |
||||
|
||||
import AudiotrackIcon from '@/material-icons/400-24px/music_note.svg?react'; |
||||
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow.svg?react'; |
||||
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react'; |
||||
import { Blurhash } from 'mastodon/components/blurhash'; |
||||
import { Icon } from 'mastodon/components/icon'; |
||||
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state'; |
||||
|
||||
export default class MediaItem extends ImmutablePureComponent { |
||||
|
||||
static propTypes = { |
||||
attachment: ImmutablePropTypes.map.isRequired, |
||||
displayWidth: PropTypes.number.isRequired, |
||||
onOpenMedia: PropTypes.func.isRequired, |
||||
}; |
||||
|
||||
state = { |
||||
visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all', |
||||
loaded: false, |
||||
}; |
||||
|
||||
handleImageLoad = () => { |
||||
this.setState({ loaded: true }); |
||||
}; |
||||
|
||||
handleMouseEnter = e => { |
||||
if (this.hoverToPlay()) { |
||||
e.target.play(); |
||||
} |
||||
}; |
||||
|
||||
handleMouseLeave = e => { |
||||
if (this.hoverToPlay()) { |
||||
e.target.pause(); |
||||
e.target.currentTime = 0; |
||||
} |
||||
}; |
||||
|
||||
hoverToPlay () { |
||||
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1; |
||||
} |
||||
|
||||
handleClick = e => { |
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { |
||||
e.preventDefault(); |
||||
|
||||
if (this.state.visible) { |
||||
this.props.onOpenMedia(this.props.attachment); |
||||
} else { |
||||
this.setState({ visible: true }); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
render () { |
||||
const { attachment, displayWidth } = this.props; |
||||
const { visible, loaded } = this.state; |
||||
|
||||
const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`; |
||||
const height = width; |
||||
const status = attachment.get('status'); |
||||
const title = status.get('spoiler_text') || attachment.get('description'); |
||||
|
||||
let thumbnail, label, icon, content; |
||||
|
||||
if (!visible) { |
||||
icon = ( |
||||
<span className='account-gallery__item__icons'> |
||||
<Icon id='eye-slash' icon={VisibilityOffIcon} /> |
||||
</span> |
||||
); |
||||
} else { |
||||
if (['audio', 'video'].includes(attachment.get('type'))) { |
||||
content = ( |
||||
<img |
||||
src={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])} |
||||
alt={attachment.get('description')} |
||||
lang={status.get('language')} |
||||
onLoad={this.handleImageLoad} |
||||
/> |
||||
); |
||||
|
||||
if (attachment.get('type') === 'audio') { |
||||
label = <Icon id='music' icon={AudiotrackIcon} />; |
||||
} else { |
||||
label = <Icon id='play' icon={PlayArrowIcon} />; |
||||
} |
||||
} else if (attachment.get('type') === 'image') { |
||||
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0; |
||||
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0; |
||||
const x = ((focusX / 2) + .5) * 100; |
||||
const y = ((focusY / -2) + .5) * 100; |
||||
|
||||
content = ( |
||||
<img |
||||
src={attachment.get('preview_url')} |
||||
alt={attachment.get('description')} |
||||
lang={status.get('language')} |
||||
style={{ objectPosition: `${x}% ${y}%` }} |
||||
onLoad={this.handleImageLoad} |
||||
/> |
||||
); |
||||
} else if (attachment.get('type') === 'gifv') { |
||||
content = ( |
||||
<video |
||||
className='media-gallery__item-gifv-thumbnail' |
||||
aria-label={attachment.get('description')} |
||||
title={attachment.get('description')} |
||||
lang={status.get('language')} |
||||
role='application' |
||||
src={attachment.get('url')} |
||||
onMouseEnter={this.handleMouseEnter} |
||||
onMouseLeave={this.handleMouseLeave} |
||||
autoPlay={autoPlayGif} |
||||
playsInline |
||||
loop |
||||
muted |
||||
/> |
||||
); |
||||
|
||||
label = 'GIF'; |
||||
} |
||||
|
||||
thumbnail = ( |
||||
<div className='media-gallery__gifv'> |
||||
{content} |
||||
|
||||
{label && ( |
||||
<div className='media-gallery__item__badges'> |
||||
<span className='media-gallery__gifv__label'>{label}</span> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div className='account-gallery__item' style={{ width, height }}> |
||||
<a className='media-gallery__item-thumbnail' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'> |
||||
<Blurhash |
||||
hash={attachment.get('blurhash')} |
||||
className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} |
||||
dummy={!useBlurhash} |
||||
/> |
||||
|
||||
{visible ? thumbnail : icon} |
||||
</a> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,197 @@
|
||||
import { useState, useCallback } from 'react'; |
||||
|
||||
import classNames from 'classnames'; |
||||
|
||||
import HeadphonesIcon from '@/material-icons/400-24px/headphones-fill.svg?react'; |
||||
import MovieIcon from '@/material-icons/400-24px/movie-fill.svg?react'; |
||||
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react'; |
||||
import { Blurhash } from 'mastodon/components/blurhash'; |
||||
import { Icon } from 'mastodon/components/icon'; |
||||
import { formatTime } from 'mastodon/features/video'; |
||||
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state'; |
||||
import type { Status, MediaAttachment } from 'mastodon/models/status'; |
||||
|
||||
export const MediaItem: React.FC<{ |
||||
attachment: MediaAttachment; |
||||
onOpenMedia: (arg0: MediaAttachment) => void; |
||||
}> = ({ attachment, onOpenMedia }) => { |
||||
const [visible, setVisible] = useState( |
||||
(displayMedia !== 'hide_all' && |
||||
!attachment.getIn(['status', 'sensitive'])) || |
||||
displayMedia === 'show_all', |
||||
); |
||||
const [loaded, setLoaded] = useState(false); |
||||
|
||||
const handleImageLoad = useCallback(() => { |
||||
setLoaded(true); |
||||
}, [setLoaded]); |
||||
|
||||
const handleMouseEnter = useCallback( |
||||
(e: React.MouseEvent<HTMLVideoElement>) => { |
||||
if (e.target instanceof HTMLVideoElement) { |
||||
void e.target.play(); |
||||
} |
||||
}, |
||||
[], |
||||
); |
||||
|
||||
const handleMouseLeave = useCallback( |
||||
(e: React.MouseEvent<HTMLVideoElement>) => { |
||||
if (e.target instanceof HTMLVideoElement) { |
||||
e.target.pause(); |
||||
e.target.currentTime = 0; |
||||
} |
||||
}, |
||||
[], |
||||
); |
||||
|
||||
const handleClick = useCallback( |
||||
(e: React.MouseEvent<HTMLAnchorElement>) => { |
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { |
||||
e.preventDefault(); |
||||
|
||||
if (visible) { |
||||
onOpenMedia(attachment); |
||||
} else { |
||||
setVisible(true); |
||||
} |
||||
} |
||||
}, |
||||
[attachment, visible, onOpenMedia, setVisible], |
||||
); |
||||
|
||||
const status = attachment.get('status') as Status; |
||||
const description = (attachment.getIn(['translation', 'description']) || |
||||
attachment.get('description')) as string | undefined; |
||||
const previewUrl = attachment.get('preview_url') as string; |
||||
const fullUrl = attachment.get('url') as string; |
||||
const avatarUrl = status.getIn(['account', 'avatar_static']) as string; |
||||
const lang = status.get('language') as string; |
||||
const blurhash = attachment.get('blurhash') as string; |
||||
const statusId = status.get('id') as string; |
||||
const acct = status.getIn(['account', 'acct']) as string; |
||||
const type = attachment.get('type') as string; |
||||
|
||||
let thumbnail; |
||||
|
||||
const badges = []; |
||||
|
||||
if (description && description.length > 0) { |
||||
badges.push( |
||||
<span key='alt' className='media-gallery__alt__label'> |
||||
ALT |
||||
</span>, |
||||
); |
||||
} |
||||
|
||||
if (!visible) { |
||||
thumbnail = ( |
||||
<div className='media-gallery__item__overlay'> |
||||
<Icon id='eye-slash' icon={VisibilityOffIcon} /> |
||||
</div> |
||||
); |
||||
} else if (type === 'audio') { |
||||
thumbnail = ( |
||||
<> |
||||
<img |
||||
src={previewUrl || avatarUrl} |
||||
alt={description} |
||||
title={description} |
||||
lang={lang} |
||||
onLoad={handleImageLoad} |
||||
/> |
||||
|
||||
<div className='media-gallery__item__overlay media-gallery__item__overlay--corner'> |
||||
<Icon id='music' icon={HeadphonesIcon} /> |
||||
</div> |
||||
</> |
||||
); |
||||
} else if (type === 'image') { |
||||
const focusX = (attachment.getIn(['meta', 'focus', 'x']) || 0) as number; |
||||
const focusY = (attachment.getIn(['meta', 'focus', 'y']) || 0) as number; |
||||
const x = (focusX / 2 + 0.5) * 100; |
||||
const y = (focusY / -2 + 0.5) * 100; |
||||
|
||||
thumbnail = ( |
||||
<img |
||||
src={previewUrl} |
||||
alt={description} |
||||
title={description} |
||||
lang={lang} |
||||
style={{ objectPosition: `${x}% ${y}%` }} |
||||
onLoad={handleImageLoad} |
||||
/> |
||||
); |
||||
} else if (['video', 'gifv'].includes(type)) { |
||||
const duration = attachment.getIn([ |
||||
'meta', |
||||
'original', |
||||
'duration', |
||||
]) as number; |
||||
|
||||
thumbnail = ( |
||||
<div className='media-gallery__gifv'> |
||||
<video |
||||
className='media-gallery__item-gifv-thumbnail' |
||||
aria-label={description} |
||||
title={description} |
||||
lang={lang} |
||||
src={fullUrl} |
||||
onMouseEnter={handleMouseEnter} |
||||
onMouseLeave={handleMouseLeave} |
||||
onLoadedData={handleImageLoad} |
||||
autoPlay={autoPlayGif} |
||||
playsInline |
||||
loop |
||||
muted |
||||
/> |
||||
|
||||
{type === 'video' && ( |
||||
<div className='media-gallery__item__overlay media-gallery__item__overlay--corner'> |
||||
<Icon id='play' icon={MovieIcon} /> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
|
||||
if (type === 'gifv') { |
||||
badges.push( |
||||
<span key='gif' className='media-gallery__gifv__label'> |
||||
GIF |
||||
</span>, |
||||
); |
||||
} else { |
||||
badges.push( |
||||
<span key='video' className='media-gallery__gifv__label'> |
||||
{formatTime(Math.floor(duration))} |
||||
</span>, |
||||
); |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<div className='media-gallery__item media-gallery__item--square'> |
||||
<Blurhash |
||||
hash={blurhash} |
||||
className={classNames('media-gallery__preview', { |
||||
'media-gallery__preview--hidden': visible && loaded, |
||||
})} |
||||
dummy={!useBlurhash} |
||||
/> |
||||
|
||||
<a |
||||
className='media-gallery__item-thumbnail' |
||||
href={`/@${acct}/${statusId}`} |
||||
onClick={handleClick} |
||||
target='_blank' |
||||
rel='noopener noreferrer' |
||||
> |
||||
{thumbnail} |
||||
</a> |
||||
|
||||
{badges.length > 0 && ( |
||||
<div className='media-gallery__item__badges'>{badges}</div> |
||||
)} |
||||
</div> |
||||
); |
||||
}; |
||||
|
Before Width: | Height: | Size: 634 B After Width: | Height: | Size: 612 B |
|
Before Width: | Height: | Size: 733 B After Width: | Height: | Size: 711 B |
|
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 293 B |
|
Before Width: | Height: | Size: 379 B After Width: | Height: | Size: 327 B |
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 329 B |
|
Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 245 B |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 296 B |
|
After Width: | Height: | Size: 350 B |
|
After Width: | Height: | Size: 426 B |
|
After Width: | Height: | Size: 771 B |
|
Before Width: | Height: | Size: 790 B After Width: | Height: | Size: 771 B |
|
After Width: | Height: | Size: 287 B |
|
After Width: | Height: | Size: 324 B |
|
Before Width: | Height: | Size: 259 B After Width: | Height: | Size: 258 B |
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 390 B |
|
Before Width: | Height: | Size: 448 B After Width: | Height: | Size: 445 B |
|
Before Width: | Height: | Size: 777 B After Width: | Height: | Size: 773 B |