diff --git a/apps/expo/src/components/player/ScraperProcess.tsx b/apps/expo/src/components/player/ScraperProcess.tsx index c9a5efc..92f4a83 100644 --- a/apps/expo/src/components/player/ScraperProcess.tsx +++ b/apps/expo/src/components/player/ScraperProcess.tsx @@ -1,8 +1,14 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { ActivityIndicator, View } from "react-native"; import { useRouter } from "expo-router"; -import type { HlsBasedStream, RunnerEvent } from "@movie-web/provider-utils"; +import type { + DiscoverEmbedsEvent, + HlsBasedStream, + InitEvent, + RunnerEvent, + UpdateEvent, +} from "@movie-web/provider-utils"; import { extractTracksFromHLS, getVideoStream, @@ -20,6 +26,12 @@ interface ScraperProcessProps { data: ItemData; } +enum ScrapeStatus { + LOADING = "loading", + SUCCESS = "success", + ERROR = "error", +} + export const ScraperProcess = ({ data }: ScraperProcessProps) => { const router = useRouter(); @@ -33,11 +45,47 @@ export const ScraperProcess = ({ data }: ScraperProcessProps) => { const setMeta = usePlayerStore((state) => state.setMeta); const [checkedSource, setCheckedSource] = useState(""); - const handleEvent = (event: RunnerEvent) => { + function isInitEvent(event: RunnerEvent): event is InitEvent { + return (event as InitEvent).sourceIds !== undefined; + } + + function isUpdateEvent(event: RunnerEvent): event is UpdateEvent { + return (event as UpdateEvent).percentage !== undefined; + } + + function isDiscoverEmbedsEvent( + event: RunnerEvent, + ): event is DiscoverEmbedsEvent { + return (event as DiscoverEmbedsEvent).sourceId !== undefined; + } + + const handleEvent = useCallback((event: RunnerEvent) => { if (typeof event === "string") { setCheckedSource(event); + setScrapeStatus({ status: ScrapeStatus.LOADING, progress: 10 }); + } else if (isUpdateEvent(event)) { + switch (event.status) { + case ScrapeStatus.SUCCESS: + setScrapeStatus({ status: ScrapeStatus.SUCCESS, progress: 100 }); + break; + case ScrapeStatus.ERROR as string: + setScrapeStatus({ status: ScrapeStatus.ERROR, progress: 0 }); + break; + case ScrapeStatus.LOADING as string: + } + setCheckedSource(event.id); + } else if (isInitEvent(event) || isDiscoverEmbedsEvent(event)) { + setScrapeStatus((prevStatus) => ({ + status: ScrapeStatus.LOADING, + progress: Math.min(prevStatus.progress + 20, 95), + })); } - }; + }, []); + + const [_scrapeStatus, setScrapeStatus] = useState({ + status: ScrapeStatus.LOADING, + progress: 0, + }); useEffect(() => { const fetchData = async () => { @@ -84,8 +132,8 @@ export const ScraperProcess = ({ data }: ScraperProcessProps) => { media: scrapeMedia, events: { // init: handleEvent, - // update: handleEvent, - // discoverEmbeds: handleEvent, + update: handleEvent, + discoverEmbeds: handleEvent, start: handleEvent, }, }); @@ -94,7 +142,7 @@ export const ScraperProcess = ({ data }: ScraperProcessProps) => { if (streamResult.stream.type === "hls") { const tracks = await extractTracksFromHLS( - streamResult.stream.playlist, // multiple tracks example: "https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8", + streamResult.stream.playlist, { ...streamResult.stream.preferredHeaders, ...streamResult.stream.headers, @@ -154,6 +202,7 @@ export const ScraperProcess = ({ data }: ScraperProcessProps) => { meta?.season?.number, meta?.episode?.number, setAudioTracks, + handleEvent, ]); return ( @@ -164,6 +213,7 @@ export const ScraperProcess = ({ data }: ScraperProcessProps) => { Checking {checkedSource} + {/* */} diff --git a/apps/expo/src/components/player/StatusCircle.tsx b/apps/expo/src/components/player/StatusCircle.tsx new file mode 100644 index 0000000..eb76e9a --- /dev/null +++ b/apps/expo/src/components/player/StatusCircle.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { StyleSheet, View } from "react-native"; +import Animated, { + Easing, + useAnimatedProps, + useSharedValue, + withTiming, +} from "react-native-reanimated"; +import { Circle, Svg } from "react-native-svg"; +import { AntDesign } from "@expo/vector-icons"; + +const AnimatedCircle = Animated.createAnimatedComponent(Circle); + +export const StatusCircle = ({ + type, + percentage = 0, +}: { + type: string; + percentage: number; +}) => { + const radius = 25; + const strokeWidth = 5; + const circleCircumference = 2 * Math.PI * radius; + + const strokeDashoffset = useSharedValue(circleCircumference); + + React.useEffect(() => { + strokeDashoffset.value = withTiming( + circleCircumference - (circleCircumference * percentage) / 100, + { + duration: 500, + easing: Easing.linear, + }, + ); + }, [circleCircumference, percentage, strokeDashoffset]); + + const animatedProps = useAnimatedProps(() => ({ + strokeDashoffset: strokeDashoffset.value, + })); + + const renderIcon = () => { + switch (type) { + case "success": + return ; + case "error": + return ; + default: + return null; + } + }; + + return ( + + + {type === "loading" && ( + + )} + + {renderIcon()} + + ); +}; + +const styles = StyleSheet.create({ + container: { + justifyContent: "center", + alignItems: "center", + position: "relative", + }, +});