feat: [StreamyfinPlugin] Jellyseerr, Search Engine, & Download settings

- Added DisabledSetting.tsx component
- Added DownloadMethod enum
- cleanup
This commit is contained in:
herrrta
2025-01-10 22:20:10 -05:00
parent 9dfcc01f17
commit 2c6823eb53
12 changed files with 193 additions and 179 deletions

View File

@@ -4,7 +4,7 @@ import { MovieCard } from "@/components/downloads/MovieCard";
import { SeriesCard } from "@/components/downloads/SeriesCard"; import { SeriesCard } from "@/components/downloads/SeriesCard";
import { DownloadedItem, useDownload } from "@/providers/DownloadProvider"; import { DownloadedItem, useDownload } from "@/providers/DownloadProvider";
import { queueAtom } from "@/utils/atoms/queue"; import { queueAtom } from "@/utils/atoms/queue";
import { useSettings } from "@/utils/atoms/settings"; import {DownloadMethod, useSettings} from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useNavigation, useRouter } from "expo-router"; import { useNavigation, useRouter } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
@@ -96,7 +96,7 @@ export default function page() {
> >
<View className="py-4"> <View className="py-4">
<View className="mb-4 flex flex-col space-y-4 px-4"> <View className="mb-4 flex flex-col space-y-4 px-4">
{settings?.downloadMethod === "remux" && ( {settings?.downloadMethod === DownloadMethod.Remux && (
<View className="bg-neutral-900 p-4 rounded-2xl"> <View className="bg-neutral-900 p-4 rounded-2xl">
<Text className="text-lg font-bold">Queue</Text> <Text className="text-lg font-bold">Queue</Text>
<Text className="text-xs opacity-70 text-red-600"> <Text className="text-xs opacity-70 text-red-600">

View File

@@ -15,7 +15,7 @@ import { useJellyfin } from "@/providers/JellyfinProvider";
import { clearLogs } from "@/utils/log"; import { clearLogs } from "@/utils/log";
import { useHaptic } from "@/hooks/useHaptic"; import { useHaptic } from "@/hooks/useHaptic";
import { useNavigation, useRouter } from "expo-router"; import { useNavigation, useRouter } from "expo-router";
import { useEffect } from "react"; import React, { useEffect } from "react";
import { ScrollView, TouchableOpacity, View } from "react-native"; import { ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";

View File

@@ -1,78 +1,16 @@
import { Text } from "@/components/common/Text";
import { JellyseerrSettings } from "@/components/settings/Jellyseerr"; import { JellyseerrSettings } from "@/components/settings/Jellyseerr";
import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm";
import { apiAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getOrSetDeviceId } from "@/utils/device"; import DisabledSetting from "@/components/settings/DisabledSetting";
import { getStatistics } from "@/utils/optimize-server";
import { useMutation } from "@tanstack/react-query";
import { useNavigation } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import { toast } from "sonner-native";
export default function page() { export default function page() {
const navigation = useNavigation(); const [settings, updateSettings, pluginSettings] = useSettings();
const [api] = useAtom(apiAtom);
const [settings, updateSettings] = useSettings();
const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] =
useState<string>(settings?.optimizedVersionsServerUrl || "");
const saveMutation = useMutation({
mutationFn: async (newVal: string) => {
if (newVal.length === 0 || !newVal.startsWith("http")) {
toast.error("Invalid URL");
return;
}
const updatedUrl = newVal.endsWith("/") ? newVal : newVal + "/";
updateSettings({
optimizedVersionsServerUrl: updatedUrl,
});
return await getStatistics({
url: settings?.optimizedVersionsServerUrl,
authHeader: api?.accessToken,
deviceId: getOrSetDeviceId(),
});
},
onSuccess: (data) => {
if (data) {
toast.success("Connected");
} else {
toast.error("Could not connect");
}
},
onError: () => {
toast.error("Could not connect");
},
});
const onSave = (newVal: string) => {
saveMutation.mutate(newVal);
};
// useEffect(() => {
// navigation.setOptions({
// title: "Optimized Server",
// headerRight: () =>
// saveMutation.isPending ? (
// <ActivityIndicator size={"small"} color={"white"} />
// ) : (
// <TouchableOpacity onPress={() => onSave(optimizedVersionsServerUrl)}>
// <Text className="text-blue-500">Save</Text>
// </TouchableOpacity>
// ),
// });
// }, [navigation, optimizedVersionsServerUrl, saveMutation.isPending]);
return ( return (
<View className="p-4"> <DisabledSetting
disabled={pluginSettings?.jellyseerrServerUrl?.locked === true}
className="p-4"
>
<JellyseerrSettings /> <JellyseerrSettings />
</View> </DisabledSetting>
); );
} }

View File

@@ -1,12 +1,10 @@
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup"; import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem"; import { ListItem } from "@/components/list/ListItem";
import { apiAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useNavigation } from "expo-router"; import { useNavigation } from "expo-router";
import { useAtom } from "jotai"; import React, {useEffect, useMemo, useState} from "react";
import { useEffect, useState } from "react";
import { import {
Linking, Linking,
Switch, Switch,
@@ -15,11 +13,12 @@ import {
View, View,
} from "react-native"; } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import DisabledSetting from "@/components/settings/DisabledSetting";
export default function page() { export default function page() {
const navigation = useNavigation(); const navigation = useNavigation();
const [settings, updateSettings] = useSettings(); const [settings, updateSettings, pluginSettings] = useSettings();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [value, setValue] = useState<string>(settings?.marlinServerUrl || ""); const [value, setValue] = useState<string>(settings?.marlinServerUrl || "");
@@ -35,69 +34,81 @@ export default function page() {
Linking.openURL("https://github.com/fredrikburmester/marlin-search"); Linking.openURL("https://github.com/fredrikburmester/marlin-search");
}; };
const disabled = useMemo(() => {
return pluginSettings?.searchEngine?.locked === true && pluginSettings?.marlinServerUrl?.locked === true
}, [pluginSettings]);
useEffect(() => { useEffect(() => {
navigation.setOptions({ if (!pluginSettings?.marlinServerUrl?.locked) {
headerRight: () => ( navigation.setOptions({
<TouchableOpacity onPress={() => onSave(value)}> headerRight: () => (
<Text className="text-blue-500">Save</Text> <TouchableOpacity onPress={() => onSave(value)}>
</TouchableOpacity> <Text className="text-blue-500">Save</Text>
), </TouchableOpacity>
}); ),
});
}
}, [navigation, value]); }, [navigation, value]);
if (!settings) return null; if (!settings) return null;
return ( return (
<View className="px-4"> <DisabledSetting
disabled={disabled}
className="px-4"
>
<ListGroup> <ListGroup>
<ListItem <DisabledSetting
title={"Enable Marlin Search"} disabled={pluginSettings?.searchEngine?.locked === true}
onPress={() => { showText={!pluginSettings?.marlinServerUrl?.locked}
updateSettings({ searchEngine: "Jellyfin" });
queryClient.invalidateQueries({ queryKey: ["search"] });
}}
> >
<Switch <ListItem
value={settings.searchEngine === "Marlin"} title={"Enable Marlin Search"}
onValueChange={(value) => { onPress={() => {
updateSettings({ searchEngine: value ? "Marlin" : "Jellyfin" }); updateSettings({ searchEngine: "Jellyfin" });
queryClient.invalidateQueries({ queryKey: ["search"] }); queryClient.invalidateQueries({ queryKey: ["search"] });
}} }}
/> >
</ListItem> <Switch
value={settings.searchEngine === "Marlin"}
onValueChange={(value) => {
updateSettings({ searchEngine: value ? "Marlin" : "Jellyfin" });
queryClient.invalidateQueries({ queryKey: ["search"] });
}}
/>
</ListItem>
</DisabledSetting>
</ListGroup> </ListGroup>
<View <DisabledSetting
className={`mt-2 ${ disabled={pluginSettings?.marlinServerUrl?.locked === true}
settings.searchEngine === "Marlin" ? "" : "opacity-50" showText={!pluginSettings?.searchEngine?.locked}
}`} className="mt-2 flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4"
> >
<View className="flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4"> <View
<View className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`}
className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`} >
> <Text className="mr-4">URL</Text>
<Text className="mr-4">URL</Text> <TextInput
<TextInput editable={settings.searchEngine === "Marlin"}
editable={settings.searchEngine === "Marlin"} className="text-white"
className="text-white" placeholder="http(s)://domain.org:port"
placeholder="http(s)://domain.org:port" value={value}
value={value} keyboardType="url"
keyboardType="url" returnKeyType="done"
returnKeyType="done" autoCapitalize="none"
autoCapitalize="none" textContentType="URL"
textContentType="URL" onChangeText={(text) => setValue(text)}
onChangeText={(text) => setValue(text)} />
/>
</View>
</View> </View>
<Text className="px-4 text-xs text-neutral-500 mt-1"> </DisabledSetting>
Enter the URL for the Marlin server. The URL should include http or <Text className="px-4 text-xs text-neutral-500 mt-1">
https and optionally the port.{" "} Enter the URL for the Marlin server. The URL should include http or
<Text className="text-blue-500" onPress={handleOpenLink}> https and optionally the port.{" "}
Read more about Marlin. <Text className="text-blue-500" onPress={handleOpenLink}>
</Text> Read more about Marlin.
</Text> </Text>
</View> </Text>
</View> </DisabledSetting>
); );
} }

View File

@@ -10,12 +10,13 @@ import { useAtom } from "jotai";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import DisabledSetting from "@/components/settings/DisabledSetting";
export default function page() { export default function page() {
const navigation = useNavigation(); const navigation = useNavigation();
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [settings, updateSettings] = useSettings(); const [settings, updateSettings, pluginSettings] = useSettings();
const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] = const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] =
useState<string>(settings?.optimizedVersionsServerUrl || ""); useState<string>(settings?.optimizedVersionsServerUrl || "");
@@ -56,25 +57,30 @@ export default function page() {
}; };
useEffect(() => { useEffect(() => {
navigation.setOptions({ if (!pluginSettings?.optimizedVersionsServerUrl?.locked) {
title: "Optimized Server", navigation.setOptions({
headerRight: () => title: "Optimized Server",
saveMutation.isPending ? ( headerRight: () =>
<ActivityIndicator size={"small"} color={"white"} /> saveMutation.isPending ? (
) : ( <ActivityIndicator size={"small"} color={"white"} />
<TouchableOpacity onPress={() => onSave(optimizedVersionsServerUrl)}> ) : (
<Text className="text-blue-500">Save</Text> <TouchableOpacity onPress={() => onSave(optimizedVersionsServerUrl)}>
</TouchableOpacity> <Text className="text-blue-500">Save</Text>
), </TouchableOpacity>
}); ),
});
}
}, [navigation, optimizedVersionsServerUrl, saveMutation.isPending]); }, [navigation, optimizedVersionsServerUrl, saveMutation.isPending]);
return ( return (
<View className="p-4"> <DisabledSetting
disabled={pluginSettings?.optimizedVersionsServerUrl?.locked === true}
className="p-4"
>
<OptimizedServerForm <OptimizedServerForm
value={optimizedVersionsServerUrl} value={optimizedVersionsServerUrl}
onChangeValue={setOptimizedVersionsServerUrl} onChangeValue={setOptimizedVersionsServerUrl}
/> />
</View> </DisabledSetting>
); );
} }

View File

@@ -2,7 +2,7 @@ import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueActions, queueAtom } from "@/utils/atoms/queue"; import { queueActions, queueAtom } from "@/utils/atoms/queue";
import { useSettings } from "@/utils/atoms/settings"; import {DownloadMethod, useSettings} from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server"; import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server";
@@ -74,7 +74,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
[user] [user]
); );
const usingOptimizedServer = useMemo( const usingOptimizedServer = useMemo(
() => settings?.downloadMethod === "optimized", () => settings?.downloadMethod === DownloadMethod.Optimized,
[settings] [settings]
); );

View File

@@ -1,7 +1,6 @@
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom } from "@/providers/JellyfinProvider"; import {DownloadMethod, useSettings} from "@/utils/atoms/settings";
import { useSettings } from "@/utils/atoms/settings";
import { JobStatus } from "@/utils/optimize-server"; import { JobStatus } from "@/utils/optimize-server";
import { formatTimeString } from "@/utils/time"; import { formatTimeString } from "@/utils/time";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
@@ -9,7 +8,6 @@ import { checkForExistingDownloads } from "@kesha-antonov/react-native-backgroun
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { FFmpegKit } from "ffmpeg-kit-react-native"; import { FFmpegKit } from "ffmpeg-kit-react-native";
import { useAtom } from "jotai";
import { import {
ActivityIndicator, ActivityIndicator,
TouchableOpacity, TouchableOpacity,
@@ -62,7 +60,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
mutationFn: async (id: string) => { mutationFn: async (id: string) => {
if (!process) throw new Error("No active download"); if (!process) throw new Error("No active download");
if (settings?.downloadMethod === "optimized") { if (settings?.downloadMethod === DownloadMethod.Optimized) {
try { try {
const tasks = await checkForExistingDownloads(); const tasks = await checkForExistingDownloads();
for (const task of tasks) { for (const task of tasks) {

View File

@@ -0,0 +1,26 @@
import {View, ViewProps} from "react-native";
import {Text} from "@/components/common/Text";
const DisabledSetting: React.FC<{disabled: boolean, showText?: boolean, text?: string} & ViewProps> = ({
disabled = false,
showText = true,
text,
children,
...props
}) => (
<View
pointerEvents={disabled ? "none" : "auto"}
style={{
opacity: disabled ? 0.5 : 1,
}}
>
<View {...props}>
{disabled && showText &&
<Text className="text-center text-red-700 my-4">{text ?? "Currently disabled by admin."}</Text>
}
{children}
</View>
</View>
)
export default DisabledSetting;

View File

@@ -1,33 +1,47 @@
import { Stepper } from "@/components/inputs/Stepper"; import {Stepper} from "@/components/inputs/Stepper";
import { useDownload } from "@/providers/DownloadProvider"; import {useDownload} from "@/providers/DownloadProvider";
import { Settings, useSettings } from "@/utils/atoms/settings"; import {DownloadMethod, Settings, useSettings} from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons"; import {Ionicons} from "@expo/vector-icons";
import { useQueryClient } from "@tanstack/react-query"; import {useQueryClient} from "@tanstack/react-query";
import { useRouter } from "expo-router"; import {useRouter} from "expo-router";
import React from "react"; import React, {useMemo} from "react";
import { Switch, TouchableOpacity, View } from "react-native"; import {Switch, TouchableOpacity} from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "zeego/dropdown-menu";
import { Text } from "../common/Text"; import {Text} from "../common/Text";
import { ListGroup } from "../list/ListGroup"; import {ListGroup} from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import {ListItem} from "../list/ListItem";
import DisabledSetting from "@/components/settings/DisabledSetting";
export const DownloadSettings: React.FC = ({ ...props }) => { export const DownloadSettings: React.FC = ({ ...props }) => {
const [settings, updateSettings] = useSettings(); const [settings, updateSettings, pluginSettings] = useSettings();
const { setProcesses } = useDownload(); const { setProcesses } = useDownload();
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const disabled = useMemo(() => (
pluginSettings?.downloadMethod?.locked === true &&
pluginSettings?.remuxConcurrentLimit?.locked === true &&
pluginSettings?.autoDownload.locked === true
), [pluginSettings])
if (!settings) return null; if (!settings) return null;
return ( return (
<View {...props} className="mb-4"> <DisabledSetting
disabled={disabled}
{...props}
className="mb-4"
>
<ListGroup title="Downloads"> <ListGroup title="Downloads">
<ListItem title="Download method"> <ListItem
title="Download method"
disabled={pluginSettings?.downloadMethod?.locked}
>
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3"> <TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
<Text className="mr-1 text-[#8E8D91]"> <Text className="mr-1 text-[#8E8D91]">
{settings.downloadMethod === "remux" {settings.downloadMethod === DownloadMethod.Remux
? "Default" ? "Default"
: "Optimized"} : "Optimized"}
</Text> </Text>
@@ -51,7 +65,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
<DropdownMenu.Item <DropdownMenu.Item
key="1" key="1"
onSelect={() => { onSelect={() => {
updateSettings({ downloadMethod: "remux" }); updateSettings({ downloadMethod: DownloadMethod.Remux });
setProcesses([]); setProcesses([]);
}} }}
> >
@@ -60,7 +74,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
<DropdownMenu.Item <DropdownMenu.Item
key="2" key="2"
onSelect={() => { onSelect={() => {
updateSettings({ downloadMethod: "optimized" }); updateSettings({ downloadMethod: DownloadMethod.Optimized });
setProcesses([]); setProcesses([]);
queryClient.invalidateQueries({ queryKey: ["search"] }); queryClient.invalidateQueries({ queryKey: ["search"] });
}} }}
@@ -73,7 +87,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
<ListItem <ListItem
title="Remux max download" title="Remux max download"
disabled={settings.downloadMethod !== "remux"} disabled={pluginSettings?.remuxConcurrentLimit?.locked || settings.downloadMethod !== DownloadMethod.Remux}
> >
<Stepper <Stepper
value={settings.remuxConcurrentLimit} value={settings.remuxConcurrentLimit}
@@ -90,22 +104,22 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
<ListItem <ListItem
title="Auto download" title="Auto download"
disabled={settings.downloadMethod !== "optimized"} disabled={pluginSettings?.autoDownload?.locked || settings.downloadMethod !== DownloadMethod.Optimized}
> >
<Switch <Switch
disabled={settings.downloadMethod !== "optimized"} disabled={pluginSettings?.autoDownload?.locked || settings.downloadMethod !== DownloadMethod.Optimized}
value={settings.autoDownload} value={settings.autoDownload}
onValueChange={(value) => updateSettings({ autoDownload: value })} onValueChange={(value) => updateSettings({ autoDownload: value })}
/> />
</ListItem> </ListItem>
<ListItem <ListItem
disabled={settings.downloadMethod !== "optimized"} disabled={pluginSettings?.optimizedVersionsServerUrl?.locked || settings.downloadMethod !== DownloadMethod.Optimized}
onPress={() => router.push("/settings/optimized-server/page")} onPress={() => router.push("/settings/optimized-server/page")}
showArrow showArrow
title="Optimized Versions Server" title="Optimized Versions Server"
></ListItem> ></ListItem>
</ListGroup> </ListGroup>
</View> </DisabledSetting>
); );
}; };

View File

@@ -21,7 +21,7 @@ export const JellyseerrSettings = () => {
} = useJellyseerr(); } = useJellyseerr();
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const [settings, updateSettings] = useSettings(); const [settings, updateSettings, pluginSettings] = useSettings();
const [promptForJellyseerrPass, setPromptForJellyseerrPass] = const [promptForJellyseerrPass, setPromptForJellyseerrPass] =
useState<boolean>(false); useState<boolean>(false);

View File

@@ -1,4 +1,4 @@
import { useSettings } from "@/utils/atoms/settings"; import {DownloadMethod, useSettings} from "@/utils/atoms/settings";
import { getOrSetDeviceId } from "@/utils/device"; import { getOrSetDeviceId } from "@/utils/device";
import { useLog, writeToLog } from "@/utils/log"; import { useLog, writeToLog } from "@/utils/log";
import { import {
@@ -106,7 +106,7 @@ function useDownloadProvider() {
const url = settings?.optimizedVersionsServerUrl; const url = settings?.optimizedVersionsServerUrl;
if ( if (
settings?.downloadMethod !== "optimized" || settings?.downloadMethod !== DownloadMethod.Optimized ||
!url || !url ||
!deviceId || !deviceId ||
!authHeader !authHeader
@@ -166,7 +166,7 @@ function useDownloadProvider() {
}, },
staleTime: 0, staleTime: 0,
refetchInterval: 2000, refetchInterval: 2000,
enabled: settings?.downloadMethod === "optimized", enabled: settings?.downloadMethod === DownloadMethod.Optimized,
}); });
useEffect(() => { useEffect(() => {

View File

@@ -1,5 +1,5 @@
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { useCallback, useEffect } from "react"; import {useCallback, useEffect, useMemo} from "react";
import * as ScreenOrientation from "expo-screen-orientation"; import * as ScreenOrientation from "expo-screen-orientation";
import { storage } from "../mmkv"; import { storage } from "../mmkv";
import { Platform } from "react-native"; import { Platform } from "react-native";
@@ -12,7 +12,7 @@ import {apiAtom} from "@/providers/JellyfinProvider";
import {getPluginsApi} from "@jellyfin/sdk/lib/utils/api"; import {getPluginsApi} from "@jellyfin/sdk/lib/utils/api";
import {writeErrorLog} from "@/utils/log"; import {writeErrorLog} from "@/utils/log";
const STREAMYFIN_PLUGIN_ID = "1e9e5d38-6e67-4615-8719-e98a5c34f004" const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004"
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS" const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS"
export type DownloadQuality = "original" | "high" | "low"; export type DownloadQuality = "original" | "high" | "low";
@@ -66,6 +66,11 @@ export type DefaultLanguageOption = {
label: string; label: string;
}; };
export enum DownloadMethod {
Remux = "remux",
Optimized = "optimized"
}
export type Settings = { export type Settings = {
autoRotate?: boolean; autoRotate?: boolean;
forceLandscapeInVideoPlayer?: boolean; forceLandscapeInVideoPlayer?: boolean;
@@ -88,7 +93,7 @@ export type Settings = {
forwardSkipTime: number; forwardSkipTime: number;
rewindSkipTime: number; rewindSkipTime: number;
optimizedVersionsServerUrl?: string | null; optimizedVersionsServerUrl?: string | null;
downloadMethod: "optimized" | "remux"; downloadMethod: DownloadMethod;
autoDownload: boolean; autoDownload: boolean;
showCustomMenuLinks: boolean; showCustomMenuLinks: boolean;
disableHapticFeedback: boolean; disableHapticFeedback: boolean;
@@ -100,7 +105,7 @@ export type Settings = {
}; };
export interface Lockable<T> { export interface Lockable<T> {
lockable: boolean; locked: boolean;
value: T value: T
} }
@@ -138,7 +143,7 @@ const loadSettings = (): Settings => {
forwardSkipTime: 30, forwardSkipTime: 30,
rewindSkipTime: 10, rewindSkipTime: 10,
optimizedVersionsServerUrl: null, optimizedVersionsServerUrl: null,
downloadMethod: "remux", downloadMethod: DownloadMethod.Remux,
autoDownload: false, autoDownload: false,
showCustomMenuLinks: false, showCustomMenuLinks: false,
disableHapticFeedback: false, disableHapticFeedback: false,
@@ -171,15 +176,15 @@ export const pluginSettingsAtom = atom(storage.get<PluginLockableSettings>(STREA
export const useSettings = () => { export const useSettings = () => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [settings, setSettings] = useAtom(settingsAtom); const [_settings, setSettings] = useAtom(settingsAtom);
const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom); const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom);
useEffect(() => { useEffect(() => {
if (settings === null) { if (_settings === null) {
const loadedSettings = loadSettings(); const loadedSettings = loadSettings();
setSettings(loadedSettings); setSettings(loadedSettings);
} }
}, [settings, setSettings]); }, [_settings, setSettings]);
const setPluginSettings = useCallback((settings: PluginLockableSettings | undefined) => { const setPluginSettings = useCallback((settings: PluginLockableSettings | undefined) => {
storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings) storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings)
@@ -217,6 +222,22 @@ export const useSettings = () => {
[api] [api]
) )
// We do not want to save over users pre-existing settings in case admin ever removes/unlocks a setting.
const settings: Settings = useMemo(() => {
const overrideSettings = Object.entries(pluginSettings || {})
.reduce((acc, [key, value]) => {
if (value) {
acc = Object.assign(acc, {[key]: value.value})
}
return acc
}, {} as Settings)
return {
..._settings,
...overrideSettings
}
}, [_settings, setSettings, pluginSettings, _setPluginSettings, setPluginSettings])
const updateSettings = (update: Partial<Settings>) => { const updateSettings = (update: Partial<Settings>) => {
if (settings) { if (settings) {
const newSettings = { ...settings, ...update }; const newSettings = { ...settings, ...update };