Redesign video player (#4911)

* Redesign video player

* Use new video player on static public pages too

* Use media gallery component on static public pages too

* Pause video when hiding it

* Full-screen sizing on WebKit

* Add aria labels to video player buttons

* Display link card on public status page

* Fix fullscreen from modal sizing issue

* Remove contain: strict property to fix fullscreen from columns
This commit is contained in:
Eugen Rochko 2017-09-14 03:39:10 +02:00 committed by GitHub
parent af00220d79
commit 2bbf987a0a
46 changed files with 1064 additions and 217 deletions

View File

@ -9,7 +9,7 @@ import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
import { MediaGallery, Video } from '../features/ui/util/async-components';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
@ -88,6 +88,10 @@ export default class Status extends ImmutablePureComponent {
return <div className='media-spoiler-video' style={{ height: '110px' }} />;
}
handleOpenVideo = startTime => {
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
}
render () {
let media = null;
let statusAvatar;
@ -127,9 +131,18 @@ export default class Status extends ImmutablePureComponent {
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]);
media = (
<Bundle fetchComponent={VideoPlayer} loading={this.renderLoadingVideoPlayer} >
{Component => <Component media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />}
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => <Component
preview={video.get('preview_url')}
src={video.get('url')}
width={239}
height={110}
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
/>}
</Bundle>
);
} else {

View File

@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import Card from '../features/status/components/card';
import { fromJS } from 'immutable';
export default class CardContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string,
card: PropTypes.array.isRequired,
};
render () {
const { card, ...props } = this.props;
return <Card card={fromJS(card)} {...props} />;
}
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import MediaGallery from '../components/media_gallery';
import { fromJS } from 'immutable';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export default class MediaGalleryContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
media: PropTypes.array.isRequired,
};
handleOpenMedia = () => {}
render () {
const { locale, media, ...props } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<MediaGallery
{...props}
media={fromJS(media)}
onOpenMedia={this.handleOpenMedia}
/>
</IntlProvider>
);
}
}

View File

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import Video from '../features/video';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export default class VideoContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
render () {
const { locale, ...props } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<Video {...props} />
</IntlProvider>
);
}
}

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import punycode from 'punycode';
import classnames from 'classnames';
@ -22,10 +23,15 @@ export default class Card extends React.PureComponent {
static propTypes = {
card: ImmutablePropTypes.map,
maxDescription: PropTypes.number,
};
static defaultProps = {
maxDescription: 50,
};
renderLink () {
const { card } = this.props;
const { card, maxDescription } = this.props;
let image = '';
let provider = card.get('provider_name');
@ -52,7 +58,7 @@ export default class Card extends React.PureComponent {
<div className='status-card__content'>
<strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
<p className='status-card__description'>{(card.get('description') || '').substring(0, 50)}</p>
<p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>
<span className='status-card__host'>{provider}</span>
</div>
</a>

View File

@ -5,12 +5,12 @@ import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery';
import VideoPlayer from '../../../components/video_player';
import AttachmentList from '../../../components/attachment_list';
import Link from 'react-router-dom/Link';
import { FormattedDate, FormattedNumber } from 'react-intl';
import CardContainer from '../containers/card_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video';
export default class DetailedStatus extends ImmutablePureComponent {
@ -34,6 +34,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
e.stopPropagation();
}
handleOpenVideo = startTime => {
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
}
render () {
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
@ -44,7 +48,18 @@ export default class DetailedStatus extends ImmutablePureComponent {
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
media = <AttachmentList media={status.get('media_attachments')} />;
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />;
const video = status.getIn(['media_attachments', 0]);
media = (
<Video
preview={video.get('preview_url')}
src={video.get('url')}
width={300}
height={150}
onOpenVideo={this.handleOpenVideo}
sensitive={status.get('sensitive')}
/>
);
} else {
media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
}

View File

@ -1,35 +1,29 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ExtendedVideoPlayer from '../../../components/extended_video_player';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Video from '../../video';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
@injectIntl
export default class VideoModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
time: PropTypes.number,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { media, intl, time, onClose } = this.props;
const url = media.get('url');
const { media, time, onClose } = this.props;
return (
<div className='modal-root__modal media-modal'>
<div>
<div className='media-modal__close'><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div>
<ExtendedVideoPlayer src={url} muted={false} controls time={time} />
<Video
preview={media.get('preview_url')}
src={media.get('url')}
startTime={time}
onCloseVideo={onClose}
/>
</div>
</div>
);

View File

@ -98,6 +98,10 @@ export function VideoPlayer () {
return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
}
export function Video () {
return import(/* webpackChunkName: "features/video" */'../../video');
}
export function EmbedModal () {
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
}

View File

@ -0,0 +1,304 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { throttle } from 'lodash';
import classNames from 'classnames';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
hide: { id: 'video.hide', defaultMessage: 'Hide video' },
expand: { id: 'video.expand', defaultMessage: 'Expand 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' },
});
const findElementPosition = el => {
let box;
if (el.getBoundingClientRect && el.parentNode) {
box = el.getBoundingClientRect();
}
if (!box) {
return {
left: 0,
top: 0,
};
}
const docEl = document.documentElement;
const body = document.body;
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
const scrollLeft = window.pageXOffset || body.scrollLeft;
const left = (box.left + scrollLeft) - clientLeft;
const clientTop = docEl.clientTop || body.clientTop || 0;
const scrollTop = window.pageYOffset || body.scrollTop;
const top = (box.top + scrollTop) - clientTop;
return {
left: Math.round(left),
top: Math.round(top),
};
};
const getPointerPosition = (el, event) => {
const position = {};
const box = findElementPosition(el);
const boxW = el.offsetWidth;
const boxH = el.offsetHeight;
const boxY = box.top;
const boxX = box.left;
let pageY = event.pageY;
let pageX = event.pageX;
if (event.changedTouches) {
pageX = event.changedTouches[0].pageX;
pageY = event.changedTouches[0].pageY;
}
position.y = Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH));
position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
return position;
};
const isFullscreen = () => document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement;
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
};
const requestFullscreen = el => {
if (el.requestFullscreen) {
el.requestFullscreen();
} else if (el.webkitRequestFullscreen) {
el.webkitRequestFullscreen();
} else if (el.mozRequestFullScreen) {
el.mozRequestFullScreen();
} else if (el.msRequestFullscreen) {
el.msRequestFullscreen();
}
};
@injectIntl
export default class Video extends React.PureComponent {
static propTypes = {
preview: PropTypes.string,
src: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
sensitive: PropTypes.bool,
startTime: PropTypes.number,
onOpenVideo: PropTypes.func,
onCloseVideo: PropTypes.func,
intl: PropTypes.object.isRequired,
};
state = {
progress: 0,
paused: true,
dragging: false,
fullscreen: false,
hovered: false,
muted: false,
revealed: !this.props.sensitive,
};
setPlayerRef = c => {
this.player = c;
}
setVideoRef = c => {
this.video = c;
}
setSeekRef = c => {
this.seek = c;
}
handlePlay = () => {
this.setState({ paused: false });
}
handlePause = () => {
this.setState({ paused: true });
}
handleTimeUpdate = () => {
this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) });
}
handleMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseMove, true);
document.addEventListener('mouseup', this.handleMouseUp, true);
document.addEventListener('touchmove', this.handleMouseMove, true);
document.addEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: true });
this.video.pause();
this.handleMouseMove(e);
}
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('mouseup', this.handleMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseMove, true);
document.removeEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: false });
this.video.play();
}
handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e);
this.video.currentTime = this.video.duration * x;
this.setState({ progress: x * 100 });
}, 60);
togglePlay = () => {
if (this.state.paused) {
this.video.play();
} else {
this.video.pause();
}
}
toggleFullscreen = () => {
if (isFullscreen()) {
exitFullscreen();
} else {
requestFullscreen(this.player);
}
}
componentDidMount () {
document.addEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
}
componentWillUnmount () {
document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
}
handleFullscreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
}
handleMouseEnter = () => {
this.setState({ hovered: true });
}
handleMouseLeave = () => {
this.setState({ hovered: false });
}
toggleMute = () => {
this.video.muted = !this.video.muted;
this.setState({ muted: this.video.muted });
}
toggleReveal = () => {
if (this.state.revealed) {
this.video.pause();
}
this.setState({ revealed: !this.state.revealed });
}
handleLoadedData = () => {
if (this.props.startTime) {
this.video.currentTime = this.props.startTime;
this.video.play();
}
}
handleOpenVideo = () => {
this.video.pause();
this.props.onOpenVideo(this.video.currentTime);
}
handleCloseVideo = () => {
this.video.pause();
this.props.onCloseVideo();
}
render () {
const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl } = this.props;
const { progress, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
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}>
<video
ref={this.setVideoRef}
src={src}
poster={preview}
preload={!!startTime}
loop
role='button'
tabIndex='0'
width={width}
height={height}
onClick={this.togglePlay}
onPlay={this.handlePlay}
onPause={this.handlePause}
onTimeUpdate={this.handleTimeUpdate}
onLoadedData={this.handleLoadedData}
/>
<button className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}>
<span className='video-player__spoiler__title'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</button>
<div className={classNames('video-player__controls', { active: paused || hovered })}>
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
<div className='video-player__seek__progress' style={{ width: `${progress}%` }} />
<span
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex='0'
style={{ left: `${progress}%` }}
/>
</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>
</div>
</div>
);
}
}

View File

@ -33,6 +33,7 @@
"column.home": "الرئيسية",
"column.mutes": "الحسابات المكتومة",
"column.notifications": "الإشعارات",
"column.pins": "Pinned toot",
"column.public": "الخيط العام الموحد",
"column_back_button.label": "العودة",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "معلومات إضافية",
"navigation_bar.logout": "خروج",
"navigation_bar.mutes": "الحسابات المكتومة",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "التفضيلات",
"navigation_bar.public_timeline": "الخيط العام الموحد",
"notification.favourite": "{name} أعجب بمنشورك",
@ -193,6 +195,15 @@
"upload_button.label": "إضافة وسائط",
"upload_form.undo": "إلغاء",
"upload_progress.label": "يرفع...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "وسّع الفيديو",
"video_player.toggle_sound": "تبديل الصوت",
"video_player.toggle_visible": "إظهار / إخفاء الفيديو",

View File

@ -33,6 +33,7 @@
"column.home": "Начало",
"column.mutes": "Muted users",
"column.notifications": "Известия",
"column.pins": "Pinned toot",
"column.public": "Публичен канал",
"column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Излизане",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Предпочитания",
"navigation_bar.public_timeline": "Публичен канал",
"notification.favourite": "{name} хареса твоята публикация",
@ -193,6 +195,15 @@
"upload_button.label": "Добави медия",
"upload_form.undo": "Отмяна",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Звук",
"video_player.toggle_visible": "Toggle visibility",

View File

@ -33,6 +33,7 @@
"column.home": "Inici",
"column.mutes": "Usuaris silenciats",
"column.notifications": "Notificacions",
"column.pins": "Pinned toot",
"column.public": "Línia de temps federada",
"column_back_button.label": "Enrere",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Informació addicional",
"navigation_bar.logout": "Tancar sessió",
"navigation_bar.mutes": "Usuaris silenciats",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferències",
"navigation_bar.public_timeline": "Línia de temps federada",
"notification.favourite": "{name} ha afavorit el teu estat",
@ -193,6 +195,15 @@
"upload_button.label": "Afegir multimèdia",
"upload_form.undo": "Desfer",
"upload_progress.label": "Pujant...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Ampliar el vídeo",
"video_player.toggle_sound": "Alternar so",
"video_player.toggle_visible": "Alternar visibilitat",

View File

@ -33,6 +33,7 @@
"column.home": "Startseite",
"column.mutes": "Stummgeschaltete Profile",
"column.notifications": "Mitteilungen",
"column.pins": "Pinned toot",
"column.public": "Gesamtes bekanntes Netz",
"column_back_button.label": "Zurück",
"column_header.hide_settings": "Einstellungen verbergen",
@ -109,6 +110,7 @@
"navigation_bar.info": "Erweiterte Informationen",
"navigation_bar.logout": "Abmelden",
"navigation_bar.mutes": "Stummgeschaltete Profile",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Föderierte Zeitleiste",
"notification.favourite": "{name} favorisierte deinen Status",
@ -193,6 +195,15 @@
"upload_button.label": "Mediendatei hinzufügen",
"upload_form.undo": "Entfernen",
"upload_progress.label": "Lade hoch…",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Videoanzeige vergrößern",
"video_player.toggle_sound": "Ton umschalten",
"video_player.toggle_visible": "Sichtbarkeit umschalten",

View File

@ -812,6 +812,10 @@
"defaultMessage": "Extended information",
"id": "navigation_bar.info"
},
{
"defaultMessage": "Pinned toots",
"id": "navigation_bar.pins"
},
{
"defaultMessage": "FAQ",
"id": "getting_started.faq"
@ -992,6 +996,15 @@
],
"path": "app/javascript/mastodon/features/notifications/index.json"
},
{
"descriptors": [
{
"defaultMessage": "Pinned toot",
"id": "column.pins"
}
],
"path": "app/javascript/mastodon/features/pinned_statuses/index.json"
},
{
"descriptors": [
{
@ -1326,5 +1339,54 @@
}
],
"path": "app/javascript/mastodon/features/ui/components/video_modal.json"
},
{
"descriptors": [
{
"defaultMessage": "Play",
"id": "video.play"
},
{
"defaultMessage": "Pause",
"id": "video.pause"
},
{
"defaultMessage": "Mute sound",
"id": "video.mute"
},
{
"defaultMessage": "Unmute sound",
"id": "video.unmute"
},
{
"defaultMessage": "Hide video",
"id": "video.hide"
},
{
"defaultMessage": "Expand video",
"id": "video.expand"
},
{
"defaultMessage": "Close video",
"id": "video.close"
},
{
"defaultMessage": "Full screen",
"id": "video.fullscreen"
},
{
"defaultMessage": "Exit full screen",
"id": "video.exit_fullscreen"
},
{
"defaultMessage": "Sensitive content",
"id": "status.sensitive_warning"
},
{
"defaultMessage": "Click to view",
"id": "status.sensitive_toggle"
}
],
"path": "app/javascript/mastodon/features/video/index.json"
}
]

View File

@ -33,8 +33,8 @@
"column.home": "Home",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
"column.public": "Federated timeline",
"column.pins": "Pinned toots",
"column.public": "Federated timeline",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
@ -110,9 +110,9 @@
"navigation_bar.info": "About this instance",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.pins": "Pinned toots",
"notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you",
@ -195,6 +195,15 @@
"upload_button.label": "Add media",
"upload_form.undo": "Undo",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Toggle sound",
"video_player.toggle_visible": "Toggle visibility",

View File

@ -33,6 +33,7 @@
"column.home": "Hejmo",
"column.mutes": "Muted users",
"column.notifications": "Sciigoj",
"column.pins": "Pinned toot",
"column.public": "Fratara tempolinio",
"column_back_button.label": "Reveni",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Elsaluti",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferoj",
"navigation_bar.public_timeline": "Fratara tempolinio",
"notification.favourite": "{name} favoris vian mesaĝon",
@ -193,6 +195,15 @@
"upload_button.label": "Aldoni enhavaĵon",
"upload_form.undo": "Malfari",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Aktivigi sonojn",
"video_player.toggle_visible": "Toggle visibility",

View File

@ -33,6 +33,7 @@
"column.home": "Inicio",
"column.mutes": "Usuarios silenciados",
"column.notifications": "Notificaciones",
"column.pins": "Pinned toot",
"column.public": "Historia federada",
"column_back_button.label": "Atrás",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Información adicional",
"navigation_bar.logout": "Cerrar sesión",
"navigation_bar.mutes": "Usuarios silenciados",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferencias",
"navigation_bar.public_timeline": "Historia federada",
"notification.favourite": "{name} marcó tu estado como favorito",
@ -193,6 +195,15 @@
"upload_button.label": "Subir multimedia",
"upload_form.undo": "Deshacer",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Act/Desac. sonido",
"video_player.toggle_visible": "Toggle visibility",

View File

@ -33,8 +33,8 @@
"column.home": "خانه",
"column.mutes": "کاربران بی‌صداشده",
"column.notifications": "اعلان‌ها",
"column.public": "نوشته‌های همه‌جا",
"column.pins": "نوشته‌های ثابت",
"column.public": "نوشته‌های همه‌جا",
"column_back_button.label": "بازگشت",
"column_header.hide_settings": "نهفتن تنظیمات",
"column_header.moveLeft_settings": "انتقال ستون به چپ",
@ -110,9 +110,9 @@
"navigation_bar.info": "اطلاعات تکمیلی",
"navigation_bar.logout": "خروج",
"navigation_bar.mutes": "کاربران بی‌صداشده",
"navigation_bar.pins": "نوشته‌های ثابت",
"navigation_bar.preferences": "ترجیحات",
"navigation_bar.public_timeline": "نوشته‌های همه‌جا",
"navigation_bar.pins": "نوشته‌های ثابت",
"notification.favourite": "{name} نوشتهٔ شما را پسندید",
"notification.follow": "{name} پیگیر شما شد",
"notification.mention": "{name} از شما نام برد",
@ -195,6 +195,15 @@
"upload_button.label": "افزودن تصویر",
"upload_form.undo": "واگردانی",
"upload_progress.label": "بارگذاری...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "بازکردن ویدیو",
"video_player.toggle_sound": "تغییر صداداری",
"video_player.toggle_visible": "تغییر پیدایی",

View File

@ -33,6 +33,7 @@
"column.home": "Koti",
"column.mutes": "Muted users",
"column.notifications": "Ilmoitukset",
"column.pins": "Pinned toot",
"column.public": "Yleinen aikajana",
"column_back_button.label": "Takaisin",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Kirjaudu ulos",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Ominaisuudet",
"navigation_bar.public_timeline": "Yleinen aikajana",
"notification.favourite": "{name} tykkäsi statuksestasi",
@ -193,6 +195,15 @@
"upload_button.label": "Lisää mediaa",
"upload_form.undo": "Peru",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Äänet päälle/pois",
"video_player.toggle_visible": "Toggle visibility",

View File

@ -33,8 +33,8 @@
"column.home": "Accueil",
"column.mutes": "Comptes masqués",
"column.notifications": "Notifications",
"column.public": "Fil public global",
"column.pins": "Pouets épinglés",
"column.public": "Fil public global",
"column_back_button.label": "Retour",
"column_header.hide_settings": "Masquer les paramètres",
"column_header.moveLeft_settings": "Déplacer la colonne vers la gauche",
@ -110,9 +110,9 @@
"navigation_bar.info": "Plus dinformations",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.mutes": "Comptes masqués",
"navigation_bar.pins": "Pouets épinglés",
"navigation_bar.preferences": "Préférences",
"navigation_bar.public_timeline": "Fil public global",
"navigation_bar.pins": "Pouets épinglés",
"notification.favourite": "{name} a ajouté à ses favoris:",
"notification.follow": "{name} vous suit.",
"notification.mention": "{name} vous a mentionné⋅e:",
@ -195,6 +195,15 @@
"upload_button.label": "Joindre un média",
"upload_form.undo": "Annuler",
"upload_progress.label": "Envoi en cours…",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Agrandir la vidéo",
"video_player.toggle_sound": "Activer/Désactiver le son",
"video_player.toggle_visible": "Afficher/Cacher la vidéo",

View File

@ -33,6 +33,7 @@
"column.home": "בבית",
"column.mutes": "השתקות",
"column.notifications": "התראות",
"column.pins": "Pinned toot",
"column.public": "בפרהסיה",
"column_back_button.label": "חזרה",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "מידע נוסף",
"navigation_bar.logout": "יציאה",
"navigation_bar.mutes": "השתקות",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "העדפות",
"navigation_bar.public_timeline": "ציר זמן בין-קהילתי",
"notification.favourite": "חצרוצך חובב על ידי {name}",
@ -193,6 +195,15 @@
"upload_button.label": "הוספת מדיה",
"upload_form.undo": "ביטול",
"upload_progress.label": "עולה...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "הרחבת וידאו",
"video_player.toggle_sound": "הפעלת\\ביטול שמע",
"video_player.toggle_visible": "הפעלת\\ביטול תצוגה",

View File

@ -33,6 +33,7 @@
"column.home": "Dom",
"column.mutes": "Utišani korisnici",
"column.notifications": "Notifikacije",
"column.pins": "Pinned toot",
"column.public": "Federalni timeline",
"column_back_button.label": "Natrag",
"column_header.hide_settings": "Hide settings",
@ -61,7 +62,6 @@
"confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Utišaj",
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
@ -110,6 +110,7 @@
"navigation_bar.info": "Više informacija",
"navigation_bar.logout": "Odjavi se",
"navigation_bar.mutes": "Utišani korisnici",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Postavke",
"navigation_bar.public_timeline": "Federalni timeline",
"notification.favourite": "{name} je lajkao tvoj status",
@ -194,6 +195,15 @@
"upload_button.label": "Dodaj media",
"upload_form.undo": "Poništi",
"upload_progress.label": "Uploadam...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Proširi video",
"video_player.toggle_sound": "Toggle zvuk",
"video_player.toggle_visible": "Preklopi vidljivost",

View File

@ -33,6 +33,7 @@
"column.home": "Kezdőlap",
"column.mutes": "Muted users",
"column.notifications": "Értesítések",
"column.pins": "Pinned toot",
"column.public": "Nyilvános",
"column_back_button.label": "Vissza",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Kijelentkezés",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Beállítások",
"navigation_bar.public_timeline": "Nyilvános időfolyam",
"notification.favourite": "{name} kedvencnek jelölte az állapotod",
@ -193,6 +195,15 @@
"upload_button.label": "Média hozzáadása",
"upload_form.undo": "Mégsem",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Hang kapcsolása",
"video_player.toggle_visible": "Toggle visibility",

View File

@ -33,6 +33,7 @@
"column.home": "Beranda",
"column.mutes": "Pengguna dibisukan",
"column.notifications": "Notifikasi",
"column.pins": "Pinned toot",
"column.public": "Linimasa gabunggan",
"column_back_button.label": "Kembali",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Informasi selengkapnya",
"navigation_bar.logout": "Keluar",
"navigation_bar.mutes": "Pengguna dibisukan",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Pengaturan",
"navigation_bar.public_timeline": "Linimasa gabungan",
"notification.favourite": "{name} menyukai status anda",
@ -193,6 +195,15 @@
"upload_button.label": "Tambahkan media",
"upload_form.undo": "Undo",
"upload_progress.label": "Mengunggah...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Tampilkan video",
"video_player.toggle_sound": "Suara",
"video_player.toggle_visible": "Tampilan",

View File

@ -33,6 +33,7 @@
"column.home": "Hemo",
"column.mutes": "Celita uzeri",
"column.notifications": "Savigi",
"column.pins": "Pinned toot",
"column.public": "Federata tempolineo",
"column_back_button.label": "Retro",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Detaloza informi",
"navigation_bar.logout": "Ekirar",
"navigation_bar.mutes": "Celita uzeri",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferi",
"navigation_bar.public_timeline": "Federata tempolineo",
"notification.favourite": "{name} favorizis tua mesajo",
@ -193,6 +195,15 @@
"upload_button.label": "Adjuntar kontenajo",
"upload_form.undo": "Desfacar",
"upload_progress.label": "Kargante...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Extensar video",
"video_player.toggle_sound": "Acendar sono",
"video_player.toggle_visible": "Chanjar videbleso",

View File

@ -33,6 +33,7 @@
"column.home": "Home",
"column.mutes": "Utenti silenziati",
"column.notifications": "Notifiche",
"column.pins": "Pinned toot",
"column.public": "Timeline federata",
"column_back_button.label": "Indietro",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Informazioni estese",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Utenti silenziati",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Impostazioni",
"navigation_bar.public_timeline": "Timeline federata",
"notification.favourite": "{name} ha apprezzato il tuo post",
@ -193,6 +195,15 @@
"upload_button.label": "Aggiungi file multimediale",