feat(ott-2852): add circle animation

keep-around/61a2661a1c7b0f958a65795b5393822fb1d16524
karela 3 years ago
parent 26cbef8763
commit dfd0d71b3b
  1. 9
      src/features/CircleAnimationBar/helpers.tsx
  2. 72
      src/features/CircleAnimationBar/index.tsx
  3. 43
      src/features/CircleAnimationBar/styled.tsx
  4. 8
      src/features/MatchPage/components/FinishedMatch/index.tsx
  5. 8
      src/features/MatchPage/store/hooks/index.tsx
  6. 3
      src/features/MatchSidePlaylists/components/EventsList/index.tsx
  7. 29
      src/features/MatchSidePlaylists/components/TabEvents/index.tsx
  8. 7
      src/features/MatchSidePlaylists/index.tsx
  9. 17
      src/features/MultiSourcePlayer/hooks/index.tsx

@ -0,0 +1,9 @@
import reduce from 'lodash/reduce'
import type { Events } from 'requests'
export const fullEpisodesDuration = (filters: Events) => reduce(
filters,
(acc, filter) => acc + (Number(filter.e) - Number(filter.s)),
0,
)

@ -0,0 +1,72 @@
import type { Dispatch, SetStateAction } from 'react'
import { useEffect } from 'react'
import isEmpty from 'lodash/isEmpty'
import type { Events } from 'requests'
import { fullEpisodesDuration } from './helpers'
import { Svg, Circle } from './styled'
export type TCircleAnimation = {
plaingOrder: number,
playedProgress: number,
playing: boolean,
ready: boolean,
}
export const initialCircleAnimation: TCircleAnimation = {
plaingOrder: 0,
playedProgress: 0,
playing: false,
ready: false,
}
type Props = {
circleAnimation?: TCircleAnimation,
filteredEvents: Events,
setWatchAllEpisodesTimer: (showTimer: boolean) => void,
}
export type TSetCircleAnimation = Dispatch<SetStateAction<TCircleAnimation>>
export const CircleAnimationBar = ({
circleAnimation,
filteredEvents,
setWatchAllEpisodesTimer,
}: Props) => {
const {
plaingOrder,
playedProgress,
playing,
ready,
} = circleAnimation!
const timeOfAllEpisodes = fullEpisodesDuration(filteredEvents)
const remainingEvents = filteredEvents.slice(plaingOrder - 1)
const fullTimeOfRemainingEpisodes = !isEmpty(remainingEvents)
? fullEpisodesDuration(remainingEvents)
: 0
const animationPause = !playing || !ready
const currentAnimationTime = Math.round(fullTimeOfRemainingEpisodes - (playedProgress / 1000))
const currentEpisodesPercent = 100 - (100 / (timeOfAllEpisodes / currentAnimationTime))
useEffect(() => {
if (currentEpisodesPercent >= 100) {
setWatchAllEpisodesTimer(false)
}
}, [currentEpisodesPercent, setWatchAllEpisodesTimer])
return (
<Svg>
<Circle
cx='50%'
cy='50%'
r='50%'
currentAnimationTime={currentAnimationTime}
animationPause={animationPause}
currentEpisodesPercent={currentEpisodesPercent}
/>
</Svg>
)
}

@ -0,0 +1,43 @@
import styled, { keyframes } from 'styled-components/macro'
type TCircle = {
animationPause?: boolean,
currentAnimationTime: number,
currentEpisodesPercent: number,
}
const strokeDashOffset = 43.5
const clockAnimation = (currentEpisodesPercent?: number) => keyframes`
from {
stroke-dashoffset: ${currentEpisodesPercent};
}
to {
stroke-dashoffset: 0;
}
`
export const Svg = styled.svg`
background-color: #5EB2FF;
width: 14px;
height: 14px;
position: relative;
border-radius: 50%;
`
export const Circle = styled.circle<TCircle>`
fill: transparent;
stroke: white;
stroke-width: 14px;
stroke-dasharray: ${strokeDashOffset};
stroke-dashoffset: ${strokeDashOffset};
transform: rotate(-90deg);
transform-origin: center;
animation-name: ${({ currentEpisodesPercent }) => (
clockAnimation(strokeDashOffset - (strokeDashOffset * currentEpisodesPercent / 100))
)};
animation-duration: ${({ currentAnimationTime }) => `${currentAnimationTime}s`};
animation-play-state: ${({ animationPause }) => (animationPause ? 'paused' : 'running')};
animation-timing-function: linear;
animation-fill-mode: forwards;
`

@ -1,7 +1,9 @@
import { Fragment } from 'react'
import { Fragment, useState } from 'react'
import isEmpty from 'lodash/isEmpty'
import type { TCircleAnimation } from 'features/CircleAnimationBar'
import { initialCircleAnimation } from 'features/CircleAnimationBar'
import { MatchSidePlaylists } from 'features/MatchSidePlaylists'
import { MultiSourcePlayer } from 'features/MultiSourcePlayer'
@ -14,6 +16,7 @@ import { MatchDescription } from '../MatchDescription'
import { useMatchPageStore } from '../../store'
export const FinishedMatch = () => {
const [circleAnimation, setCircleAnimation] = useState<TCircleAnimation>(initialCircleAnimation)
const { isOpenPopup, profile } = useMatchPageStore()
const {
chapters,
@ -44,6 +47,7 @@ export const FinishedMatch = () => {
{!isEmpty(chapters) && (
<Fragment>
<MultiSourcePlayer
setCircleAnimation={setCircleAnimation}
isOpenPopup={isOpenPopup}
chapters={chapters}
onPlayingChange={onPlayingChange}
@ -55,6 +59,8 @@ export const FinishedMatch = () => {
</Container>
<MatchSidePlaylists
setCircleAnimation={setCircleAnimation}
circleAnimation={circleAnimation}
selectedPlaylist={selectedPlaylist}
onSelect={onPlaylistSelect}
/>

@ -24,6 +24,7 @@ import { useTabEvents } from './useTabEvents'
export const useMatchPage = () => {
const [matchProfile, setMatchProfile] = useState<MatchInfo>(null)
const [watchAllEpisodesTimer, setWatchAllEpisodesTimer] = useState(false)
const { profileId: matchId, sportType } = usePageParams()
const {
close: hideProfileCard,
@ -124,6 +125,10 @@ export const useMatchPage = () => {
setPlaingOrder(currentOrder + 1)
}
const playEpisodes = () => {
if (!watchAllEpisodesTimer) {
setWatchAllEpisodesTimer(true)
}
setIsPlayinFiltersEpisodes(true)
if (matchProfile?.live) {
@ -162,6 +167,7 @@ export const useMatchPage = () => {
likeImage,
likeToggle,
matchPlaylists,
plaingOrder,
playEpisodes,
playNextEpisode,
profile,
@ -175,10 +181,12 @@ export const useMatchPage = () => {
setPlaingOrder,
setReversed,
setUnreversed,
setWatchAllEpisodesTimer,
showProfileCard,
toggleActiveEvents,
toggleActivePlayers,
togglePopup,
tournamentData,
watchAllEpisodesTimer,
}
}

@ -26,6 +26,7 @@ type Props = {
onSelect: (option: PlaylistOption) => void,
profile: MatchInfo,
selectedPlaylist?: PlaylistOption,
setWatchAllEpisodesTimer: (showTimer: boolean) => void,
}
export const EventsList = ({
@ -34,6 +35,7 @@ export const EventsList = ({
onSelect,
profile,
selectedPlaylist,
setWatchAllEpisodesTimer,
}: Props) => (
<List>
{map(events, (event) => {
@ -67,6 +69,7 @@ export const EventsList = ({
const eventWithoutClick = isLffClient && profile?.live
const eventClick = () => {
setWatchAllEpisodesTimer(false)
!eventWithoutClick && onSelect(eventPlaylist)
if (disablePlayingEpisodes) {
disablePlayingEpisodes()

@ -1,4 +1,4 @@
import { Fragment } from 'react'
import { Fragment, useEffect } from 'react'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
@ -8,6 +8,8 @@ import size from 'lodash/size'
import { T9n } from 'features/T9n'
import type { PlaylistOption } from 'features/MatchPage/types'
import { useMatchPageStore } from 'features/MatchPage/store'
import type { TCircleAnimation, TSetCircleAnimation } from 'features/CircleAnimationBar'
import { CircleAnimationBar } from 'features/CircleAnimationBar'
import type { MatchInfo } from 'requests'
import { EventsList } from '../EventsList'
@ -28,31 +30,48 @@ import {
} from './styled'
type Props = {
circleAnimation?: TCircleAnimation,
onSelect: (option: PlaylistOption) => void,
profile: MatchInfo,
selectedPlaylist?: PlaylistOption,
setCircleAnimation?: TSetCircleAnimation,
}
export const TabEvents = ({
circleAnimation,
onSelect,
profile,
selectedPlaylist,
setCircleAnimation,
}: Props) => {
const {
activeStatus,
countOfFilters,
disablePlayingEpisodes,
filteredEvents,
isEmptyFilters,
isLiveMatch,
likeImage,
likeToggle,
plaingOrder,
playEpisodes,
reversedGroupEvents,
setReversed,
setUnreversed,
setWatchAllEpisodesTimer,
togglePopup,
watchAllEpisodesTimer,
} = useMatchPageStore()
useEffect(() => {
if (setCircleAnimation) {
setCircleAnimation((state) => ({
...state,
plaingOrder,
}))
}
}, [setCircleAnimation, plaingOrder])
if (!profile) return null
return (
@ -85,6 +104,13 @@ export const TabEvents = ({
<WatchButton onClick={playEpisodes}>
<T9n t='watch_all' />
</WatchButton>
{watchAllEpisodesTimer && (
<CircleAnimationBar
filteredEvents={filteredEvents}
circleAnimation={circleAnimation}
setWatchAllEpisodesTimer={setWatchAllEpisodesTimer}
/>
)}
</SelectedEpisodes>
)}
{isEmpty(reversedGroupEvents)
@ -111,6 +137,7 @@ export const TabEvents = ({
onSelect={onSelect}
profile={profile}
selectedPlaylist={selectedPlaylist}
setWatchAllEpisodesTimer={setWatchAllEpisodesTimer}
/>
</HalfEvents>
)

@ -4,6 +4,7 @@ import {
useState,
} from 'react'
import type { TCircleAnimation, TSetCircleAnimation } from 'features/CircleAnimationBar'
import type { PlaylistOption } from 'features/MatchPage/types'
import { Tab, TabsGroup } from 'features/Common'
import { T9n } from 'features/T9n'
@ -31,13 +32,17 @@ const tabPanes = {
}
type Props = {
circleAnimation?: TCircleAnimation,
onSelect: (option: PlaylistOption) => void,
selectedPlaylist?: PlaylistOption,
setCircleAnimation?: TSetCircleAnimation,
}
export const MatchSidePlaylists = ({
circleAnimation,
onSelect,
selectedPlaylist,
setCircleAnimation,
}: Props) => {
const {
hideProfileCard,
@ -128,6 +133,8 @@ export const MatchSidePlaylists = ({
forVideoTab={selectedTab === Tabs.VIDEO}
>
<TabPane
setCircleAnimation={setCircleAnimation}
circleAnimation={circleAnimation}
tournamentData={tournamentData}
onSelect={onSelect}
playlists={playlists}

@ -7,6 +7,7 @@ import {
import size from 'lodash/size'
import type { TSetCircleAnimation } from 'features/CircleAnimationBar'
import { useControlsVisibility } from 'features/StreamPlayer/hooks/useControlsVisibility'
import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen'
import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
@ -48,12 +49,14 @@ export type Props = {
onError?: () => void,
onPlayingChange: (playing: boolean) => void,
profile: MatchInfo,
setCircleAnimation: TSetCircleAnimation,
}
export const useMultiSourcePlayer = ({
chapters,
onError,
onPlayingChange,
setCircleAnimation,
}: Props) => {
const {
isPlayFilterEpisodes,
@ -195,6 +198,20 @@ export const useMultiSourcePlayer = ({
onPlayingChange(playing)
}, [playing, onPlayingChange])
useEffect(() => {
setCircleAnimation((state) => ({
...state,
playedProgress,
playing,
ready,
}))
}, [
playedProgress,
playing,
ready,
setCircleAnimation,
])
useEffect(() => {
setPlayerState((state) => ({
...initialState,

Loading…
Cancel
Save