feat: play downloads

This commit is contained in:
Adrian Castro
2024-03-21 14:14:30 +01:00
parent 13143a2664
commit 21b574ee87
8 changed files with 79 additions and 23 deletions

View File

@@ -1,18 +1,37 @@
import type { Asset } from "expo-media-library";
import React from "react"; import React from "react";
import { ScrollView } from "react-native-gesture-handler"; import { ScrollView } from "react-native-gesture-handler";
import { useRouter } from "expo-router";
import { DownloadItem } from "~/components/DownloadItem"; import { DownloadItem } from "~/components/DownloadItem";
import ScreenLayout from "~/components/layout/ScreenLayout"; import ScreenLayout from "~/components/layout/ScreenLayout";
import { useDownloadManager } from "~/hooks/DownloadManagerContext"; import { useDownloadManager } from "~/hooks/DownloadManagerContext";
import { usePlayerStore } from "~/stores/player/store";
const DownloadsScreen: React.FC = () => { const DownloadsScreen: React.FC = () => {
const { downloads, removeDownload } = useDownloadManager(); 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 ( return (
<ScreenLayout title="Downloads"> <ScreenLayout title="Downloads">
<ScrollView> <ScrollView>
{downloads.map((item) => ( {downloads.map((item) => (
<DownloadItem key={item.id} {...item} onLongPress={removeDownload} /> <DownloadItem
key={item.id}
{...item}
onPress={() => handlePress(item.asset)}
onLongPress={removeDownload}
/>
))} ))}
</ScrollView> </ScrollView>
</ScreenLayout> </ScreenLayout>

View File

@@ -61,11 +61,9 @@ export default function RootLayout() {
} }
return ( return (
<DownloadManagerProvider>
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<RootLayoutNav /> <RootLayoutNav />
</GestureHandlerRootView> </GestureHandlerRootView>
</DownloadManagerProvider>
); );
} }
@@ -108,11 +106,13 @@ function RootLayoutNav() {
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<TamaguiProvider config={tamaguiConfig} defaultTheme="main"> <TamaguiProvider config={tamaguiConfig} defaultTheme="main">
<ToastProvider> <ToastProvider>
<DownloadManagerProvider>
<ThemeProvider value={DarkTheme}> <ThemeProvider value={DarkTheme}>
<Theme name={themeStore}> <Theme name={themeStore}>
<ScreenStacks /> <ScreenStacks />
</Theme> </Theme>
</ThemeProvider> </ThemeProvider>
</DownloadManagerProvider>
<ToastViewport /> <ToastViewport />
</ToastProvider> </ToastProvider>
</TamaguiProvider> </TamaguiProvider>

View File

@@ -9,6 +9,7 @@ import { usePlayerStore } from "~/stores/player/store";
export default function VideoPlayerWrapper() { export default function VideoPlayerWrapper() {
const playerStatus = usePlayerStore((state) => state.interface.playerStatus); const playerStatus = usePlayerStore((state) => state.interface.playerStatus);
const asset = usePlayerStore((state) => state.asset);
const { presentFullscreenPlayer } = usePlayer(); const { presentFullscreenPlayer } = usePlayer();
const router = useRouter(); const router = useRouter();
@@ -21,8 +22,15 @@ export default function VideoPlayerWrapper() {
void presentFullscreenPlayer(); void presentFullscreenPlayer();
if (playerStatus === PlayerStatus.SCRAPING) if (asset) {
return <ScraperProcess data={data} />; return <VideoPlayer />;
}
if (playerStatus === PlayerStatus.READY) return <VideoPlayer />; if (playerStatus === PlayerStatus.SCRAPING) {
return <ScraperProcess data={data} />;
}
if (playerStatus === PlayerStatus.READY) {
return <VideoPlayer />;
}
} }

View File

@@ -1,3 +1,4 @@
import type { Asset } from "expo-media-library";
import React from "react"; import React from "react";
import { TouchableOpacity } from "react-native-gesture-handler"; import { TouchableOpacity } from "react-native-gesture-handler";
import { Progress, Spinner, Text, View } from "tamagui"; import { Progress, Spinner, Text, View } from "tamagui";
@@ -12,6 +13,8 @@ export interface DownloadItemProps {
isFinished: boolean; isFinished: boolean;
onLongPress: (id: string) => void; onLongPress: (id: string) => void;
statusText?: string; statusText?: string;
asset?: Asset;
onPress: (asset?: Asset) => void;
} }
const formatBytes = (bytes: number, decimals = 2) => { const formatBytes = (bytes: number, decimals = 2) => {
@@ -33,6 +36,8 @@ export const DownloadItem: React.FC<DownloadItemProps> = ({
isFinished, isFinished,
onLongPress, onLongPress,
statusText, statusText,
asset,
onPress,
}) => { }) => {
const percentage = progress * 100; const percentage = progress * 100;
const formattedFileSize = formatBytes(fileSize); const formattedFileSize = formatBytes(fileSize);
@@ -64,7 +69,11 @@ export const DownloadItem: React.FC<DownloadItemProps> = ({
}; };
return ( return (
<TouchableOpacity onLongPress={() => onLongPress(id)} activeOpacity={0.7}> <TouchableOpacity
onPress={() => onPress(asset)}
onLongPress={() => onLongPress(id)}
activeOpacity={0.7}
>
<View marginBottom={16} borderRadius={8} borderColor="white" padding={16}> <View marginBottom={16} borderRadius={8} borderColor="white" padding={16}>
<Text marginBottom={4} fontSize={16}> <Text marginBottom={4} fontSize={16}>
{filename} {filename}

View File

@@ -5,7 +5,7 @@ import ContextMenu from "react-native-context-menu-view";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { Image, Text, View } from "tamagui"; import { Image, Text, View } from "tamagui";
// import { useDownloadManager } from "~/hooks/DownloadManagerContext"; import { useDownloadManager } from "~/hooks/DownloadManagerContext";
import { usePlayerStore } from "~/stores/player/store"; import { usePlayerStore } from "~/stores/player/store";
export interface ItemData { export interface ItemData {
@@ -19,7 +19,7 @@ export interface ItemData {
export default function Item({ data }: { data: ItemData }) { export default function Item({ data }: { data: ItemData }) {
const resetVideo = usePlayerStore((state) => state.resetVideo); const resetVideo = usePlayerStore((state) => state.resetVideo);
const router = useRouter(); const router = useRouter();
// const { startDownload } = useDownloadManager(); const { startDownload } = useDownloadManager();
const { title, type, year, posterUrl } = data; const { title, type, year, posterUrl } = data;
@@ -41,10 +41,10 @@ export default function Item({ data }: { data: ItemData }) {
e: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>, e: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>,
) => { ) => {
console.log(e.nativeEvent.name); console.log(e.nativeEvent.name);
// startDownload( startDownload(
// "https://samplelib.com/lib/preview/mp4/sample-5s.mp4", "https://samplelib.com/lib/preview/mp4/sample-5s.mp4",
// "mp4", "mp4",
// ).catch(console.error); ).catch(console.error);
}; };
return ( return (

View File

@@ -56,6 +56,7 @@ export const VideoPlayer = () => {
const stream = usePlayerStore((state) => state.interface.currentStream); const stream = usePlayerStore((state) => state.interface.currentStream);
const selectedAudioTrack = useAudioTrackStore((state) => state.selectedTrack); const selectedAudioTrack = useAudioTrackStore((state) => state.selectedTrack);
const videoRef = usePlayerStore((state) => state.videoRef); const videoRef = usePlayerStore((state) => state.videoRef);
const asset = usePlayerStore((state) => state.asset);
const setVideoRef = usePlayerStore((state) => state.setVideoRef); const setVideoRef = usePlayerStore((state) => state.setVideoRef);
const setStatus = usePlayerStore((state) => state.setStatus); const setStatus = usePlayerStore((state) => state.setStatus);
const setIsIdle = usePlayerStore((state) => state.setIsIdle); const setIsIdle = usePlayerStore((state) => state.setIsIdle);
@@ -167,6 +168,12 @@ export const VideoPlayer = () => {
useEffect(() => { useEffect(() => {
const initializePlayer = async () => { const initializePlayer = async () => {
if (asset) {
setVideoSrc(asset);
setIsLoading(false);
return;
}
if (!stream) { if (!stream) {
await dismissFullscreenPlayer(); await dismissFullscreenPlayer();
return router.back(); return router.back();
@@ -214,6 +221,7 @@ export const VideoPlayer = () => {
void synchronizePlayback(); void synchronizePlayback();
}; };
}, [ }, [
asset,
dismissFullscreenPlayer, dismissFullscreenPlayer,
hasStartedPlaying, hasStartedPlaying,
router, router,

View File

@@ -1,3 +1,4 @@
import type { Asset } from "expo-media-library";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import React, { createContext, useContext, useEffect, useState } from "react"; import React, { createContext, useContext, useEffect, useState } from "react";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
@@ -17,6 +18,7 @@ export interface DownloadItem {
type: "mp4" | "hls"; type: "mp4" | "hls";
isFinished: boolean; isFinished: boolean;
statusText?: string; statusText?: string;
asset?: Asset;
} }
interface DownloadManagerContextType { interface DownloadManagerContextType {
@@ -168,11 +170,12 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
throw new Error("MediaLibrary permission not granted"); throw new Error("MediaLibrary permission not granted");
} }
await MediaLibrary.saveToLibraryAsync(fileUri); const asset = await MediaLibrary.createAssetAsync(fileUri);
await FileSystem.deleteAsync(fileUri); await FileSystem.deleteAsync(fileUri);
updateDownloadItem(downloadId, { updateDownloadItem(downloadId, {
statusText: undefined, statusText: undefined,
asset,
isFinished: true, isFinished: true,
}); });
console.log("File saved to media library and original deleted"); console.log("File saved to media library and original deleted");

View File

@@ -1,4 +1,5 @@
import type { AVPlaybackStatus, Video } from "expo-av"; import type { AVPlaybackStatus, Video } from "expo-av";
import type { Asset } from "expo-media-library";
import type { ScrapeMedia } from "@movie-web/provider-utils"; import type { ScrapeMedia } from "@movie-web/provider-utils";
@@ -31,10 +32,12 @@ export interface VideoSlice {
videoRef: Video | null; videoRef: Video | null;
status: AVPlaybackStatus | null; status: AVPlaybackStatus | null;
meta: PlayerMeta | null; meta: PlayerMeta | null;
asset: Asset | null;
setVideoRef(ref: Video | null): void; setVideoRef(ref: Video | null): void;
setStatus(status: AVPlaybackStatus | null): void; setStatus(status: AVPlaybackStatus | null): void;
setMeta(meta: PlayerMeta | null): void; setMeta(meta: PlayerMeta | null): void;
setAsset(asset: Asset | null): void;
resetVideo(): void; resetVideo(): void;
} }
@@ -66,6 +69,7 @@ export const createVideoSlice: MakeSlice<VideoSlice> = (set) => ({
videoRef: null, videoRef: null,
status: null, status: null,
meta: null, meta: null,
asset: null,
setVideoRef: (ref) => { setVideoRef: (ref) => {
set({ videoRef: ref }); set({ videoRef: ref });
@@ -81,8 +85,13 @@ export const createVideoSlice: MakeSlice<VideoSlice> = (set) => ({
s.meta = meta; s.meta = meta;
}); });
}, },
setAsset: (asset) => {
set((s) => {
s.asset = asset;
});
},
resetVideo() { resetVideo() {
set({ videoRef: null, status: null, meta: null }); set({ videoRef: null, status: null, meta: null, asset: null });
set((s) => { set((s) => {
s.interface.playerStatus = PlayerStatus.SCRAPING; s.interface.playerStatus = PlayerStatus.SCRAPING;
}); });