downloads refactor

This commit is contained in:
Jorrin
2024-04-06 16:53:54 +02:00
parent bf6bd7af2f
commit c61f18941e
19 changed files with 179 additions and 195 deletions

View File

@@ -1,4 +1,3 @@
import type { Asset } from "expo-media-library";
import React from "react"; import React from "react";
import { Alert, Platform } from "react-native"; import { Alert, Platform } from "react-native";
import { ScrollView } from "react-native-gesture-handler"; import { ScrollView } from "react-native-gesture-handler";
@@ -12,13 +11,16 @@ import type { ScrapeMedia } from "@movie-web/provider-utils";
import { DownloadItem } from "~/components/DownloadItem"; import { DownloadItem } from "~/components/DownloadItem";
import ScreenLayout from "~/components/layout/ScreenLayout"; import ScreenLayout from "~/components/layout/ScreenLayout";
import { MWButton } from "~/components/ui/Button"; import { MWButton } from "~/components/ui/Button";
import { useDownloadManager } from "~/hooks/DownloadManagerContext"; import { useDownloadManager } from "~/contexts/DownloadManagerContext";
import { PlayerStatus } from "~/stores/player/slices/interface";
import { usePlayerStore } from "~/stores/player/store"; import { usePlayerStore } from "~/stores/player/store";
const DownloadsScreen: React.FC = () => { const DownloadsScreen: React.FC = () => {
const { startDownload, downloads } = useDownloadManager(); const { startDownload, downloads } = useDownloadManager();
const resetVideo = usePlayerStore((state) => state.resetVideo); const resetVideo = usePlayerStore((state) => state.resetVideo);
const setAsset = usePlayerStore((state) => state.setAsset); const setVideoSrc = usePlayerStore((state) => state.setVideoSrc);
const setIsLocalFile = usePlayerStore((state) => state.setIsLocalFile);
const setPlayerStatus = usePlayerStore((state) => state.setPlayerStatus);
const router = useRouter(); const router = useRouter();
const theme = useTheme(); const theme = useTheme();
@@ -39,10 +41,14 @@ const DownloadsScreen: React.FC = () => {
}, [router]), }, [router]),
); );
const handlePress = (asset?: Asset) => { const handlePress = (localPath?: string) => {
if (!asset) return; if (!localPath) return;
resetVideo(); resetVideo();
setAsset(asset); setIsLocalFile(true);
setPlayerStatus(PlayerStatus.READY);
setVideoSrc({
uri: localPath,
});
router.push({ router.push({
pathname: "/videoPlayer", pathname: "/videoPlayer",
}); });
@@ -112,8 +118,8 @@ const DownloadsScreen: React.FC = () => {
{downloads.map((item) => ( {downloads.map((item) => (
<DownloadItem <DownloadItem
key={item.id} key={item.id}
{...item} item={item}
onPress={() => handlePress(item.asset)} onPress={() => handlePress(item.localPath)}
/> />
))} ))}
</ScrollView> </ScrollView>

View File

@@ -118,8 +118,8 @@ export default function SettingsScreen() {
return ( return (
<ScreenLayout> <ScreenLayout>
<View padding={4}> <View>
<YStack gap="$4"> <YStack gap="$8">
<YStack gap="$4"> <YStack gap="$4">
<Text fontSize="$7" fontWeight="$bold"> <Text fontSize="$7" fontWeight="$bold">
Appearance Appearance
@@ -181,7 +181,7 @@ export default function SettingsScreen() {
</Text> </Text>
<DefaultQualitySelector qualityType="data" /> <DefaultQualitySelector qualityType="data" />
</XStack> </XStack>
<XStack gap="$4" alignItems="center"> <XStack gap="$3" alignItems="center">
<Text fontWeight="$semibold" flexGrow={1}> <Text fontWeight="$semibold" flexGrow={1}>
Allow downloads on mobile data Allow downloads on mobile data
</Text> </Text>
@@ -451,7 +451,7 @@ export function DefaultQualitySelector(props: DefaultQualitySelectorProps) {
{...props} {...props}
> >
<MWSelect.Trigger <MWSelect.Trigger
maxWidth="$12" maxWidth="$10"
iconAfter={ iconAfter={
<FontAwesome name="chevron-down" color={theme.inputIconColor.val} /> <FontAwesome name="chevron-down" color={theme.inputIconColor.val} />
} }

View File

@@ -10,7 +10,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { TamaguiProvider, Theme, useTheme } from "tamagui"; import { TamaguiProvider, Theme, useTheme } from "tamagui";
import tamaguiConfig from "tamagui.config"; import tamaguiConfig from "tamagui.config";
import { DownloadManagerProvider } from "~/hooks/DownloadManagerContext"; import { DownloadManagerProvider } from "~/contexts/DownloadManagerContext";
import { useThemeStore } from "~/stores/theme"; import { useThemeStore } from "~/stores/theme";
// @ts-expect-error - Without named import it causes an infinite loop // @ts-expect-error - Without named import it causes an infinite loop
import _styles from "../../tamagui-web.css"; import _styles from "../../tamagui-web.css";

View File

@@ -11,7 +11,6 @@ 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 params = useLocalSearchParams(); const params = useLocalSearchParams();
@@ -32,10 +31,6 @@ export default function VideoPlayerWrapper() {
void presentFullscreenPlayer(); void presentFullscreenPlayer();
if (asset) {
return <VideoPlayer />;
}
if (download) { if (download) {
return <ScraperProcess data={data} download />; return <ScraperProcess data={data} download />;
} }

View File

@@ -10,9 +10,9 @@ export function BrandPill() {
flexDirection="row" flexDirection="row"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
paddingHorizontal="$2.5" paddingHorizontal="$3"
paddingVertical="$2" paddingVertical="$2.5"
gap={2} gap="$2.5"
opacity={0.8} opacity={0.8}
backgroundColor="$pillBackground" backgroundColor="$pillBackground"
borderRadius={24} borderRadius={24}
@@ -24,11 +24,10 @@ export function BrandPill() {
> >
<MovieWebSvg <MovieWebSvg
fillColor={theme.tabBarIconFocused.val} fillColor={theme.tabBarIconFocused.val}
width={12} width={20}
height={12} height={20}
/> />
<Text fontSize="$4" fontWeight="$bold" paddingRight={5} paddingLeft={3}> <Text fontSize="$6" fontWeight="$bold">
{/* padding might need adjusting */}
movie-web movie-web
</Text> </Text>
</View> </View>

View File

@@ -1,25 +1,17 @@
import type { Asset } from "expo-media-library";
import type { NativeSyntheticEvent } from "react-native"; import type { NativeSyntheticEvent } from "react-native";
import type { ContextMenuOnPressNativeEvent } from "react-native-context-menu-view"; import type { ContextMenuOnPressNativeEvent } from "react-native-context-menu-view";
import React from "react"; import React from "react";
import ContextMenu from "react-native-context-menu-view"; import ContextMenu from "react-native-context-menu-view";
import { TouchableOpacity } from "react-native-gesture-handler"; import { TouchableOpacity } from "react-native-gesture-handler";
import { Progress, Spinner, Text, View } from "tamagui"; import { Image, Text, View, XStack, YStack } from "tamagui";
import { useDownloadManager } from "~/hooks/DownloadManagerContext"; import type { Download } from "~/contexts/DownloadManagerContext";
import { useDownloadManager } from "~/contexts/DownloadManagerContext";
import { MWProgress } from "./ui/Progress";
export interface DownloadItemProps { export interface DownloadItemProps {
id: string; item: Download;
filename: string; onPress: (localPath?: string) => void;
progress: number;
speed: number;
fileSize: number;
downloaded: number;
isFinished: boolean;
statusText?: string;
asset?: Asset;
isHLS?: boolean;
onPress: (asset?: Asset) => void;
} }
enum ContextMenuActions { enum ContextMenuActions {
@@ -27,6 +19,15 @@ enum ContextMenuActions {
Remove = "Remove", Remove = "Remove",
} }
const statusToTextMap: Record<Download["status"], string> = {
downloading: "Downloading",
finished: "Finished",
error: "Error",
merging: "Merging",
cancelled: "Cancelled",
importing: "Importing",
};
const formatBytes = (bytes: number, decimals = 2) => { const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return "0 Bytes"; if (bytes === 0) return "0 Bytes";
const k = 1024; const k = 1024;
@@ -36,64 +37,28 @@ const formatBytes = (bytes: number, decimals = 2) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}; };
export const DownloadItem: React.FC<DownloadItemProps> = ({ export function DownloadItem(props: DownloadItemProps) {
id, const percentage = props.item.progress * 100;
filename, const formattedFileSize = formatBytes(props.item.fileSize);
progress, const formattedDownloaded = formatBytes(props.item.downloaded);
speed,
fileSize,
downloaded,
isFinished,
statusText,
asset,
isHLS,
onPress,
}) => {
const percentage = progress * 100;
const formattedFileSize = formatBytes(fileSize);
const formattedDownloaded = formatBytes(downloaded);
const { removeDownload, cancelDownload } = useDownloadManager(); const { removeDownload, cancelDownload } = useDownloadManager();
const contextMenuActions = [ const contextMenuActions = [
{ {
title: ContextMenuActions.Remove, title: ContextMenuActions.Remove,
}, },
...(!isFinished ? [{ title: ContextMenuActions.Cancel }] : []), ...(props.item.status !== "finished"
? [{ title: ContextMenuActions.Cancel }]
: []),
]; ];
const onContextMenuPress = ( const onContextMenuPress = (
e: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>, e: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>,
) => { ) => {
if (e.nativeEvent.name === ContextMenuActions.Cancel) { if (e.nativeEvent.name === ContextMenuActions.Cancel) {
void cancelDownload(id); void cancelDownload(props.item.id);
} else if (e.nativeEvent.name === ContextMenuActions.Remove) { } else if (e.nativeEvent.name === ContextMenuActions.Remove) {
removeDownload(id); removeDownload(props.item.id);
}
};
const renderStatus = () => {
if (statusText) {
return (
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Spinner size="small" color="$loadingIndicator" />
<Text fontSize={12} color="gray" style={{ marginLeft: 8 }}>
{statusText}
</Text>
</View>
);
} else if (isFinished) {
return (
<Text fontSize={12} color="gray">
Finished
</Text>
);
} else {
if (isHLS) return null;
return (
<Text fontSize={12} color="gray">
{speed.toFixed(2)} MB/s
</Text>
);
} }
}; };
@@ -103,41 +68,63 @@ export const DownloadItem: React.FC<DownloadItemProps> = ({
onPress={onContextMenuPress} onPress={onContextMenuPress}
previewBackgroundColor="transparent" previewBackgroundColor="transparent"
> >
<TouchableOpacity onPress={() => onPress(asset)} activeOpacity={0.7}> <TouchableOpacity
<View onPress={() => props.onPress(props.item.localPath)}
marginBottom={16} onLongPress={() => {
borderRadius={8} return;
borderColor="white" }}
padding={16} activeOpacity={0.7}
> >
<Text marginBottom={4} fontSize={16}> <XStack gap="$4" alignItems="center">
{filename}
</Text>
<Progress
value={percentage}
height={10}
backgroundColor="$progressBackground"
>
<Progress.Indicator
animation="bounce"
backgroundColor="$progressFilled"
/>
</Progress>
<View <View
marginTop={8} aspectRatio={9 / 14}
flexDirection="row" width={70}
alignItems="center" maxHeight={180}
justifyContent="space-between" overflow="hidden"
borderRadius="$2"
> >
<Text fontSize={12} color="gray"> <Image
{isHLS source={{
? `${percentage.toFixed()}% - ${downloaded} of ${fileSize} segments` uri: "https://image.tmdb.org/t/p/original//or06FN3Dka5tukK1e9sl16pB3iy.jpg",
: `${percentage.toFixed()}% - ${formattedDownloaded} of ${formattedFileSize}`} }}
</Text> width="100%"
{renderStatus()} height="100%"
/>
</View> </View>
</View> <YStack gap="$2">
<XStack gap="$5">
<Text
fontWeight="$bold"
ellipse
maxWidth={props.item.type === "hls" ? "70%" : "40%"}
flexGrow={1}
>
{props.item.media.title}
</Text>
{props.item.type !== "hls" && (
<Text fontSize={12} color="gray">
{props.item.speed.toFixed(2)} MB/s
</Text>
)}
</XStack>
<MWProgress value={percentage} height={10}>
<MWProgress.Indicator />
</MWProgress>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize={12} color="gray">
{props.item.type === "hls"
? `${percentage.toFixed()}% - ${props.item.downloaded} of ${props.item.fileSize} segments`
: `${percentage.toFixed()}% - ${formattedDownloaded} of ${formattedFileSize}`}
</Text>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text fontSize={12} color="gray">
{statusToTextMap[props.item.status]}
</Text>
</View>
</XStack>
</YStack>
</XStack>
</TouchableOpacity> </TouchableOpacity>
</ContextMenu> </ContextMenu>
); );
}; }

View File

@@ -18,21 +18,12 @@ export const ItemListSection = ({
}) => { }) => {
return ( return (
<View> <View>
<Text marginBottom={8} marginTop={16} fontWeight="500" fontSize={20}> <Text marginBottom={8} marginTop={16} fontWeight="bold" fontSize="$8">
{title} {title}
</Text> </Text>
<ScrollView <ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
horizontal={true}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 3 }}
>
{items.map((item, index) => ( {items.map((item, index) => (
<View <View key={index} width={itemWidth} paddingBottom={padding}>
key={index}
width={itemWidth}
paddingHorizontal={padding / 2}
paddingBottom={padding}
>
<Item data={item} /> <Item data={item} />
</View> </View>
))} ))}

View File

@@ -142,8 +142,8 @@ export default function Item({ data }: { data: ItemData }) {
{type === "tv" ? "Show" : "Movie"} {type === "tv" ? "Show" : "Movie"}
</Text> </Text>
<View <View
height={8} height={6}
width={8} width={6}
borderRadius={24} borderRadius={24}
backgroundColor="$ash100" backgroundColor="$ash100"
/> />

View File

@@ -21,7 +21,7 @@ export function Header() {
<Circle <Circle
backgroundColor="$pillBackground" backgroundColor="$pillBackground"
size="$2.5" size="$4.5"
pressStyle={{ pressStyle={{
opacity: 1, opacity: 1,
scale: 1.05, scale: 1.05,
@@ -33,11 +33,11 @@ export function Header() {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
} }
> >
<MaterialIcons name="discord" size={20} color="white" /> <MaterialIcons name="discord" size={32} color="white" />
</Circle> </Circle>
<Circle <Circle
backgroundColor="$pillBackground" backgroundColor="$pillBackground"
size="$2.5" size="$4.5"
pressStyle={{ pressStyle={{
opacity: 1, opacity: 1,
scale: 1.05, scale: 1.05,
@@ -49,7 +49,7 @@ export function Header() {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
} }
> >
<FontAwesome6 name="github" size={20} color="white" /> <FontAwesome6 name="github" size={32} color="white" />
</Circle> </Circle>
</View> </View>
); );

View File

@@ -1,4 +1,4 @@
import { View } from "tamagui"; import { ScrollView } from "tamagui";
import { LinearGradient } from "tamagui/linear-gradient"; import { LinearGradient } from "tamagui/linear-gradient";
import { Header } from "./Header"; import { Header } from "./Header";
@@ -12,7 +12,7 @@ export default function ScreenLayout({ children }: Props) {
<LinearGradient <LinearGradient
flex={1} flex={1}
paddingVertical="$4" paddingVertical="$4"
paddingHorizontal="$7" paddingHorizontal="$4"
colors={[ colors={[
"$shade900", "$shade900",
"$purple900", "$purple900",
@@ -26,9 +26,13 @@ export default function ScreenLayout({ children }: Props) {
flexGrow={1} flexGrow={1}
> >
<Header /> <Header />
<View paddingVertical="$4" flexGrow={1}> <ScrollView
marginTop="$4"
flexGrow={1}
showsVerticalScrollIndicator={false}
>
{children} {children}
</View> </ScrollView>
</LinearGradient> </LinearGradient>
); );
} }

View File

@@ -14,9 +14,10 @@ import { SettingsSelector } from "./SettingsSelector";
import { SourceSelector } from "./SourceSelector"; import { SourceSelector } from "./SourceSelector";
import { mapMillisecondsToTime } from "./utils"; import { mapMillisecondsToTime } from "./utils";
export const BottomControls = ({ isLocalAsset }: { isLocalAsset: boolean }) => { export const BottomControls = () => {
const status = usePlayerStore((state) => state.status); const status = usePlayerStore((state) => state.status);
const setIsIdle = usePlayerStore((state) => state.setIsIdle); const setIsIdle = usePlayerStore((state) => state.setIsIdle);
const isLocalFile = usePlayerStore((state) => state.isLocalFile);
const [showRemaining, setShowRemaining] = useState(false); const [showRemaining, setShowRemaining] = useState(false);
const toggleTimeDisplay = useCallback(() => { const toggleTimeDisplay = useCallback(() => {
@@ -76,7 +77,7 @@ export const BottomControls = ({ isLocalAsset }: { isLocalAsset: boolean }) => {
gap={4} gap={4}
paddingBottom={40} paddingBottom={40}
> >
{!isLocalAsset && ( {!isLocalFile && (
<> <>
<SeasonSelector /> <SeasonSelector />
<CaptionsSelector /> <CaptionsSelector />

View File

@@ -4,13 +4,7 @@ import { BottomControls } from "./BottomControls";
import { Header } from "./Header"; import { Header } from "./Header";
import { MiddleControls } from "./MiddleControls"; import { MiddleControls } from "./MiddleControls";
export const ControlsOverlay = ({ export const ControlsOverlay = ({ isLoading }: { isLoading: boolean }) => {
isLoading,
isLocalAsset,
}: {
isLoading: boolean;
isLocalAsset: boolean;
}) => {
return ( return (
<View <View
width="100%" width="100%"
@@ -20,7 +14,7 @@ export const ControlsOverlay = ({
> >
<Header /> <Header />
{!isLoading && <MiddleControls />} {!isLoading && <MiddleControls />}
<BottomControls isLocalAsset={isLocalAsset} /> <BottomControls />
</View> </View>
); );
}; };

View File

@@ -3,7 +3,7 @@ import { useTheme } from "tamagui";
import { findHighestQuality } from "@movie-web/provider-utils"; import { findHighestQuality } from "@movie-web/provider-utils";
import { useDownloadManager } from "~/hooks/DownloadManagerContext"; import { useDownloadManager } from "~/contexts/DownloadManagerContext";
import { convertMetaToScrapeMedia } from "~/lib/meta"; import { convertMetaToScrapeMedia } from "~/lib/meta";
import { usePlayerStore } from "~/stores/player/store"; import { usePlayerStore } from "~/stores/player/store";
import { MWButton } from "../ui/Button"; import { MWButton } from "../ui/Button";

View File

@@ -18,7 +18,7 @@ import {
import type { ItemData } from "../item/item"; import type { ItemData } from "../item/item";
import type { AudioTrack } from "./AudioTrackSelector"; import type { AudioTrack } from "./AudioTrackSelector";
import type { PlayerMeta } from "~/stores/player/slices/video"; import type { PlayerMeta } from "~/stores/player/slices/video";
import { useDownloadManager } from "~/hooks/DownloadManagerContext"; import { useDownloadManager } from "~/contexts/DownloadManagerContext";
import { useMeta } from "~/hooks/player/useMeta"; import { useMeta } from "~/hooks/player/useMeta";
import { useScrape } from "~/hooks/player/useSourceScrape"; import { useScrape } from "~/hooks/player/useSourceScrape";
import { convertMetaToScrapeMedia } from "~/lib/meta"; import { convertMetaToScrapeMedia } from "~/lib/meta";

View File

@@ -11,7 +11,6 @@ import Animated, {
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ResizeMode, Video } from "expo-av"; import { ResizeMode, Video } from "expo-av";
import * as Haptics from "expo-haptics"; import * as Haptics from "expo-haptics";
import * as MediaLibrary from "expo-media-library";
import * as NavigationBar from "expo-navigation-bar"; import * as NavigationBar from "expo-navigation-bar";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import * as StatusBar from "expo-status-bar"; import * as StatusBar from "expo-status-bar";
@@ -63,7 +62,6 @@ 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 videoSrc = usePlayerStore((state) => state.videoSrc) ?? undefined; const videoSrc = usePlayerStore((state) => state.videoSrc) ?? undefined;
const setVideoSrc = usePlayerStore((state) => state.setVideoSrc); const setVideoSrc = usePlayerStore((state) => state.setVideoSrc);
@@ -73,6 +71,7 @@ export const VideoPlayer = () => {
const toggleState = usePlayerStore((state) => state.toggleState); const toggleState = usePlayerStore((state) => state.toggleState);
const meta = usePlayerStore((state) => state.meta); const meta = usePlayerStore((state) => state.meta);
const setMeta = usePlayerStore((state) => state.setMeta); const setMeta = usePlayerStore((state) => state.setMeta);
const isLocalFile = usePlayerStore((state) => state.isLocalFile);
const { gestureControls, autoPlay } = usePlayerSettingsStore(); const { gestureControls, autoPlay } = usePlayerSettingsStore();
const { updateWatchHistory, removeFromWatchHistory, getWatchHistoryItem } = const { updateWatchHistory, removeFromWatchHistory, getWatchHistoryItem } =
@@ -151,15 +150,7 @@ export const VideoPlayer = () => {
useEffect(() => { useEffect(() => {
const initializePlayer = async () => { const initializePlayer = async () => {
if (asset) { if (videoSrc?.uri && isLocalFile) return;
const assetInfo = await MediaLibrary.getAssetInfoAsync(asset);
if (!assetInfo.localUri) return;
setVideoSrc({
uri: assetInfo.localUri,
});
setIsLoading(false);
return;
}
if (!stream) { if (!stream) {
await dismissFullscreenPlayer(); await dismissFullscreenPlayer();
@@ -194,7 +185,6 @@ export const VideoPlayer = () => {
setIsLoading(false); setIsLoading(false);
}; };
setIsLoading(true);
void initializePlayer(); void initializePlayer();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
@@ -217,7 +207,7 @@ export const VideoPlayer = () => {
void synchronizePlayback(); void synchronizePlayback();
}; };
}, [ }, [
asset, isLocalFile,
dismissFullscreenPlayer, dismissFullscreenPlayer,
hasStartedPlaying, hasStartedPlaying,
meta, meta,
@@ -228,6 +218,7 @@ export const VideoPlayer = () => {
synchronizePlayback, synchronizePlayback,
updateWatchHistory, updateWatchHistory,
videoRef?.props.positionMillis, videoRef?.props.positionMillis,
videoSrc?.uri,
]); ]);
const onVideoLoadStart = () => { const onVideoLoadStart = () => {
@@ -322,7 +313,7 @@ export const VideoPlayer = () => {
position="absolute" position="absolute"
/> />
)} )}
<ControlsOverlay isLoading={isLoading} isLocalAsset={!!asset} /> <ControlsOverlay isLoading={isLoading} />
</View> </View>
{showVolumeOverlay && <GestureOverlay value={volume} type="volume" />} {showVolumeOverlay && <GestureOverlay value={volume} type="volume" />}
{showBrightnessOverlay && ( {showBrightnessOverlay && (

View File

@@ -0,0 +1,14 @@
import { Progress, styled, withStaticProperties } from "tamagui";
const MWProgressFrame = styled(Progress, {
backgroundColor: "$progressBackground",
});
const MWProgressIndicator = styled(Progress.Indicator, {
backgroundColor: "$progressFilled",
animation: "bounce",
});
export const MWProgress = withStaticProperties(MWProgressFrame, {
Indicator: MWProgressIndicator,
});

View File

@@ -23,23 +23,31 @@ import {
useNetworkSettingsStore, useNetworkSettingsStore,
} from "~/stores/settings"; } from "~/stores/settings";
export interface DownloadItem { export interface Download {
id: string; id: string;
filename: string;
progress: number; progress: number;
speed: number; speed: number;
fileSize: number; fileSize: number;
downloaded: number; downloaded: number;
url: string; url: string;
type: "mp4" | "hls"; type: "mp4" | "hls";
isFinished: boolean; status:
statusText?: string; | "downloading"
asset?: Asset; | "finished"
isHLS?: boolean; | "error"
| "merging"
| "cancelled"
| "importing";
localPath?: string;
media: ScrapeMedia; media: ScrapeMedia;
downloadTask?: DownloadTask; downloadTask?: DownloadTask;
} }
export interface DownloadContent {
media: Pick<ScrapeMedia, "title" | "releaseYear" | "type" | "tmdbId">;
downloads: Download[];
}
// @ts-expect-error - types are not up to date // @ts-expect-error - types are not up to date
setConfig({ setConfig({
isLogsEnabled: false, isLogsEnabled: false,
@@ -47,7 +55,7 @@ setConfig({
}); });
interface DownloadManagerContextType { interface DownloadManagerContextType {
downloads: DownloadItem[]; downloads: Download[];
startDownload: ( startDownload: (
url: string, url: string,
type: "mp4" | "hls", type: "mp4" | "hls",
@@ -75,7 +83,7 @@ export const useDownloadManager = () => {
export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
children, children,
}) => { }) => {
const [downloads, setDownloads] = useState<DownloadItem[]>([]); const [downloads, setDownloads] = useState<Download[]>([]);
const toastController = useToastController(); const toastController = useToastController();
useEffect(() => { useEffect(() => {
@@ -172,17 +180,15 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
duration: 500, duration: 500,
}); });
const newDownload: DownloadItem = { const newDownload: Download = {
id: `download-${Date.now()}-${Math.random().toString(16).slice(2)}`, id: `download-${Date.now()}-${Math.random().toString(16).slice(2)}`,
filename: url.split("/").pop() ?? "unknown",
progress: 0, progress: 0,
speed: 0, speed: 0,
fileSize: 0, fileSize: 0,
downloaded: 0, downloaded: 0,
type, type,
url, url,
isFinished: false, status: "downloading",
isHLS: type === "hls",
media, media,
}; };
@@ -197,7 +203,7 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
} }
}; };
const updateDownloadItem = (id: string, updates: Partial<DownloadItem>) => { const updateDownloadItem = (id: string, updates: Partial<Download>) => {
setDownloads((currentDownloads) => setDownloads((currentDownloads) =>
currentDownloads.map((download) => currentDownloads.map((download) =>
download.id === id ? { ...download, ...updates } : download, download.id === id ? { ...download, ...updates } : download,
@@ -342,7 +348,7 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
return removeDownload(downloadId); return removeDownload(downloadId);
} }
updateDownloadItem(downloadId, { statusText: "Merging" }); updateDownloadItem(downloadId, { status: "merging" });
const uri = await VideoManager.mergeVideos( const uri = await VideoManager.mergeVideos(
localSegmentPaths, localSegmentPaths,
`${FileSystem.cacheDirectory}movie-web/output.mp4`, `${FileSystem.cacheDirectory}movie-web/output.mp4`,
@@ -394,15 +400,14 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
downloadId: string, downloadId: string,
): Promise<Asset | void> => { ): Promise<Asset | void> => {
try { try {
updateDownloadItem(downloadId, { statusText: "Importing" }); updateDownloadItem(downloadId, { status: "importing" });
const asset = await MediaLibrary.createAssetAsync(fileUri); const asset = await MediaLibrary.createAssetAsync(fileUri);
await FileSystem.deleteAsync(fileUri); await FileSystem.deleteAsync(fileUri);
updateDownloadItem(downloadId, { updateDownloadItem(downloadId, {
statusText: undefined, status: "finished",
asset, localPath: asset.uri,
isFinished: true,
}); });
console.log("File saved to media library and original deleted"); console.log("File saved to media library and original deleted");
toastController.show("Download finished", { toastController.show("Download finished", {

View File

@@ -1,5 +1,4 @@
import type { AVPlaybackSourceObject, AVPlaybackStatus, Video } from "expo-av"; import type { AVPlaybackSourceObject, AVPlaybackStatus, Video } from "expo-av";
import type { Asset } from "expo-media-library";
import type { MakeSlice } from "./types"; import type { MakeSlice } from "./types";
import { PlayerStatus } from "./interface"; import { PlayerStatus } from "./interface";
@@ -31,13 +30,13 @@ export interface VideoSlice {
videoSrc: AVPlaybackSourceObject | null; videoSrc: AVPlaybackSourceObject | null;
status: AVPlaybackStatus | null; status: AVPlaybackStatus | null;
meta: PlayerMeta | null; meta: PlayerMeta | null;
asset: Asset | null; isLocalFile: boolean;
setVideoRef(ref: Video | null): void; setVideoRef(ref: Video | null): void;
setVideoSrc(src: AVPlaybackSourceObject | null): void; setVideoSrc(src: AVPlaybackSourceObject | 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; setIsLocalFile(isLocalFile: boolean): void;
resetVideo(): void; resetVideo(): void;
} }
@@ -46,7 +45,7 @@ export const createVideoSlice: MakeSlice<VideoSlice> = (set) => ({
videoSrc: null, videoSrc: null,
status: null, status: null,
meta: null, meta: null,
asset: null, isLocalFile: false,
setVideoRef: (ref) => { setVideoRef: (ref) => {
set({ videoRef: ref }); set({ videoRef: ref });
@@ -67,13 +66,11 @@ export const createVideoSlice: MakeSlice<VideoSlice> = (set) => ({
s.meta = meta; s.meta = meta;
}); });
}, },
setAsset: (asset) => { setIsLocalFile: (isLocalFile) => {
set((s) => { set({ isLocalFile });
s.asset = asset;
});
}, },
resetVideo() { resetVideo() {
set({ videoRef: null, status: null, meta: null, asset: null }); set({ videoRef: null, status: null, meta: null, isLocalFile: false });
set((s) => { set((s) => {
s.interface.playerStatus = PlayerStatus.SCRAPING; s.interface.playerStatus = PlayerStatus.SCRAPING;
}); });

View File

@@ -7,7 +7,7 @@ import { createJSONStorage, persist } from "zustand/middleware";
import type { ScrapeMedia } from "@movie-web/provider-utils"; import type { ScrapeMedia } from "@movie-web/provider-utils";
import type { ItemData } from "~/components/item/item"; import type { ItemData } from "~/components/item/item";
import type { DownloadItem } from "~/hooks/DownloadManagerContext"; import type { Download } from "~/contexts/DownloadManagerContext";
import type { ThemeStoreOption } from "~/stores/theme"; import type { ThemeStoreOption } from "~/stores/theme";
const storage = new MMKV(); const storage = new MMKV();
@@ -77,8 +77,8 @@ export const usePlayerSettingsStore = create<
); );
interface DownloadHistoryStoreState { interface DownloadHistoryStoreState {
downloads: DownloadItem[]; downloads: Download[];
setDownloads: (downloads: DownloadItem[]) => void; setDownloads: (downloads: Download[]) => void;
} }
export const useDownloadHistoryStore = create< export const useDownloadHistoryStore = create<
@@ -88,7 +88,7 @@ export const useDownloadHistoryStore = create<
persist( persist(
(set) => ({ (set) => ({
downloads: [], downloads: [],
setDownloads: (downloads: DownloadItem[]) => set({ downloads }), setDownloads: (downloads: Download[]) => set({ downloads }),
}), }),
{ {
name: "download-history", name: "download-history",