diff --git a/apps/expo/src/app/(downloads)/[tmdbId].tsx b/apps/expo/src/app/(downloads)/[tmdbId].tsx
new file mode 100644
index 0000000..794e2d2
--- /dev/null
+++ b/apps/expo/src/app/(downloads)/[tmdbId].tsx
@@ -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 (
+
+
+
+ {download?.downloads.map((download) => {
+ return (
+ handlePress(download.localPath)}
+ />
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/expo/src/app/(downloads)/_layout.tsx b/apps/expo/src/app/(downloads)/_layout.tsx
new file mode 100644
index 0000000..a6a9e05
--- /dev/null
+++ b/apps/expo/src/app/(downloads)/_layout.tsx
@@ -0,0 +1,14 @@
+import { Stack } from "expo-router";
+
+import { BrandPill } from "~/components/BrandPill";
+
+export default function Layout() {
+ return (
+
+ );
+}
diff --git a/apps/expo/src/app/(tabs)/downloads.tsx b/apps/expo/src/app/(tabs)/downloads.tsx
index a705d12..bfb2fab 100644
--- a/apps/expo/src/app/(tabs)/downloads.tsx
+++ b/apps/expo/src/app/(tabs)/downloads.tsx
@@ -7,7 +7,7 @@ import { ScrollView, useTheme, YStack } from "tamagui";
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 { MWButton } from "~/components/ui/Button";
import { useDownloadManager } from "~/hooks/useDownloadManager";
@@ -15,15 +15,69 @@ import { PlayerStatus } from "~/stores/player/slices/interface";
import { usePlayerStore } from "~/stores/player/store";
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 theme = useTheme();
+ return (
+
+ }
+ 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"}
+
+ );
+};
+
+const DownloadsScreen: React.FC = () => {
const downloads = 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 theme = useTheme();
useFocusEffect(
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 (
-
- }
- onPress={async () => {
- await startDownload(
- "https://samplelib.com/lib/preview/mp4/sample-5s.mp4",
- "mp4",
- exampleShowMedia,
- ).catch(console.error);
- }}
- >
- test download (mp4)
-
-
- }
- 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)
-
+
+
+
+
- {/* TODO: Differentiate movies/shows, shows in new page */}
- {downloads
- .map((item) => item.downloads)
- .flat()
- .map((item) => (
- handlePress(item.localPath)}
- />
- ))}
+ {downloads.map((download) => {
+ if (download.downloads.length === 0) return null;
+ if (download.media.type === "movie") {
+ return (
+ handlePress(download.downloads[0]!.localPath)}
+ />
+ );
+ } else {
+ return (
+
+ );
+ }
+ })}
);
diff --git a/apps/expo/src/components/DownloadItem.tsx b/apps/expo/src/components/DownloadItem.tsx
index af3b8f0..aab3df9 100644
--- a/apps/expo/src/components/DownloadItem.tsx
+++ b/apps/expo/src/components/DownloadItem.tsx
@@ -3,10 +3,12 @@ import type { ContextMenuOnPressNativeEvent } from "react-native-context-menu-vi
import React from "react";
import ContextMenu from "react-native-context-menu-view";
import { TouchableOpacity } from "react-native-gesture-handler";
+import { useRouter } from "expo-router";
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 { mapSeasonAndEpisodeNumberToText } from "./player/utils";
import { MWProgress } from "./ui/Progress";
import { FlashingText } from "./ui/Text";
@@ -101,6 +103,11 @@ export function DownloadItem(props: DownloadItemProps) {
+ {props.item.media.type === "show" &&
+ mapSeasonAndEpisodeNumberToText(
+ props.item.media.season.number,
+ props.item.media.episode.number,
+ ) + " "}
{props.item.media.title}
{props.item.type !== "hls" && (
@@ -136,3 +143,54 @@ export function DownloadItem(props: DownloadItemProps) {
);
}
+
+export function ShowDownloadItem({ download }: { download: DownloadContent }) {
+ const router = useRouter();
+
+ return (
+
+ router.push({
+ pathname: "/(downloads)/[tmdbId]",
+ params: { tmdbId: download.media.tmdbId },
+ })
+ }
+ activeOpacity={0.7}
+ >
+
+
+
+
+
+
+
+ {download.media.title}
+
+
+ {download.downloads.length} Episode
+ {download.downloads.length > 1 ? "s" : ""} |{" "}
+ {formatBytes(
+ download.downloads.reduce(
+ (acc, curr) => acc + curr.fileSize,
+ 0,
+ ),
+ )}
+
+
+
+
+
+ );
+}
diff --git a/apps/expo/src/components/item/item.tsx b/apps/expo/src/components/item/item.tsx
index 5478c6c..badd332 100644
--- a/apps/expo/src/components/item/item.tsx
+++ b/apps/expo/src/components/item/item.tsx
@@ -124,6 +124,7 @@ export default function Item({ data }: { data: ItemData }) {
width="100%"
overflow="hidden"
borderRadius={24}
+ height="$14"
>
diff --git a/apps/expo/src/components/layout/Header.tsx b/apps/expo/src/components/layout/Header.tsx
index 5f17a37..07b6efc 100644
--- a/apps/expo/src/components/layout/Header.tsx
+++ b/apps/expo/src/components/layout/Header.tsx
@@ -1,5 +1,4 @@
import { Linking } from "react-native";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
import * as Haptics from "expo-haptics";
import { FontAwesome6, MaterialIcons } from "@expo/vector-icons";
import { Circle, View } from "tamagui";
@@ -8,20 +7,13 @@ import { DISCORD_LINK, GITHUB_LINK } from "~/constants/core";
import { BrandPill } from "../BrandPill";
export function Header() {
- const insets = useSafeAreaInsets();
-
return (
-
+
-
+
-
+
);
diff --git a/apps/expo/src/components/layout/ScreenLayout.tsx b/apps/expo/src/components/layout/ScreenLayout.tsx
index d965878..45be60f 100644
--- a/apps/expo/src/components/layout/ScreenLayout.tsx
+++ b/apps/expo/src/components/layout/ScreenLayout.tsx
@@ -1,3 +1,4 @@
+import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ScrollView } from "tamagui";
import { LinearGradient } from "tamagui/linear-gradient";
@@ -7,6 +8,7 @@ interface Props {
children?: React.ReactNode;
onScrollBeginDrag?: () => void;
onMomentumScrollEnd?: () => void;
+ showHeader?: boolean;
scrollEnabled?: boolean;
keyboardDismissMode?: "none" | "on-drag" | "interactive";
keyboardShouldPersistTaps?: "always" | "never" | "handled";
@@ -17,11 +19,14 @@ export default function ScreenLayout({
children,
onScrollBeginDrag,
onMomentumScrollEnd,
+ showHeader = true,
scrollEnabled,
keyboardDismissMode,
keyboardShouldPersistTaps,
contentContainerStyle,
}: Props) {
+ const insets = useSafeAreaInsets();
+
return (
-
+ {showHeader && }
{
- return `S${season.toString().padStart(2, "0")}E${episode.toString().padStart(2, "0")}`;
-};
+import { mapSeasonAndEpisodeNumberToText } from "./utils";
export const Header = () => {
const isIdle = usePlayerStore((state) => state.interface.isIdle);
diff --git a/apps/expo/src/components/player/utils.ts b/apps/expo/src/components/player/utils.ts
index ab8e2ef..2a192d5 100644
--- a/apps/expo/src/components/player/utils.ts
+++ b/apps/expo/src/components/player/utils.ts
@@ -16,3 +16,10 @@ export const mapMillisecondsToTime = (milliseconds: number): string => {
return formattedTime;
};
+
+export const mapSeasonAndEpisodeNumberToText = (
+ season: number,
+ episode: number,
+) => {
+ return `S${season.toString().padStart(2, "0")}E${episode.toString().padStart(2, "0")}`;
+};
diff --git a/apps/expo/src/hooks/useDownloadManager.tsx b/apps/expo/src/hooks/useDownloadManager.tsx
index 9517d8c..b21e4bd 100644
--- a/apps/expo/src/hooks/useDownloadManager.tsx
+++ b/apps/expo/src/hooks/useDownloadManager.tsx
@@ -404,19 +404,18 @@ export const useDownloadManager = () => {
media,
};
- const newDownloadContent = existingDownload
- ? {
- ...existingDownload,
- downloads: [newDownload, ...existingDownload.downloads],
- }
- : {
- media,
- downloads: [newDownload],
- };
-
- setDownloads((prev) => {
- return [...prev, newDownloadContent];
- });
+ if (existingDownload) {
+ existingDownload.downloads.push(newDownload);
+ setDownloads((prev) => {
+ return prev.map((d) =>
+ d.media.tmdbId === media.tmdbId ? existingDownload : d,
+ );
+ });
+ } else {
+ setDownloads((prev) => {
+ return [...prev, { media, downloads: [newDownload] }];
+ });
+ }
if (type === "mp4") {
const asset = await downloadMP4(url, newDownload, headers ?? {});