mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 17:43:25 +00:00
feat: play downloads
This commit is contained in:
@@ -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 (
|
||||
<ScreenLayout title="Downloads">
|
||||
<ScrollView>
|
||||
{downloads.map((item) => (
|
||||
<DownloadItem key={item.id} {...item} onLongPress={removeDownload} />
|
||||
<DownloadItem
|
||||
key={item.id}
|
||||
{...item}
|
||||
onPress={() => handlePress(item.asset)}
|
||||
onLongPress={removeDownload}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</ScreenLayout>
|
||||
|
@@ -61,11 +61,9 @@ export default function RootLayout() {
|
||||
}
|
||||
|
||||
return (
|
||||
<DownloadManagerProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<RootLayoutNav />
|
||||
</GestureHandlerRootView>
|
||||
</DownloadManagerProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<RootLayoutNav />
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,11 +106,13 @@ function RootLayoutNav() {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TamaguiProvider config={tamaguiConfig} defaultTheme="main">
|
||||
<ToastProvider>
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<Theme name={themeStore}>
|
||||
<ScreenStacks />
|
||||
</Theme>
|
||||
</ThemeProvider>
|
||||
<DownloadManagerProvider>
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<Theme name={themeStore}>
|
||||
<ScreenStacks />
|
||||
</Theme>
|
||||
</ThemeProvider>
|
||||
</DownloadManagerProvider>
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
</TamaguiProvider>
|
||||
|
@@ -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 <ScraperProcess data={data} />;
|
||||
if (asset) {
|
||||
return <VideoPlayer />;
|
||||
}
|
||||
|
||||
if (playerStatus === PlayerStatus.READY) return <VideoPlayer />;
|
||||
if (playerStatus === PlayerStatus.SCRAPING) {
|
||||
return <ScraperProcess data={data} />;
|
||||
}
|
||||
|
||||
if (playerStatus === PlayerStatus.READY) {
|
||||
return <VideoPlayer />;
|
||||
}
|
||||
}
|
||||
|
@@ -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<DownloadItemProps> = ({
|
||||
isFinished,
|
||||
onLongPress,
|
||||
statusText,
|
||||
asset,
|
||||
onPress,
|
||||
}) => {
|
||||
const percentage = progress * 100;
|
||||
const formattedFileSize = formatBytes(fileSize);
|
||||
@@ -64,7 +69,11 @@ export const DownloadItem: React.FC<DownloadItemProps> = ({
|
||||
};
|
||||
|
||||
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}>
|
||||
<Text marginBottom={4} fontSize={16}>
|
||||
{filename}
|
||||
|
@@ -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<ContextMenuOnPressNativeEvent>,
|
||||
) => {
|
||||
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 (
|
||||
|
@@ -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,
|
||||
|
@@ -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");
|
||||
|
@@ -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<VideoSlice> = (set) => ({
|
||||
videoRef: null,
|
||||
status: null,
|
||||
meta: null,
|
||||
asset: null,
|
||||
|
||||
setVideoRef: (ref) => {
|
||||
set({ videoRef: ref });
|
||||
@@ -81,8 +85,13 @@ export const createVideoSlice: MakeSlice<VideoSlice> = (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;
|
||||
});
|
||||
|
Reference in New Issue
Block a user