diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2020-06-23 12:20:14 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-23 12:20:14 +0200 |
commit | 419ad6248beb192f34ef581306138c3ff0d600a9 (patch) | |
tree | e3a6147f22e3d5787dc4370ed40643023c31e6e5 | |
parent | aaf91abffae590b2db1b9e6a7dd2a9e2b06b06ca (diff) |
Change volume control and download buttons in web UI (#14122)
* Fix audio download button not starting download in web UI * Fix volume controls on audio and video players in web UI * Remove download button from video player in web UI
5 files changed, 105 insertions, 94 deletions
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 4ed8cbdd9..5f42534ba 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -347,9 +347,8 @@ class Status extends ImmutablePureComponent { 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} /> )} diff --git a/app/javascript/mastodon/features/audio/index.js b/app/javascript/mastodon/features/audio/index.js index 029750d85..cde2357ac 100644 --- a/app/javascript/mastodon/features/audio/index.js +++ b/app/javascript/mastodon/features/audio/index.js @@ -6,7 +6,7 @@ import Icon from 'mastodon/components/icon'; 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', @@ -140,7 +140,7 @@ const messages = defineMessages({ }); const TICK_SIZE = 10; -const PADDING = 180; +const PADDING = 180; export default @injectIntl class Audio extends React.PureComponent { @@ -150,10 +150,8 @@ 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, @@ -171,17 +169,6 @@ class Audio extends React.PureComponent { 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; @@ -364,20 +351,11 @@ class Audio extends React.PureComponent { } 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); @@ -395,6 +373,14 @@ class Audio extends React.PureComponent { } }, 150, { trailing: true }); + handleMouseEnter = () => { + this.setState({ hovered: true }); + } + + handleMouseLeave = () => { + this.setState({ hovered: false }); + } + _initAudioContext () { const context = new AudioContext(); const analyser = context.createAnalyser(); @@ -430,6 +416,24 @@ class Audio extends React.PureComponent { }); } + 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(); @@ -593,13 +597,10 @@ class Audio extends React.PureComponent { 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} @@ -657,18 +658,17 @@ class Audio extends React.PureComponent { <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> @@ -676,11 +676,7 @@ class Audio extends React.PureComponent { </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> diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 6ccc281a3..72d15ddf7 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -118,8 +118,7 @@ export default class DetailedStatus extends ImmutablePureComponent { 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') { diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 95e107618..72c23bc0c 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -19,7 +19,6 @@ const messages = defineMessages({ 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 => { @@ -87,6 +86,14 @@ export const getPointerPosition = (el, event) => { 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 { @@ -126,17 +133,6 @@ 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; @@ -206,20 +202,12 @@ class Video extends React.PureComponent { } 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); @@ -421,9 +409,6 @@ class Video extends React.PureComponent { 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; @@ -510,18 +495,18 @@ class Video extends React.PureComponent { <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> @@ -535,7 +520,6 @@ class Video extends React.PureComponent { {(!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> diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 65e075037..8554c19eb 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -5309,13 +5309,21 @@ a.status-card.compact:hover { 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 { @@ -5333,10 +5341,6 @@ a.status-card.compact:hover { .video-player__time-current { color: $black; } - - .video-player__volume::before { - background: rgba($black, 0.35); - } } .video-player__seek::before, @@ -5364,6 +5368,7 @@ a.status-card.compact:hover { border-radius: 4px; box-sizing: border-box; direction: ltr; + color: $white; &.editable { border-radius: 0; @@ -5476,6 +5481,10 @@ a.status-card.compact:hover { } &__buttons { + display: flex; + flex: 0 1 auto; + min-width: 30px; + align-items: center; font-size: 16px; white-space: nowrap; overflow: hidden; @@ -5494,6 +5503,7 @@ a.status-card.compact:hover { } button { + flex: 0 0 auto; background: transparent; padding: 2px 10px; font-size: 16px; @@ -5508,6 +5518,13 @@ a.status-card.compact:hover { } } + &__time { + display: inline; + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; + } + &__time-sep, &__time-total, &__time-current { @@ -5517,7 +5534,6 @@ a.status-card.compact:hover { &__time-current { color: $white; - margin-left: 60px; } &__time-sep { @@ -5531,9 +5547,22 @@ a.status-card.compact:hover { } &__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: ""; @@ -5543,8 +5572,9 @@ a.status-card.compact:hover { display: block; position: absolute; height: 4px; - left: 70px; - bottom: 20px; + left: 0; + top: 50%; + transform: translate(0, -50%); } &__current { @@ -5552,8 +5582,9 @@ a.status-card.compact:hover { 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%); } @@ -5563,8 +5594,10 @@ a.status-card.compact:hover { 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); |