From 2c6823eb53283f2f53eb810ca40f46d056e8ba08 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:20:10 -0500 Subject: [PATCH] feat: [StreamyfinPlugin] Jellyseerr, Search Engine, & Download settings - Added DisabledSetting.tsx component - Added DownloadMethod enum - cleanup --- app/(auth)/(tabs)/(home)/downloads/index.tsx | 4 +- app/(auth)/(tabs)/(home)/settings.tsx | 2 +- .../(home)/settings/jellyseerr/page.tsx | 76 ++---------- .../(home)/settings/marlin-search/page.tsx | 117 ++++++++++-------- .../(home)/settings/optimized-server/page.tsx | 34 ++--- components/DownloadItem.tsx | 4 +- components/downloads/ActiveDownloads.tsx | 6 +- components/settings/DisabledSetting.tsx | 26 ++++ components/settings/DownloadSettings.tsx | 58 +++++---- components/settings/Jellyseerr.tsx | 2 +- providers/DownloadProvider.tsx | 6 +- utils/atoms/settings.ts | 37 ++++-- 12 files changed, 193 insertions(+), 179 deletions(-) create mode 100644 components/settings/DisabledSetting.tsx diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index 56f21af3..2e78e84f 100644 --- a/app/(auth)/(tabs)/(home)/downloads/index.tsx +++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx @@ -4,7 +4,7 @@ import { MovieCard } from "@/components/downloads/MovieCard"; import { SeriesCard } from "@/components/downloads/SeriesCard"; import { DownloadedItem, useDownload } from "@/providers/DownloadProvider"; 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 { useNavigation, useRouter } from "expo-router"; import { useAtom } from "jotai"; @@ -96,7 +96,7 @@ export default function page() { > - {settings?.downloadMethod === "remux" && ( + {settings?.downloadMethod === DownloadMethod.Remux && ( Queue diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index 38a0d34a..b96802a9 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -15,7 +15,7 @@ import { useJellyfin } from "@/providers/JellyfinProvider"; import { clearLogs } from "@/utils/log"; import { useHaptic } from "@/hooks/useHaptic"; import { useNavigation, useRouter } from "expo-router"; -import { useEffect } from "react"; +import React, { useEffect } from "react"; import { ScrollView, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { storage } from "@/utils/mmkv"; diff --git a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx index af4247d5..5da08ff1 100644 --- a/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx @@ -1,78 +1,16 @@ -import { Text } from "@/components/common/Text"; import { JellyseerrSettings } from "@/components/settings/Jellyseerr"; -import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm"; -import { apiAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; -import { getOrSetDeviceId } from "@/utils/device"; -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"; +import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { - const navigation = useNavigation(); - - const [api] = useAtom(apiAtom); - const [settings, updateSettings] = useSettings(); - - const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] = - useState(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 ? ( - // - // ) : ( - // onSave(optimizedVersionsServerUrl)}> - // Save - // - // ), - // }); - // }, [navigation, optimizedVersionsServerUrl, saveMutation.isPending]); + const [settings, updateSettings, pluginSettings] = useSettings(); return ( - + - + ); } diff --git a/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx b/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx index b8255c6e..dab489cb 100644 --- a/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx @@ -1,12 +1,10 @@ import { Text } from "@/components/common/Text"; import { ListGroup } from "@/components/list/ListGroup"; import { ListItem } from "@/components/list/ListItem"; -import { apiAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { useQueryClient } from "@tanstack/react-query"; import { useNavigation } from "expo-router"; -import { useAtom } from "jotai"; -import { useEffect, useState } from "react"; +import React, {useEffect, useMemo, useState} from "react"; import { Linking, Switch, @@ -15,11 +13,12 @@ import { View, } from "react-native"; import { toast } from "sonner-native"; +import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const navigation = useNavigation(); - const [settings, updateSettings] = useSettings(); + const [settings, updateSettings, pluginSettings] = useSettings(); const queryClient = useQueryClient(); const [value, setValue] = useState(settings?.marlinServerUrl || ""); @@ -35,69 +34,81 @@ export default function page() { Linking.openURL("https://github.com/fredrikburmester/marlin-search"); }; + const disabled = useMemo(() => { + return pluginSettings?.searchEngine?.locked === true && pluginSettings?.marlinServerUrl?.locked === true + }, [pluginSettings]); + useEffect(() => { - navigation.setOptions({ - headerRight: () => ( - onSave(value)}> - Save - - ), - }); + if (!pluginSettings?.marlinServerUrl?.locked) { + navigation.setOptions({ + headerRight: () => ( + onSave(value)}> + Save + + ), + }); + } }, [navigation, value]); if (!settings) return null; return ( - + - { - updateSettings({ searchEngine: "Jellyfin" }); - queryClient.invalidateQueries({ queryKey: ["search"] }); - }} + - { - updateSettings({ searchEngine: value ? "Marlin" : "Jellyfin" }); + { + updateSettings({ searchEngine: "Jellyfin" }); queryClient.invalidateQueries({ queryKey: ["search"] }); }} - /> - + > + { + updateSettings({ searchEngine: value ? "Marlin" : "Jellyfin" }); + queryClient.invalidateQueries({ queryKey: ["search"] }); + }} + /> + + - - - - URL - setValue(text)} - /> - + + URL + setValue(text)} + /> - - Enter the URL for the Marlin server. The URL should include http or - https and optionally the port.{" "} - - Read more about Marlin. - + + + Enter the URL for the Marlin server. The URL should include http or + https and optionally the port.{" "} + + Read more about Marlin. - - + + ); } diff --git a/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx b/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx index b47d565f..11930607 100644 --- a/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx @@ -10,12 +10,13 @@ import { useAtom } from "jotai"; import { useEffect, useState } from "react"; import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { toast } from "sonner-native"; +import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const navigation = useNavigation(); const [api] = useAtom(apiAtom); - const [settings, updateSettings] = useSettings(); + const [settings, updateSettings, pluginSettings] = useSettings(); const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] = useState(settings?.optimizedVersionsServerUrl || ""); @@ -56,25 +57,30 @@ export default function page() { }; useEffect(() => { - navigation.setOptions({ - title: "Optimized Server", - headerRight: () => - saveMutation.isPending ? ( - - ) : ( - onSave(optimizedVersionsServerUrl)}> - Save - - ), - }); + if (!pluginSettings?.optimizedVersionsServerUrl?.locked) { + navigation.setOptions({ + title: "Optimized Server", + headerRight: () => + saveMutation.isPending ? ( + + ) : ( + onSave(optimizedVersionsServerUrl)}> + Save + + ), + }); + } }, [navigation, optimizedVersionsServerUrl, saveMutation.isPending]); return ( - + - + ); } diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx index 4618bb4f..35ed063c 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -2,7 +2,7 @@ import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4"; import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; 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 { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server"; @@ -74,7 +74,7 @@ export const DownloadItems: React.FC = ({ [user] ); const usingOptimizedServer = useMemo( - () => settings?.downloadMethod === "optimized", + () => settings?.downloadMethod === DownloadMethod.Optimized, [settings] ); diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index 556ae8c7..e42027ab 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -1,7 +1,6 @@ import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; -import { apiAtom } from "@/providers/JellyfinProvider"; -import { useSettings } from "@/utils/atoms/settings"; +import {DownloadMethod, useSettings} from "@/utils/atoms/settings"; import { JobStatus } from "@/utils/optimize-server"; import { formatTimeString } from "@/utils/time"; 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 { useRouter } from "expo-router"; import { FFmpegKit } from "ffmpeg-kit-react-native"; -import { useAtom } from "jotai"; import { ActivityIndicator, TouchableOpacity, @@ -62,7 +60,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { mutationFn: async (id: string) => { if (!process) throw new Error("No active download"); - if (settings?.downloadMethod === "optimized") { + if (settings?.downloadMethod === DownloadMethod.Optimized) { try { const tasks = await checkForExistingDownloads(); for (const task of tasks) { diff --git a/components/settings/DisabledSetting.tsx b/components/settings/DisabledSetting.tsx new file mode 100644 index 00000000..b340fb96 --- /dev/null +++ b/components/settings/DisabledSetting.tsx @@ -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 +}) => ( + + + {disabled && showText && + {text ?? "Currently disabled by admin."} + } + {children} + + +) + +export default DisabledSetting; \ No newline at end of file diff --git a/components/settings/DownloadSettings.tsx b/components/settings/DownloadSettings.tsx index f330dc04..7937b8a8 100644 --- a/components/settings/DownloadSettings.tsx +++ b/components/settings/DownloadSettings.tsx @@ -1,33 +1,47 @@ -import { Stepper } from "@/components/inputs/Stepper"; -import { useDownload } from "@/providers/DownloadProvider"; -import { Settings, useSettings } from "@/utils/atoms/settings"; -import { Ionicons } from "@expo/vector-icons"; -import { useQueryClient } from "@tanstack/react-query"; -import { useRouter } from "expo-router"; -import React from "react"; -import { Switch, TouchableOpacity, View } from "react-native"; +import {Stepper} from "@/components/inputs/Stepper"; +import {useDownload} from "@/providers/DownloadProvider"; +import {DownloadMethod, Settings, useSettings} from "@/utils/atoms/settings"; +import {Ionicons} from "@expo/vector-icons"; +import {useQueryClient} from "@tanstack/react-query"; +import {useRouter} from "expo-router"; +import React, {useMemo} from "react"; +import {Switch, TouchableOpacity} from "react-native"; import * as DropdownMenu from "zeego/dropdown-menu"; -import { Text } from "../common/Text"; -import { ListGroup } from "../list/ListGroup"; -import { ListItem } from "../list/ListItem"; +import {Text} from "../common/Text"; +import {ListGroup} from "../list/ListGroup"; +import {ListItem} from "../list/ListItem"; +import DisabledSetting from "@/components/settings/DisabledSetting"; export const DownloadSettings: React.FC = ({ ...props }) => { - const [settings, updateSettings] = useSettings(); + const [settings, updateSettings, pluginSettings] = useSettings(); const { setProcesses } = useDownload(); const router = useRouter(); const queryClient = useQueryClient(); + const disabled = useMemo(() => ( + pluginSettings?.downloadMethod?.locked === true && + pluginSettings?.remuxConcurrentLimit?.locked === true && + pluginSettings?.autoDownload.locked === true + ), [pluginSettings]) + if (!settings) return null; return ( - + - + - {settings.downloadMethod === "remux" + {settings.downloadMethod === DownloadMethod.Remux ? "Default" : "Optimized"} @@ -51,7 +65,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => { { - updateSettings({ downloadMethod: "remux" }); + updateSettings({ downloadMethod: DownloadMethod.Remux }); setProcesses([]); }} > @@ -60,7 +74,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => { { - updateSettings({ downloadMethod: "optimized" }); + updateSettings({ downloadMethod: DownloadMethod.Optimized }); setProcesses([]); queryClient.invalidateQueries({ queryKey: ["search"] }); }} @@ -73,7 +87,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => { { updateSettings({ autoDownload: value })} /> router.push("/settings/optimized-server/page")} showArrow title="Optimized Versions Server" > - + ); }; diff --git a/components/settings/Jellyseerr.tsx b/components/settings/Jellyseerr.tsx index 148f1823..d0ebd9df 100644 --- a/components/settings/Jellyseerr.tsx +++ b/components/settings/Jellyseerr.tsx @@ -21,7 +21,7 @@ export const JellyseerrSettings = () => { } = useJellyseerr(); const [user] = useAtom(userAtom); - const [settings, updateSettings] = useSettings(); + const [settings, updateSettings, pluginSettings] = useSettings(); const [promptForJellyseerrPass, setPromptForJellyseerrPass] = useState(false); diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index fb8b137f..51f1f35c 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -1,4 +1,4 @@ -import { useSettings } from "@/utils/atoms/settings"; +import {DownloadMethod, useSettings} from "@/utils/atoms/settings"; import { getOrSetDeviceId } from "@/utils/device"; import { useLog, writeToLog } from "@/utils/log"; import { @@ -106,7 +106,7 @@ function useDownloadProvider() { const url = settings?.optimizedVersionsServerUrl; if ( - settings?.downloadMethod !== "optimized" || + settings?.downloadMethod !== DownloadMethod.Optimized || !url || !deviceId || !authHeader @@ -166,7 +166,7 @@ function useDownloadProvider() { }, staleTime: 0, refetchInterval: 2000, - enabled: settings?.downloadMethod === "optimized", + enabled: settings?.downloadMethod === DownloadMethod.Optimized, }); useEffect(() => { diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index becb1ea8..0ed9199b 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -1,5 +1,5 @@ import { atom, useAtom } from "jotai"; -import { useCallback, useEffect } from "react"; +import {useCallback, useEffect, useMemo} from "react"; import * as ScreenOrientation from "expo-screen-orientation"; import { storage } from "../mmkv"; import { Platform } from "react-native"; @@ -12,7 +12,7 @@ import {apiAtom} from "@/providers/JellyfinProvider"; import {getPluginsApi} from "@jellyfin/sdk/lib/utils/api"; 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" export type DownloadQuality = "original" | "high" | "low"; @@ -66,6 +66,11 @@ export type DefaultLanguageOption = { label: string; }; +export enum DownloadMethod { + Remux = "remux", + Optimized = "optimized" +} + export type Settings = { autoRotate?: boolean; forceLandscapeInVideoPlayer?: boolean; @@ -88,7 +93,7 @@ export type Settings = { forwardSkipTime: number; rewindSkipTime: number; optimizedVersionsServerUrl?: string | null; - downloadMethod: "optimized" | "remux"; + downloadMethod: DownloadMethod; autoDownload: boolean; showCustomMenuLinks: boolean; disableHapticFeedback: boolean; @@ -100,7 +105,7 @@ export type Settings = { }; export interface Lockable { - lockable: boolean; + locked: boolean; value: T } @@ -138,7 +143,7 @@ const loadSettings = (): Settings => { forwardSkipTime: 30, rewindSkipTime: 10, optimizedVersionsServerUrl: null, - downloadMethod: "remux", + downloadMethod: DownloadMethod.Remux, autoDownload: false, showCustomMenuLinks: false, disableHapticFeedback: false, @@ -171,15 +176,15 @@ export const pluginSettingsAtom = atom(storage.get(STREA export const useSettings = () => { const [api] = useAtom(apiAtom); - const [settings, setSettings] = useAtom(settingsAtom); + const [_settings, setSettings] = useAtom(settingsAtom); const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom); useEffect(() => { - if (settings === null) { + if (_settings === null) { const loadedSettings = loadSettings(); setSettings(loadedSettings); } - }, [settings, setSettings]); + }, [_settings, setSettings]); const setPluginSettings = useCallback((settings: PluginLockableSettings | undefined) => { storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings) @@ -217,6 +222,22 @@ export const useSettings = () => { [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) => { if (settings) { const newSettings = { ...settings, ...update };