[Glitch] Change volume control and download buttons in web UI

Port 419ad6248b to glitch-soc

Signed-off-by: Thibaut Girka <thib@sitedethib.com>
This commit is contained in:
Eugen Rochko 2020-06-23 12:20:14 +02:00 committed by ThibG
parent 1ad79d11f3
commit 1c58420831
5 changed files with 105 additions and 91 deletions

View File

@ -593,9 +593,8 @@ class Status extends ImmutablePureComponent {
alt={attachment.get('description')} alt={attachment.get('description')}
poster={status.getIn(['account', 'avatar_static'])} poster={status.getIn(['account', 'avatar_static'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
peaks={[0]}
width={this.props.cachedMediaWidth} width={this.props.cachedMediaWidth}
height={70} height={110}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
/> />
)} )}

View File

@ -6,7 +6,7 @@ import Icon from 'flavours/glitch/components/icon';
import classNames from 'classnames'; import classNames from 'classnames';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { encode, decode } from 'blurhash'; import { encode, decode } from 'blurhash';
import { getPointerPosition } from 'mastodon/features/video'; import { getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video';
const digitCharacters = [ const digitCharacters = [
'0', '0',
@ -140,7 +140,7 @@ const messages = defineMessages({
}); });
const TICK_SIZE = 10; const TICK_SIZE = 10;
const PADDING = 180; const PADDING = 180;
export default @injectIntl export default @injectIntl
class Audio extends React.PureComponent { class Audio extends React.PureComponent {
@ -150,10 +150,8 @@ class Audio extends React.PureComponent {
alt: PropTypes.string, alt: PropTypes.string,
poster: PropTypes.string, poster: PropTypes.string,
duration: PropTypes.number, duration: PropTypes.number,
peaks: PropTypes.arrayOf(PropTypes.number),
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
preload: PropTypes.bool,
editable: PropTypes.bool, editable: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
@ -171,18 +169,6 @@ class Audio extends React.PureComponent {
color: { r: 255, g: 255, b: 255 }, 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 => { setPlayerRef = c => {
this.player = c; this.player = c;
@ -359,24 +345,23 @@ class Audio extends React.PureComponent {
} }
handleMouseVolSlide = throttle(e => { handleMouseVolSlide = throttle(e => {
const rect = this.volume.getBoundingClientRect(); const { x } = getPointerPosition(this.volume, e);
const x = (e.clientX - rect.left) / this.volWidth; // x position within the element.
if(!isNaN(x)) { if(!isNaN(x)) {
let slideamt = x; this.setState({ volume: x }, () => {
this.audio.volume = x;
if (x > 1) {
slideamt = 1;
} else if(x < 0) {
slideamt = 0;
}
this.setState({ volume: slideamt }, () => {
this.audio.volume = slideamt;
}); });
} }
}, 60); }, 60);
handleMouseEnter = () => {
this.setState({ hovered: true });
}
handleMouseLeave = () => {
this.setState({ hovered: false });
}
_initAudioContext () { _initAudioContext () {
const context = new AudioContext(); const context = new AudioContext();
const analyser = context.createAnalyser(); const analyser = context.createAnalyser();
@ -412,6 +397,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 () { _renderCanvas () {
requestAnimationFrame(() => { requestAnimationFrame(() => {
this._clear(); this._clear();
@ -575,13 +578,10 @@ class Audio extends React.PureComponent {
render () { render () {
const { src, intl, alt, editable } = this.props; const { src, intl, alt, editable } = this.props;
const { paused, muted, volume, currentTime, duration, buffer, darkText, dragging } = this.state; const { paused, muted, volume, currentTime, duration, buffer, darkText, dragging } = this.state;
const progress = (currentTime / duration) * 100;
const volumeWidth = muted ? 0 : volume * this.volWidth;
const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume);
const progress = (currentTime / duration) * 100;
return ( 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 <audio
src={src} src={src}
ref={this.setAudioRef} ref={this.setAudioRef}
@ -639,18 +639,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(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> <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={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
&nbsp; <div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getColor() }} />
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px`, backgroundColor: this._getColor() }} />
<span <span
className={classNames('video-player__volume__handle')} className={classNames('video-player__volume__handle')}
tabIndex='0' tabIndex='0'
style={{ left: `${volumeHandleLoc}px`, backgroundColor: this._getColor() }} style={{ left: `${volume * 100}%`, backgroundColor: this._getColor() }}
/> />
</div> </div>
<span> <span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(currentTime)}</span> <span className='video-player__time-current'>{formatTime(currentTime)}</span>
<span className='video-player__time-sep'>/</span> <span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span> <span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
@ -658,11 +657,7 @@ class Audio extends React.PureComponent {
</div> </div>
<div className='video-player__buttons right'> <div className='video-player__buttons right'>
<button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}> <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} onClick={this.handleDownload}><Icon id='download' fixedWidth /></button>
<a className='video-player__download__icon' href={this.props.src} download>
<Icon id='download' fixedWidth />
</a>
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -143,8 +143,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
alt={attachment.get('description')} alt={attachment.get('description')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={status.getIn(['account', 'avatar_static'])} poster={status.getIn(['account', 'avatar_static'])}
height={110} height={150}
preload
/> />
); );
mediaIcon = 'music'; mediaIcon = 'music';

View File

@ -19,7 +19,6 @@ const messages = defineMessages({
close: { id: 'video.close', defaultMessage: 'Close video' }, close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' }, fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' }, exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
download: { id: 'video.download', defaultMessage: 'Download file' },
}); });
export const formatTime = secondsNum => { export const formatTime = secondsNum => {
@ -86,6 +85,14 @@ export const getPointerPosition = (el, event) => {
return position; 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 export default @injectIntl
class Video extends React.PureComponent { class Video extends React.PureComponent {
@ -134,15 +141,6 @@ class Video extends React.PureComponent {
} }
} }
// 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 => { setPlayerRef = c => {
this.player = c; this.player = c;
@ -229,18 +227,12 @@ class Video extends React.PureComponent {
} }
handleMouseVolSlide = throttle(e => { handleMouseVolSlide = throttle(e => {
const rect = this.volume.getBoundingClientRect(); const { x } = getPointerPosition(this.volume, e);
const x = (e.clientX - rect.left) / this.volWidth; //x position within the element.
if(!isNaN(x)) { if(!isNaN(x)) {
var slideamt = x; this.setState({ volume: x }, () => {
if(x > 1) { this.video.volume = x;
slideamt = 1; });
} else if(x < 0) {
slideamt = 0;
}
this.video.volume = slideamt;
this.setState({ volume: slideamt });
} }
}, 15); }, 15);
@ -430,9 +422,6 @@ class Video extends React.PureComponent {
const progress = (currentTime / duration) * 100; const progress = (currentTime / duration) * 100;
const playerStyle = {}; const playerStyle = {};
const volumeWidth = (muted) ? 0 : volume * this.volWidth;
const volumeHandleLoc = (muted) ? this.volHandleOffset(0) : this.volHandleOffset(volume);
const computedClass = classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable, letterbox, 'full-width': fullwidth }); const computedClass = classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable, letterbox, 'full-width': fullwidth });
let { width, height } = this.props; let { width, height } = this.props;
@ -519,18 +508,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(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> <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={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp; <div className='video-player__volume__current' style={{ width: `${volume * 100}%` }} />
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
<span <span
className={classNames('video-player__volume__handle')} className={classNames('video-player__volume__handle')}
tabIndex='0' tabIndex='0'
style={{ left: `${volumeHandleLoc}px` }} style={{ left: `${volume * 100}%` }}
/> />
</div> </div>
{(detailed || fullscreen) && ( {(detailed || fullscreen) && (
<span> <span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span> <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
<span className='video-player__time-sep'>/</span> <span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(duration)}</span> <span className='video-player__time-total'>{formatTime(duration)}</span>
@ -544,7 +533,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>} {(!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>} {(!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>} {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> <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>
</div> </div>

View File

@ -345,13 +345,21 @@
height: 100%; height: 100%;
} }
.video-player__volume::before,
.video-player__seek::before {
background: rgba($white, 0.15);
}
&.with-light-background { &.with-light-background {
color: $black;
.video-player__volume::before,
.video-player__seek::before { .video-player__seek::before {
color: rgba($black, 0.35); background: rgba($black, 0.15);
} }
.video-player__seek__seek { .video-player__seek__buffer {
color: rgba($black, 0.2); background: rgba($black, 0.2);
} }
.video-player__buttons button { .video-player__buttons button {
@ -369,10 +377,6 @@
.video-player__time-current { .video-player__time-current {
color: $black; color: $black;
} }
.video-player__volume::before {
background: rgba($black, 0.35);
}
} }
.video-player__seek::before, .video-player__seek::before,
@ -400,6 +404,7 @@
border-radius: 4px; border-radius: 4px;
box-sizing: border-box; box-sizing: border-box;
direction: ltr; direction: ltr;
color: $white;
&.editable { &.editable {
border-radius: 0; border-radius: 0;
@ -527,6 +532,10 @@
} }
&__buttons { &__buttons {
display: flex;
flex: 0 1 auto;
min-width: 30px;
align-items: center;
font-size: 16px; font-size: 16px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -545,6 +554,7 @@
} }
button { button {
flex: 0 0 auto;
background: transparent; background: transparent;
padding: 2px 10px; padding: 2px 10px;
font-size: 16px; font-size: 16px;
@ -559,6 +569,13 @@
} }
} }
&__time {
display: inline;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
}
&__time-sep, &__time-sep,
&__time-total, &__time-total,
&__time-current { &__time-current {
@ -568,7 +585,6 @@
&__time-current { &__time-current {
color: $white; color: $white;
margin-left: 60px;
} }
&__time-sep { &__time-sep {
@ -582,9 +598,22 @@
} }
&__volume { &__volume {
flex: 0 0 auto;
display: inline-flex;
cursor: pointer; cursor: pointer;
height: 24px; height: 24px;
display: inline; position: relative;
overflow: hidden;
.no-reduce-motion & {
transition: all 100ms linear;
}
&.active {
overflow: visible;
width: 50px;
margin-right: 10px;
}
&::before { &::before {
content: ""; content: "";
@ -594,8 +623,9 @@
display: block; display: block;
position: absolute; position: absolute;
height: 4px; height: 4px;
left: 70px; left: 0;
bottom: 20px; top: 50%;
transform: translate(0, -50%);
} }
&__current { &__current {
@ -603,8 +633,9 @@
position: absolute; position: absolute;
height: 4px; height: 4px;
border-radius: 4px; border-radius: 4px;
left: 70px; left: 0;
bottom: 20px; top: 50%;
transform: translate(0, -50%);
background: lighten($ui-highlight-color, 8%); background: lighten($ui-highlight-color, 8%);
} }
@ -614,8 +645,10 @@
border-radius: 50%; border-radius: 50%;
width: 12px; width: 12px;
height: 12px; height: 12px;
bottom: 16px; top: 50%;
left: 70px; left: 0;
margin-left: -6px;
transform: translate(0, -50%);
transition: opacity .1s ease; transition: opacity .1s ease;
background: lighten($ui-highlight-color, 8%); background: lighten($ui-highlight-color, 8%);
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2); box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);