Files
native-app/apps/expo/src/components/DownloadItem.tsx
2024-04-06 17:30:22 +02:00

139 lines
4.4 KiB
TypeScript

import type { NativeSyntheticEvent } from "react-native";
import type { ContextMenuOnPressNativeEvent } from "react-native-context-menu-view";
import React from "react";
import ContextMenu from "react-native-context-menu-view";
import { TouchableOpacity } from "react-native-gesture-handler";
import { Image, Text, View, XStack, YStack } from "tamagui";
import type { Download } from "~/contexts/DownloadManagerContext";
import { useDownloadManager } from "~/contexts/DownloadManagerContext";
import { MWProgress } from "./ui/Progress";
import { FlashingText } from "./ui/Text";
export interface DownloadItemProps {
item: Download;
onPress: (localPath?: string) => void;
}
enum ContextMenuActions {
Cancel = "Cancel",
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) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
};
export function DownloadItem(props: DownloadItemProps) {
const percentage = props.item.progress * 100;
const formattedFileSize = formatBytes(props.item.fileSize);
const formattedDownloaded = formatBytes(props.item.downloaded);
const { removeDownload, cancelDownload } = useDownloadManager();
const contextMenuActions = [
{
title: ContextMenuActions.Remove,
},
...(props.item.status !== "finished"
? [{ title: ContextMenuActions.Cancel }]
: []),
];
const onContextMenuPress = (
e: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>,
) => {
if (e.nativeEvent.name === ContextMenuActions.Cancel) {
void cancelDownload(props.item.id);
} else if (e.nativeEvent.name === ContextMenuActions.Remove) {
removeDownload(props.item.id);
}
};
const isInProgress = !(
props.item.status === "finished" ||
props.item.status === "error" ||
props.item.status === "cancelled"
);
return (
<ContextMenu
actions={contextMenuActions}
onPress={onContextMenuPress}
previewBackgroundColor="transparent"
>
<TouchableOpacity
onPress={() => props.onPress(props.item.localPath)}
onLongPress={() => {
return;
}}
activeOpacity={0.7}
>
<XStack gap="$4" alignItems="center">
<View
aspectRatio={9 / 14}
width={70}
maxHeight={180}
overflow="hidden"
borderRadius="$2"
>
<Image
source={{
uri: "https://image.tmdb.org/t/p/original//or06FN3Dka5tukK1e9sl16pB3iy.jpg",
}}
width="100%"
height="100%"
/>
</View>
<YStack gap="$2">
<XStack gap="$6" maxWidth="65%">
<Text fontWeight="$bold" ellipse flexGrow={1}>
{props.item.media.title}
</Text>
{props.item.type !== "hls" && (
<Text fontSize="$2" color="gray">
{props.item.speed.toFixed(2)} MB/s
</Text>
)}
</XStack>
<MWProgress value={percentage} height={10} maxWidth="100%">
<MWProgress.Indicator />
</MWProgress>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$2" 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" }}>
<FlashingText
isInProgress={isInProgress}
style={{
fontSize: 12,
color: "gray",
}}
>
{statusToTextMap[props.item.status]}
</FlashingText>
</View>
</XStack>
</YStack>
</XStack>
</TouchableOpacity>
</ContextMenu>
);
}