From 559d8474bc37482523332ec3a0020c46120672fa Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Thu, 3 Oct 2024 18:26:59 +0200 Subject: [PATCH] wip --- app/(auth)/(tabs)/(home)/downloads.tsx | 24 ++- app/(auth)/(tabs)/(search)/index.tsx | 223 ++++++++++------------- app/(auth)/play.tsx | 6 +- bun.lockb | Bin 600657 -> 619713 bytes components/FullScreenVideoPlayer.tsx | 39 ++-- components/ItemContent.tsx | 2 +- components/downloads/ActiveDownloads.tsx | 30 ++- components/downloads/EpisodeCard.tsx | 40 +++- components/downloads/MovieCard.tsx | 47 +++-- components/downloads/SeriesCard.tsx | 22 ++- hooks/useImageStorage.ts | 89 +++++++++ package.json | 22 ++- providers/DownloadProvider.tsx | 26 ++- providers/PlaybackProvider.tsx | 7 +- utils/optimize-server.ts | 1 + 15 files changed, 373 insertions(+), 205 deletions(-) create mode 100644 hooks/useImageStorage.ts diff --git a/app/(auth)/(tabs)/(home)/downloads.tsx b/app/(auth)/(tabs)/(home)/downloads.tsx index af7cf7d5..30ba6352 100644 --- a/app/(auth)/(tabs)/(home)/downloads.tsx +++ b/app/(auth)/(tabs)/(home)/downloads.tsx @@ -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, }} > - - + + {settings?.downloadMethod === "remux" && ( Queue @@ -88,26 +87,31 @@ const downloads: React.FC = () => { + {movies.length > 0 && ( - + Movies {movies?.length} - {movies?.map((item: BaseItemDto) => ( - - + + + {movies?.map((item: BaseItemDto) => ( + + + + ))} - ))} + )} {groupedBySeries?.map((items: BaseItemDto[], index: number) => ( ))} {downloadedFiles?.length === 0 && ( - + No downloaded items )} diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 456dae9c..e3ef4f09 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -226,12 +226,13 @@ export default function search() { contentContainerStyle={{ paddingLeft: insets.left, paddingRight: insets.right, + paddingBottom: 16, }} style={{ marginBottom: TAB_HEIGHT, }} > - + {Platform.OS === "android" && ( m.Id!)} - renderItem={(data) => ( - ( - - - - {item.Name} - - - {item.ProductionYear} - - - )} - /> + renderItem={(item) => ( + + + + {item.Name} + + + {item.ProductionYear} + + )} /> m.Id!)} header="Series" - renderItem={(data) => ( - ( - - - - {item.Name} - - - {item.ProductionYear} - - - )} - /> + renderItem={(item) => ( + + + + {item.Name} + + + {item.ProductionYear} + + )} /> m.Id!)} header="Episodes" - renderItem={(data) => ( - ( - - - - - )} - /> + renderItem={(item) => ( + + + + )} /> m.Id!)} header="Collections" - renderItem={(data) => ( - ( - - - - {item.Name} - - - )} - /> + renderItem={(item) => ( + + + + {item.Name} + + )} /> m.Id!)} header="Actors" - renderItem={(data) => ( - ( - - - - - )} - /> + renderItem={(item) => ( + + + + )} /> m.Id!)} header="Artists" - renderItem={(data) => ( - ( - - - - - )} - /> + renderItem={(item) => ( + + + + )} /> m.Id!)} header="Albums" - renderItem={(data) => ( - ( - - - - - )} - /> + renderItem={(item) => ( + + + + )} /> m.Id!)} header="Songs" - renderItem={(data) => ( - ( - - - - - )} - /> + renderItem={(item) => ( + + + + )} /> {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 = ({ ids, renderItem, header }) => { return ( <> - {header} - {renderItem(data)} + {header} + + {data.map((item) => renderItem(item))} + ); }; diff --git a/app/(auth)/play.tsx b/app/(auth)/play.tsx index 0d3b3891..5ca25b2b 100644 --- a/app/(auth)/play.tsx +++ b/app/(auth)/play.tsx @@ -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 {} diff --git a/bun.lockb b/bun.lockb index 36ba3d5ebb0fc747999283f719148826fe76bd1c..c717c66730329b3973fd72263b9fded64be266e1 100755 GIT binary patch delta 40779 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 S+u{BlCRbMA_<{bXv;03&2TNoC diff --git a/components/FullScreenVideoPlayer.tsx b/components/FullScreenVideoPlayer.tsx index ef762299..d19a65eb 100644 --- a/components/FullScreenVideoPlayer.tsx +++ b/components/FullScreenVideoPlayer.tsx @@ -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 diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index 30d936cf..47f97e9a 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -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], diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index e8485627..2f68689f 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -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 ( router.push(`/(auth)/items/page?id=${process.item.Id}`)} @@ -114,15 +121,29 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { }} > )} - - - + + + {base64Image && ( + + + + )} + {process.item.Type} {process.item.Name} {process.item.ProductionYear} - + {process.progress === 0 ? ( ) : ( @@ -143,6 +164,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { cancelJobMutation.mutate(process.id)} + className="ml-auto" > {cancelJobMutation.isPending ? ( diff --git a/components/downloads/EpisodeCard.tsx b/components/downloads/EpisodeCard.tsx index 456563db..dc867516 100644 --- a/components/downloads/EpisodeCard.tsx +++ b/components/downloads/EpisodeCard.tsx @@ -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 = ({ 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 = ({ item }) => { - {item.Name} - Episode {item.IndexNumber} + {base64Image ? ( + + + + ) : ( + + + + )} + ); }; diff --git a/components/downloads/MovieCard.tsx b/components/downloads/MovieCard.tsx index 8be14bf8..54381c08 100644 --- a/components/downloads/MovieCard.tsx +++ b/components/downloads/MovieCard.tsx @@ -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 = ({ 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 = ({ item }) => { }, [showActionSheetWithOptions, handleDeleteFile]); return ( - - {item.Name} - - {item.ProductionYear} - - {runtimeTicksToMinutes(item.RunTimeTicks)} - - + + {base64Image ? ( + + + + ) : ( + + + + )} + ); }; diff --git a/components/downloads/SeriesCard.tsx b/components/downloads/SeriesCard.tsx index bd057a0b..5fe67611 100644 --- a/components/downloads/SeriesCard.tsx +++ b/components/downloads/SeriesCard.tsx @@ -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 ( - + {items[0].SeriesName} {items.length} - TV-Series + TV-Series {groupBySeason.map((seasonItems, seasonIndex) => ( - + {seasonItems[0].SeasonName} - {seasonItems.map((item, index) => ( - - + + + {seasonItems.sort(sortByIndex)?.map((item, index) => ( + + ))} - ))} + ))} diff --git a/hooks/useImageStorage.ts b/hooks/useImageStorage.ts new file mode 100644 index 00000000..ad271f07 --- /dev/null +++ b/hooks/useImageStorage.ts @@ -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((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; diff --git a/package.json b/package.json index f8c73f04..ea67341b 100644 --- a/package.json +++ b/package.json @@ -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" + ] + } + } } diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index 0ee942e8..1e342994 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -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([]); 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", diff --git a/providers/PlaybackProvider.tsx b/providers/PlaybackProvider.tsx index 00b4d284..f3569a03 100644 --- a/providers/PlaybackProvider.tsx +++ b/providers/PlaybackProvider.tsx @@ -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; diff --git a/utils/optimize-server.ts b/utils/optimize-server.ts index 443bdf59..b7bb10fe 100644 --- a/utils/optimize-server.ts +++ b/utils/optimize-server.ts @@ -25,6 +25,7 @@ export interface JobStatus { item: Partial; speed?: number; timestamp: Date; + base64Image?: string; } /**