This commit is contained in:
Fredrik Burmester
2024-10-03 18:26:59 +02:00
parent 83548da2c5
commit 559d8474bc
15 changed files with 373 additions and 205 deletions

View File

@@ -7,10 +7,9 @@ import { queueAtom } from "@/utils/atoms/queue";
import { useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import * as FileSystem from "expo-file-system";
import { router } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import { ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -45,8 +44,8 @@ const downloads: React.FC = () => {
paddingBottom: 100,
}}
>
<View className="px-4 py-4">
<View className="mb-4 flex flex-col space-y-4">
<View className="py-4">
<View className="mb-4 flex flex-col space-y-4 px-4">
{settings?.downloadMethod === "remux" && (
<View className="bg-neutral-900 p-4 rounded-2xl">
<Text className="text-lg font-bold">Queue</Text>
@@ -88,26 +87,31 @@ const downloads: React.FC = () => {
<ActiveDownloads />
</View>
{movies.length > 0 && (
<View className="mb-4">
<View className="flex flex-row items-center justify-between mb-2">
<View className="flex flex-row items-center justify-between mb-2 px-4">
<Text className="text-lg font-bold">Movies</Text>
<View className="bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center">
<Text className="text-xs font-bold">{movies?.length}</Text>
</View>
</View>
{movies?.map((item: BaseItemDto) => (
<View className="mb-2 last:mb-0" key={item.Id}>
<MovieCard item={item} />
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View className="px-4 flex flex-row">
{movies?.map((item: BaseItemDto) => (
<View className="mb-2 last:mb-0" key={item.Id}>
<MovieCard item={item} />
</View>
))}
</View>
))}
</ScrollView>
</View>
)}
{groupedBySeries?.map((items: BaseItemDto[], index: number) => (
<SeriesCard items={items} key={items[0].SeriesId} />
))}
{downloadedFiles?.length === 0 && (
<View className="flex ">
<View className="flex px-4">
<Text className="opacity-50">No downloaded items</Text>
</View>
)}

View File

@@ -226,12 +226,13 @@ export default function search() {
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 16,
}}
style={{
marginBottom: TAB_HEIGHT,
}}
>
<View className="flex flex-col pt-4">
<View className="flex flex-col pt-2">
{Platform.OS === "android" && (
<View className="mb-4 px-4">
<Input
@@ -254,165 +255,125 @@ export default function search() {
<SearchItemWrapper
header="Movies"
ids={movies?.map((m) => m.Id!)}
renderItem={(data) => (
<HorizontalScroll
data={data}
renderItem={(item) => (
<TouchableItemRouter
key={item.Id}
className="flex flex-col w-28"
item={item}
>
<MoviePoster item={item} key={item.Id} />
<Text numberOfLines={2} className="mt-2">
{item.Name}
</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
</TouchableItemRouter>
)}
/>
renderItem={(item) => (
<TouchableItemRouter
key={item.Id}
className="flex flex-col w-28 mr-2"
item={item}
>
<MoviePoster item={item} key={item.Id} />
<Text numberOfLines={2} className="mt-2">
{item.Name}
</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
ids={series?.map((m) => m.Id!)}
header="Series"
renderItem={(data) => (
<HorizontalScroll
data={data}
renderItem={(item) => (
<TouchableItemRouter
key={item.Id}
item={item}
className="flex flex-col w-28"
>
<SeriesPoster item={item} key={item.Id} />
<Text numberOfLines={2} className="mt-2">
{item.Name}
</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
</TouchableItemRouter>
)}
/>
renderItem={(item) => (
<TouchableItemRouter
key={item.Id}
item={item}
className="flex flex-col w-28"
>
<SeriesPoster item={item} key={item.Id} />
<Text numberOfLines={2} className="mt-2">
{item.Name}
</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
ids={episodes?.map((m) => m.Id!)}
header="Episodes"
renderItem={(data) => (
<HorizontalScroll
data={data}
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-44"
>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-44"
>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
ids={collections?.map((m) => m.Id!)}
header="Collections"
renderItem={(data) => (
<HorizontalScroll
data={data}
renderItem={(item) => (
<TouchableItemRouter
key={item.Id}
item={item}
className="flex flex-col w-28"
>
<MoviePoster item={item} key={item.Id} />
<Text numberOfLines={2} className="mt-2">
{item.Name}
</Text>
</TouchableItemRouter>
)}
/>
renderItem={(item) => (
<TouchableItemRouter
key={item.Id}
item={item}
className="flex flex-col w-28"
>
<MoviePoster item={item} key={item.Id} />
<Text numberOfLines={2} className="mt-2">
{item.Name}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
ids={actors?.map((m) => m.Id!)}
header="Actors"
renderItem={(data) => (
<HorizontalScroll
data={data}
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-28"
>
<MoviePoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-28"
>
<MoviePoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
ids={artists?.map((m) => m.Id!)}
header="Artists"
renderItem={(data) => (
<HorizontalScroll
data={data}
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-28"
>
<AlbumCover id={item.Id} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-28"
>
<AlbumCover id={item.Id} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
ids={albums?.map((m) => m.Id!)}
header="Albums"
renderItem={(data) => (
<HorizontalScroll
data={data}
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-28"
>
<AlbumCover id={item.Id} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-28"
>
<AlbumCover id={item.Id} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
ids={songs?.map((m) => m.Id!)}
header="Songs"
renderItem={(data) => (
<HorizontalScroll
data={data}
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-28"
>
<AlbumCover id={item.AlbumId} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
renderItem={(item) => (
<TouchableItemRouter
item={item}
key={item.Id}
className="flex flex-col w-28"
>
<AlbumCover id={item.AlbumId} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
{loading ? (
@@ -449,7 +410,7 @@ export default function search() {
type Props = {
ids?: string[] | null;
renderItem: (data: BaseItemDto[]) => React.ReactNode;
renderItem: (item: BaseItemDto) => React.ReactNode;
header?: string;
};
@@ -487,8 +448,14 @@ const SearchItemWrapper: React.FC<Props> = ({ ids, renderItem, header }) => {
return (
<>
<Text className="font-bold text-2xl px-4 my-2">{header}</Text>
{renderItem(data)}
<Text className="font-bold text-lg px-4 mb-2">{header}</Text>
<ScrollView
horizontal
className="px-4 mb-2"
showsHorizontalScrollIndicator={false}
>
{data.map((item) => renderItem(item))}
</ScrollView>
</>
);
};

View File

@@ -1,10 +1,10 @@
import { FullScreenVideoPlayer } from "@/components/FullScreenVideoPlayer";
import { useSettings } from "@/utils/atoms/settings";
import * as NavigationBar from "expo-navigation-bar";
import { StatusBar } from "expo-status-bar";
import { useEffect, useState } from "react";
import { Platform, View, ViewProps } from "react-native";
import * as ScreenOrientation from "expo-screen-orientation";
import { StatusBar } from "expo-status-bar";
import { useEffect } from "react";
import { Platform, View, ViewProps } from "react-native";
interface Props extends ViewProps {}

BIN
bun.lockb

Binary file not shown.

View File

@@ -87,14 +87,29 @@ export const FullScreenVideoPlayer: React.FC = () => {
});
useEffect(() => {
const subscription = Dimensions.addEventListener(
const dimensionsSubscription = Dimensions.addEventListener(
"change",
({ window, screen }) => {
setDimensions({ window, screen });
}
);
return () => subscription?.remove();
});
const orientationSubscription =
ScreenOrientation.addOrientationChangeListener((event) => {
setOrientation(
orientationToOrientationLock(event.orientationInfo.orientation)
);
});
ScreenOrientation.getOrientationAsync().then((orientation) => {
setOrientation(orientationToOrientationLock(orientation));
});
return () => {
dimensionsSubscription.remove();
orientationSubscription.remove();
};
}, []);
const from = useMemo(() => segments[2], [segments]);
@@ -165,24 +180,6 @@ export const FullScreenVideoPlayer: React.FC = () => {
return () => backHandler.remove();
}, [currentlyPlaying, stopPlayback, router]);
useEffect(() => {
const subscription = ScreenOrientation.addOrientationChangeListener(
(event) => {
setOrientation(
orientationToOrientationLock(event.orientationInfo.orientation)
);
}
);
ScreenOrientation.getOrientationAsync().then((orientation) => {
setOrientation(orientationToOrientationLock(orientation));
});
return () => {
subscription.remove();
};
}, []);
const isLandscape = useMemo(() => {
return orientation === ScreenOrientation.OrientationLock.LANDSCAPE_LEFT ||
orientation === ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT

View File

@@ -169,7 +169,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
if (item?.Type === "Episode") headerHeightRef.current = 400;
else if (item?.Type === "Movie") headerHeightRef.current = 500;
else headerHeightRef.current = 400;
}, [item]);
}, [item, orientation]);
const { data: sessionData } = useQuery({
queryKey: ["sessionData", item?.Id],

View File

@@ -19,6 +19,9 @@ import {
} from "react-native";
import { toast } from "sonner-native";
import { Button } from "../Button";
import { Image } from "expo-image";
import { useMemo } from "react";
import { storage } from "@/utils/mmkv";
interface Props extends ViewProps {}
@@ -95,6 +98,10 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
return formatTimeString(timeLeft, true);
};
const base64Image = useMemo(() => {
return storage.getString(process.item.Id!);
}, []);
return (
<TouchableOpacity
onPress={() => router.push(`/(auth)/items/page?id=${process.item.Id}`)}
@@ -114,15 +121,29 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
}}
></View>
)}
<View className="p-4 flex flex-col w-full">
<View className="flex flex-row items-center justify-between w-full">
<View className="shrink">
<View className="px-3 py-1.5 flex flex-col w-full">
<View className="flex flex-row items-center w-full">
{base64Image && (
<View className="w-14 aspect-[10/15] rounded-lg overflow-hidden mr-4">
<Image
source={{
uri: `data:image/jpeg;base64,${base64Image}`,
}}
style={{
width: "100%",
height: "100%",
resizeMode: "cover",
}}
/>
</View>
)}
<View className="shrink mb-1">
<Text className="text-xs opacity-50">{process.item.Type}</Text>
<Text className="font-semibold shrink">{process.item.Name}</Text>
<Text className="text-xs opacity-50">
{process.item.ProductionYear}
</Text>
<View className="flex flex-row items-center space-x-2 mt-2 text-purple-600">
<View className="flex flex-row items-center space-x-2 mt-1 text-purple-600">
{process.progress === 0 ? (
<ActivityIndicator size={"small"} color={"white"} />
) : (
@@ -143,6 +164,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
<TouchableOpacity
disabled={cancelJobMutation.isPending}
onPress={() => cancelJobMutation.mutate(process.id)}
className="ml-auto"
>
{cancelJobMutation.isPending ? (
<ActivityIndicator size="small" color="white" />

View File

@@ -1,7 +1,7 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import * as Haptics from "expo-haptics";
import React, { useCallback, useRef } from "react";
import { TouchableOpacity } from "react-native";
import React, { useCallback, useMemo, useRef } from "react";
import { TouchableOpacity, View } from "react-native";
import {
ActionSheetProvider,
useActionSheet,
@@ -10,6 +10,10 @@ import {
import { useFileOpener } from "@/hooks/useDownloadedFileOpener";
import { Text } from "../common/Text";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
import { Image } from "expo-image";
import { ItemCardText } from "../ItemCardText";
import { Ionicons } from "@expo/vector-icons";
interface EpisodeCardProps {
item: BaseItemDto;
@@ -25,6 +29,10 @@ export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item }) => {
const { openFile } = useFileOpener();
const { showActionSheetWithOptions } = useActionSheet();
const base64Image = useMemo(() => {
return storage.getString(item.Id!);
}, []);
const handleOpenFile = useCallback(() => {
openFile(item);
}, [item, openFile]);
@@ -68,10 +76,32 @@ export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item }) => {
<TouchableOpacity
onPress={handleOpenFile}
onLongPress={showActionSheet}
className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4"
className="flex flex-col"
>
<Text className="font-bold">{item.Name}</Text>
<Text className="text-xs opacity-50">Episode {item.IndexNumber}</Text>
{base64Image ? (
<View className="w-44 aspect-video rounded-lg overflow-hidden mr-2">
<Image
source={{
uri: `data:image/jpeg;base64,${base64Image}`,
}}
style={{
width: "100%",
height: "100%",
resizeMode: "cover",
}}
/>
</View>
) : (
<View className="w-44 aspect-video rounded-lg bg-neutral-900 mr-2 flex items-center justify-center">
<Ionicons
name="image-outline"
size={24}
color="gray"
className="self-center mt-16"
/>
</View>
)}
<ItemCardText item={item} />
</TouchableOpacity>
);
};

View File

@@ -1,6 +1,6 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import * as Haptics from "expo-haptics";
import React, { useCallback } from "react";
import React, { useCallback, useMemo } from "react";
import { TouchableOpacity, View } from "react-native";
import {
ActionSheetProvider,
@@ -12,6 +12,10 @@ import { Text } from "../common/Text";
import { useFileOpener } from "@/hooks/useDownloadedFileOpener";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
import { Image } from "expo-image";
import { Ionicons } from "@expo/vector-icons";
import { ItemCardText } from "../ItemCardText";
interface MovieCardProps {
item: BaseItemDto;
@@ -31,6 +35,10 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
openFile(item);
}, [item, openFile]);
const base64Image = useMemo(() => {
return storage.getString(item.Id!);
}, []);
/**
* Handles deleting the file with haptic feedback.
*/
@@ -67,18 +75,31 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
}, [showActionSheetWithOptions, handleDeleteFile]);
return (
<TouchableOpacity
onPress={handleOpenFile}
onLongPress={showActionSheet}
className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4"
>
<Text className="font-bold">{item.Name}</Text>
<View className="flex flex-col">
<Text className="text-xs opacity-50">{item.ProductionYear}</Text>
<Text className="text-xs opacity-50">
{runtimeTicksToMinutes(item.RunTimeTicks)}
</Text>
</View>
<TouchableOpacity onPress={handleOpenFile} onLongPress={showActionSheet}>
{base64Image ? (
<View className="w-28 aspect-[10/15] rounded-lg overflow-hidden mr-2 border border-neutral-900">
<Image
source={{
uri: `data:image/jpeg;base64,${base64Image}`,
}}
style={{
width: "100%",
height: "100%",
resizeMode: "cover",
}}
/>
</View>
) : (
<View className="w-28 aspect-[10/15] rounded-lg bg-neutral-900 mr-2 flex items-center justify-center">
<Ionicons
name="image-outline"
size={24}
color="gray"
className="self-center mt-16"
/>
</View>
)}
<ItemCardText item={item} />
</TouchableOpacity>
);
};

View File

@@ -1,5 +1,5 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { View } from "react-native";
import { ScrollView, View } from "react-native";
import { EpisodeCard } from "./EpisodeCard";
import { Text } from "../common/Text";
import { useMemo } from "react";
@@ -22,26 +22,32 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
);
}, [items]);
const sortByIndex = (a: BaseItemDto, b: BaseItemDto) => {
return a.IndexNumber! > b.IndexNumber! ? 1 : -1;
};
return (
<View>
<View className="flex flex-row items-center justify-between">
<View className="flex flex-row items-center justify-between px-4">
<Text className="text-lg font-bold shrink">{items[0].SeriesName}</Text>
<View className="bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center">
<Text className="text-xs font-bold">{items.length}</Text>
</View>
</View>
<Text className="opacity-50 mb-2">TV-Series</Text>
<Text className="opacity-50 mb-2 px-4">TV-Series</Text>
{groupBySeason.map((seasonItems, seasonIndex) => (
<View key={seasonIndex}>
<Text className="mb-2 font-semibold">
<Text className="mb-2 font-semibold px-4">
{seasonItems[0].SeasonName}
</Text>
{seasonItems.map((item, index) => (
<View className="mb-2" key={index}>
<EpisodeCard item={item} />
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View className="px-4 flex flex-row">
{seasonItems.sort(sortByIndex)?.map((item, index) => (
<EpisodeCard item={item} />
))}
</View>
))}
</ScrollView>
</View>
))}
</View>

89
hooks/useImageStorage.ts Normal file
View File

@@ -0,0 +1,89 @@
import { useState, useCallback } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as FileSystem from "expo-file-system";
import { storage } from "@/utils/mmkv";
const useImageStorage = () => {
const saveBase64Image = useCallback(async (base64: string, key: string) => {
try {
// Save the base64 string to AsyncStorage
storage.set(key, base64);
console.log("Image saved successfully");
} catch (error) {
console.error("Error saving image:", error);
throw error;
}
}, []);
const image2Base64 = useCallback(async (url?: string | null) => {
if (!url) return null;
let blob: Blob;
try {
// Fetch the data from the URL
const response = await fetch(url);
blob = await response.blob();
} catch (error) {
console.warn("Error fetching image:", error);
return null;
}
// Create a FileReader instance
const reader = new FileReader();
// Convert blob to base64
return new Promise<string>((resolve, reject) => {
reader.onloadend = () => {
if (typeof reader.result === "string") {
// Extract the base64 string (remove the data URL prefix)
const base64 = reader.result.split(",")[1];
resolve(base64);
} else {
reject(new Error("Failed to convert image to base64"));
}
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}, []);
const saveImage = useCallback(
async (key?: string | null, imageUrl?: string | null) => {
if (!imageUrl || !key) {
console.warn("Invalid image URL or key");
return;
}
try {
const base64Image = await image2Base64(imageUrl);
if (!base64Image || base64Image.length === 0) {
console.warn("Failed to convert image to base64");
return;
}
saveBase64Image(base64Image, key);
} catch (error) {
console.warn("Error saving image:", error);
}
},
[]
);
const loadImage = useCallback(async (key: string) => {
try {
// Retrieve the base64 string from AsyncStorage
const base64Image = storage.getString(key);
if (base64Image !== null) {
// Set the loaded image state
return `data:image/jpeg;base64,${base64Image}`;
}
return null;
} catch (error) {
console.error("Error loading image:", error);
throw error;
}
}, []);
return { saveImage, loadImage, saveBase64Image, image2Base64 };
};
export default useImageStorage;

View File

@@ -61,21 +61,21 @@
"nativewind": "^2.0.11",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.5",
"react-native": "~0.75.0",
"react-native-awesome-slider": "^2.5.3",
"react-native-circular-progress": "^1.4.0",
"react-native-compressor": "^1.8.25",
"react-native-gesture-handler": "~2.16.1",
"react-native-gesture-handler": "~2.18.1",
"react-native-get-random-values": "^1.11.0",
"react-native-google-cast": "^4.8.3",
"react-native-image-colors": "^2.4.0",
"react-native-ios-context-menu": "^2.5.1",
"react-native-ios-utilities": "^4.4.5",
"react-native-mmkv": "^3.0.2",
"react-native-reanimated": "~3.10.1",
"react-native-mmkv": "^2.12.2",
"react-native-reanimated": "~3.15.0",
"react-native-reanimated-carousel": "4.0.0-canary.15",
"react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1",
"react-native-screens": "~3.34.0",
"react-native-svg": "15.2.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.2",
@@ -98,5 +98,15 @@
"react-test-renderer": "18.2.0",
"typescript": "~5.3.3"
},
"private": true
"private": true,
"expo": {
"install": {
"exclude": [
"react-native@~0.74.0",
"react-native-reanimated@~3.10.0",
"react-native-gesture-handler@~2.16.1",
"react-native-screens@~3.31.1"
]
}
}
}

View File

@@ -38,6 +38,8 @@ import { AppState, AppStateStatus } from "react-native";
import { toast } from "sonner-native";
import { apiAtom } from "./JellyfinProvider";
import * as Notifications from "expo-notifications";
import { getItemImage } from "@/utils/getItemImage";
import useImageStorage from "@/hooks/useImageStorage";
function onAppStateChange(status: AppStateStatus) {
focusManager.setFocused(status === "active");
@@ -53,6 +55,9 @@ function useDownloadProvider() {
const router = useRouter();
const [api] = useAtom(apiAtom);
const { loadImage, saveImage, image2Base64, saveBase64Image } =
useImageStorage();
const [processes, setProcesses] = useState<JobStatus[]>([]);
const authHeader = useMemo(() => {
@@ -294,11 +299,30 @@ function useDownloadProvider() {
const startBackgroundDownload = useCallback(
async (url: string, item: BaseItemDto, fileExtension: string) => {
if (!api || !item.Id || !authHeader)
throw new Error("startBackgroundDownload ~ Missing required params");
try {
const deviceId = await getOrSetDeviceId();
const itemImage = getItemImage({
item,
api,
variant: "Primary",
quality: 90,
width: 500,
});
await saveImage(item.Id, itemImage?.uri);
const response = await axios.post(
settings?.optimizedVersionsServerUrl + "optimize-version",
{ url, fileExtension, deviceId, itemId: item.Id, item },
{
url,
fileExtension,
deviceId,
itemId: item.Id,
item,
},
{
headers: {
"Content-Type": "application/json",

View File

@@ -11,6 +11,7 @@ import React, {
import { useSettings } from "@/utils/atoms/settings";
import { getDeviceId } from "@/utils/device";
import { SubtitleTrack } from "@/utils/hls/parseM3U8ForSubtitles";
import { reportPlaybackProgress } from "@/utils/jellyfin/playstate/reportPlaybackProgress";
import { reportPlaybackStopped } from "@/utils/jellyfin/playstate/reportPlaybackStopped";
import { postCapabilities } from "@/utils/jellyfin/session/capabilities";
@@ -20,16 +21,12 @@ import {
} from "@jellyfin/sdk/lib/generated-client/models";
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
import * as Linking from "expo-linking";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
import { debounce } from "lodash";
import { Alert } from "react-native";
import { OnProgressData, type VideoRef } from "react-native-video";
import { apiAtom, userAtom } from "./JellyfinProvider";
import {
parseM3U8ForSubtitles,
SubtitleTrack,
} from "@/utils/hls/parseM3U8ForSubtitles";
import { useRouter } from "expo-router";
export type CurrentlyPlayingState = {
url: string;

View File

@@ -25,6 +25,7 @@ export interface JobStatus {
item: Partial<BaseItemDto>;
speed?: number;
timestamp: Date;
base64Image?: string;
}
/**