|  | 
| 1 | 1 | import { Pause, PlayArrow, Stop } from "@mui/icons-material"; | 
| 2 |  | -import { Box, FormControlLabel, IconButton, Stack, Switch } from "@mui/material"; | 
|  | 2 | +import { Box, CircularProgress, FormControlLabel, IconButton, Stack, Switch } from "@mui/material"; | 
|  | 3 | +import { ErrorBoundary } from "@suspensive/react"; | 
| 3 | 4 | import * as React from "react"; | 
| 4 |  | -import Lottie from "react-lottie"; | 
|  | 5 | +import Lottie, { Options } from "react-lottie"; | 
|  | 6 | + | 
|  | 7 | +import { ErrorFallback } from "./error_handler"; | 
|  | 8 | +import { isValidHttpUrl } from "../utils/string"; | 
| 5 | 9 | 
 | 
| 6 | 10 | type PlayState = "playing" | "paused" | "stopped"; | 
| 7 | 11 | 
 | 
|  | 12 | +type LottiePlayerProps = { | 
|  | 13 | +  data: unknown; | 
|  | 14 | +  playState?: PlayState; | 
|  | 15 | +  disableLoop?: boolean; | 
|  | 16 | +  renderSettings?: Options["rendererSettings"]; | 
|  | 17 | +  style?: React.CSSProperties; | 
|  | 18 | +}; | 
|  | 19 | + | 
| 8 | 20 | type LottiePlayerStateType = { | 
|  | 21 | +  playState: PlayState; | 
|  | 22 | +}; | 
|  | 23 | + | 
|  | 24 | +type LottieDebugPanelStateType = LottiePlayerStateType & { | 
| 9 | 25 |   loop: boolean; | 
| 10 |  | -  isStopped: boolean; | 
| 11 |  | -  isPaused: boolean; | 
| 12 | 26 | }; | 
| 13 | 27 | 
 | 
| 14 |  | -export const LottieDebugPanel: React.FC<{ animationData: unknown }> = ({ animationData }) => { | 
| 15 |  | -  const [playerState, setPlayerState] = React.useState<LottiePlayerStateType>({ | 
| 16 |  | -    loop: true, | 
| 17 |  | -    isStopped: false, | 
| 18 |  | -    isPaused: false, | 
|  | 28 | +const playStateToLottiePlayerState = (playState: PlayState): { isStopped: boolean; isPaused: boolean } => { | 
|  | 29 | +  if (playState === "playing") return { isStopped: false, isPaused: false }; | 
|  | 30 | +  if (playState === "paused") return { isStopped: false, isPaused: true }; | 
|  | 31 | +  return { isStopped: true, isPaused: true }; | 
|  | 32 | +}; | 
|  | 33 | + | 
|  | 34 | +export const LottieDebugPanel: React.FC<LottiePlayerProps> = ({ | 
|  | 35 | +  data, | 
|  | 36 | +  playState = "playing", | 
|  | 37 | +  disableLoop = false, | 
|  | 38 | +  renderSettings = {}, | 
|  | 39 | +  style, | 
|  | 40 | +}) => { | 
|  | 41 | +  const [playerState, setPlayerState] = React.useState<LottieDebugPanelStateType>({ | 
|  | 42 | +    playState, | 
|  | 43 | +    loop: !disableLoop, | 
| 19 | 44 |   }); | 
|  | 45 | +  const isPlaying = playerState.playState === "playing"; | 
| 20 | 46 | 
 | 
| 21 | 47 |   const toggleLoop = () => setPlayerState((ps) => ({ ...ps, loop: !ps.loop })); | 
| 22 |  | -  const setPlayState = (playState: PlayState) => { | 
| 23 |  | -    if (playState === "playing") setPlayerState((ps) => ({ ...ps, isStopped: false, isPaused: false })); | 
| 24 |  | -    if (playState === "paused") setPlayerState((ps) => ({ ...ps, isStopped: false, isPaused: true })); | 
| 25 |  | -    if (playState === "stopped") setPlayerState((ps) => ({ ...ps, isStopped: true, isPaused: true })); | 
| 26 |  | -  }; | 
|  | 48 | +  const setPlayState = (playState: PlayState) => setPlayerState((ps) => ({ ...ps, playState })); | 
| 27 | 49 | 
 | 
| 28 | 50 |   const stop = () => setPlayState("stopped"); | 
| 29 |  | -  const togglePause = () => setPlayState(playerState.isPaused ? "playing" : "paused"); | 
|  | 51 | +  const togglePause = () => setPlayState(!isPlaying ? "playing" : "paused"); | 
| 30 | 52 | 
 | 
| 31 | 53 |   return ( | 
| 32 | 54 |     <Stack direction="column"> | 
| 33 | 55 |       <Box> | 
| 34 | 56 |         <Lottie | 
| 35 |  | -          isStopped={playerState.isStopped} | 
| 36 |  | -          isPaused={playerState.isPaused} | 
|  | 57 | +          {...playStateToLottiePlayerState(playerState.playState)} | 
| 37 | 58 |           options={{ | 
| 38 |  | -            animationData, | 
|  | 59 | +            animationData: data, | 
| 39 | 60 |             loop: playerState.loop, | 
| 40 | 61 |             autoplay: true, | 
| 41 |  | -            rendererSettings: { preserveAspectRatio: "xMidYMid slice" }, | 
|  | 62 | +            rendererSettings: { preserveAspectRatio: "xMidYMid slice", ...renderSettings }, | 
| 42 | 63 |           }} | 
|  | 64 | +          style={style} | 
| 43 | 65 |         /> | 
| 44 | 66 |       </Box> | 
| 45 | 67 |       <Stack direction="row" spacing={2}> | 
| 46 |  | -        <IconButton onClick={togglePause} children={playerState.isPaused ? <PlayArrow /> : <Pause />} /> | 
|  | 68 | +        <IconButton onClick={togglePause} children={!isPlaying ? <PlayArrow /> : <Pause />} /> | 
| 47 | 69 |         <IconButton onClick={stop} children={<Stop />} /> | 
| 48 | 70 |         <FormControlLabel control={<Switch checked={playerState.loop} onChange={toggleLoop} />} label="반복 재생" /> | 
| 49 | 71 |       </Stack> | 
| 50 | 72 |     </Stack> | 
| 51 | 73 |   ); | 
| 52 | 74 | }; | 
|  | 75 | + | 
|  | 76 | +export const LottiePlayer: React.FC<LottiePlayerProps> = ({ | 
|  | 77 | +  data, | 
|  | 78 | +  playState = "playing", | 
|  | 79 | +  disableLoop = false, | 
|  | 80 | +  renderSettings = {}, | 
|  | 81 | +  style, | 
|  | 82 | +}) => ( | 
|  | 83 | +  <Lottie | 
|  | 84 | +    {...playStateToLottiePlayerState(playState)} | 
|  | 85 | +    options={{ | 
|  | 86 | +      animationData: data, | 
|  | 87 | +      loop: !disableLoop, | 
|  | 88 | +      autoplay: playState === "playing", | 
|  | 89 | +      rendererSettings: { preserveAspectRatio: "xMidYMid slice", ...renderSettings }, | 
|  | 90 | +    }} | 
|  | 91 | +    style={style} | 
|  | 92 | +  /> | 
|  | 93 | +); | 
|  | 94 | + | 
|  | 95 | +type NetworkLottiePlayerProps = Omit<LottiePlayerProps, "data"> & { | 
|  | 96 | +  url: string; | 
|  | 97 | +  fetchOptions?: RequestInit; | 
|  | 98 | +}; | 
|  | 99 | + | 
|  | 100 | +type NetworkLottiePlayerStateType = { | 
|  | 101 | +  data?: unknown | null; | 
|  | 102 | +}; | 
|  | 103 | + | 
|  | 104 | +export const NetworkLottiePlayer: React.FC<NetworkLottiePlayerProps> = ErrorBoundary.with( | 
|  | 105 | +  { fallback: ErrorFallback }, | 
|  | 106 | +  (props) => { | 
|  | 107 | +    const [playerState, setPlayerState] = React.useState<NetworkLottiePlayerStateType>({}); | 
|  | 108 | + | 
|  | 109 | +    React.useEffect(() => { | 
|  | 110 | +      (async () => { | 
|  | 111 | +        if (!isValidHttpUrl(props.url)) throw new Error("Invalid URL for NetworkLottiePlayer: " + props.url); | 
|  | 112 | + | 
|  | 113 | +        const data = JSON.parse(await (await fetch(props.url, props.fetchOptions)).text()); | 
|  | 114 | +        setPlayerState((ps) => ({ ...ps, data })); | 
|  | 115 | +      })(); | 
|  | 116 | +    }, [props.url, props.fetchOptions]); | 
|  | 117 | + | 
|  | 118 | +    return playerState.data === undefined ? <CircularProgress /> : <LottiePlayer {...props} data={playerState.data} />; | 
|  | 119 | +  } | 
|  | 120 | +); | 
0 commit comments