From 96b00064c6e9103858d01cd53d705ae98fb7ef77 Mon Sep 17 00:00:00 2001 From: Jorrin Date: Mon, 8 Apr 2024 21:22:09 +0200 Subject: [PATCH] add episode download section --- apps/expo/src/app/(downloads)/[tmdbId].tsx | 61 +++++++ apps/expo/src/app/(downloads)/_layout.tsx | 14 ++ apps/expo/src/app/(tabs)/downloads.tsx | 169 ++++++++++-------- apps/expo/src/components/DownloadItem.tsx | 60 ++++++- apps/expo/src/components/item/item.tsx | 1 + apps/expo/src/components/layout/Header.tsx | 18 +- .../src/components/layout/ScreenLayout.tsx | 8 +- apps/expo/src/components/player/Header.tsx | 5 +- apps/expo/src/components/player/utils.ts | 7 + apps/expo/src/hooks/useDownloadManager.tsx | 25 ++- 10 files changed, 263 insertions(+), 105 deletions(-) create mode 100644 apps/expo/src/app/(downloads)/[tmdbId].tsx create mode 100644 apps/expo/src/app/(downloads)/_layout.tsx diff --git a/apps/expo/src/app/(downloads)/[tmdbId].tsx b/apps/expo/src/app/(downloads)/[tmdbId].tsx new file mode 100644 index 0000000..794e2d2 --- /dev/null +++ b/apps/expo/src/app/(downloads)/[tmdbId].tsx @@ -0,0 +1,61 @@ +import { useMemo } from "react"; +import { Stack, useLocalSearchParams, useRouter } from "expo-router"; +import { YStack } from "tamagui"; + +import { DownloadItem } from "~/components/DownloadItem"; +import ScreenLayout from "~/components/layout/ScreenLayout"; +import { PlayerStatus } from "~/stores/player/slices/interface"; +import { usePlayerStore } from "~/stores/player/store"; +import { useDownloadHistoryStore } from "~/stores/settings"; + +export default function Page() { + const { tmdbId } = useLocalSearchParams(); + const allDownloads = useDownloadHistoryStore((state) => state.downloads); + const resetVideo = usePlayerStore((state) => state.resetVideo); + const setVideoSrc = usePlayerStore((state) => state.setVideoSrc); + const setIsLocalFile = usePlayerStore((state) => state.setIsLocalFile); + const setPlayerStatus = usePlayerStore((state) => state.setPlayerStatus); + const router = useRouter(); + + const download = useMemo(() => { + return allDownloads.find((download) => download.media.tmdbId === tmdbId); + }, [allDownloads, tmdbId]); + + const handlePress = (localPath?: string) => { + if (!localPath) return; + resetVideo(); + setIsLocalFile(true); + setPlayerStatus(PlayerStatus.READY); + setVideoSrc({ + uri: localPath, + }); + router.push({ + pathname: "/videoPlayer", + }); + }; + + return ( + + + + {download?.downloads.map((download) => { + return ( + handlePress(download.localPath)} + /> + ); + })} + + + ); +} diff --git a/apps/expo/src/app/(downloads)/_layout.tsx b/apps/expo/src/app/(downloads)/_layout.tsx new file mode 100644 index 0000000..a6a9e05 --- /dev/null +++ b/apps/expo/src/app/(downloads)/_layout.tsx @@ -0,0 +1,14 @@ +import { Stack } from "expo-router"; + +import { BrandPill } from "~/components/BrandPill"; + +export default function Layout() { + return ( + + ); +} diff --git a/apps/expo/src/app/(tabs)/downloads.tsx b/apps/expo/src/app/(tabs)/downloads.tsx index a705d12..bfb2fab 100644 --- a/apps/expo/src/app/(tabs)/downloads.tsx +++ b/apps/expo/src/app/(tabs)/downloads.tsx @@ -7,7 +7,7 @@ import { ScrollView, useTheme, YStack } from "tamagui"; import type { ScrapeMedia } from "@movie-web/provider-utils"; -import { DownloadItem } from "~/components/DownloadItem"; +import { DownloadItem, ShowDownloadItem } from "~/components/DownloadItem"; import ScreenLayout from "~/components/layout/ScreenLayout"; import { MWButton } from "~/components/ui/Button"; import { useDownloadManager } from "~/hooks/useDownloadManager"; @@ -15,15 +15,69 @@ import { PlayerStatus } from "~/stores/player/slices/interface"; import { usePlayerStore } from "~/stores/player/store"; import { useDownloadHistoryStore } from "~/stores/settings"; -const DownloadsScreen: React.FC = () => { +const exampleMovieMedia: ScrapeMedia = { + type: "movie", + title: "Avengers: Endgame", + releaseYear: 2019, + imdbId: "tt4154796", + tmdbId: "299534", +}; + +const getExampleShowMedia = (seasonNumber: number, episodeNumber: number) => + ({ + type: "show", + title: "Loki", + releaseYear: 2021, + imdbId: "tt9140554", + tmdbId: "84958", + season: { + number: seasonNumber, + tmdbId: seasonNumber.toString(), + }, + episode: { + number: episodeNumber, + tmdbId: episodeNumber.toString(), + }, + }) as const; + +const TestDownloadButton = (props: { + media: ScrapeMedia; + type: "hls" | "mp4"; + url: string; +}) => { const { startDownload } = useDownloadManager(); + const theme = useTheme(); + return ( + + } + onPress={async () => { + await startDownload(props.url, props.type, props.media).catch( + console.error, + ); + }} + > + test download + {props.type === "hls" ? " (hls)" : "(mp4)"}{" "} + {props.media.type === "show" ? "show" : "movie"} + + ); +}; + +const DownloadsScreen: React.FC = () => { const downloads = useDownloadHistoryStore((state) => state.downloads); const resetVideo = usePlayerStore((state) => state.resetVideo); const setVideoSrc = usePlayerStore((state) => state.setVideoSrc); const setIsLocalFile = usePlayerStore((state) => state.setIsLocalFile); const setPlayerStatus = usePlayerStore((state) => state.setPlayerStatus); const router = useRouter(); - const theme = useTheme(); useFocusEffect( React.useCallback(() => { @@ -55,85 +109,54 @@ const DownloadsScreen: React.FC = () => { }); }; - const exampleShowMedia: ScrapeMedia = { - type: "show", - title: "Example Show Title", - releaseYear: 2022, - imdbId: "tt1234567", - tmdbId: "12345", - season: { - number: 1, - tmdbId: "54321", - }, - episode: { - number: 3, - tmdbId: "98765", - }, - }; - return ( - - } - onPress={async () => { - await startDownload( - "https://samplelib.com/lib/preview/mp4/sample-5s.mp4", - "mp4", - exampleShowMedia, - ).catch(console.error); - }} - > - test download (mp4) - - - } - onPress={async () => { - await startDownload( - "http://sample.vodobox.com/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8", - "hls", - { - ...exampleShowMedia, - tmdbId: "123456", - }, - ).catch(console.error); - }} - > - test download (hls) - + + + + - {/* TODO: Differentiate movies/shows, shows in new page */} - {downloads - .map((item) => item.downloads) - .flat() - .map((item) => ( - handlePress(item.localPath)} - /> - ))} + {downloads.map((download) => { + if (download.downloads.length === 0) return null; + if (download.media.type === "movie") { + return ( + handlePress(download.downloads[0]!.localPath)} + /> + ); + } else { + return ( + + ); + } + })} ); diff --git a/apps/expo/src/components/DownloadItem.tsx b/apps/expo/src/components/DownloadItem.tsx index af3b8f0..aab3df9 100644 --- a/apps/expo/src/components/DownloadItem.tsx +++ b/apps/expo/src/components/DownloadItem.tsx @@ -3,10 +3,12 @@ import type { ContextMenuOnPressNativeEvent } from "react-native-context-menu-vi import React from "react"; import ContextMenu from "react-native-context-menu-view"; import { TouchableOpacity } from "react-native-gesture-handler"; +import { useRouter } from "expo-router"; import { Image, Text, View, XStack, YStack } from "tamagui"; -import type { Download } from "~/hooks/useDownloadManager"; +import type { Download, DownloadContent } from "~/hooks/useDownloadManager"; import { useDownloadManager } from "~/hooks/useDownloadManager"; +import { mapSeasonAndEpisodeNumberToText } from "./player/utils"; import { MWProgress } from "./ui/Progress"; import { FlashingText } from "./ui/Text"; @@ -101,6 +103,11 @@ export function DownloadItem(props: DownloadItemProps) { + {props.item.media.type === "show" && + mapSeasonAndEpisodeNumberToText( + props.item.media.season.number, + props.item.media.episode.number, + ) + " "} {props.item.media.title} {props.item.type !== "hls" && ( @@ -136,3 +143,54 @@ export function DownloadItem(props: DownloadItemProps) { ); } + +export function ShowDownloadItem({ download }: { download: DownloadContent }) { + const router = useRouter(); + + return ( + + router.push({ + pathname: "/(downloads)/[tmdbId]", + params: { tmdbId: download.media.tmdbId }, + }) + } + activeOpacity={0.7} + > + + + + + + + + {download.media.title} + + + {download.downloads.length} Episode + {download.downloads.length > 1 ? "s" : ""} |{" "} + {formatBytes( + download.downloads.reduce( + (acc, curr) => acc + curr.fileSize, + 0, + ), + )} + + + + + + ); +} diff --git a/apps/expo/src/components/item/item.tsx b/apps/expo/src/components/item/item.tsx index 5478c6c..badd332 100644 --- a/apps/expo/src/components/item/item.tsx +++ b/apps/expo/src/components/item/item.tsx @@ -124,6 +124,7 @@ export default function Item({ data }: { data: ItemData }) { width="100%" overflow="hidden" borderRadius={24} + height="$14" > diff --git a/apps/expo/src/components/layout/Header.tsx b/apps/expo/src/components/layout/Header.tsx index 5f17a37..07b6efc 100644 --- a/apps/expo/src/components/layout/Header.tsx +++ b/apps/expo/src/components/layout/Header.tsx @@ -1,5 +1,4 @@ import { Linking } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; import * as Haptics from "expo-haptics"; import { FontAwesome6, MaterialIcons } from "@expo/vector-icons"; import { Circle, View } from "tamagui"; @@ -8,20 +7,13 @@ import { DISCORD_LINK, GITHUB_LINK } from "~/constants/core"; import { BrandPill } from "../BrandPill"; export function Header() { - const insets = useSafeAreaInsets(); - return ( - + - + - + ); diff --git a/apps/expo/src/components/layout/ScreenLayout.tsx b/apps/expo/src/components/layout/ScreenLayout.tsx index d965878..45be60f 100644 --- a/apps/expo/src/components/layout/ScreenLayout.tsx +++ b/apps/expo/src/components/layout/ScreenLayout.tsx @@ -1,3 +1,4 @@ +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { ScrollView } from "tamagui"; import { LinearGradient } from "tamagui/linear-gradient"; @@ -7,6 +8,7 @@ interface Props { children?: React.ReactNode; onScrollBeginDrag?: () => void; onMomentumScrollEnd?: () => void; + showHeader?: boolean; scrollEnabled?: boolean; keyboardDismissMode?: "none" | "on-drag" | "interactive"; keyboardShouldPersistTaps?: "always" | "never" | "handled"; @@ -17,11 +19,14 @@ export default function ScreenLayout({ children, onScrollBeginDrag, onMomentumScrollEnd, + showHeader = true, scrollEnabled, keyboardDismissMode, keyboardShouldPersistTaps, contentContainerStyle, }: Props) { + const insets = useSafeAreaInsets(); + return ( -
+ {showHeader &&
} { - return `S${season.toString().padStart(2, "0")}E${episode.toString().padStart(2, "0")}`; -}; +import { mapSeasonAndEpisodeNumberToText } from "./utils"; export const Header = () => { const isIdle = usePlayerStore((state) => state.interface.isIdle); diff --git a/apps/expo/src/components/player/utils.ts b/apps/expo/src/components/player/utils.ts index ab8e2ef..2a192d5 100644 --- a/apps/expo/src/components/player/utils.ts +++ b/apps/expo/src/components/player/utils.ts @@ -16,3 +16,10 @@ export const mapMillisecondsToTime = (milliseconds: number): string => { return formattedTime; }; + +export const mapSeasonAndEpisodeNumberToText = ( + season: number, + episode: number, +) => { + return `S${season.toString().padStart(2, "0")}E${episode.toString().padStart(2, "0")}`; +}; diff --git a/apps/expo/src/hooks/useDownloadManager.tsx b/apps/expo/src/hooks/useDownloadManager.tsx index 9517d8c..b21e4bd 100644 --- a/apps/expo/src/hooks/useDownloadManager.tsx +++ b/apps/expo/src/hooks/useDownloadManager.tsx @@ -404,19 +404,18 @@ export const useDownloadManager = () => { media, }; - const newDownloadContent = existingDownload - ? { - ...existingDownload, - downloads: [newDownload, ...existingDownload.downloads], - } - : { - media, - downloads: [newDownload], - }; - - setDownloads((prev) => { - return [...prev, newDownloadContent]; - }); + if (existingDownload) { + existingDownload.downloads.push(newDownload); + setDownloads((prev) => { + return prev.map((d) => + d.media.tmdbId === media.tmdbId ? existingDownload : d, + ); + }); + } else { + setDownloads((prev) => { + return [...prev, { media, downloads: [newDownload] }]; + }); + } if (type === "mp4") { const asset = await downloadMP4(url, newDownload, headers ?? {});