From 21b574ee87a5ac376a540139fe01d650250661bd Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Thu, 21 Mar 2024 14:14:30 +0100 Subject: [PATCH] feat: play downloads --- apps/expo/src/app/(tabs)/downloads.tsx | 21 ++++++++++++++++++- apps/expo/src/app/_layout.tsx | 20 +++++++++--------- apps/expo/src/app/videoPlayer.tsx | 14 ++++++++++--- apps/expo/src/components/DownloadItem.tsx | 11 +++++++++- apps/expo/src/components/item/item.tsx | 12 +++++------ .../src/components/player/VideoPlayer.tsx | 8 +++++++ .../expo/src/hooks/DownloadManagerContext.tsx | 5 ++++- apps/expo/src/stores/player/slices/video.ts | 11 +++++++++- 8 files changed, 79 insertions(+), 23 deletions(-) diff --git a/apps/expo/src/app/(tabs)/downloads.tsx b/apps/expo/src/app/(tabs)/downloads.tsx index 0cef369..2c126ef 100644 --- a/apps/expo/src/app/(tabs)/downloads.tsx +++ b/apps/expo/src/app/(tabs)/downloads.tsx @@ -1,18 +1,37 @@ +import type { Asset } from "expo-media-library"; import React from "react"; import { ScrollView } from "react-native-gesture-handler"; +import { useRouter } from "expo-router"; import { DownloadItem } from "~/components/DownloadItem"; import ScreenLayout from "~/components/layout/ScreenLayout"; import { useDownloadManager } from "~/hooks/DownloadManagerContext"; +import { usePlayerStore } from "~/stores/player/store"; const DownloadsScreen: React.FC = () => { const { downloads, removeDownload } = useDownloadManager(); + const resetVideo = usePlayerStore((state) => state.resetVideo); + const router = useRouter(); + + const handlePress = (asset?: Asset) => { + if (!asset) return; + resetVideo(); + router.push({ + pathname: "/videoPlayer", + params: { data: JSON.stringify(asset) }, + }); + }; return ( {downloads.map((item) => ( - + handlePress(item.asset)} + onLongPress={removeDownload} + /> ))} diff --git a/apps/expo/src/app/_layout.tsx b/apps/expo/src/app/_layout.tsx index c9a1a6d..6c1029d 100644 --- a/apps/expo/src/app/_layout.tsx +++ b/apps/expo/src/app/_layout.tsx @@ -61,11 +61,9 @@ export default function RootLayout() { } return ( - - - - - + + + ); } @@ -108,11 +106,13 @@ function RootLayoutNav() { - - - - - + + + + + + + diff --git a/apps/expo/src/app/videoPlayer.tsx b/apps/expo/src/app/videoPlayer.tsx index d19f0fd..618dd1d 100644 --- a/apps/expo/src/app/videoPlayer.tsx +++ b/apps/expo/src/app/videoPlayer.tsx @@ -9,6 +9,7 @@ import { usePlayerStore } from "~/stores/player/store"; export default function VideoPlayerWrapper() { const playerStatus = usePlayerStore((state) => state.interface.playerStatus); + const asset = usePlayerStore((state) => state.asset); const { presentFullscreenPlayer } = usePlayer(); const router = useRouter(); @@ -21,8 +22,15 @@ export default function VideoPlayerWrapper() { void presentFullscreenPlayer(); - if (playerStatus === PlayerStatus.SCRAPING) - return ; + if (asset) { + return ; + } - if (playerStatus === PlayerStatus.READY) return ; + if (playerStatus === PlayerStatus.SCRAPING) { + return ; + } + + if (playerStatus === PlayerStatus.READY) { + return ; + } } diff --git a/apps/expo/src/components/DownloadItem.tsx b/apps/expo/src/components/DownloadItem.tsx index 93da300..d5bb084 100644 --- a/apps/expo/src/components/DownloadItem.tsx +++ b/apps/expo/src/components/DownloadItem.tsx @@ -1,3 +1,4 @@ +import type { Asset } from "expo-media-library"; import React from "react"; import { TouchableOpacity } from "react-native-gesture-handler"; import { Progress, Spinner, Text, View } from "tamagui"; @@ -12,6 +13,8 @@ export interface DownloadItemProps { isFinished: boolean; onLongPress: (id: string) => void; statusText?: string; + asset?: Asset; + onPress: (asset?: Asset) => void; } const formatBytes = (bytes: number, decimals = 2) => { @@ -33,6 +36,8 @@ export const DownloadItem: React.FC = ({ isFinished, onLongPress, statusText, + asset, + onPress, }) => { const percentage = progress * 100; const formattedFileSize = formatBytes(fileSize); @@ -64,7 +69,11 @@ export const DownloadItem: React.FC = ({ }; return ( - onLongPress(id)} activeOpacity={0.7}> + onPress(asset)} + onLongPress={() => onLongPress(id)} + activeOpacity={0.7} + > {filename} diff --git a/apps/expo/src/components/item/item.tsx b/apps/expo/src/components/item/item.tsx index 380f0c9..0b1cd53 100644 --- a/apps/expo/src/components/item/item.tsx +++ b/apps/expo/src/components/item/item.tsx @@ -5,7 +5,7 @@ import ContextMenu from "react-native-context-menu-view"; import { useRouter } from "expo-router"; import { Image, Text, View } from "tamagui"; -// import { useDownloadManager } from "~/hooks/DownloadManagerContext"; +import { useDownloadManager } from "~/hooks/DownloadManagerContext"; import { usePlayerStore } from "~/stores/player/store"; export interface ItemData { @@ -19,7 +19,7 @@ export interface ItemData { export default function Item({ data }: { data: ItemData }) { const resetVideo = usePlayerStore((state) => state.resetVideo); const router = useRouter(); - // const { startDownload } = useDownloadManager(); + const { startDownload } = useDownloadManager(); const { title, type, year, posterUrl } = data; @@ -41,10 +41,10 @@ export default function Item({ data }: { data: ItemData }) { e: NativeSyntheticEvent, ) => { console.log(e.nativeEvent.name); - // startDownload( - // "https://samplelib.com/lib/preview/mp4/sample-5s.mp4", - // "mp4", - // ).catch(console.error); + startDownload( + "https://samplelib.com/lib/preview/mp4/sample-5s.mp4", + "mp4", + ).catch(console.error); }; return ( diff --git a/apps/expo/src/components/player/VideoPlayer.tsx b/apps/expo/src/components/player/VideoPlayer.tsx index 2591fca..4662062 100644 --- a/apps/expo/src/components/player/VideoPlayer.tsx +++ b/apps/expo/src/components/player/VideoPlayer.tsx @@ -56,6 +56,7 @@ export const VideoPlayer = () => { const stream = usePlayerStore((state) => state.interface.currentStream); const selectedAudioTrack = useAudioTrackStore((state) => state.selectedTrack); const videoRef = usePlayerStore((state) => state.videoRef); + const asset = usePlayerStore((state) => state.asset); const setVideoRef = usePlayerStore((state) => state.setVideoRef); const setStatus = usePlayerStore((state) => state.setStatus); const setIsIdle = usePlayerStore((state) => state.setIsIdle); @@ -167,6 +168,12 @@ export const VideoPlayer = () => { useEffect(() => { const initializePlayer = async () => { + if (asset) { + setVideoSrc(asset); + setIsLoading(false); + return; + } + if (!stream) { await dismissFullscreenPlayer(); return router.back(); @@ -214,6 +221,7 @@ export const VideoPlayer = () => { void synchronizePlayback(); }; }, [ + asset, dismissFullscreenPlayer, hasStartedPlaying, router, diff --git a/apps/expo/src/hooks/DownloadManagerContext.tsx b/apps/expo/src/hooks/DownloadManagerContext.tsx index fc7958a..9e02380 100644 --- a/apps/expo/src/hooks/DownloadManagerContext.tsx +++ b/apps/expo/src/hooks/DownloadManagerContext.tsx @@ -1,3 +1,4 @@ +import type { Asset } from "expo-media-library"; import type { ReactNode } from "react"; import React, { createContext, useContext, useEffect, useState } from "react"; import * as FileSystem from "expo-file-system"; @@ -17,6 +18,7 @@ export interface DownloadItem { type: "mp4" | "hls"; isFinished: boolean; statusText?: string; + asset?: Asset; } interface DownloadManagerContextType { @@ -168,11 +170,12 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({ throw new Error("MediaLibrary permission not granted"); } - await MediaLibrary.saveToLibraryAsync(fileUri); + const asset = await MediaLibrary.createAssetAsync(fileUri); await FileSystem.deleteAsync(fileUri); updateDownloadItem(downloadId, { statusText: undefined, + asset, isFinished: true, }); console.log("File saved to media library and original deleted"); diff --git a/apps/expo/src/stores/player/slices/video.ts b/apps/expo/src/stores/player/slices/video.ts index 863f8a4..26a9516 100644 --- a/apps/expo/src/stores/player/slices/video.ts +++ b/apps/expo/src/stores/player/slices/video.ts @@ -1,4 +1,5 @@ import type { AVPlaybackStatus, Video } from "expo-av"; +import type { Asset } from "expo-media-library"; import type { ScrapeMedia } from "@movie-web/provider-utils"; @@ -31,10 +32,12 @@ export interface VideoSlice { videoRef: Video | null; status: AVPlaybackStatus | null; meta: PlayerMeta | null; + asset: Asset | null; setVideoRef(ref: Video | null): void; setStatus(status: AVPlaybackStatus | null): void; setMeta(meta: PlayerMeta | null): void; + setAsset(asset: Asset | null): void; resetVideo(): void; } @@ -66,6 +69,7 @@ export const createVideoSlice: MakeSlice = (set) => ({ videoRef: null, status: null, meta: null, + asset: null, setVideoRef: (ref) => { set({ videoRef: ref }); @@ -81,8 +85,13 @@ export const createVideoSlice: MakeSlice = (set) => ({ s.meta = meta; }); }, + setAsset: (asset) => { + set((s) => { + s.asset = asset; + }); + }, resetVideo() { - set({ videoRef: null, status: null, meta: null }); + set({ videoRef: null, status: null, meta: null, asset: null }); set((s) => { s.interface.playerStatus = PlayerStatus.SCRAPING; });