[Glitch] Add pop-out player for audio/video in web UI

port d88a79b456 to glitch-soc

Signed-off-by: Thibaut Girka <thib@sitedethib.com>
This commit is contained in:
Eugen Rochko
2020-09-28 13:29:43 +02:00
committed by Thibaut Girka
parent 9c88792f0a
commit 8f950e540b
21 changed files with 681 additions and 56 deletions

View File

@ -5,10 +5,21 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
import { reduceMotion } from 'flavours/glitch/util/initial_state';
const obfuscatedCount = count => {
if (count < 0) {
return 0;
} else if (count <= 1) {
return count;
} else {
return '1+';
}
};
export default class AnimatedNumber extends React.PureComponent {
static propTypes = {
value: PropTypes.number.isRequired,
obfuscate: PropTypes.bool,
};
state = {
@ -36,11 +47,11 @@ export default class AnimatedNumber extends React.PureComponent {
}
render () {
const { value } = this.props;
const { value, obfuscate } = this.props;
const { direction } = this.state;
if (reduceMotion) {
return <FormattedNumber value={value} />;
return obfuscate ? obfuscatedCount(value) : <FormattedNumber value={value} />;
}
const styles = [{
@ -54,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent {
{items => (
<span className='animated-number'>
{items.map(({ key, data, style }) => (
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}><FormattedNumber value={data} /></span>
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
))}
</span>
)}

View File

@ -4,6 +4,7 @@ import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import AnimatedNumber from 'flavours/glitch/components/animated_number';
export default class IconButton extends React.PureComponent {
@ -27,6 +28,8 @@ export default class IconButton extends React.PureComponent {
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
label: PropTypes.string,
counter: PropTypes.number,
obfuscateCount: PropTypes.bool,
};
static defaultProps = {
@ -104,6 +107,8 @@ export default class IconButton extends React.PureComponent {
pressed,
tabIndex,
title,
counter,
obfuscateCount,
} = this.props;
const {
@ -120,6 +125,10 @@ export default class IconButton extends React.PureComponent {
overlayed: overlay,
});
if (typeof counter !== 'undefined') {
style.width = 'auto';
}
return (
<button
aria-label={title}
@ -135,7 +144,7 @@ export default class IconButton extends React.PureComponent {
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
<Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
{this.props.label}
</button>
);

View File

@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'flavours/glitch/components/icon';
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { FormattedMessage } from 'react-intl';
export default @connect()
class PictureInPicturePlaceholder extends React.PureComponent {
static propTypes = {
width: PropTypes.number,
dispatch: PropTypes.func.isRequired,
};
state = {
width: this.props.width,
height: this.props.width && (this.props.width / (16/9)),
};
handleClick = () => {
const { dispatch } = this.props;
dispatch(removePictureInPicture());
}
setRef = c => {
this.node = c;
if (this.node) {
this._setDimensions();
}
}
_setDimensions () {
const width = this.node.offsetWidth;
const height = width / (16/9);
this.setState({ width, height });
}
componentDidMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
}
handleResize = debounce(() => {
if (this.node) {
this._setDimensions();
}
}, 250, {
trailing: true,
});
render () {
const { height } = this.state;
return (
<div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex='0' onClick={this.handleClick}>
<Icon id='window-restore' />
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
</div>
);
}
}

View File

@ -17,6 +17,7 @@ import classNames from 'classnames';
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
import PollContainer from 'flavours/glitch/containers/poll_container';
import { displayMedia } from 'flavours/glitch/util/initial_state';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
@ -97,6 +98,8 @@ class Status extends ImmutablePureComponent {
cachedMediaWidth: PropTypes.number,
onClick: PropTypes.func,
scrollKey: PropTypes.string,
deployPictureInPicture: PropTypes.func,
usingPiP: PropTypes.bool,
};
state = {
@ -123,6 +126,7 @@ class Status extends ImmutablePureComponent {
'hidden',
'expanded',
'unread',
'usingPiP',
]
updateOnStates = [
@ -394,6 +398,12 @@ class Status extends ImmutablePureComponent {
}
}
handleDeployPictureInPicture = (type, mediaProps) => {
const { deployPictureInPicture, status } = this.props;
deployPictureInPicture(status, type, mediaProps);
}
handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this.props.status, this.context.router.history);
@ -496,6 +506,7 @@ class Status extends ImmutablePureComponent {
hidden,
unread,
featured,
usingPiP,
...other
} = this.props;
const { isExpanded, isCollapsed, forceFilter } = this.state;
@ -576,6 +587,9 @@ class Status extends ImmutablePureComponent {
if (status.get('poll')) {
media = <PollContainer pollId={status.get('poll')} />;
mediaIcon = 'tasks';
} else if (usingPiP) {
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
mediaIcon = 'video-camera';
} else if (attachments.size > 0) {
if (muted || attachments.some(item => item.get('type') === 'unknown')) {
media = (
@ -601,6 +615,7 @@ class Status extends ImmutablePureComponent {
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={this.handleDeployPictureInPicture}
/>
)}
</Bundle>
@ -624,6 +639,7 @@ class Status extends ImmutablePureComponent {
onOpenVideo={this.handleOpenVideo}
width={this.props.cachedMediaWidth}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={this.handleDeployPictureInPicture}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>)}

View File

@ -40,16 +40,6 @@ const messages = defineMessages({
hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
});
const obfuscatedCount = count => {
if (count < 0) {
return 0;
} else if (count <= 1) {
return count;
} else {
return '1+';
}
};
export default @injectIntl
class StatusActionBar extends ImmutablePureComponent {
@ -284,10 +274,14 @@ class StatusActionBar extends ImmutablePureComponent {
);
if (showReplyCount) {
replyButton = (
<div className='status__action-bar__counter'>
{replyButton}
<span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span>
</div>
<IconButton
className='status__action-bar-button'
title={replyTitle}
icon={replyIcon}
onClick={this.handleReplyClick}
counter={status.get('replies_count')}
obfuscateCount
/>
);
}