mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-29 15:13:11 +01:00
[WIP] Initial status work
This commit is contained in:
191
app/javascript/glitch/components/status/content/gallery/index.js
Normal file
191
app/javascript/glitch/components/status/content/gallery/index.js
Normal file
@@ -0,0 +1,191 @@
|
||||
// <StatusContentGallery>
|
||||
// ======================
|
||||
|
||||
// For code documentation, please see:
|
||||
// https://glitch-soc.github.io/docs/javascript/glitch/status/content/gallery
|
||||
|
||||
// For more information, please contact:
|
||||
// @kibi@glitch.social
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// Imports:
|
||||
// --------
|
||||
|
||||
// Package imports.
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
// Our imports.
|
||||
import StatusContentGalleryItem from './item';
|
||||
import StatusContentGalleryPlayer from './player';
|
||||
import CommonButton from 'glitch/components/common/button';
|
||||
|
||||
// Stylesheet imports.
|
||||
import './style';
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// Initial setup
|
||||
// -------------
|
||||
|
||||
// Holds our localization messages.
|
||||
const messages = defineMessages({
|
||||
hide: { id: 'media_gallery.hide_media', defaultMessage: 'Hide media' },
|
||||
});
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// The component
|
||||
// -------------
|
||||
|
||||
export default class StatusContentGallery extends ImmutablePureComponent {
|
||||
|
||||
// Props and state.
|
||||
static propTypes = {
|
||||
attachments: ImmutablePropTypes.list.isRequired,
|
||||
autoPlayGif: PropTypes.bool,
|
||||
fullwidth: PropTypes.bool,
|
||||
height: PropTypes.number.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
letterbox: PropTypes.bool,
|
||||
onOpenMedia: PropTypes.func.isRequired,
|
||||
onOpenVideo: PropTypes.func.isRequired,
|
||||
sensitive: PropTypes.bool,
|
||||
standalone: PropTypes.bool,
|
||||
};
|
||||
state = {
|
||||
visible: !this.props.sensitive,
|
||||
};
|
||||
|
||||
// Handles media clicks.
|
||||
handleMediaClick = index => {
|
||||
const { attachments, onOpenMedia, standalone } = this.props;
|
||||
if (standalone) return;
|
||||
onOpenMedia(attachments, index);
|
||||
}
|
||||
|
||||
// Handles showing and hiding.
|
||||
handleToggle = () => {
|
||||
this.setState({ visible: !this.state.visible });
|
||||
}
|
||||
|
||||
// Handles video clicks.
|
||||
handleVideoClick = time => {
|
||||
const { attachments, onOpenVideo, standalone } = this.props;
|
||||
if (standalone) return;
|
||||
onOpenVideo(attachments.get(0), time);
|
||||
}
|
||||
|
||||
// Renders.
|
||||
render () {
|
||||
const { handleMediaClick, handleToggle, handleVideoClick } = this;
|
||||
const {
|
||||
attachments,
|
||||
autoPlayGif,
|
||||
fullwidth,
|
||||
intl,
|
||||
letterbox,
|
||||
sensitive,
|
||||
} = this.props;
|
||||
const { visible } = this.state;
|
||||
const computedClass = classNames('glitch', 'glitch__status__content__gallery', {
|
||||
_fullwidth: fullwidth,
|
||||
});
|
||||
const useableAttachments = attachments.take(4);
|
||||
let button;
|
||||
let children;
|
||||
let size;
|
||||
|
||||
// This handles hidden media
|
||||
if (!this.state.visible) {
|
||||
button = (
|
||||
<CommonButton
|
||||
active
|
||||
className='gallery\sensitive gallery\curtain'
|
||||
title={intl.formatMessage(messages.hide)}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<span className='gallery\message'>
|
||||
<strong className='gallery\warning'>
|
||||
{sensitive ? (
|
||||
<FormattedMessage
|
||||
id='status.sensitive_warning'
|
||||
defaultMessage='Sensitive content'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='status.media_hidden'
|
||||
defaultMessage='Media hidden'
|
||||
/>
|
||||
)}
|
||||
</strong>
|
||||
<FormattedMessage
|
||||
defaultMessage='Click to view'
|
||||
id='status.sensitive_toggle'
|
||||
/>
|
||||
</span>
|
||||
</CommonButton>
|
||||
); // No children with hidden media
|
||||
|
||||
// If our media is visible, then we render it alongside the
|
||||
// "eyeball" button.
|
||||
} else {
|
||||
button = (
|
||||
<CommonButton
|
||||
className='gallery\sensitive gallery\button'
|
||||
icon={visible ? 'eye' : 'eye-slash'}
|
||||
title={intl.formatMessage(messages.hide)}
|
||||
onClick={handleToggle}
|
||||
/>
|
||||
);
|
||||
|
||||
// If our first item is a video, we render a player. Otherwise,
|
||||
// we render our images.
|
||||
if (attachments.getIn([0, 'type']) === 'video') {
|
||||
size = 1;
|
||||
children = (
|
||||
<StatusContentGalleryPlayer
|
||||
attachment={attachments.get(0)}
|
||||
autoPlayGif={autoPlayGif}
|
||||
intl={intl}
|
||||
letterbox={letterbox}
|
||||
onClick={handleVideoClick}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
size = useableAttachments.size;
|
||||
children = useableAttachments.map(
|
||||
(attachment, index) => (
|
||||
<StatusContentGalleryItem
|
||||
attachment={attachment}
|
||||
autoPlayGif={autoPlayGif}
|
||||
gallerySize={size}
|
||||
index={index}
|
||||
intl={intl}
|
||||
key={attachment.get('id')}
|
||||
letterbox={letterbox}
|
||||
onClick={handleMediaClick}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Renders the gallery.
|
||||
return (
|
||||
<div
|
||||
className={computedClass}
|
||||
style={{ height: `${this.props.height}px` }}
|
||||
>
|
||||
{button}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// <StatusContentGalleryItem>
|
||||
// ==============
|
||||
|
||||
// For code documentation, please see:
|
||||
// https://glitch-soc.github.io/docs/javascript/glitch/status/content/gallery/item
|
||||
|
||||
// For more information, please contact:
|
||||
// @kibi@glitch.social
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// Imports:
|
||||
// --------
|
||||
|
||||
// Package imports.
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
// Mastodon imports.
|
||||
import { isIOS } from 'mastodon/is_mobile';
|
||||
|
||||
// Our imports.
|
||||
import CommonButton from 'glitch/components/common/button';
|
||||
|
||||
// Stylesheet imports.
|
||||
import './style';
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// Initial setup
|
||||
// -------------
|
||||
|
||||
// Holds our localization messages.
|
||||
const messages = defineMessages({
|
||||
expand: { id: 'media_gallery.expand', defaultMessage: 'Expand image' },
|
||||
});
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// The component
|
||||
// -------------
|
||||
|
||||
export default class StatusContentGalleryItem extends ImmutablePureComponent {
|
||||
|
||||
// Props.
|
||||
static propTypes = {
|
||||
attachment: ImmutablePropTypes.map.isRequired,
|
||||
autoPlayGif: PropTypes.bool,
|
||||
gallerySize: PropTypes.number.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
letterbox: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
// Click handling.
|
||||
handleClick = this.props.onClick.bind(this, this.props.index);
|
||||
|
||||
// Item rendering.
|
||||
render () {
|
||||
const { handleClick } = this;
|
||||
const {
|
||||
attachment,
|
||||
autoPlayGif,
|
||||
gallerySize,
|
||||
intl,
|
||||
letterbox,
|
||||
} = this.props;
|
||||
const originalUrl = attachment.get('url');
|
||||
const previewUrl = attachment.get('preview_url');
|
||||
const remoteUrl = attachment.get('remote_url');
|
||||
let thumbnail = '';
|
||||
const computedClass = classNames('glitch', 'glitch__status__content__gallery__item', {
|
||||
_letterbox: letterbox,
|
||||
});
|
||||
|
||||
// If our gallery has more than one item, our images only take up
|
||||
// half the width. We need this for image `sizes` calculations.
|
||||
let multiplier = gallerySize === 1 ? 1 : .5;
|
||||
|
||||
// Image attachments
|
||||
if (attachment.get('type') === 'image') {
|
||||
const previewWidth = attachment.getIn(['meta', 'small', 'width']);
|
||||
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
||||
|
||||
// This lets the browser conditionally select the preview or
|
||||
// original image depending on what the rendered size ends up
|
||||
// being. We, of course, have no way of knowing what the width
|
||||
// of the gallery will be post–CSS, but we conservatively roll
|
||||
// with 400px. (Note: Upstream Mastodon used media queries here,
|
||||
// but because our page layout is user-configurable, we don't
|
||||
// bother.)
|
||||
const srcSet = `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`;
|
||||
const sizes = `${(400 * multiplier) >> 0}px`;
|
||||
|
||||
// The image.
|
||||
thumbnail = (
|
||||
<img
|
||||
alt=''
|
||||
className='item\image'
|
||||
sizes={sizes}
|
||||
src={previewUrl}
|
||||
srcSet={srcSet}
|
||||
/>
|
||||
);
|
||||
|
||||
// Gifv attachments.
|
||||
} else if (attachment.get('type') === 'gifv') {
|
||||
const autoPlay = !isIOS() && autoPlayGif;
|
||||
thumbnail = (
|
||||
<video
|
||||
autoPlay={autoPlay}
|
||||
className='item\gifv'
|
||||
loop
|
||||
muted
|
||||
poster={previewUrl}
|
||||
src={originalUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Rendering. We render the item inside of a button+link, which
|
||||
// provides the original. (We can do this for gifvs because we
|
||||
// don't show the controls.)
|
||||
return (
|
||||
<CommonButton
|
||||
className={computedClass}
|
||||
data-gallery-size={gallerySize}
|
||||
href={remoteUrl || originalUrl}
|
||||
key={attachment.get('id')}
|
||||
onClick={handleClick}
|
||||
title={intl.formatMessage(messages.expand)}
|
||||
>{thumbnail}</CommonButton>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@import 'variables';
|
||||
|
||||
.glitch.glitch__status__content__gallery__item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: zoom-in;
|
||||
|
||||
.item\\image,
|
||||
.item\\gifv {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
|
||||
@supports (object-fit: cover) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&.letterbox {
|
||||
.item\\image,
|
||||
.item\\gifv {
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: fill;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-gallery-size="2"] {
|
||||
width: calc(50% - .5625em);
|
||||
height: calc(100% - .75em);
|
||||
margin: .375em .1875em .375em .375em;
|
||||
|
||||
&:last-child {
|
||||
margin: .375em .375em .375em .1875em;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-gallery-size="3"] {
|
||||
width: calc(50% - .5625em);
|
||||
height: calc(100% - .75em);
|
||||
margin: .375em .1875em .375em .375em;
|
||||
|
||||
&:nth-last-child(2) {
|
||||
float: right;
|
||||
height: calc(50% - .5625em);
|
||||
margin: .375em .375em .1875em .1875em;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
float: right;
|
||||
height: calc(50% - .5625em);
|
||||
margin: .1875em .375em .1875em .375em;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-gallery-size="4"] {
|
||||
width: calc(50% - .5625em);
|
||||
height: calc(50% - .5625em);
|
||||
margin: .375em .1875em .1875em .375em;
|
||||
|
||||
&:nth-last-child(3) {
|
||||
margin: .375em .375em .1875em .1875em;
|
||||
}
|
||||
|
||||
&:nth-last-child(2) {
|
||||
margin: .1875em .1875em .375em .375em;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin: .1875em .375em .375em .1875em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// add GIF label in CSS
|
||||
@@ -0,0 +1,233 @@
|
||||
// <StatusContentGalleryPlayer>
|
||||
// ==============
|
||||
|
||||
// For code documentation, please see:
|
||||
// https://glitch-soc.github.io/docs/javascript/glitch/status/content/gallery/player
|
||||
|
||||
// For more information, please contact:
|
||||
// @kibi@glitch.social
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// Imports:
|
||||
// --------
|
||||
|
||||
// Package imports.
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
// Mastodon imports.
|
||||
import { isIOS } from 'mastodon/is_mobile';
|
||||
|
||||
// Our imports.
|
||||
import CommonButton from 'glitch/components/common/button';
|
||||
|
||||
// Stylesheet imports.
|
||||
import './style';
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// Initial setup
|
||||
// -------------
|
||||
|
||||
// Holds our localization messages.
|
||||
const messages = defineMessages({
|
||||
mute: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
|
||||
open: { id: 'video_player.open', defaultMessage: 'Open video' },
|
||||
play: { id: 'video_player.play', defaultMessage: 'Play video' },
|
||||
pause: { id: 'video_player.pause', defaultMessage: 'Pause video' },
|
||||
expand: { id: 'video_player.expand', defaultMessage: 'Expand video' },
|
||||
});
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// The component
|
||||
// -------------
|
||||
|
||||
export default class StatusContentGalleryPlayer extends ImmutablePureComponent {
|
||||
|
||||
// Props and state.
|
||||
static propTypes = {
|
||||
attachment: ImmutablePropTypes.map.isRequired,
|
||||
autoPlayGif: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
letterbox: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
}
|
||||
state = {
|
||||
hasAudio: true,
|
||||
muted: true,
|
||||
preview: !isIOS() && this.props.autoPlayGif,
|
||||
videoError: false,
|
||||
}
|
||||
|
||||
// Basic video controls.
|
||||
handleMute = () => {
|
||||
this.setState({ muted: !this.state.muted });
|
||||
}
|
||||
handlePlayPause = () => {
|
||||
const { video } = this;
|
||||
if (video.paused) {
|
||||
video.play();
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
}
|
||||
|
||||
// When clicking we either open (de-preview) the video or we
|
||||
// expand it, depending. Note that when we de-preview the video will
|
||||
// also begin playing (except on iOS) due to its `autoplay`
|
||||
// attribute.
|
||||
handleClick = () => {
|
||||
const { setState, video } = this;
|
||||
const { onClick } = this.props;
|
||||
const { preview } = this.state;
|
||||
if (preview) setState({ preview: false });
|
||||
else {
|
||||
video.pause();
|
||||
onClick(video.currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Loading and errors. We have to do some hacks in order to check if
|
||||
// the video has audio imo. There's probably a better way to do this
|
||||
// but that's how upstream has it.
|
||||
handleLoadedData = () => {
|
||||
const { video } = this;
|
||||
if (('WebkitAppearance' in document.documentElement.style && video.audioTracks.length === 0) || video.mozHasAudio === false) {
|
||||
this.setState({ hasAudio: false });
|
||||
}
|
||||
}
|
||||
handleVideoError = () => {
|
||||
this.setState({ videoError: true });
|
||||
}
|
||||
|
||||
// On mounting or update, we ensure our video has the needed event
|
||||
// listeners. We can't necessarily do this right away because there
|
||||
// might be a preview up.
|
||||
componentDidMount () {
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
componentDidUpdate () {
|
||||
const { handleLoadedData, handleVideoError, video } = this;
|
||||
if (!video) return;
|
||||
video.addEventListener('loadeddata', handleLoadedData);
|
||||
video.addEventListener('error', handleVideoError);
|
||||
}
|
||||
|
||||
// On unmounting, we remove the listeners from the video element.
|
||||
componentWillUnmount () {
|
||||
const { handleLoadedData, handleVideoError, video } = this;
|
||||
if (!video) return;
|
||||
video.removeEventListener('loadeddata', handleLoadedData);
|
||||
video.removeEventListener('error', handleVideoError);
|
||||
}
|
||||
|
||||
// Getting a reference to our video.
|
||||
setRef = (c) => {
|
||||
this.video = c;
|
||||
}
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const {
|
||||
handleClick,
|
||||
handleMute,
|
||||
handlePlayPause,
|
||||
setRef,
|
||||
video,
|
||||
} = this;
|
||||
const {
|
||||
attachment,
|
||||
letterbox,
|
||||
intl,
|
||||
} = this.props;
|
||||
const {
|
||||
hasAudio,
|
||||
muted,
|
||||
preview,
|
||||
videoError,
|
||||
} = this.state;
|
||||
const originalUrl = attachment.get('url');
|
||||
const previewUrl = attachment.get('preview_url');
|
||||
const remoteUrl = attachment.get('remote_url');
|
||||
let content = null;
|
||||
const computedClass = classNames('glitch', 'glitch__status__content__gallery__player', {
|
||||
_letterbox: letterbox,
|
||||
});
|
||||
|
||||
// This gets our content: either a preview image, an error
|
||||
// message, or the video.
|
||||
switch (true) {
|
||||
case preview:
|
||||
content = (
|
||||
<img
|
||||
alt=''
|
||||
className='player\preview'
|
||||
src={previewUrl}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case videoError:
|
||||
content = (
|
||||
<span className='player\error'>
|
||||
<FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' />
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
content = (
|
||||
<video
|
||||
autoPlay={!isIOS()}
|
||||
className='player\video'
|
||||
loop
|
||||
muted={muted}
|
||||
poster={previewUrl}
|
||||
ref={setRef}
|
||||
src={originalUrl}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Everything goes inside of a button because everything is a
|
||||
// button. This is okay wrt the video element because it doesn't
|
||||
// have controls.
|
||||
return (
|
||||
<div className={computedClass}>
|
||||
<CommonButton
|
||||
className='player\box'
|
||||
href={remoteUrl || originalUrl}
|
||||
key='box'
|
||||
onClick={handleClick}
|
||||
title={intl.formatMessage(preview ? messages.open : messages.expand)}
|
||||
>{content}</CommonButton>
|
||||
{!preview ? (
|
||||
<CommonButton
|
||||
active={!video.paused}
|
||||
className='player\play-pause player\button'
|
||||
icon={video.paused ? 'play' : 'pause'}
|
||||
key='play'
|
||||
onClick={handlePlayPause}
|
||||
title={intl.formatMessage(messages.play)}
|
||||
/>
|
||||
) : null}
|
||||
{!preview && hasAudio ? (
|
||||
<CommonButton
|
||||
active={!muted}
|
||||
className='player\mute player\button'
|
||||
icon={muted ? 'volume-off' : 'volume-up'}
|
||||
key='mute'
|
||||
onClick={handleMute}
|
||||
title={intl.formatMessage(messages.mute)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
@import 'variables';
|
||||
|
||||
.glitch.glitch__status__content__gallery__player {
|
||||
display: block;
|
||||
padding: (1.5em * 1.35) 0; // Creates black bars at the bottom/top
|
||||
width: 100%;
|
||||
height: calc(100% - (1.5em * 1.35 * 2));
|
||||
cursor: zoom-in;
|
||||
|
||||
.player\\box {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
& > img,
|
||||
& > video {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
|
||||
@supports (object-fit: cover) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.player\\button {
|
||||
position: absolute;
|
||||
margin: .35em;
|
||||
border-radius: .35em;
|
||||
padding: .1625em;
|
||||
height: 1em; // 1 + 2*.35 + 2*.1625 = 1.5*1.35
|
||||
color: $primary-text-color;
|
||||
background: $base-overlay-background;
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
opacity: .7;
|
||||
|
||||
&.player\\play-pause {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.player\\mute {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&._letterbox {
|
||||
.player\\box {
|
||||
& > img,
|
||||
& > video {
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
@import 'variables';
|
||||
|
||||
.glitch.glitch__status__content__gallery {
|
||||
display: block;
|
||||
position: relative;
|
||||
color: $ui-primary-color;
|
||||
background: $base-shadow-color;
|
||||
|
||||
.gallery\\button {
|
||||
position: absolute;
|
||||
margin: .35em;
|
||||
border-radius: .35em;
|
||||
padding: .1625em;
|
||||
height: 1em; // 1 + 2*.35 + 2*.1625 = 1.5*1.35
|
||||
color: $primary-text-color;
|
||||
background: $base-overlay-background;
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
opacity: .7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.gallery\\sensitive {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.gallery\\curtain.gallery\\sensitive {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
color: $ui-secondary-color;
|
||||
background: $base-overlay-background;
|
||||
font-size: (1.25em / 1.35); // approx. .925em
|
||||
line-height: 1.35;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: color ($glitch-animation-speed * .15s) ease-in;
|
||||
|
||||
.gallery\\message {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2.6em;
|
||||
margin: auto;
|
||||
|
||||
.gallery\\warning {
|
||||
display: block;
|
||||
font-size: (1.35em / 1.25);
|
||||
line-height: 1.35;
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: $primary-text-color;
|
||||
background: $base-overlay-background; // No change
|
||||
transition: color ($glitch-animation-speed * .3s) ease-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user