forked from treehouse/mastodon
Polish video player CSS, add timer on fullscreen/modal/public pages (#5928)
parent
b0db4dad79
commit
70ce2a2095
|
@ -23,6 +23,7 @@ export default class VideoModal extends ImmutablePureComponent {
|
||||||
src={media.get('url')}
|
src={media.get('url')}
|
||||||
startTime={time}
|
startTime={time}
|
||||||
onCloseVideo={onClose}
|
onCloseVideo={onClose}
|
||||||
|
detailed
|
||||||
description={media.get('description')}
|
description={media.get('description')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,6 +17,18 @@ const messages = defineMessages({
|
||||||
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
|
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formatTime = secondsNum => {
|
||||||
|
let hours = Math.floor(secondsNum / 3600);
|
||||||
|
let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
|
||||||
|
let seconds = secondsNum - (hours * 3600) - (minutes * 60);
|
||||||
|
|
||||||
|
if (hours < 10) hours = '0' + hours;
|
||||||
|
if (minutes < 10) minutes = '0' + minutes;
|
||||||
|
if (seconds < 10) seconds = '0' + seconds;
|
||||||
|
|
||||||
|
return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
const findElementPosition = el => {
|
const findElementPosition = el => {
|
||||||
let box;
|
let box;
|
||||||
|
|
||||||
|
@ -83,11 +95,13 @@ export default class Video extends React.PureComponent {
|
||||||
startTime: PropTypes.number,
|
startTime: PropTypes.number,
|
||||||
onOpenVideo: PropTypes.func,
|
onOpenVideo: PropTypes.func,
|
||||||
onCloseVideo: PropTypes.func,
|
onCloseVideo: PropTypes.func,
|
||||||
|
detailed: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
progress: 0,
|
currentTime: 0,
|
||||||
|
duration: 0,
|
||||||
paused: true,
|
paused: true,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
|
@ -117,7 +131,10 @@ export default class Video extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTimeUpdate = () => {
|
handleTimeUpdate = () => {
|
||||||
this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) });
|
this.setState({
|
||||||
|
currentTime: Math.floor(this.video.currentTime),
|
||||||
|
duration: Math.floor(this.video.duration),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseDown = e => {
|
handleMouseDown = e => {
|
||||||
|
@ -143,8 +160,10 @@ export default class Video extends React.PureComponent {
|
||||||
|
|
||||||
handleMouseMove = throttle(e => {
|
handleMouseMove = throttle(e => {
|
||||||
const { x } = getPointerPosition(this.seek, e);
|
const { x } = getPointerPosition(this.seek, e);
|
||||||
this.video.currentTime = this.video.duration * x;
|
const currentTime = Math.floor(this.video.duration * x);
|
||||||
this.setState({ progress: x * 100 });
|
|
||||||
|
this.video.currentTime = currentTime;
|
||||||
|
this.setState({ currentTime });
|
||||||
}, 60);
|
}, 60);
|
||||||
|
|
||||||
togglePlay = () => {
|
togglePlay = () => {
|
||||||
|
@ -226,11 +245,12 @@ export default class Video extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt } = this.props;
|
const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props;
|
||||||
const { progress, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
const { currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||||
|
const progress = (currentTime / duration) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('video-player', { inactive: !revealed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
<div className={classNames('video-player', { inactive: !revealed, detailed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
<video
|
<video
|
||||||
ref={this.setVideoRef}
|
ref={this.setVideoRef}
|
||||||
src={src}
|
src={src}
|
||||||
|
@ -267,16 +287,27 @@ export default class Video extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='video-player__buttons left'>
|
<div className='video-player__buttons-bar'>
|
||||||
<button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button>
|
<div className='video-player__buttons left'>
|
||||||
<button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button>
|
<button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button>
|
||||||
{!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>}
|
<button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='video-player__buttons right'>
|
{!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>}
|
||||||
{(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
|
|
||||||
{onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-times' /></button>}
|
{(detailed || fullscreen) &&
|
||||||
<button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button>
|
<span>
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='video-player__buttons right'>
|
||||||
|
{(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
|
||||||
|
{onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>}
|
||||||
|
<button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3998,6 +3998,7 @@ button.icon-button.active i.fa-retweet {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $base-shadow-color;
|
background: $base-shadow-color;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
video {
|
video {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -4032,8 +4033,8 @@ button.icon-button.active i.fa-retweet {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 60%, transparent);
|
background: linear-gradient(0deg, rgba($base-shadow-color, 0.85) 0, rgba($base-shadow-color, 0.45) 60%, transparent);
|
||||||
padding: 0 10px;
|
padding: 0 15px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .1s ease;
|
transition: opacity .1s ease;
|
||||||
|
|
||||||
|
@ -4086,40 +4087,67 @@ button.icon-button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__buttons {
|
&__buttons-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
&.left {
|
&.left {
|
||||||
float: left;
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding-right: 10px;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.right {
|
&.right {
|
||||||
float: right;
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding-left: 10px;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 2px 10px;
|
||||||
|
font-size: 16px;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: $white;
|
color: rgba($white, 0.75);
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: $ui-highlight-color;
|
color: $white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__time-sep,
|
||||||
|
&__time-total,
|
||||||
|
&__time-current {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time-current {
|
||||||
|
color: $white;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time-sep {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time-sep,
|
||||||
|
&__time-total {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
&__seek {
|
&__seek {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
@ -4129,6 +4157,7 @@ button.icon-button.active i.fa-retweet {
|
||||||
content: "";
|
content: "";
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: rgba($white, 0.35);
|
background: rgba($white, 0.35);
|
||||||
|
border-radius: 4px;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
|
@ -4140,8 +4169,9 @@ button.icon-button.active i.fa-retweet {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
background: $ui-highlight-color;
|
background: lighten($ui-highlight-color, 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__buffer {
|
&__buffer {
|
||||||
|
@ -4158,7 +4188,8 @@ button.icon-button.active i.fa-retweet {
|
||||||
top: 6px;
|
top: 6px;
|
||||||
margin-left: -6px;
|
margin-left: -6px;
|
||||||
transition: opacity .1s ease;
|
transition: opacity .1s ease;
|
||||||
background: $ui-highlight-color;
|
background: lighten($ui-highlight-color, 8%);
|
||||||
|
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
|
@ -4172,6 +4203,16 @@ button.icon-button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.detailed,
|
||||||
|
&.fullscreen {
|
||||||
|
.video-player__buttons {
|
||||||
|
button {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-spoiler-video {
|
.media-spoiler-video {
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
- if !status.media_attachments.empty?
|
- if !status.media_attachments.empty?
|
||||||
- if status.media_attachments.first.video?
|
- if status.media_attachments.first.video?
|
||||||
- video = status.media_attachments.first
|
- video = status.media_attachments.first
|
||||||
%div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380) }}
|
%div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380, detailed: true) }}
|
||||||
- else
|
- else
|
||||||
%div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}
|
%div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}
|
||||||
- elsif status.preview_cards.first
|
- elsif status.preview_cards.first
|
||||||
|
|
Loading…
Reference in New Issue