This commit is contained in:
Fredrik Burmester
2024-10-08 15:39:44 +02:00
parent a5b4f6cc78
commit ec0843d737
33 changed files with 895 additions and 527 deletions

View File

@@ -1,64 +1,98 @@
import { Api } from "@jellyfin/sdk";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import index from "@/app/(auth)/(tabs)/(home)";
import { apiAtom } from "@/providers/JellyfinProvider";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api";
import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
interface AdjacentEpisodesProps {
item?: BaseItemDto | null;
}
export const useAdjacentEpisodes = ({ item }: AdjacentEpisodesProps) => {
const [api] = useAtom(apiAtom);
export const useAdjacentItems = ({ item }: AdjacentEpisodesProps) => {
const api = useAtomValue(apiAtom);
const { data: previousItem } = useQuery({
queryKey: ["previousItem", item?.ParentId, item?.IndexNumber],
queryKey: ["previousItem", item?.Id, item?.ParentId, item?.IndexNumber],
queryFn: async (): Promise<BaseItemDto | null> => {
const parentId = item?.AlbumId || item?.ParentId;
const indexNumber = item?.IndexNumber;
console.log("Getting previous item for " + indexNumber);
if (
!api ||
!item?.ParentId ||
item?.IndexNumber === undefined ||
item?.IndexNumber === null ||
item?.IndexNumber - 2 < 0
!parentId ||
indexNumber === undefined ||
indexNumber === null ||
indexNumber - 1 < 1
) {
console.log("No previous item");
console.log("No previous item", {
itemIndex: indexNumber,
itemId: item?.Id,
parentId: parentId,
indexNumber: indexNumber,
});
return null;
}
const newIndexNumber = indexNumber - 2;
const res = await getItemsApi(api).getItems({
parentId: item.ParentId!,
startIndex: item.IndexNumber! - 2,
parentId: parentId!,
startIndex: newIndexNumber,
limit: 1,
sortBy: ["IndexNumber"],
includeItemTypes: ["Episode", "Audio"],
fields: ["MediaSources", "MediaStreams", "ParentId"],
});
if (res.data.Items?.[0]?.IndexNumber !== indexNumber - 1) {
throw new Error("Previous item is not correct");
}
return res.data.Items?.[0] || null;
},
enabled: item?.Type === "Episode",
enabled: item?.Type === "Episode" || item?.Type === "Audio",
staleTime: 0,
});
const { data: nextItem } = useQuery({
queryKey: ["nextItem", item?.ParentId, item?.IndexNumber],
queryKey: ["nextItem", item?.Id, item?.ParentId, item?.IndexNumber],
queryFn: async (): Promise<BaseItemDto | null> => {
const parentId = item?.AlbumId || item?.ParentId;
const indexNumber = item?.IndexNumber;
if (
!api ||
!item?.ParentId ||
item?.IndexNumber === undefined ||
item?.IndexNumber === null
!parentId ||
indexNumber === undefined ||
indexNumber === null
) {
console.log("No next item");
console.log("No next item", {
itemId: item?.Id,
parentId: parentId,
indexNumber: indexNumber,
});
return null;
}
const res = await getItemsApi(api).getItems({
parentId: item.ParentId!,
startIndex: item.IndexNumber!,
parentId: parentId!,
startIndex: indexNumber,
sortBy: ["IndexNumber"],
limit: 1,
includeItemTypes: ["Episode", "Audio"],
fields: ["MediaSources", "MediaStreams", "ParentId"],
});
if (res.data.Items?.[0]?.IndexNumber !== indexNumber + 1) {
throw new Error("Previous item is not correct");
}
return res.data.Items?.[0] || null;
},
enabled: item?.Type === "Episode",
enabled: item?.Type === "Episode" || item?.Type === "Audio",
staleTime: 0,
});
return { previousItem, nextItem };

View File

@@ -0,0 +1,17 @@
import * as NavigationBar from "expo-navigation-bar";
import { useEffect } from "react";
import { Platform } from "react-native";
export const useAndroidNavigationBar = () => {
useEffect(() => {
if (Platform.OS === "android") {
NavigationBar.setVisibilityAsync("hidden");
NavigationBar.setBehaviorAsync("overlay-swipe");
return () => {
NavigationBar.setVisibilityAsync("visible");
NavigationBar.setBehaviorAsync("inset-swipe");
};
}
}, []);
};

View File

@@ -61,6 +61,7 @@ export const useCreditSkipper = (
}, [creditTimestamps, currentTime]);
const skipCredit = useCallback(() => {
console.log("skipCredits");
if (!creditTimestamps || !videoRef.current) return;
try {
videoRef.current.seek(creditTimestamps.Credits.End);

View File

@@ -1,6 +1,7 @@
// hooks/useFileOpener.ts
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
import { writeToLog } from "@/utils/log";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import * as FileSystem from "expo-file-system";
import { useRouter } from "expo-router";
@@ -42,8 +43,8 @@ export const useFileOpener = () => {
router.push("/play-offline-video");
} catch (error) {
writeToLog("ERROR", "Error opening file", error);
console.error("Error opening file:", error);
// Handle the error appropriately, e.g., show an error message to the user
}
}, []);

View File

@@ -57,6 +57,7 @@ export const useIntroSkipper = (
}, [introTimestamps, currentTime]);
const skipIntro = useCallback(() => {
console.log("skipIntro");
if (!introTimestamps || !videoRef.current) return;
try {
videoRef.current.seek(introTimestamps.IntroEnd);

28
hooks/useOrientation.ts Normal file
View File

@@ -0,0 +1,28 @@
import orientationToOrientationLock from "@/utils/OrientationLockConverter";
import * as ScreenOrientation from "expo-screen-orientation";
import { useEffect, useState } from "react";
export const useOrientation = () => {
const [orientation, setOrientation] = useState(
ScreenOrientation.OrientationLock.UNKNOWN
);
useEffect(() => {
const orientationSubscription =
ScreenOrientation.addOrientationChangeListener((event) => {
setOrientation(
orientationToOrientationLock(event.orientationInfo.orientation)
);
});
ScreenOrientation.getOrientationAsync().then((orientation) => {
setOrientation(orientationToOrientationLock(orientation));
});
return () => {
orientationSubscription.remove();
};
}, []);
return { orientation };
};

View File

@@ -0,0 +1,25 @@
import { useSettings } from "@/utils/atoms/settings";
import * as ScreenOrientation from "expo-screen-orientation";
import { useEffect } from "react";
export const useOrientationSettings = () => {
const [settings] = useSettings();
useEffect(() => {
if (settings?.autoRotate) {
// Don't need to do anything
} else if (settings?.defaultVideoOrientation) {
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
}
return () => {
if (settings?.autoRotate) {
ScreenOrientation.unlockAsync();
} else {
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP
);
}
};
}, [settings]);
};

View File

@@ -1,5 +1,5 @@
import { useCallback } from "react";
import { useAtom } from "jotai";
import { useAtom, useAtomValue } from "jotai";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as FileSystem from "expo-file-system";
import { FFmpegKit, FFmpegKitConfig } from "ffmpeg-kit-react-native";
@@ -10,6 +10,9 @@ import { toast } from "sonner-native";
import { useDownload } from "@/providers/DownloadProvider";
import { useRouter } from "expo-router";
import { JobStatus } from "@/utils/optimize-server";
import useImageStorage from "./useImageStorage";
import { getItemImage } from "@/utils/getItemImage";
import { apiAtom } from "@/providers/JellyfinProvider";
/**
* Custom hook for remuxing HLS to MP4 using FFmpeg.
@@ -19,9 +22,12 @@ import { JobStatus } from "@/utils/optimize-server";
* @returns An object with remuxing-related functions
*/
export const useRemuxHlsToMp4 = (item: BaseItemDto) => {
const api = useAtomValue(apiAtom);
const queryClient = useQueryClient();
const { saveDownloadedItemInfo, setProcesses } = useDownload();
const router = useRouter();
const { loadImage, saveImage, image2Base64, saveBase64Image } =
useImageStorage();
if (!item.Id || !item.Name) {
writeToLog("ERROR", "useRemuxHlsToMp4 ~ missing arguments");
@@ -32,8 +38,19 @@ export const useRemuxHlsToMp4 = (item: BaseItemDto) => {
const startRemuxing = useCallback(
async (url: string) => {
if (!api) throw new Error("API is not defined");
if (!item.Id) throw new Error("Item must have an Id");
const itemImage = getItemImage({
item,
api,
variant: "Primary",
quality: 90,
width: 500,
});
await saveImage(item.Id, itemImage?.uri);
toast.success(`Download started for ${item.Name}`, {
action: {
label: "Go to download",

View File

@@ -26,14 +26,14 @@ interface TrickplayUrl {
url: string;
}
export const useTrickplay = (item: BaseItemDto) => {
export const useTrickplay = (item: BaseItemDto, enabled = true) => {
const [api] = useAtom(apiAtom);
const [trickPlayUrl, setTrickPlayUrl] = useState<TrickplayUrl | null>(null);
const lastCalculationTime = useRef(0);
const throttleDelay = 200; // 200ms throttle
const trickplayInfo = useMemo(() => {
if (!item.Id || !item.Trickplay) {
if (!enabled || !item.Id || !item.Trickplay) {
return null;
}
@@ -55,10 +55,14 @@ export const useTrickplay = (item: BaseItemDto) => {
data: trickplayData[firstResolution],
}
: null;
}, [item]);
}, [item, enabled]);
const calculateTrickplayUrl = useCallback(
(progress: number) => {
if (!enabled) {
return null;
}
const now = Date.now();
if (now - lastCalculationTime.current < throttleDelay) {
return null;
@@ -97,8 +101,12 @@ export const useTrickplay = (item: BaseItemDto) => {
setTrickPlayUrl(newTrickPlayUrl);
return newTrickPlayUrl;
},
[trickplayInfo, item, api]
[trickplayInfo, item, api, enabled]
);
return { trickPlayUrl, calculateTrickplayUrl, trickplayInfo };
return {
trickPlayUrl: enabled ? trickPlayUrl : null,
calculateTrickplayUrl: enabled ? calculateTrickplayUrl : () => null,
trickplayInfo: enabled ? trickplayInfo : null,
};
};