lodash-es#round TypeScript Examples

The following examples show how to use lodash-es#round. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: VideoCategoryHero.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
VideoCategoryHero: React.FC<VideoCategoryHeroProps> = ({ header, videos }) => {
  const [activeVideoIdx, setActiveVideoIdx] = useState(0)
  const [videoProgress, setVideoProgress] = useState(0)

  const videosLength = videos?.length || 0

  const handleVideoClick = (idx: number) => {
    setActiveVideoIdx(idx)
    setVideoProgress(0)
  }

  const handleEnded = () => {
    const idx = activeVideoIdx + 1 >= videosLength ? 0 : activeVideoIdx + 1
    setVideoProgress(0)
    setActiveVideoIdx(idx)
  }

  const handleTimeUpdate = (e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
    const currentTime = e.currentTarget.currentTime
    const duration = e.currentTarget.duration
    if (duration && currentTime) {
      const progressInPercentage = round((currentTime / duration) * 100, 2)
      setVideoProgress(progressInPercentage)
    } else {
      setVideoProgress(0)
    }
  }

  const videosWithProgress = videos?.map((video, idx) =>
    video ? { ...video, progress: idx === activeVideoIdx ? videoProgress : 0 } : null
  )

  const shouldShowSlider = videosLength > 1
  const currentVideoData = videos?.[activeVideoIdx]

  return (
    <VideoHero
      isCategory
      onTimeUpdate={handleTimeUpdate}
      onEnded={handleEnded}
      videoHeroData={{
        video: currentVideoData?.video,
        heroTitle: currentVideoData?.video.title || '',
        heroVideoCutUrl: currentVideoData?.videoCutUrl || '',
        heroPosterUrl: null,
      }}
      headerNode={
        !!header.title &&
        !!header.icon && <VideoHeroHeader icon={header.icon} title={header.title} loading={!videos?.[activeVideoIdx]} />
      }
      sliderNode={
        shouldShowSlider ? (
          <VideoHeroSlider activeVideoIdx={activeVideoIdx} videos={videosWithProgress} onTileClick={handleVideoClick} />
        ) : undefined
      }
    />
  )
}
Example #2
Source File: CustomTimeline.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
CustomTimeline: React.FC<CustomTimelineProps> = ({
  player,
  isFullScreen,
  playerState,
  playVideo,
  pauseVideo,
  setPlayerState,
}) => {
  const playProgressThumbRef = useRef<HTMLButtonElement>(null)
  const playProgressRef = useRef<HTMLDivElement>(null)
  const seekBarRef = useRef<HTMLDivElement>(null)
  const mouseDisplayTooltipRef = useRef<HTMLDivElement>(null)

  const [playProgressWidth, setPlayProgressWidth] = useState(0)
  const [loadProgressWidth, setLoadProgressWidth] = useState(0)
  const [mouseDisplayWidth, setMouseDisplayWidth] = useState(0)
  const [mouseDisplayTooltipTime, setMouseDisplayTooltipTime] = useState('0:00')
  const [mouseDisplayTooltipWidth, setMouseDisplayTooltipWidth] = useState(0)
  const [playProgressThumbWidth, setPlayProgressThumbWidth] = useState(0)
  const [isScrubbing, setIsScrubbing] = useState(false)
  const [playedBefore, setPlayedBefore] = useState(false)

  useEffect(() => {
    if (!player) {
      return
    }
    const handler = (event: Event) => {
      if (event.type === 'seeking' && isScrubbing && !player.paused()) {
        setPlayedBefore(true)
        pauseVideo(player)
      }
      if (event.type === 'seeked' && !isScrubbing) {
        if (playedBefore) {
          playVideo(player)
          setPlayedBefore(false)
        }
      }
    }
    player.on(['seeking', 'seeked'], handler)
    return () => {
      player.off(['seeking', 'seeked'], handler)
    }
  }, [isScrubbing, player, playedBefore, pauseVideo, playVideo])

  useEffect(() => {
    const playProgress = playProgressRef.current
    const playProgressThumb = playProgressThumbRef.current
    if (
      !player ||
      !playerState ||
      playerState === 'ended' ||
      playerState === 'error' ||
      !playProgress ||
      isScrubbing ||
      !playProgressThumb
    ) {
      return
    }

    const interval = window.setInterval(() => {
      const duration = player.duration()
      const currentTime = player.currentTime()
      const buffered = player.buffered()

      // set playProgress

      const progressPercentage = round((currentTime / duration) * 100, 2)
      setPlayProgressWidth(progressPercentage)
      setPlayProgressThumbWidth(playProgressThumb.clientWidth)

      if (progressPercentage === 100) {
        setPlayerState('ended')
      }

      // get all buffered time ranges
      const bufferedTimeRanges = Array.from({ length: buffered.length }).map((_, idx) => ({
        bufferedStart: buffered.start(idx),
        bufferedEnd: buffered.end(idx),
      }))

      const currentBufferedTimeRange = bufferedTimeRanges.find(
        (el) => el.bufferedEnd > currentTime && el.bufferedStart < currentTime
      )

      if (currentBufferedTimeRange) {
        const loadProgressPercentage = round((currentBufferedTimeRange.bufferedEnd / duration) * 100, 2)
        setLoadProgressWidth(loadProgressPercentage)
      } else {
        setLoadProgressWidth(0)
      }
    }, UPDATE_INTERVAL)
    return () => {
      clearInterval(interval)
    }
  }, [isScrubbing, player, playerState, setPlayerState])

  const handleMouseAndTouchMove = (e: React.MouseEvent | React.TouchEvent) => {
    const seekBar = seekBarRef.current
    const mouseDisplayTooltip = mouseDisplayTooltipRef.current
    if (!seekBar || !mouseDisplayTooltip || !player) {
      return
    }
    // this will prevent hiding controls when scrubbing on mobile
    player.enableTouchActivity()

    const duration = player.duration()

    // position of seekBar
    const { x: seekBarPosition, width: seekBarWidth } = seekBar.getBoundingClientRect()
    const position = 'clientX' in e ? e.clientX - seekBarPosition : e.touches[0].clientX - seekBarPosition
    const percentage = clamp(round((position / seekBarWidth) * 100, 2), 0, 100)
    setMouseDisplayWidth(percentage)
    setMouseDisplayTooltipWidth(mouseDisplayTooltip.clientWidth)

    // tooltip text
    if (duration) {
      setMouseDisplayTooltipTime(formatDurationShort(round((percentage / 100) * duration)))
    }
    if (isScrubbing) {
      setPlayProgressWidth(percentage)
    }
  }

  const handleJumpToTime = (e: React.MouseEvent | React.TouchEvent) => {
    const seekBar = seekBarRef.current
    if (!seekBar || (e.type === 'mouseleave' && !isScrubbing) || !player) {
      return
    }

    const { x: seekBarPosition, width: seekBarWidth } = seekBar.getBoundingClientRect()
    const mouseOrTouchPosition =
      'clientX' in e ? e.clientX - seekBarPosition : e.changedTouches[0].clientX - seekBarPosition

    const percentage = clamp(round(mouseOrTouchPosition / seekBarWidth, 4), 0, 100)
    const newTime = percentage * (player?.duration() || 0)
    player.currentTime(newTime)
    setIsScrubbing(false)
  }

  return (
    <ProgressControl
      onClick={(event) => event.stopPropagation()}
      isScrubbing={isScrubbing}
      isFullScreen={isFullScreen}
      onMouseMove={handleMouseAndTouchMove}
      onTouchMove={handleMouseAndTouchMove}
      onMouseLeave={handleJumpToTime}
      onMouseDown={() => setIsScrubbing(true)}
      onTouchStart={() => setIsScrubbing(true)}
      onMouseUp={handleJumpToTime}
      onTouchEnd={handleJumpToTime}
    >
      <SeekBar ref={seekBarRef}>
        <LoadProgress style={{ width: loadProgressWidth + '%' }} />
        <MouseDisplayWrapper>
          <MouseDisplay style={{ width: mouseDisplayWidth + '%' }} />
          <MouseDisplayTooltip
            ref={mouseDisplayTooltipRef}
            style={{
              left: `clamp(0px, calc(${mouseDisplayWidth}% - ${
                mouseDisplayTooltipWidth / 2
              }px), calc(100% - ${mouseDisplayTooltipWidth}px))`,
            }}
            isFullScreen={isFullScreen}
          >
            <StyledTooltipText variant="t200">{mouseDisplayTooltipTime}</StyledTooltipText>
          </MouseDisplayTooltip>
        </MouseDisplayWrapper>
        <PlayProgressWrapper>
          <PlayProgress style={{ width: playProgressWidth + '%' }} ref={playProgressRef} />
          <PlayProgressThumb
            ref={playProgressThumbRef}
            style={{
              left: `clamp(0px, calc(${playProgressWidth}% - ${
                playProgressThumbWidth / 2
              }px), calc(100% - ${playProgressThumbWidth}px))`,
            }}
          />
        </PlayProgressWrapper>
      </SeekBar>
    </ProgressControl>
  )
}
Example #3
Source File: VideoPlayer.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, VideoPlayerProps> = (
  {
    isVideoPending,
    className,
    playing,
    nextVideo,
    channelId,
    videoId,
    autoplay,
    videoStyle,
    isEmbedded,
    isPlayNextDisabled,
    ...videoJsConfig
  },
  externalRef
) => {
  const [player, playerRef] = useVideoJsPlayer(videoJsConfig)
  const [isPlaying, setIsPlaying] = useState(false)
  const {
    currentVolume,
    cachedVolume,
    cinematicView,
    actions: { setCurrentVolume, setCachedVolume, setCinematicView },
  } = usePersonalDataStore((state) => state)
  const [volumeToSave, setVolumeToSave] = useState(0)

  const [videoTime, setVideoTime] = useState(0)
  const [isFullScreen, setIsFullScreen] = useState(false)
  const [isPiPEnabled, setIsPiPEnabled] = useState(false)

  const [playerState, setPlayerState] = useState<PlayerState>('loading')
  const [isLoaded, setIsLoaded] = useState(false)
  const [needsManualPlay, setNeedsManualPlay] = useState(!autoplay)
  const mdMatch = useMediaMatch('md')

  const playVideo = useCallback(
    async (player: VideoJsPlayer | null, withIndicator?: boolean, callback?: () => void) => {
      if (!player) {
        return
      }
      withIndicator && player.trigger(CustomVideojsEvents.PlayControl)
      try {
        setNeedsManualPlay(false)
        const playPromise = await player.play()
        if (playPromise && callback) callback()
      } catch (error) {
        if (error.name === 'AbortError') {
          // this will prevent throwing harmless error `the play() request was interrupted by a call to pause()`
          // Video.js doing something similiar, check:
          // https://github.com/videojs/video.js/issues/6998
          // https://github.com/videojs/video.js/blob/4238f5c1d88890547153e7e1de7bd0d1d8e0b236/src/js/utils/promise.js
          return
        }
        if (error.name === 'NotAllowedError') {
          ConsoleLogger.warn('Video playback failed', error)
        } else {
          SentryLogger.error('Video playback failed', 'VideoPlayer', error, {
            video: { id: videoId, url: videoJsConfig.src },
          })
        }
      }
    },
    [videoId, videoJsConfig.src]
  )

  const pauseVideo = useCallback((player: VideoJsPlayer | null, withIndicator?: boolean, callback?: () => void) => {
    if (!player) {
      return
    }
    withIndicator && player.trigger(CustomVideojsEvents.PauseControl)
    callback?.()
    player.pause()
  }, [])

  useEffect(() => {
    if (!isVideoPending) {
      return
    }
    setPlayerState('pending')
  }, [isVideoPending])

  // handle hotkeys
  useEffect(() => {
    if (!player) {
      return
    }

    const handler = (event: KeyboardEvent) => {
      if (
        (document.activeElement?.tagName === 'BUTTON' && event.key === ' ') ||
        document.activeElement?.tagName === 'INPUT'
      ) {
        return
      }

      const playerReservedKeys = ['k', ' ', 'ArrowLeft', 'ArrowRight', 'j', 'l', 'ArrowUp', 'ArrowDown', 'm', 'f', 'c']
      if (
        !event.altKey &&
        !event.ctrlKey &&
        !event.metaKey &&
        !event.shiftKey &&
        playerReservedKeys.includes(event.key)
      ) {
        event.preventDefault()
        hotkeysHandler(event, player, playVideo, pauseVideo, () => setCinematicView(!cinematicView))
      }
    }
    document.addEventListener('keydown', handler)

    return () => document.removeEventListener('keydown', handler)
  }, [cinematicView, pauseVideo, playVideo, player, playerState, setCinematicView])

  // handle error
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      setPlayerState('error')
    }
    player.on('error', handler)
    return () => {
      player.off('error', handler)
    }
  })

  // handle video loading
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = (event: Event) => {
      if (event.type === 'waiting' || event.type === 'seeking') {
        setPlayerState('loading')
      }
      if (event.type === 'canplay' || event.type === 'seeked') {
        setPlayerState('playingOrPaused')
      }
    }
    player.on(['waiting', 'canplay', 'seeking', 'seeked'], handler)
    return () => {
      player.off(['waiting', 'canplay', 'seeking', 'seeked'], handler)
    }
  }, [player, playerState])

  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      setPlayerState('ended')
    }
    player.on('ended', handler)
    return () => {
      player.off('ended', handler)
    }
  }, [nextVideo, player])

  // handle loadstart
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      setIsLoaded(true)
    }
    player.on('loadstart', handler)
    return () => {
      player.off('loadstart', handler)
    }
  }, [player])

  // handle autoplay
  useEffect(() => {
    if (!player || !isLoaded || !autoplay) {
      return
    }
    const playPromise = player.play()
    if (playPromise) {
      playPromise
        .then(() => {
          setIsPlaying(true)
        })
        .catch((e) => {
          setNeedsManualPlay(true)
          ConsoleLogger.warn('Video autoplay failed', e)
        })
    }
  }, [player, isLoaded, autoplay])

  // handle playing and pausing from outside the component
  useEffect(() => {
    if (!player) {
      return
    }
    if (playing) {
      playVideo(player)
    } else {
      player.pause()
    }
  }, [playVideo, player, playing])

  // handle playing and pausing
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = (event: Event) => {
      if (event.type === 'play') {
        setIsPlaying(true)
      }
      if (event.type === 'pause') {
        setIsPlaying(false)
      }
    }
    player.on(['play', 'pause'], handler)
    return () => {
      player.off(['play', 'pause'], handler)
    }
  }, [player, playerState])

  useEffect(() => {
    if (!externalRef) {
      return
    }
    if (typeof externalRef === 'function') {
      externalRef(playerRef.current)
    } else {
      externalRef.current = playerRef.current
    }
  }, [externalRef, playerRef])

  // handle video timer
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      const currentTime = round(player.currentTime())
      setVideoTime(currentTime)
    }
    player.on('timeupdate', handler)
    return () => {
      player.off('timeupdate', handler)
    }
  }, [player])

  // handle seeking
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      if (playerState === 'ended') {
        playVideo(player)
      }
    }
    player.on('seeking', handler)
    return () => {
      player.off('seeking', handler)
    }
  }, [playVideo, player, playerState])

  // handle fullscreen mode
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = () => {
      // will remove focus from fullscreen button and apply to player.
      player.focus()
      setIsFullScreen(player.isFullscreen())
    }
    player.on('fullscreenchange', handler)
    return () => {
      player.off('fullscreenchange', handler)
    }
  }, [player])

  // handle picture in picture
  useEffect(() => {
    if (!player) {
      return
    }
    const handler = (event: Event) => {
      if (event.type === 'enterpictureinpicture') {
        setIsPiPEnabled(true)
      }
      if (event.type === 'leavepictureinpicture') {
        setIsPiPEnabled(false)
      }
    }
    player.on(['enterpictureinpicture', 'leavepictureinpicture'], handler)
    return () => {
      player.off(['enterpictureinpicture', 'leavepictureinpicture'], handler)
    }
  }, [player])

  // update volume on keyboard input
  useEffect(() => {
    if (!player) {
      return
    }
    const events = [
      CustomVideojsEvents.VolumeIncrease,
      CustomVideojsEvents.VolumeDecrease,
      CustomVideojsEvents.Muted,
      CustomVideojsEvents.Unmuted,
    ]

    const handler = (event: Event) => {
      if (event.type === CustomVideojsEvents.Muted) {
        if (currentVolume) {
          setCachedVolume(currentVolume)
        }
        setCurrentVolume(0)
        return
      }
      if (event.type === CustomVideojsEvents.Unmuted) {
        setCurrentVolume(cachedVolume || VOLUME_STEP)
        return
      }
      if (event.type === CustomVideojsEvents.VolumeIncrease || CustomVideojsEvents.VolumeDecrease) {
        setCurrentVolume(player.volume())
      }
    }
    player.on(events, handler)
    return () => {
      player.off(events, handler)
    }
  }, [currentVolume, player, cachedVolume, setCachedVolume, setCurrentVolume])

  const debouncedVolumeChange = useRef(
    debounce((volume: number) => {
      setVolumeToSave(volume)
    }, 125)
  )
  // update volume on mouse input
  useEffect(() => {
    if (!player) {
      return
    }
    player?.volume(currentVolume)

    debouncedVolumeChange.current(currentVolume)
    if (currentVolume) {
      player.muted(false)
    } else {
      if (volumeToSave) {
        setCachedVolume(volumeToSave)
      }
      player.muted(true)
    }
  }, [currentVolume, volumeToSave, player, setCachedVolume])

  // button/input handlers
  const handlePlayPause = useCallback(() => {
    if (playerState === 'error') {
      return
    }
    if (isPlaying) {
      pauseVideo(player, true, () => setIsPlaying(false))
    } else {
      playVideo(player, true, () => setIsPlaying(true))
    }
  }, [isPlaying, pauseVideo, playVideo, player, playerState])

  const handleChangeVolume = (event: React.ChangeEvent<HTMLInputElement>) => {
    setCurrentVolume(Number(event.target.value))
  }

  const handleMute = () => {
    if (currentVolume === 0) {
      setCurrentVolume(cachedVolume || 0.05)
    } else {
      setCurrentVolume(0)
    }
  }

  const handlePictureInPicture = (event: React.MouseEvent) => {
    event.stopPropagation()
    if (document.pictureInPictureElement) {
      // @ts-ignore @types/video.js is outdated and doesn't provide types for some newer video.js features
      player.exitPictureInPicture()
    } else {
      if (document.pictureInPictureEnabled) {
        // @ts-ignore @types/video.js is outdated and doesn't provide types for some newer video.js features
        player.requestPictureInPicture().catch((e) => {
          ConsoleLogger.warn('Picture in picture failed', e)
        })
      }
    }
  }

  const handleFullScreen = (event: React.MouseEvent) => {
    event.stopPropagation()
    if (!isFullScreenEnabled) {
      return
    }
    if (player?.isFullscreen()) {
      player?.exitFullscreen()
    } else {
      player?.requestFullscreen()
    }
  }

  const onVideoClick = useCallback(
    () =>
      player?.paused()
        ? player?.trigger(CustomVideojsEvents.PauseControl)
        : player?.trigger(CustomVideojsEvents.PlayControl),
    [player]
  )

  const renderVolumeButton = () => {
    if (currentVolume === 0) {
      return <StyledSvgPlayerSoundOff />
    } else {
      return currentVolume <= 0.5 ? <StyledSvgControlsSoundLowVolume /> : <StyledSvgPlayerSoundOn />
    }
  }

  const toggleCinematicView = (event: React.MouseEvent) => {
    event.stopPropagation()
    setCinematicView(!cinematicView)
  }

  const showPlayerControls = isLoaded && playerState
  const showControlsIndicator = playerState !== 'ended'

  return (
    <Container isFullScreen={isFullScreen} className={className}>
      <div data-vjs-player onClick={handlePlayPause}>
        {needsManualPlay && (
          <BigPlayButtonContainer onClick={handlePlayPause}>
            <BigPlayButton onClick={handlePlayPause}>
              <StyledSvgControlsPlay />
            </BigPlayButton>
          </BigPlayButtonContainer>
        )}
        <video style={videoStyle} ref={playerRef} className="video-js" onClick={onVideoClick} />
        {showPlayerControls && (
          <>
            <ControlsOverlay isFullScreen={isFullScreen}>
              <CustomTimeline
                playVideo={playVideo}
                pauseVideo={pauseVideo}
                player={player}
                isFullScreen={isFullScreen}
                playerState={playerState}
                setPlayerState={setPlayerState}
              />
              <CustomControls isFullScreen={isFullScreen} isEnded={playerState === 'ended'}>
                <PlayControl isLoading={playerState === 'loading'}>
                  {(!needsManualPlay || mdMatch) && (
                    <PlayButton
                      isEnded={playerState === 'ended'}
                      onClick={handlePlayPause}
                      tooltipText={isPlaying ? 'Pause (k)' : playerState === 'ended' ? 'Play again (k)' : 'Play (k)'}
                      tooltipPosition="left"
                    >
                      {playerState === 'ended' ? (
                        <StyledSvgControlsReplay />
                      ) : isPlaying ? (
                        <StyledSvgControlsPause />
                      ) : (
                        <StyledSvgControlsPlay />
                      )}
                    </PlayButton>
                  )}
                </PlayControl>
                <VolumeControl onClick={(e) => e.stopPropagation()}>
                  <VolumeButton tooltipText="Volume" showTooltipOnlyOnFocus onClick={handleMute}>
                    {renderVolumeButton()}
                  </VolumeButton>
                  <VolumeSliderContainer>
                    <VolumeSlider
                      step={0.01}
                      max={1}
                      min={0}
                      value={currentVolume}
                      onChange={handleChangeVolume}
                      type="range"
                    />
                  </VolumeSliderContainer>
                </VolumeControl>
                <CurrentTimeWrapper>
                  <CurrentTime variant="t200">
                    {formatDurationShort(videoTime)} / {formatDurationShort(round(player?.duration() || 0))}
                  </CurrentTime>
                </CurrentTimeWrapper>
                <ScreenControls>
                  {mdMatch && !isEmbedded && !player?.isFullscreen() && (
                    <PlayerControlButton
                      onClick={toggleCinematicView}
                      tooltipText={cinematicView ? 'Exit cinematic mode (c)' : 'Cinematic view (c)'}
                    >
                      {cinematicView ? (
                        <StyledSvgControlsVideoModeCompactView />
                      ) : (
                        <StyledSvgControlsVideoModeCinemaView />
                      )}
                    </PlayerControlButton>
                  )}
                  {isPiPSupported && (
                    <PlayerControlButton onClick={handlePictureInPicture} tooltipText="Picture-in-picture">
                      {isPiPEnabled ? <StyledSvgControlsPipOff /> : <StyledSvgControlsPipOn />}
                    </PlayerControlButton>
                  )}
                  <PlayerControlButton
                    isDisabled={!isFullScreenEnabled}
                    tooltipPosition="right"
                    tooltipText={isFullScreen ? 'Exit full screen (f)' : 'Full screen (f)'}
                    onClick={handleFullScreen}
                  >
                    {isFullScreen ? <StyledSvgControlsSmallScreen /> : <StyledSvgControlsFullScreen />}
                  </PlayerControlButton>
                </ScreenControls>
              </CustomControls>
            </ControlsOverlay>
          </>
        )}
        <VideoOverlay
          videoId={videoId}
          isFullScreen={isFullScreen}
          isPlayNextDisabled={isPlayNextDisabled}
          playerState={playerState}
          onPlay={handlePlayPause}
          channelId={channelId}
          currentThumbnailUrl={videoJsConfig.posterUrl}
          playRandomVideoOnEnded={!isEmbedded}
        />
        {showControlsIndicator && <ControlsIndicator player={player} isLoading={playerState === 'loading'} />}
      </div>
    </Container>
  )
}
Example #4
Source File: store.ts    From atlas with GNU General Public License v3.0 4 votes vote down vote up
usePersonalDataStore = createStore<PersonalDataStoreState, PersonalDataStoreActions>(
  {
    state: initialState,
    actionsFactory: (set) => ({
      updateWatchedVideos: (__typename, id, timestamp) => {
        set((state) => {
          const currentVideo = state.watchedVideos.find((v) => v.id === id)
          if (!currentVideo) {
            const newVideo = __typename === 'COMPLETED' ? { __typename, id } : { __typename, id, timestamp }
            state.watchedVideos.push(newVideo)
          } else {
            const index = state.watchedVideos.findIndex((v) => v.id === id)
            if (index !== -1) state.watchedVideos[index] = { __typename, id, timestamp }
          }
        })
      },
      updateChannelFollowing: (id, follow) => {
        set((state) => {
          const isFollowing = state.followedChannels.some((channel) => channel.id === id)
          if (isFollowing && !follow) {
            state.followedChannels = state.followedChannels.filter((channel) => channel.id !== id)
          }
          if (!isFollowing && follow) {
            state.followedChannels.push({ id })
          }
        })
      },
      addRecentSearch: (title) => {
        set((state) => {
          const filteredCurrentSearches = state.recentSearches.filter((item) => item.title !== title)
          const newSearches = [{ title }, ...filteredCurrentSearches]
          state.recentSearches = newSearches.slice(0, 6)
        })
      },
      deleteRecentSearch: (title) => {
        set((state) => {
          state.recentSearches = state.recentSearches.filter((search) => search.title !== title)
        })
      },
      updateDismissedMessages: (id, add = true) => {
        set((state) => {
          state.dismissedMessages = state.dismissedMessages.filter((dissmissedMessage) => dissmissedMessage.id !== id)
          if (add) {
            state.dismissedMessages.unshift({ id })
          }
        })
      },
      setCurrentVolume: (volume) =>
        set((state) => {
          state.currentVolume = round(volume, 2)
        }),
      setCachedVolume: (volume) =>
        set((state) => {
          state.cachedVolume = round(volume, 2)
        }),
      setCinematicView: (cinematicView) =>
        set((state) => {
          state.cinematicView = cinematicView
        }),
      setCookiesAccepted: (accept) =>
        set((state) => {
          state.cookiesAccepted = accept
        }),
    }),
  },
  {
    persist: {
      key: 'personalData',
      whitelist: WHITELIST,
      version: 2,
      migrate: (oldState) => {
        const typedOldState = oldState as PersonalDataStoreState

        const migratedWatchedVideos = typedOldState.watchedVideos.reduce((acc, cur) => {
          const migratedId = (videoIdsMapEntries as Record<string, string>)[cur.id]
          if (migratedId) {
            return [...acc, { ...cur, id: migratedId }]
          }
          return acc
        }, [] as WatchedVideo[])

        const migratedFollowedChannels = typedOldState.followedChannels.reduce((acc, cur) => {
          const migratedId = (channelIdsMapEntries as Record<string, string>)[cur.id]
          if (migratedId) {
            return [...acc, { ...cur, id: migratedId }]
          }
          return acc
        }, [] as FollowedChannel[])

        const migratedState: PersonalDataStoreState = {
          ...typedOldState,
          watchedVideos: migratedWatchedVideos,
          followedChannels: migratedFollowedChannels,
        }
        return migratedState
      },
    },
  }
)