mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 16:33:26 +00:00
add episode download section
This commit is contained in:
61
apps/expo/src/app/(downloads)/[tmdbId].tsx
Normal file
61
apps/expo/src/app/(downloads)/[tmdbId].tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||||
|
import { YStack } from "tamagui";
|
||||||
|
|
||||||
|
import { DownloadItem } from "~/components/DownloadItem";
|
||||||
|
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
|
import { PlayerStatus } from "~/stores/player/slices/interface";
|
||||||
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
|
import { useDownloadHistoryStore } from "~/stores/settings";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const { tmdbId } = useLocalSearchParams();
|
||||||
|
const allDownloads = useDownloadHistoryStore((state) => state.downloads);
|
||||||
|
const resetVideo = usePlayerStore((state) => state.resetVideo);
|
||||||
|
const setVideoSrc = usePlayerStore((state) => state.setVideoSrc);
|
||||||
|
const setIsLocalFile = usePlayerStore((state) => state.setIsLocalFile);
|
||||||
|
const setPlayerStatus = usePlayerStore((state) => state.setPlayerStatus);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const download = useMemo(() => {
|
||||||
|
return allDownloads.find((download) => download.media.tmdbId === tmdbId);
|
||||||
|
}, [allDownloads, tmdbId]);
|
||||||
|
|
||||||
|
const handlePress = (localPath?: string) => {
|
||||||
|
if (!localPath) return;
|
||||||
|
resetVideo();
|
||||||
|
setIsLocalFile(true);
|
||||||
|
setPlayerStatus(PlayerStatus.READY);
|
||||||
|
setVideoSrc({
|
||||||
|
uri: localPath,
|
||||||
|
});
|
||||||
|
router.push({
|
||||||
|
pathname: "/videoPlayer",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenLayout showHeader={false}>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: download?.media.title ?? "Downloads",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YStack gap="$3">
|
||||||
|
{download?.downloads.map((download) => {
|
||||||
|
return (
|
||||||
|
<DownloadItem
|
||||||
|
key={
|
||||||
|
download.media.type === "show"
|
||||||
|
? download.media.episode.tmdbId
|
||||||
|
: download.media.tmdbId
|
||||||
|
}
|
||||||
|
item={download}
|
||||||
|
onPress={() => handlePress(download.localPath)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</YStack>
|
||||||
|
</ScreenLayout>
|
||||||
|
);
|
||||||
|
}
|
14
apps/expo/src/app/(downloads)/_layout.tsx
Normal file
14
apps/expo/src/app/(downloads)/_layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Stack } from "expo-router";
|
||||||
|
|
||||||
|
import { BrandPill } from "~/components/BrandPill";
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
screenOptions={{
|
||||||
|
headerTransparent: true,
|
||||||
|
headerRight: BrandPill,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -7,7 +7,7 @@ import { ScrollView, useTheme, YStack } from "tamagui";
|
|||||||
|
|
||||||
import type { ScrapeMedia } from "@movie-web/provider-utils";
|
import type { ScrapeMedia } from "@movie-web/provider-utils";
|
||||||
|
|
||||||
import { DownloadItem } from "~/components/DownloadItem";
|
import { DownloadItem, ShowDownloadItem } 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/useDownloadManager";
|
import { useDownloadManager } from "~/hooks/useDownloadManager";
|
||||||
@@ -15,15 +15,69 @@ import { PlayerStatus } from "~/stores/player/slices/interface";
|
|||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
import { useDownloadHistoryStore } from "~/stores/settings";
|
import { useDownloadHistoryStore } from "~/stores/settings";
|
||||||
|
|
||||||
const DownloadsScreen: React.FC = () => {
|
const exampleMovieMedia: ScrapeMedia = {
|
||||||
|
type: "movie",
|
||||||
|
title: "Avengers: Endgame",
|
||||||
|
releaseYear: 2019,
|
||||||
|
imdbId: "tt4154796",
|
||||||
|
tmdbId: "299534",
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExampleShowMedia = (seasonNumber: number, episodeNumber: number) =>
|
||||||
|
({
|
||||||
|
type: "show",
|
||||||
|
title: "Loki",
|
||||||
|
releaseYear: 2021,
|
||||||
|
imdbId: "tt9140554",
|
||||||
|
tmdbId: "84958",
|
||||||
|
season: {
|
||||||
|
number: seasonNumber,
|
||||||
|
tmdbId: seasonNumber.toString(),
|
||||||
|
},
|
||||||
|
episode: {
|
||||||
|
number: episodeNumber,
|
||||||
|
tmdbId: episodeNumber.toString(),
|
||||||
|
},
|
||||||
|
}) as const;
|
||||||
|
|
||||||
|
const TestDownloadButton = (props: {
|
||||||
|
media: ScrapeMedia;
|
||||||
|
type: "hls" | "mp4";
|
||||||
|
url: string;
|
||||||
|
}) => {
|
||||||
const { startDownload } = useDownloadManager();
|
const { startDownload } = useDownloadManager();
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<MWButton
|
||||||
|
type="secondary"
|
||||||
|
backgroundColor="$sheetItemBackground"
|
||||||
|
icon={
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="download"
|
||||||
|
size={24}
|
||||||
|
color={theme.buttonSecondaryText.val}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onPress={async () => {
|
||||||
|
await startDownload(props.url, props.type, props.media).catch(
|
||||||
|
console.error,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
test download
|
||||||
|
{props.type === "hls" ? " (hls)" : "(mp4)"}{" "}
|
||||||
|
{props.media.type === "show" ? "show" : "movie"}
|
||||||
|
</MWButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DownloadsScreen: React.FC = () => {
|
||||||
const downloads = useDownloadHistoryStore((state) => state.downloads);
|
const downloads = useDownloadHistoryStore((state) => state.downloads);
|
||||||
const resetVideo = usePlayerStore((state) => state.resetVideo);
|
const resetVideo = usePlayerStore((state) => state.resetVideo);
|
||||||
const setVideoSrc = usePlayerStore((state) => state.setVideoSrc);
|
const setVideoSrc = usePlayerStore((state) => state.setVideoSrc);
|
||||||
const setIsLocalFile = usePlayerStore((state) => state.setIsLocalFile);
|
const setIsLocalFile = usePlayerStore((state) => state.setIsLocalFile);
|
||||||
const setPlayerStatus = usePlayerStore((state) => state.setPlayerStatus);
|
const setPlayerStatus = usePlayerStore((state) => state.setPlayerStatus);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
@@ -55,85 +109,54 @@ const DownloadsScreen: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const exampleShowMedia: ScrapeMedia = {
|
|
||||||
type: "show",
|
|
||||||
title: "Example Show Title",
|
|
||||||
releaseYear: 2022,
|
|
||||||
imdbId: "tt1234567",
|
|
||||||
tmdbId: "12345",
|
|
||||||
season: {
|
|
||||||
number: 1,
|
|
||||||
tmdbId: "54321",
|
|
||||||
},
|
|
||||||
episode: {
|
|
||||||
number: 3,
|
|
||||||
tmdbId: "98765",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenLayout>
|
<ScreenLayout>
|
||||||
<YStack gap={2} style={{ padding: 10 }}>
|
<YStack gap={2} style={{ padding: 10 }}>
|
||||||
<MWButton
|
<TestDownloadButton
|
||||||
type="secondary"
|
media={exampleMovieMedia}
|
||||||
backgroundColor="$sheetItemBackground"
|
type="mp4"
|
||||||
icon={
|
url="https://samplelib.com/lib/preview/mp4/sample-5s.mp4"
|
||||||
<MaterialCommunityIcons
|
/>
|
||||||
name="download"
|
<TestDownloadButton
|
||||||
size={24}
|
media={getExampleShowMedia(1, 1)}
|
||||||
color={theme.buttonSecondaryText.val}
|
type="mp4"
|
||||||
/>
|
url="https://samplelib.com/lib/preview/mp4/sample-5s.mp4"
|
||||||
}
|
/>
|
||||||
onPress={async () => {
|
<TestDownloadButton
|
||||||
await startDownload(
|
media={getExampleShowMedia(1, 2)}
|
||||||
"https://samplelib.com/lib/preview/mp4/sample-5s.mp4",
|
type="mp4"
|
||||||
"mp4",
|
url="https://samplelib.com/lib/preview/mp4/sample-5s.mp4"
|
||||||
exampleShowMedia,
|
/>
|
||||||
).catch(console.error);
|
<TestDownloadButton
|
||||||
}}
|
media={getExampleShowMedia(1, 1)}
|
||||||
>
|
type="hls"
|
||||||
test download (mp4)
|
url="http://sample.vodobox.com/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8"
|
||||||
</MWButton>
|
/>
|
||||||
<MWButton
|
|
||||||
type="secondary"
|
|
||||||
backgroundColor="$sheetItemBackground"
|
|
||||||
icon={
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="download"
|
|
||||||
size={24}
|
|
||||||
color={theme.buttonSecondaryText.val}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onPress={async () => {
|
|
||||||
await startDownload(
|
|
||||||
"http://sample.vodobox.com/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8",
|
|
||||||
"hls",
|
|
||||||
{
|
|
||||||
...exampleShowMedia,
|
|
||||||
tmdbId: "123456",
|
|
||||||
},
|
|
||||||
).catch(console.error);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
test download (hls)
|
|
||||||
</MWButton>
|
|
||||||
</YStack>
|
</YStack>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
gap: "$4",
|
gap: "$4",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* TODO: Differentiate movies/shows, shows in new page */}
|
{downloads.map((download) => {
|
||||||
{downloads
|
if (download.downloads.length === 0) return null;
|
||||||
.map((item) => item.downloads)
|
if (download.media.type === "movie") {
|
||||||
.flat()
|
return (
|
||||||
.map((item) => (
|
<DownloadItem
|
||||||
<DownloadItem
|
key={download.media.tmdbId}
|
||||||
key={item.id}
|
item={download.downloads[0]!}
|
||||||
item={item}
|
onPress={() => handlePress(download.downloads[0]!.localPath)}
|
||||||
onPress={() => handlePress(item.localPath)}
|
/>
|
||||||
/>
|
);
|
||||||
))}
|
} else {
|
||||||
|
return (
|
||||||
|
<ShowDownloadItem
|
||||||
|
key={download.media.tmdbId}
|
||||||
|
download={download}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</ScreenLayout>
|
</ScreenLayout>
|
||||||
);
|
);
|
||||||
|
@@ -3,10 +3,12 @@ import type { ContextMenuOnPressNativeEvent } from "react-native-context-menu-vi
|
|||||||
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 { useRouter } from "expo-router";
|
||||||
import { Image, Text, View, XStack, YStack } from "tamagui";
|
import { Image, Text, View, XStack, YStack } from "tamagui";
|
||||||
|
|
||||||
import type { Download } from "~/hooks/useDownloadManager";
|
import type { Download, DownloadContent } from "~/hooks/useDownloadManager";
|
||||||
import { useDownloadManager } from "~/hooks/useDownloadManager";
|
import { useDownloadManager } from "~/hooks/useDownloadManager";
|
||||||
|
import { mapSeasonAndEpisodeNumberToText } from "./player/utils";
|
||||||
import { MWProgress } from "./ui/Progress";
|
import { MWProgress } from "./ui/Progress";
|
||||||
import { FlashingText } from "./ui/Text";
|
import { FlashingText } from "./ui/Text";
|
||||||
|
|
||||||
@@ -101,6 +103,11 @@ export function DownloadItem(props: DownloadItemProps) {
|
|||||||
<YStack gap="$2">
|
<YStack gap="$2">
|
||||||
<XStack gap="$6" maxWidth="65%">
|
<XStack gap="$6" maxWidth="65%">
|
||||||
<Text fontWeight="$bold" ellipse flexGrow={1}>
|
<Text fontWeight="$bold" ellipse flexGrow={1}>
|
||||||
|
{props.item.media.type === "show" &&
|
||||||
|
mapSeasonAndEpisodeNumberToText(
|
||||||
|
props.item.media.season.number,
|
||||||
|
props.item.media.episode.number,
|
||||||
|
) + " "}
|
||||||
{props.item.media.title}
|
{props.item.media.title}
|
||||||
</Text>
|
</Text>
|
||||||
{props.item.type !== "hls" && (
|
{props.item.type !== "hls" && (
|
||||||
@@ -136,3 +143,54 @@ export function DownloadItem(props: DownloadItemProps) {
|
|||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ShowDownloadItem({ download }: { download: DownloadContent }) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() =>
|
||||||
|
router.push({
|
||||||
|
pathname: "/(downloads)/[tmdbId]",
|
||||||
|
params: { tmdbId: download.media.tmdbId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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">
|
||||||
|
<YStack gap="$1">
|
||||||
|
<Text fontWeight="$bold" ellipse flexGrow={1} fontSize="$5">
|
||||||
|
{download.media.title}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="$2">
|
||||||
|
{download.downloads.length} Episode
|
||||||
|
{download.downloads.length > 1 ? "s" : ""} |{" "}
|
||||||
|
{formatBytes(
|
||||||
|
download.downloads.reduce(
|
||||||
|
(acc, curr) => acc + curr.fileSize,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@@ -124,6 +124,7 @@ export default function Item({ data }: { data: ItemData }) {
|
|||||||
width="100%"
|
width="100%"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
borderRadius={24}
|
borderRadius={24}
|
||||||
|
height="$14"
|
||||||
>
|
>
|
||||||
<Image source={{ uri: posterUrl }} width="100%" height="100%" />
|
<Image source={{ uri: posterUrl }} width="100%" height="100%" />
|
||||||
</View>
|
</View>
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Linking } from "react-native";
|
import { Linking } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import * as Haptics from "expo-haptics";
|
import * as Haptics from "expo-haptics";
|
||||||
import { FontAwesome6, MaterialIcons } from "@expo/vector-icons";
|
import { FontAwesome6, MaterialIcons } from "@expo/vector-icons";
|
||||||
import { Circle, View } from "tamagui";
|
import { Circle, View } from "tamagui";
|
||||||
@@ -8,20 +7,13 @@ import { DISCORD_LINK, GITHUB_LINK } from "~/constants/core";
|
|||||||
import { BrandPill } from "../BrandPill";
|
import { BrandPill } from "../BrandPill";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View alignItems="center" gap="$3" flexDirection="row">
|
||||||
paddingTop={insets.top}
|
|
||||||
alignItems="center"
|
|
||||||
gap="$3"
|
|
||||||
flexDirection="row"
|
|
||||||
>
|
|
||||||
<BrandPill />
|
<BrandPill />
|
||||||
|
|
||||||
<Circle
|
<Circle
|
||||||
backgroundColor="$pillBackground"
|
backgroundColor="$pillBackground"
|
||||||
size="$4.5"
|
size="$3.5"
|
||||||
pressStyle={{
|
pressStyle={{
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: 1.05,
|
scale: 1.05,
|
||||||
@@ -33,11 +25,11 @@ export function Header() {
|
|||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="discord" size={32} color="white" />
|
<MaterialIcons name="discord" size={28} color="white" />
|
||||||
</Circle>
|
</Circle>
|
||||||
<Circle
|
<Circle
|
||||||
backgroundColor="$pillBackground"
|
backgroundColor="$pillBackground"
|
||||||
size="$4.5"
|
size="$3.5"
|
||||||
pressStyle={{
|
pressStyle={{
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: 1.05,
|
scale: 1.05,
|
||||||
@@ -49,7 +41,7 @@ export function Header() {
|
|||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FontAwesome6 name="github" size={32} color="white" />
|
<FontAwesome6 name="github" size={28} color="white" />
|
||||||
</Circle>
|
</Circle>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { ScrollView } from "tamagui";
|
import { ScrollView } from "tamagui";
|
||||||
import { LinearGradient } from "tamagui/linear-gradient";
|
import { LinearGradient } from "tamagui/linear-gradient";
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ interface Props {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onScrollBeginDrag?: () => void;
|
onScrollBeginDrag?: () => void;
|
||||||
onMomentumScrollEnd?: () => void;
|
onMomentumScrollEnd?: () => void;
|
||||||
|
showHeader?: boolean;
|
||||||
scrollEnabled?: boolean;
|
scrollEnabled?: boolean;
|
||||||
keyboardDismissMode?: "none" | "on-drag" | "interactive";
|
keyboardDismissMode?: "none" | "on-drag" | "interactive";
|
||||||
keyboardShouldPersistTaps?: "always" | "never" | "handled";
|
keyboardShouldPersistTaps?: "always" | "never" | "handled";
|
||||||
@@ -17,11 +19,14 @@ export default function ScreenLayout({
|
|||||||
children,
|
children,
|
||||||
onScrollBeginDrag,
|
onScrollBeginDrag,
|
||||||
onMomentumScrollEnd,
|
onMomentumScrollEnd,
|
||||||
|
showHeader = true,
|
||||||
scrollEnabled,
|
scrollEnabled,
|
||||||
keyboardDismissMode,
|
keyboardDismissMode,
|
||||||
keyboardShouldPersistTaps,
|
keyboardShouldPersistTaps,
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
flex={1}
|
flex={1}
|
||||||
@@ -38,8 +43,9 @@ export default function ScreenLayout({
|
|||||||
start={[0, 0]}
|
start={[0, 0]}
|
||||||
end={[1, 1]}
|
end={[1, 1]}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
|
paddingTop={showHeader ? insets.top : insets.top + 50}
|
||||||
>
|
>
|
||||||
<Header />
|
{showHeader && <Header />}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
onScrollBeginDrag={onScrollBeginDrag}
|
onScrollBeginDrag={onScrollBeginDrag}
|
||||||
onMomentumScrollEnd={onMomentumScrollEnd}
|
onMomentumScrollEnd={onMomentumScrollEnd}
|
||||||
|
@@ -4,10 +4,7 @@ import { usePlayerStore } from "~/stores/player/store";
|
|||||||
import { BrandPill } from "../BrandPill";
|
import { BrandPill } from "../BrandPill";
|
||||||
import { BackButton } from "./BackButton";
|
import { BackButton } from "./BackButton";
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
|
import { mapSeasonAndEpisodeNumberToText } from "./utils";
|
||||||
const mapSeasonAndEpisodeNumberToText = (season: number, episode: number) => {
|
|
||||||
return `S${season.toString().padStart(2, "0")}E${episode.toString().padStart(2, "0")}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const isIdle = usePlayerStore((state) => state.interface.isIdle);
|
const isIdle = usePlayerStore((state) => state.interface.isIdle);
|
||||||
|
@@ -16,3 +16,10 @@ export const mapMillisecondsToTime = (milliseconds: number): string => {
|
|||||||
|
|
||||||
return formattedTime;
|
return formattedTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mapSeasonAndEpisodeNumberToText = (
|
||||||
|
season: number,
|
||||||
|
episode: number,
|
||||||
|
) => {
|
||||||
|
return `S${season.toString().padStart(2, "0")}E${episode.toString().padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
@@ -404,19 +404,18 @@ export const useDownloadManager = () => {
|
|||||||
media,
|
media,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newDownloadContent = existingDownload
|
if (existingDownload) {
|
||||||
? {
|
existingDownload.downloads.push(newDownload);
|
||||||
...existingDownload,
|
setDownloads((prev) => {
|
||||||
downloads: [newDownload, ...existingDownload.downloads],
|
return prev.map((d) =>
|
||||||
}
|
d.media.tmdbId === media.tmdbId ? existingDownload : d,
|
||||||
: {
|
);
|
||||||
media,
|
});
|
||||||
downloads: [newDownload],
|
} else {
|
||||||
};
|
setDownloads((prev) => {
|
||||||
|
return [...prev, { media, downloads: [newDownload] }];
|
||||||
setDownloads((prev) => {
|
});
|
||||||
return [...prev, newDownloadContent];
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (type === "mp4") {
|
if (type === "mp4") {
|
||||||
const asset = await downloadMP4(url, newDownload, headers ?? {});
|
const asset = await downloadMP4(url, newDownload, headers ?? {});
|
||||||
|
Reference in New Issue
Block a user