alt={attachment.get('description')}
poster={status.getIn(['account', 'avatar_static'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
- peaks={[0]}
width={this.props.cachedMediaWidth}
- height={70}
+ height={110}
cacheWidth={this.props.cacheMediaWidth}
/>
)}
import classNames from 'classnames';
import { throttle } from 'lodash';
import { encode, decode } from 'blurhash';
-import { getPointerPosition } from 'mastodon/features/video';
+import { getPointerPosition, fileNameFromURL } from 'mastodon/features/video';
const digitCharacters = [
'0',
});
const TICK_SIZE = 10;
-const PADDING = 180;
+const PADDING = 180;
export default @injectIntl
class Audio extends React.PureComponent {
alt: PropTypes.string,
poster: PropTypes.string,
duration: PropTypes.number,
- peaks: PropTypes.arrayOf(PropTypes.number),
width: PropTypes.number,
height: PropTypes.number,
- preload: PropTypes.bool,
editable: PropTypes.bool,
intl: PropTypes.object.isRequired,
cacheWidth: PropTypes.func,
color: { r: 255, g: 255, b: 255 },
};
- // Hard coded in components.scss
- // Any way to get ::before values programatically?
- volWidth = 50;
- volOffset = 70;
-
- volHandleOffset = v => {
- const offset = v * this.volWidth + this.volOffset;
-
- return (offset > 110) ? 110 : offset;
- }
-
setPlayerRef = c => {
this.player = c;
}
handleMouseVolSlide = throttle(e => {
- const rect = this.volume.getBoundingClientRect();
- const x = (e.clientX - rect.left) / this.volWidth; // x position within the element.
+ const { x } = getPointerPosition(this.volume, e);
if(!isNaN(x)) {
- let slideamt = x;
-
- if (x > 1) {
- slideamt = 1;
- } else if(x < 0) {
- slideamt = 0;
- }
-
- this.setState({ volume: slideamt }, () => {
- this.audio.volume = slideamt;
+ this.setState({ volume: x }, () => {
+ this.audio.volume = x;
});
}
}, 60);
}
}, 150, { trailing: true });
+ handleMouseEnter = () => {
+ this.setState({ hovered: true });
+ }
+
+ handleMouseLeave = () => {
+ this.setState({ hovered: false });
+ }
+
_initAudioContext () {
const context = new AudioContext();
const analyser = context.createAnalyser();
});
}
+ handleDownload = () => {
+ fetch(this.props.src).then(res => res.blob()).then(blob => {
+ const element = document.createElement('a');
+ const objectURL = URL.createObjectURL(blob);
+
+ element.setAttribute('href', objectURL);
+ element.setAttribute('download', fileNameFromURL(this.props.src));
+
+ document.body.appendChild(element);
+ element.click();
+ document.body.removeChild(element);
+
+ URL.revokeObjectURL(objectURL);
+ }).catch(err => {
+ console.error(err);
+ });
+ }
+
_renderCanvas () {
requestAnimationFrame(() => {
this._clear();
render () {
const { src, intl, alt, editable } = this.props;
const { paused, muted, volume, currentTime, duration, buffer, darkText, dragging } = this.state;
-
- const volumeWidth = muted ? 0 : volume * this.volWidth;
- const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume);
- const progress = (currentTime / duration) * 100;
+ const progress = (currentTime / duration) * 100;
return (
- <div className={classNames('audio-player', { editable, 'with-light-background': darkText })} ref={this.setPlayerRef} style={{ width: '100%', height: this.state.height || this.props.height }}>
+ <div className={classNames('audio-player', { editable, 'with-light-background': darkText })} ref={this.setPlayerRef} style={{ width: '100%', height: this.state.height || this.props.height }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<audio
src={src}
ref={this.setAudioRef}
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
- <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
-
- <div className='video-player__volume__current' style={{ width: `${volumeWidth}px`, backgroundColor: this._getColor() }} />
+ <div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
+ <div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getColor() }} />
<span
className={classNames('video-player__volume__handle')}
tabIndex='0'
- style={{ left: `${volumeHandleLoc}px`, backgroundColor: this._getColor() }}
+ style={{ left: `${volume * 100}%`, backgroundColor: this._getColor() }}
/>
</div>
- <span>
+ <span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(currentTime)}</span>
<span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
</div>
<div className='video-player__buttons right'>
- <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}>
- <a className='video-player__download__icon' href={this.props.src} download>
- <Icon id='download' fixedWidth />
- </a>
- </button>
+ <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} onClick={this.handleDownload}><Icon id='download' fixedWidth /></button>
</div>
</div>
</div>
alt={attachment.get('description')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={status.getIn(['account', 'avatar_static'])}
- height={110}
- preload
+ height={150}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
- download: { id: 'video.download', defaultMessage: 'Download file' },
});
export const formatTime = secondsNum => {
return position;
};
+export const fileNameFromURL = str => {
+ const url = new URL(str);
+ const pathname = url.pathname;
+ const index = pathname.lastIndexOf('/');
+
+ return pathname.substring(index + 1);
+};
+
export default @injectIntl
class Video extends React.PureComponent {
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
};
- // Hard-coded in components.scss
- // Any way to get ::before values programatically?
- volWidth = 50;
- volOffset = 70;
-
- volHandleOffset = v => {
- const offset = v * this.volWidth + this.volOffset;
-
- return (offset > 110) ? 110 : offset;
- }
-
setPlayerRef = c => {
this.player = c;
}
handleMouseVolSlide = throttle(e => {
- const rect = this.volume.getBoundingClientRect();
- const x = (e.clientX - rect.left) / this.volWidth; //x position within the element.
+ const { x } = getPointerPosition(this.volume, e);
if(!isNaN(x)) {
- let slideamt = x;
-
- if(x > 1) {
- slideamt = 1;
- } else if(x < 0) {
- slideamt = 0;
- }
-
- this.video.volume = slideamt;
- this.setState({ volume: slideamt });
+ this.setState({ volume: x }, () => {
+ this.video.volume = x;
+ });
}
}, 60);
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable } = this.props;
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = (currentTime / duration) * 100;
-
- const volumeWidth = (muted) ? 0 : volume * this.volWidth;
- const volumeHandleLoc = (muted) ? this.volHandleOffset(0) : this.volHandleOffset(volume);
const playerStyle = {};
let { width, height } = this.props;
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
- <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
-
- <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
+ <div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
+ <div className='video-player__volume__current' style={{ width: `${volume * 100}%` }} />
+
<span
className={classNames('video-player__volume__handle')}
tabIndex='0'
- style={{ left: `${volumeHandleLoc}px` }}
+ style={{ left: `${volume * 100}%` }}
/>
</div>
{(detailed || fullscreen) && (
- <span>
+ <span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(currentTime)}</span>
<span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(duration)}</span>
{(!onCloseVideo && !editable && !fullscreen) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
{(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
{onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
- <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}><a className='video-player__download__icon' href={this.props.src} download><Icon id={'download'} fixedWidth /></a></button>
<button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
</div>
</div>
height: 100%;
}
+ .video-player__volume::before,
+ .video-player__seek::before {
+ background: rgba($white, 0.15);
+ }
+
&.with-light-background {
+ color: $black;
+
+ .video-player__volume::before,
.video-player__seek::before {
- color: rgba($black, 0.35);
+ background: rgba($black, 0.15);
}
- .video-player__seek__seek {
- color: rgba($black, 0.2);
+ .video-player__seek__buffer {
+ background: rgba($black, 0.2);
}
.video-player__buttons button {
.video-player__time-current {
color: $black;
}
-
- .video-player__volume::before {
- background: rgba($black, 0.35);
- }
}
.video-player__seek::before,
border-radius: 4px;
box-sizing: border-box;
direction: ltr;
+ color: $white;
&.editable {
border-radius: 0;
}
&__buttons {
+ display: flex;
+ flex: 0 1 auto;
+ min-width: 30px;
+ align-items: center;
font-size: 16px;
white-space: nowrap;
overflow: hidden;
}
button {
+ flex: 0 0 auto;
background: transparent;
padding: 2px 10px;
font-size: 16px;
}
}
+ &__time {
+ display: inline;
+ flex: 0 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
&__time-sep,
&__time-total,
&__time-current {
&__time-current {
color: $white;
- margin-left: 60px;
}
&__time-sep {
}
&__volume {
+ flex: 0 0 auto;
+ display: inline-flex;
cursor: pointer;
height: 24px;
- display: inline;
+ position: relative;
+ overflow: hidden;
+
+ .no-reduce-motion & {
+ transition: all 100ms linear;
+ }
+
+ &.active {
+ overflow: visible;
+ width: 50px;
+ margin-right: 10px;
+ }
&::before {
content: "";
display: block;
position: absolute;
height: 4px;
- left: 70px;
- bottom: 20px;
+ left: 0;
+ top: 50%;
+ transform: translate(0, -50%);
}
&__current {
position: absolute;
height: 4px;
border-radius: 4px;
- left: 70px;
- bottom: 20px;
+ left: 0;
+ top: 50%;
+ transform: translate(0, -50%);
background: lighten($ui-highlight-color, 8%);
}
border-radius: 50%;
width: 12px;
height: 12px;
- bottom: 16px;
- left: 70px;
+ top: 50%;
+ left: 0;
+ margin-left: -6px;
+ transform: translate(0, -50%);
transition: opacity .1s ease;
background: lighten($ui-highlight-color, 8%);
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);