Skip to content

Commit

Permalink
Ending, loading & error player screen (#996)
Browse files Browse the repository at this point in the history
* add missing restart icon

* update CirculaarProgressbar

* add player ending screen

* install lottie and add lottie animation and fallback

* add loading overlay

* add BigPlayButtonOverlay

* add restart glyph icon

* add EndingOverlay

* rebase

* fix loading fallback image

* add ErrorOverlay, fix position issues

* fix player container height

* fix indicator sizes for mobile

* fix error animation

* add current thumbnail

* overlays improvements

* refactor, move overlays to separate folder

* create ControlsIndicatorManager, bring back indicator on video click

* add short transition for videos overlay

* remove previous animation files, use Loaded and AnimatedError

* remove player loader fallback

* remove videojs default error screen

* cr fixes

* timeout type
  • Loading branch information
drillprop authored Jul 22, 2021
1 parent 5b0f72f commit 1a155e1
Show file tree
Hide file tree
Showing 23 changed files with 1,044 additions and 284 deletions.
2 changes: 2 additions & 0 deletions src/config/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export const WEB3_APP_NAME = 'Joystream Atlas'
export const STORAGE_URL_PATH = 'asset/v0'

export const COVER_VIDEO_INFO_URL = 'https://eu-central-1.linodeobjects.com/atlas-hero/cover-info.json'

export const JOYSTREAM_DISCORD_URL = 'https://discord.gg/DE9UN3YpRP'
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
import styled from '@emotion/styled'

import theme from '@/shared/theme'
import { colors } from '@/shared/theme'

import { Path } from './Path'

export type TrailVariant = 'default' | 'player'

type TrailProps = {
variant?: TrailVariant
}

const getStrokeColor = (variant?: TrailVariant) => {
switch (variant) {
case 'default':
return colors.gray[700]
case 'player':
return colors.transparentWhite[32]
default:
return colors.gray[700]
}
}

export const SVG = styled.svg`
/* needed when parent container has display: flex */
width: 100%;
`
export const Trail = styled(Path)`
stroke: ${theme.colors.gray[700]};
export const Trail = styled(Path)<TrailProps>`
stroke: ${({ variant }) => getStrokeColor(variant)};
`
export const StyledPath = styled(Path)`
stroke: ${theme.colors.blue[500]};
stroke: ${colors.blue[500]};
transition: stroke-dashoffset 0.5s ease 0s;
`
export const Background = styled.circle`
fill: ${theme.colors.gray[800]};
fill: ${colors.gray[800]};
`
21 changes: 14 additions & 7 deletions src/shared/components/CircularProgressbar/CircularProgressbar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'

import { Background, SVG, StyledPath, Trail } from './CircularProgressbar.style'
import { Background, SVG, StyledPath, Trail, TrailVariant } from './CircularProgressbar.style'

export const VIEWBOX_WIDTH = 100
export const VIEWBOX_HEIGHT = 100
Expand All @@ -18,6 +18,8 @@ export type CircularProgressbarProps = {
background?: boolean
backgroundPadding?: number
className?: string
variant?: TrailVariant
noTrail?: boolean
}

export const CircularProgressbar: React.FC<CircularProgressbarProps> = ({
Expand All @@ -29,6 +31,8 @@ export const CircularProgressbar: React.FC<CircularProgressbarProps> = ({
maxValue = 100,
minValue = 0,
strokeWidth = 15,
variant = 'default',
noTrail,
className,
}) => {
const getBackgroundPadding = () => (background ? backgroundPadding : 0)
Expand All @@ -46,12 +50,15 @@ export const CircularProgressbar: React.FC<CircularProgressbarProps> = ({
<>
<SVG viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`} className={className}>
{background ? <Background cx={VIEWBOX_CENTER_X} cy={VIEWBOX_CENTER_Y} r={VIEWBOX_HEIGHT_HALF} /> : null}
<Trail
counterClockwise={counterClockwise}
dashRatio={circleRatio}
pathRadius={pathRadius}
strokeWidth={strokeWidth}
/>
{noTrail && (
<Trail
counterClockwise={counterClockwise}
dashRatio={circleRatio}
pathRadius={pathRadius}
variant={variant}
strokeWidth={strokeWidth}
/>
)}
<StyledPath
counterClockwise={counterClockwise}
dashRatio={pathRatio * circleRatio}
Expand Down
83 changes: 83 additions & 0 deletions src/shared/components/VideoPlayer/ControlsIndicator.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import styled from '@emotion/styled'

import { colors, media, sizes } from '@/shared/theme'

export const ControlsIndicatorWrapper = styled.div`
position: absolute;
display: flex;
flex-direction: column;
top: calc(50% - ${sizes(10)});
left: calc(50% - ${sizes(10)});
${media.small} {
top: calc(50% - ${sizes(16)});
left: calc(50% - ${sizes(16)});
}
`

export const ControlsIndicatorIconWrapper = styled.div`
width: ${sizes(20)};
height: ${sizes(20)};
backdrop-filter: blur(${sizes(6)});
background-color: ${colors.transparentBlack[54]};
border-radius: 100%;
display: flex;
transform: scale(0.5);
justify-content: center;
align-items: center;
> svg {
transform: scale(0.75);
width: ${sizes(12)};
height: ${sizes(12)};
}
${media.small} {
width: ${sizes(32)};
height: ${sizes(32)};
> svg {
transform: scale(0.75);
width: ${sizes(18)};
height: ${sizes(18)};
}
}
`

export const ControlsIndicatorTooltip = styled.div`
user-select: none;
display: none;
align-self: center;
background-color: ${colors.transparentBlack[54]};
padding: ${sizes(2)};
text-align: center;
margin-top: ${sizes(3)};
backdrop-filter: blur(${sizes(8)});
${media.small} {
display: block;
}
`

const animationEasing = 'cubic-bezier(0, 0, 0.3, 1)'

export const ControlsIndicatorTransitions = styled.div`
.indicator-exit {
opacity: 1;
}
.indicator-exit-active {
${ControlsIndicatorIconWrapper} {
transform: scale(1);
opacity: 0;
transition: transform 750ms ${animationEasing}, opacity 600ms 150ms ${animationEasing};
> svg {
transform: scale(1);
transition: transform 750ms ${animationEasing};
}
}
${ControlsIndicatorTooltip} {
opacity: 0;
transition: transform 750ms ${animationEasing}, opacity 600ms 150ms ${animationEasing};
}
}
`
157 changes: 157 additions & 0 deletions src/shared/components/VideoPlayer/ControlsIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, { useEffect, useState } from 'react'
import { CSSTransition } from 'react-transition-group'
import { VideoJsPlayer } from 'video.js'

import {
SvgPlayerBackwardFiveSec,
SvgPlayerBackwardTenSec,
SvgPlayerForwardFiveSec,
SvgPlayerForwardTenSec,
SvgPlayerPause,
SvgPlayerPlay,
SvgPlayerSoundHalf,
SvgPlayerSoundOff,
SvgPlayerSoundOn,
} from '@/shared/icons'

import {
ControlsIndicatorIconWrapper,
ControlsIndicatorTooltip,
ControlsIndicatorTransitions,
ControlsIndicatorWrapper,
} from './ControlsIndicator.style'
import { CustomVideojsEvents } from './videoJsPlayer'

import { Text } from '../Text'

type VideoEvent = CustomVideojsEvents | null

type EventState = {
type: VideoEvent
description: string | null
icon: React.ReactNode | null
isVisible: boolean
}

type ControlsIndicatorProps = {
player: VideoJsPlayer | null
}

export const ControlsIndicator: React.FC<ControlsIndicatorProps> = ({ player }) => {
const [indicator, setIndicator] = useState<EventState | null>(null)
useEffect(() => {
if (!player) {
return
}
let timeout: number
const indicatorEvents = Object.values(CustomVideojsEvents)

const handler = (e: Event) => {
// This setTimeout is needed to get current value from `player.volume()`
// if we omit this we'll get stale results
timeout = window.setTimeout(() => {
const indicator = createIndicator(e.type as VideoEvent, player.volume(), player.muted())
if (indicator) {
setIndicator({ ...indicator, isVisible: true })
}
}, 0)
}
player.on(indicatorEvents, handler)

return () => {
clearTimeout(timeout)
player.off(indicatorEvents, handler)
}
}, [player])

return (
<ControlsIndicatorTransitions>
<CSSTransition
in={indicator?.isVisible}
timeout={indicator?.isVisible ? 0 : 750}
classNames="indicator"
mountOnEnter
unmountOnExit
onEntered={() => setIndicator((indicator) => (indicator ? { ...indicator, isVisible: false } : null))}
onExited={() => setIndicator(null)}
>
<ControlsIndicatorWrapper>
<ControlsIndicatorIconWrapper>{indicator?.icon}</ControlsIndicatorIconWrapper>
<ControlsIndicatorTooltip>
<Text variant="caption">{indicator?.description}</Text>
</ControlsIndicatorTooltip>
</ControlsIndicatorWrapper>
</CSSTransition>
</ControlsIndicatorTransitions>
)
}

const createIndicator = (type: VideoEvent | null, playerVolume: number, playerMuted: boolean) => {
const formattedVolume = Math.floor(playerVolume * 100) + '%'
const isMuted = playerMuted || !Number(playerVolume.toFixed(2))

switch (type) {
case CustomVideojsEvents.PauseControl:
return {
icon: <SvgPlayerPause />,
description: 'Pause',
type,
}
case CustomVideojsEvents.PlayControl:
return {
icon: <SvgPlayerPlay />,
description: 'Play',
type,
}
case CustomVideojsEvents.BackwardFiveSec:
return {
icon: <SvgPlayerBackwardFiveSec />,
description: 'Backward 5s',
type,
}
case CustomVideojsEvents.ForwardFiveSec:
return {
icon: <SvgPlayerForwardFiveSec />,
description: 'Forward 5s',
type,
}
case CustomVideojsEvents.BackwardTenSec:
return {
icon: <SvgPlayerBackwardTenSec />,
description: 'Backward 10s',
type,
}
case CustomVideojsEvents.ForwardTenSec:
return {
icon: <SvgPlayerForwardTenSec />,
description: 'Forward 10s',
type,
}
case CustomVideojsEvents.Unmuted:
return {
icon: <SvgPlayerSoundOn />,
description: formattedVolume,
type,
}
case CustomVideojsEvents.Muted:
return {
icon: <SvgPlayerSoundOff />,
description: 'Mute',
type,
}
case CustomVideojsEvents.VolumeIncrease:
return {
icon: <SvgPlayerSoundOn />,
description: formattedVolume,
type,
}
case CustomVideojsEvents.VolumeDecrease:
return {
icon: isMuted ? <SvgPlayerSoundOff /> : <SvgPlayerSoundHalf />,
description: isMuted ? 'Mute' : formattedVolume,
type,
}
default:
return null
}
}
Loading

0 comments on commit 1a155e1

Please sign in to comment.