diff --git a/.gitignore b/.gitignore index 01a43002..27dc1f71 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ npm-debug.* *.mobileprovision *.orig.* web-build/ +modules/vlc-player/android/build # macOS .DS_Store @@ -26,10 +27,11 @@ package-lock.json /ios /android +modules/player/android + pc-api-7079014811501811218-719-3b9f15aeccf8.json credentials.json *.apk *.ipa .continuerc.json -/modules/vlc-player/android \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml new file mode 100644 index 00000000..b81700b5 --- /dev/null +++ b/.idea/caches/deviceStreaming.xml @@ -0,0 +1,329 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..639900d1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..ba6d5c31 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/streamyfin.iml b/.idea/streamyfin.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/.idea/streamyfin.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 2fe7c24f..4571e3a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,10 @@ "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true - } + }, + "[swift]": { + "editor.defaultFormatter": "sswg.swift-lang" + }, + "java.configuration.updateBuildConfiguration": "interactive", + "java.compile.nullAnalysis.mode": "automatic" } diff --git a/app.json b/app.json index 00b060b9..67e5224f 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Streamyfin", "slug": "streamyfin", - "version": "0.18.0", + "version": "0.21.0", "orientation": "default", "icon": "./assets/images/icon.png", "scheme": "streamyfin", @@ -99,7 +99,14 @@ { "motionPermission": "Allow Streamyfin to access your device motion for landscape video watching." } - ] + ], + "expo-asset", + [ + "react-native-edge-to-edge", + { "android": { "parentTheme": "Material3" } } + ], + ["react-native-bottom-tabs"], + ["./plugins/withChangeNativeAndroidTextToWhite.js"] ], "experiments": { "typedRoutes": true diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index f51ecbf5..83a4472e 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -27,7 +27,6 @@ export default function IndexLayout() { onPress={() => { router.push("/(auth)/settings"); }} - className="p-2 " > diff --git a/app/(auth)/(tabs)/(home)/downloads.tsx b/app/(auth)/(tabs)/(home)/downloads.tsx index 30ba6352..02109e99 100644 --- a/app/(auth)/(tabs)/(home)/downloads.tsx +++ b/app/(auth)/(tabs)/(home)/downloads.tsx @@ -2,36 +2,46 @@ import { Text } from "@/components/common/Text"; import { ActiveDownloads } from "@/components/downloads/ActiveDownloads"; import { MovieCard } from "@/components/downloads/MovieCard"; import { SeriesCard } from "@/components/downloads/SeriesCard"; -import { useDownload } from "@/providers/DownloadProvider"; +import { DownloadedItem, useDownload } from "@/providers/DownloadProvider"; 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 { router } from "expo-router"; +import { useRouter } from "expo-router"; import { useAtom } from "jotai"; import { useMemo } from "react"; -import { ScrollView, TouchableOpacity, View } from "react-native"; +import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -const downloads: React.FC = () => { +export default function page() { const [queue, setQueue] = useAtom(queueAtom); const { removeProcess, downloadedFiles } = useDownload(); - + const router = useRouter(); const [settings] = useSettings(); - const movies = useMemo( - () => downloadedFiles?.filter((f) => f.Type === "Movie") || [], - [downloadedFiles] - ); + const movies = useMemo(() => { + try { + return downloadedFiles?.filter((f) => f.item.Type === "Movie") || []; + } catch { + migration_20241124(); + return []; + } + }, [downloadedFiles]); const groupedBySeries = useMemo(() => { - const episodes = downloadedFiles?.filter((f) => f.Type === "Episode"); - const series: { [key: string]: BaseItemDto[] } = {}; - episodes?.forEach((e) => { - if (!series[e.SeriesName!]) series[e.SeriesName!] = []; - series[e.SeriesName!].push(e); - }); - return Object.values(series); + try { + const episodes = downloadedFiles?.filter( + (f) => f.item.Type === "Episode" + ); + const series: { [key: string]: DownloadedItem[] } = {}; + episodes?.forEach((e) => { + if (!series[e.item.SeriesName!]) series[e.item.SeriesName!] = []; + series[e.item.SeriesName!].push(e); + }); + return Object.values(series); + } catch { + migration_20241124(); + return []; + } }, [downloadedFiles]); const insets = useSafeAreaInsets(); @@ -98,17 +108,20 @@ const downloads: React.FC = () => { - {movies?.map((item: BaseItemDto) => ( - - + {movies?.map((item) => ( + + ))} )} - {groupedBySeries?.map((items: BaseItemDto[], index: number) => ( - + {groupedBySeries?.map((items, index) => ( + i.item)} + key={items[0].item.SeriesId} + /> ))} {downloadedFiles?.length === 0 && ( @@ -118,6 +131,24 @@ const downloads: React.FC = () => { ); -}; +} -export default downloads; +function migration_20241124() { + const router = useRouter(); + const { deleteAllFiles } = useDownload(); + Alert.alert( + "New app version requires re-download", + "The new update reqires content to be downloaded again. Please remove all downloaded content and try again.", + [ + { + text: "Back", + onPress: () => router.back(), + }, + { + text: "Delete", + style: "destructive", + onPress: async () => await deleteAllFiles(), + }, + ] + ); +} diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index 364cf52c..c9d3bcb7 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -5,7 +5,7 @@ import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionLi import { Loader } from "@/components/Loader"; import { MediaListSection } from "@/components/medialists/MediaListSection"; import { Colors } from "@/constants/Colors"; -import { TAB_HEIGHT } from "@/constants/Values"; +import { useRevalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; @@ -53,7 +53,6 @@ type MediaListSection = { type Section = ScrollingCollectionListSection | MediaListSection; export default function index() { - const queryClient = useQueryClient(); const router = useRouter(); const api = useAtomValue(apiAtom); @@ -68,6 +67,8 @@ export default function index() { const { downloadedFiles } = useDownload(); const navigation = useNavigation(); + const insets = useSafeAreaInsets(); + useEffect(() => { const hasDownloads = downloadedFiles && downloadedFiles.length > 0; navigation.setOptions({ @@ -164,28 +165,13 @@ export default function index() { ); }, [userViews]); + const invalidateCache = useRevalidatePlaybackProgressCache(); + const refetch = useCallback(async () => { setLoading(true); - await queryClient.invalidateQueries({ - queryKey: ["home"], - refetchType: "all", - type: "all", - exact: false, - }); - await queryClient.invalidateQueries({ - queryKey: ["home"], - refetchType: "all", - type: "all", - exact: false, - }); - await queryClient.invalidateQueries({ - queryKey: ["item"], - refetchType: "all", - type: "all", - exact: false, - }); + await invalidateCache(); setLoading(false); - }, [queryClient]); + }, []); const createCollectionConfig = useCallback( ( @@ -241,7 +227,7 @@ export default function index() { const ss: Section[] = [ { title: "Continue Watching", - queryKey: ["home", "resumeItems", user.Id], + queryKey: ["home", "resumeItems"], queryFn: async () => ( await getItemsApi(api).getResumeItems({ @@ -255,7 +241,7 @@ export default function index() { }, { title: "Next Up", - queryKey: ["home", "nextUp-all", user?.Id], + queryKey: ["home", "nextUp-all"], queryFn: async () => ( await getTvShowsApi(api).getNextUp({ @@ -361,8 +347,6 @@ export default function index() { ); } - const insets = useSafeAreaInsets(); - if (e1 || e2) return ( @@ -393,9 +377,6 @@ export default function index() { paddingRight: insets.right, paddingBottom: 16, }} - style={{ - marginBottom: TAB_HEIGHT, - }} > diff --git a/app/(auth)/(tabs)/(home,libraries,search)/items/page.tsx b/app/(auth)/(tabs)/(home,libraries,search)/items/page.tsx index 071d9127..8caf07c6 100644 --- a/app/(auth)/(tabs)/(home,libraries,search)/items/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search)/items/page.tsx @@ -2,6 +2,10 @@ import { Text } from "@/components/common/Text"; import { ItemContent } from "@/components/ItemContent"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; +import { + getMediaInfoApi, + getUserLibraryApi, +} from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useLocalSearchParams } from "expo-router"; import { useAtom } from "jotai"; @@ -22,16 +26,18 @@ const Page: React.FC = () => { const { data: item, isError } = useQuery({ queryKey: ["item", id], queryFn: async () => { - const res = await getUserItemData({ - api, - userId: user?.Id, + if (!api || !user || !id) return; + const res = await getUserLibraryApi(api).getItem({ itemId: id, + userId: user?.Id, }); - return res; + return res.data; }, - enabled: !!id && !!api, - staleTime: 60 * 1000 * 5, // 5 minutes + staleTime: 0, + refetchOnMount: true, + refetchOnWindowFocus: true, + refetchOnReconnect: true, }); const opacity = useSharedValue(1); diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 91ae1842..9df17c69 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -1,4 +1,3 @@ -import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; import { Input } from "@/components/common/Input"; import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; @@ -8,7 +7,6 @@ import { Loader } from "@/components/Loader"; import AlbumCover from "@/components/posters/AlbumCover"; import MoviePoster from "@/components/posters/MoviePoster"; import SeriesPoster from "@/components/posters/SeriesPoster"; -import { TAB_HEIGHT } from "@/constants/Values"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; @@ -226,10 +224,6 @@ export default function search() { contentContainerStyle={{ paddingLeft: insets.left, paddingRight: insets.right, - paddingBottom: 16, - }} - style={{ - marginBottom: TAB_HEIGHT, }} > diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index b8772e68..e717d16d 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -1,87 +1,77 @@ -import { TabBarIcon } from "@/components/navigation/TabBarIcon"; +import React from "react"; +import { Platform } from "react-native"; + +import { withLayoutContext } from "expo-router"; + +import { + createNativeBottomTabNavigator, + NativeBottomTabNavigationEventMap, +} from "react-native-bottom-tabs/react-navigation"; + +const { Navigator } = createNativeBottomTabNavigator(); + +import { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs"; + import { Colors } from "@/constants/Colors"; -import { BlurView } from "expo-blur"; -import * as NavigationBar from "expo-navigation-bar"; -import { Tabs } from "expo-router"; -import React, { useEffect } from "react"; -import { Platform, StyleSheet } from "react-native"; +import type { + ParamListBase, + TabNavigationState, +} from "@react-navigation/native"; +import { SystemBars } from "react-native-edge-to-edge"; + +export const NativeTabs = withLayoutContext< + BottomTabNavigationOptions, + typeof Navigator, + TabNavigationState, + NativeBottomTabNavigationEventMap +>(Navigator); export default function TabLayout() { - useEffect(() => { - if (Platform.OS === "android") { - NavigationBar.setBackgroundColorAsync("#121212"); - NavigationBar.setBorderColorAsync("#121212"); - } - }, []); - return ( - - Platform.OS === "ios" ? ( - - ) : undefined, - }} - > - - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - + <> +