diff options
Diffstat (limited to 'app')
4 files changed, 105 insertions, 32 deletions
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js index 1437deeb0..6a883759f 100644 --- a/app/javascript/mastodon/features/ui/components/video_modal.js +++ b/app/javascript/mastodon/features/ui/components/video_modal.js @@ -23,6 +23,7 @@ export default class VideoModal extends ImmutablePureComponent { src={media.get('url')} startTime={time} onCloseVideo={onClose} + detailed description={media.get('description')} /> </div> diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 003bf23a8..0ee8bb6c8 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -17,6 +17,18 @@ const messages = defineMessages({ 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 => { let box; @@ -83,11 +95,13 @@ export default class Video extends React.PureComponent { startTime: PropTypes.number, onOpenVideo: PropTypes.func, onCloseVideo: PropTypes.func, + detailed: PropTypes.bool, intl: PropTypes.object.isRequired, }; state = { - progress: 0, + currentTime: 0, + duration: 0, paused: true, dragging: false, fullscreen: false, @@ -117,7 +131,10 @@ export default class Video extends React.PureComponent { } 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 => { @@ -143,8 +160,10 @@ export default class Video extends React.PureComponent { handleMouseMove = throttle(e => { const { x } = getPointerPosition(this.seek, e); - this.video.currentTime = this.video.duration * x; - this.setState({ progress: x * 100 }); + const currentTime = Math.floor(this.video.duration * x); + + this.video.currentTime = currentTime; + this.setState({ currentTime }); }, 60); togglePlay = () => { @@ -226,11 +245,12 @@ export default class Video extends React.PureComponent { } render () { - const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt } = this.props; - const { progress, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; + const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props; + const { currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; + const progress = (currentTime / duration) * 100; 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 ref={this.setVideoRef} src={src} @@ -267,16 +287,27 @@ export default class Video extends React.PureComponent { /> </div> - <div className='video-player__buttons left'> - <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> - <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> - {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>} - </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-times' /></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 className='video-player__buttons-bar'> + <div className='video-player__buttons left'> + <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> + <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> + + {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>} + + {(detailed || fullscreen) && + <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> diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 64a77adc7..dd61dc519 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3998,6 +3998,7 @@ button.icon-button.active i.fa-retweet { position: relative; background: $base-shadow-color; max-width: 100%; + border-radius: 4px; video { height: 100%; @@ -4032,8 +4033,8 @@ button.icon-button.active i.fa-retweet { left: 0; right: 0; box-sizing: border-box; - background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 60%, transparent); - padding: 0 10px; + background: linear-gradient(0deg, rgba($base-shadow-color, 0.85) 0, rgba($base-shadow-color, 0.45) 60%, transparent); + padding: 0 15px; opacity: 0; 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; + } + + &__buttons { font-size: 16px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; &.left { - float: left; - button { - padding-right: 10px; + padding-left: 0; } } &.right { - float: right; - button { - padding-left: 10px; + padding-right: 0; } } button { background: transparent; - padding: 0; + padding: 2px 10px; + font-size: 16px; border: 0; - color: $white; + color: rgba($white, 0.75); &:active, &:hover, &: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 { cursor: pointer; height: 24px; @@ -4129,6 +4157,7 @@ button.icon-button.active i.fa-retweet { content: ""; width: 100%; background: rgba($white, 0.35); + border-radius: 4px; display: block; position: absolute; height: 4px; @@ -4140,8 +4169,9 @@ button.icon-button.active i.fa-retweet { display: block; position: absolute; height: 4px; + border-radius: 4px; top: 10px; - background: $ui-highlight-color; + background: lighten($ui-highlight-color, 8%); } &__buffer { @@ -4158,7 +4188,8 @@ button.icon-button.active i.fa-retweet { top: 6px; margin-left: -6px; 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; &.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 { diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 3119ebf4b..94e081c84 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -22,7 +22,7 @@ - if !status.media_attachments.empty? - if status.media_attachments.first.video? - 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 %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 |