From 75820adcbcfc8a7e48e7f6be8970ba7eb215691f Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Wed, 8 Jan 2025 21:52:31 -0500 Subject: [PATCH 01/12] initial changes --- augmentations/mmkv.ts | 7 +++- providers/JellyfinProvider.tsx | 18 +++++++++- utils/atoms/settings.ts | 60 ++++++++++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/augmentations/mmkv.ts b/augmentations/mmkv.ts index 80fbeede..5667502f 100644 --- a/augmentations/mmkv.ts +++ b/augmentations/mmkv.ts @@ -13,5 +13,10 @@ MMKV.prototype.get = function (key: string): T | undefined { } MMKV.prototype.setAny = function (key: string, value: any | undefined): void { - this.set(key, JSON.stringify(value)); + if (value === undefined) { + this.delete(key) + } + else { + this.set(key, JSON.stringify(value)); + } } \ No newline at end of file diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index f4ccce75..2b602323 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -1,3 +1,4 @@ +import "@/augmentations"; import { useInterval } from "@/hooks/useInterval"; import { storage } from "@/utils/mmkv"; import { Api, Jellyfin } from "@jellyfin/sdk"; @@ -19,7 +20,8 @@ import React, { import { Platform } from "react-native"; import uuid from "react-native-uuid"; import { getDeviceName } from "react-native-device-info"; -import { toast } from "sonner-native"; +import { useSettings } from "@/utils/atoms/settings"; +import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; interface Server { address: string; @@ -70,6 +72,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const [user, setUser] = useAtom(userAtom); const [isPolling, setIsPolling] = useState(false); const [secret, setSecret] = useState(null); + const [settings, updateSettings, pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings] = useSettings(); + const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr(); useQuery({ queryKey: ["user", api], @@ -226,6 +230,16 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ storage.set("user", JSON.stringify(auth.data.User)); setApi(jellyfin.createApi(api?.basePath, auth.data?.AccessToken)); storage.set("token", auth.data?.AccessToken); + + const recentPluginSettings = await refreshStreamyfinPluginSettings(); + if (recentPluginSettings?.jellyseerrServerUrl?.value) { + const jellyseerrApi = new JellyseerrApi(recentPluginSettings.jellyseerrServerUrl.value); + await jellyseerrApi.test().then((result) => { + if (result.isValid && result.requiresPass) { + jellyseerrApi.login(username, password).then(setJellyseerrUser); + } + }) + } } } catch (error) { if (axios.isAxiosError(error)) { @@ -262,6 +276,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ mutationFn: async () => { storage.delete("token"); setUser(null); + setPluginSettings(undefined); + await clearAllJellyseerData(); }, onError: (error) => { console.error("Logout failed:", error); diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index b473198d..eac6826e 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -1,12 +1,19 @@ import { atom, useAtom } from "jotai"; -import { useEffect } from "react"; +import { useCallback, useEffect } from "react"; import * as ScreenOrientation from "expo-screen-orientation"; import { storage } from "../mmkv"; import { Platform } from "react-native"; import { CultureDto, + PluginStatus, SubtitlePlaybackMode, } from "@jellyfin/sdk/lib/generated-client"; +import {apiAtom} from "@/providers/JellyfinProvider"; +import {getPluginsApi} from "@jellyfin/sdk/lib/utils/api"; +import {writeErrorLog} from "@/utils/log"; +import {AUTHORIZATION_HEADER} from "@jellyfin/sdk"; + +const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS" export type DownloadQuality = "original" | "high" | "low"; @@ -92,6 +99,13 @@ export type Settings = { hiddenLibraries?: string[]; }; +export interface Lockable { + lockable: boolean; + value: T +} + +export type PluginLockableSettings = { [K in keyof Settings]: Lockable }; + const loadSettings = (): Settings => { const defaultValues: Settings = { autoRotate: true, @@ -150,9 +164,12 @@ const saveSettings = (settings: Settings) => { }; export const settingsAtom = atom(null); +export const pluginSettingsAtom = atom(storage.get(STREAMYFIN_PLUGIN_SETTINGS)); export const useSettings = () => { + const [api] = useAtom(apiAtom); const [settings, setSettings] = useAtom(settingsAtom); + const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom); useEffect(() => { if (settings === null) { @@ -161,6 +178,45 @@ export const useSettings = () => { } }, [settings, setSettings]); + const setPluginSettings = useCallback((settings: PluginLockableSettings | undefined) => { + storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings) + _setPluginSettings(settings) + }, + [_setPluginSettings] + ) + + const refreshStreamyfinPluginSettings = useCallback( + async () => { + if (!api) + return + + const plugins = await getPluginsApi(api).getPlugins().then(({data}) => data); + + if (plugins && plugins.length > 0) { + const streamyfinPlugin = plugins.find(plugin => plugin.Name === "Streamyfin"); + + if (streamyfinPlugin?.Status != PluginStatus.Active) { + writeErrorLog( + "Streamyfin plugin is currently not active.\n" + + `Current status is: ${streamyfinPlugin?.Status}` + ); + setPluginSettings(undefined); + return; + } + + const settings = await api.axiosInstance + .get(`${api.basePath}/Streamyfin/config`, { headers: { [AUTHORIZATION_HEADER]: api.authorizationHeader } }) + .then(response => { + return response.data['settings'] as PluginLockableSettings + }) + + setPluginSettings(settings); + return settings; + } + }, + [api] + ) + const updateSettings = (update: Partial) => { if (settings) { const newSettings = { ...settings, ...update }; @@ -170,5 +226,5 @@ export const useSettings = () => { } }; - return [settings, updateSettings] as const; + return [settings, updateSettings, pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings] as const; }; From 882d0ea188f26d1fde1c00643089bfcf46628ab9 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Thu, 9 Jan 2025 08:51:53 -0500 Subject: [PATCH 02/12] api augmentations & added streamyfin plugin id --- augmentations/api.ts | 30 ++++++++++++++++++++++++++++++ utils/atoms/settings.ts | 16 ++++++++-------- 2 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 augmentations/api.ts diff --git a/augmentations/api.ts b/augmentations/api.ts new file mode 100644 index 00000000..72b8c45f --- /dev/null +++ b/augmentations/api.ts @@ -0,0 +1,30 @@ +import {Api, AUTHORIZATION_HEADER} from "@jellyfin/sdk"; +import {AxiosRequestConfig, AxiosResponse} from "axios"; +import {StreamyfinPluginConfig} from "@/utils/atoms/settings"; + +declare module '@jellyfin/sdk' { + interface Api { + get(url: string, config?: AxiosRequestConfig): Promise> + post(url: string, data: D, config?: AxiosRequestConfig): Promise> + getStreamyfinPluginConfig(): Promise> + } +} + +Api.prototype.get = function (url: string, config: AxiosRequestConfig = {}): Promise> { + return this.axiosInstance.get(url, { + ...(config ?? {}), + headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader } + }) +} + +Api.prototype.post = function (url: string, data: D, config: AxiosRequestConfig): Promise> { + return this.axiosInstance.get(`${this.basePath}${url}`, { + ...(config || {}), + data, + headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader }} + ) +} + +Api.prototype.getStreamyfinPluginConfig = function (): Promise> { + return this.get("/Streamyfin/config") +} \ No newline at end of file diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index eac6826e..becb1ea8 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -11,8 +11,8 @@ import { import {apiAtom} from "@/providers/JellyfinProvider"; import {getPluginsApi} from "@jellyfin/sdk/lib/utils/api"; import {writeErrorLog} from "@/utils/log"; -import {AUTHORIZATION_HEADER} from "@jellyfin/sdk"; +const STREAMYFIN_PLUGIN_ID = "1e9e5d38-6e67-4615-8719-e98a5c34f004" const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS" export type DownloadQuality = "original" | "high" | "low"; @@ -105,6 +105,9 @@ export interface Lockable { } export type PluginLockableSettings = { [K in keyof Settings]: Lockable }; +export type StreamyfinPluginConfig = { + settings: PluginLockableSettings +} const loadSettings = (): Settings => { const defaultValues: Settings = { @@ -193,9 +196,9 @@ export const useSettings = () => { const plugins = await getPluginsApi(api).getPlugins().then(({data}) => data); if (plugins && plugins.length > 0) { - const streamyfinPlugin = plugins.find(plugin => plugin.Name === "Streamyfin"); + const streamyfinPlugin = plugins.find(plugin => plugin.Id === STREAMYFIN_PLUGIN_ID); - if (streamyfinPlugin?.Status != PluginStatus.Active) { + if (!streamyfinPlugin || streamyfinPlugin.Status != PluginStatus.Active) { writeErrorLog( "Streamyfin plugin is currently not active.\n" + `Current status is: ${streamyfinPlugin?.Status}` @@ -204,11 +207,8 @@ export const useSettings = () => { return; } - const settings = await api.axiosInstance - .get(`${api.basePath}/Streamyfin/config`, { headers: { [AUTHORIZATION_HEADER]: api.authorizationHeader } }) - .then(response => { - return response.data['settings'] as PluginLockableSettings - }) + const settings = await api.getStreamyfinPluginConfig() + .then(({data}) => data.settings) setPluginSettings(settings); return settings; From e1720a00da611794cbd5ea950a476c41619f9a2b Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Wed, 8 Jan 2025 21:52:31 -0500 Subject: [PATCH 03/12] initial changes --- augmentations/mmkv.ts | 7 +++- providers/JellyfinProvider.tsx | 18 +++++++++- utils/atoms/settings.ts | 60 ++++++++++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/augmentations/mmkv.ts b/augmentations/mmkv.ts index 80fbeede..5667502f 100644 --- a/augmentations/mmkv.ts +++ b/augmentations/mmkv.ts @@ -13,5 +13,10 @@ MMKV.prototype.get = function (key: string): T | undefined { } MMKV.prototype.setAny = function (key: string, value: any | undefined): void { - this.set(key, JSON.stringify(value)); + if (value === undefined) { + this.delete(key) + } + else { + this.set(key, JSON.stringify(value)); + } } \ No newline at end of file diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index f4ccce75..2b602323 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -1,3 +1,4 @@ +import "@/augmentations"; import { useInterval } from "@/hooks/useInterval"; import { storage } from "@/utils/mmkv"; import { Api, Jellyfin } from "@jellyfin/sdk"; @@ -19,7 +20,8 @@ import React, { import { Platform } from "react-native"; import uuid from "react-native-uuid"; import { getDeviceName } from "react-native-device-info"; -import { toast } from "sonner-native"; +import { useSettings } from "@/utils/atoms/settings"; +import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; interface Server { address: string; @@ -70,6 +72,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const [user, setUser] = useAtom(userAtom); const [isPolling, setIsPolling] = useState(false); const [secret, setSecret] = useState(null); + const [settings, updateSettings, pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings] = useSettings(); + const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr(); useQuery({ queryKey: ["user", api], @@ -226,6 +230,16 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ storage.set("user", JSON.stringify(auth.data.User)); setApi(jellyfin.createApi(api?.basePath, auth.data?.AccessToken)); storage.set("token", auth.data?.AccessToken); + + const recentPluginSettings = await refreshStreamyfinPluginSettings(); + if (recentPluginSettings?.jellyseerrServerUrl?.value) { + const jellyseerrApi = new JellyseerrApi(recentPluginSettings.jellyseerrServerUrl.value); + await jellyseerrApi.test().then((result) => { + if (result.isValid && result.requiresPass) { + jellyseerrApi.login(username, password).then(setJellyseerrUser); + } + }) + } } } catch (error) { if (axios.isAxiosError(error)) { @@ -262,6 +276,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ mutationFn: async () => { storage.delete("token"); setUser(null); + setPluginSettings(undefined); + await clearAllJellyseerData(); }, onError: (error) => { console.error("Logout failed:", error); diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index b473198d..eac6826e 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -1,12 +1,19 @@ import { atom, useAtom } from "jotai"; -import { useEffect } from "react"; +import { useCallback, useEffect } from "react"; import * as ScreenOrientation from "expo-screen-orientation"; import { storage } from "../mmkv"; import { Platform } from "react-native"; import { CultureDto, + PluginStatus, SubtitlePlaybackMode, } from "@jellyfin/sdk/lib/generated-client"; +import {apiAtom} from "@/providers/JellyfinProvider"; +import {getPluginsApi} from "@jellyfin/sdk/lib/utils/api"; +import {writeErrorLog} from "@/utils/log"; +import {AUTHORIZATION_HEADER} from "@jellyfin/sdk"; + +const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS" export type DownloadQuality = "original" | "high" | "low"; @@ -92,6 +99,13 @@ export type Settings = { hiddenLibraries?: string[]; }; +export interface Lockable { + lockable: boolean; + value: T +} + +export type PluginLockableSettings = { [K in keyof Settings]: Lockable }; + const loadSettings = (): Settings => { const defaultValues: Settings = { autoRotate: true, @@ -150,9 +164,12 @@ const saveSettings = (settings: Settings) => { }; export const settingsAtom = atom(null); +export const pluginSettingsAtom = atom(storage.get(STREAMYFIN_PLUGIN_SETTINGS)); export const useSettings = () => { + const [api] = useAtom(apiAtom); const [settings, setSettings] = useAtom(settingsAtom); + const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom); useEffect(() => { if (settings === null) { @@ -161,6 +178,45 @@ export const useSettings = () => { } }, [settings, setSettings]); + const setPluginSettings = useCallback((settings: PluginLockableSettings | undefined) => { + storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings) + _setPluginSettings(settings) + }, + [_setPluginSettings] + ) + + const refreshStreamyfinPluginSettings = useCallback( + async () => { + if (!api) + return + + const plugins = await getPluginsApi(api).getPlugins().then(({data}) => data); + + if (plugins && plugins.length > 0) { + const streamyfinPlugin = plugins.find(plugin => plugin.Name === "Streamyfin"); + + if (streamyfinPlugin?.Status != PluginStatus.Active) { + writeErrorLog( + "Streamyfin plugin is currently not active.\n" + + `Current status is: ${streamyfinPlugin?.Status}` + ); + setPluginSettings(undefined); + return; + } + + const settings = await api.axiosInstance + .get(`${api.basePath}/Streamyfin/config`, { headers: { [AUTHORIZATION_HEADER]: api.authorizationHeader } }) + .then(response => { + return response.data['settings'] as PluginLockableSettings + }) + + setPluginSettings(settings); + return settings; + } + }, + [api] + ) + const updateSettings = (update: Partial) => { if (settings) { const newSettings = { ...settings, ...update }; @@ -170,5 +226,5 @@ export const useSettings = () => { } }; - return [settings, updateSettings] as const; + return [settings, updateSettings, pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings] as const; }; From 54af64abeffe79ccae9110a35845bd7895ae4710 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Thu, 9 Jan 2025 08:51:53 -0500 Subject: [PATCH 04/12] api augmentations & added streamyfin plugin id --- augmentations/api.ts | 30 ++++++++++++++++++++++++++++++ utils/atoms/settings.ts | 16 ++++++++-------- 2 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 augmentations/api.ts diff --git a/augmentations/api.ts b/augmentations/api.ts new file mode 100644 index 00000000..17673298 --- /dev/null +++ b/augmentations/api.ts @@ -0,0 +1,30 @@ +import {Api, AUTHORIZATION_HEADER} from "@jellyfin/sdk"; +import {AxiosRequestConfig, AxiosResponse} from "axios"; +import {StreamyfinPluginConfig} from "@/utils/atoms/settings"; + +declare module '@jellyfin/sdk' { + interface Api { + get(url: string, config?: AxiosRequestConfig): Promise> + post(url: string, data: D, config?: AxiosRequestConfig): Promise> + getStreamyfinPluginConfig(): Promise> + } +} + +Api.prototype.get = function (url: string, config: AxiosRequestConfig = {}): Promise> { + return this.axiosInstance.get(`${this.basePath}${url}`, { + ...(config ?? {}), + headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader } + }) +} + +Api.prototype.post = function (url: string, data: D, config: AxiosRequestConfig): Promise> { + return this.axiosInstance.post(`${this.basePath}${url}`, { + ...(config || {}), + data, + headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader }} + ) +} + +Api.prototype.getStreamyfinPluginConfig = function (): Promise> { + return this.get("/Streamyfin/config") +} \ No newline at end of file diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index eac6826e..becb1ea8 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -11,8 +11,8 @@ import { import {apiAtom} from "@/providers/JellyfinProvider"; import {getPluginsApi} from "@jellyfin/sdk/lib/utils/api"; import {writeErrorLog} from "@/utils/log"; -import {AUTHORIZATION_HEADER} from "@jellyfin/sdk"; +const STREAMYFIN_PLUGIN_ID = "1e9e5d38-6e67-4615-8719-e98a5c34f004" const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS" export type DownloadQuality = "original" | "high" | "low"; @@ -105,6 +105,9 @@ export interface Lockable { } export type PluginLockableSettings = { [K in keyof Settings]: Lockable }; +export type StreamyfinPluginConfig = { + settings: PluginLockableSettings +} const loadSettings = (): Settings => { const defaultValues: Settings = { @@ -193,9 +196,9 @@ export const useSettings = () => { const plugins = await getPluginsApi(api).getPlugins().then(({data}) => data); if (plugins && plugins.length > 0) { - const streamyfinPlugin = plugins.find(plugin => plugin.Name === "Streamyfin"); + const streamyfinPlugin = plugins.find(plugin => plugin.Id === STREAMYFIN_PLUGIN_ID); - if (streamyfinPlugin?.Status != PluginStatus.Active) { + if (!streamyfinPlugin || streamyfinPlugin.Status != PluginStatus.Active) { writeErrorLog( "Streamyfin plugin is currently not active.\n" + `Current status is: ${streamyfinPlugin?.Status}` @@ -204,11 +207,8 @@ export const useSettings = () => { return; } - const settings = await api.axiosInstance - .get(`${api.basePath}/Streamyfin/config`, { headers: { [AUTHORIZATION_HEADER]: api.authorizationHeader } }) - .then(response => { - return response.data['settings'] as PluginLockableSettings - }) + const settings = await api.getStreamyfinPluginConfig() + .then(({data}) => data.settings) setPluginSettings(settings); return settings; From 9dfcc01f17eae598402b3523b3b32117a9e3629d Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:39:32 -0500 Subject: [PATCH 05/12] chore --- augmentations/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/augmentations/index.ts b/augmentations/index.ts index 22ca2cb0..abec02c9 100644 --- a/augmentations/index.ts +++ b/augmentations/index.ts @@ -1,3 +1,4 @@ +export * from "./api"; export * from "./mmkv"; export * from "./number"; export * from "./string"; 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 06/12] 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 }; From 2d9aaccfe0ce2a20e84fb5885302b33a92176eb6 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:38:26 -0500 Subject: [PATCH 07/12] feat: [StreamyfinPlugin] Media Toggles settings --- components/inputs/Stepper.tsx | 11 +++- components/settings/MediaToggles.tsx | 95 ++++++++++++---------------- utils/atoms/settings.ts | 11 +++- 3 files changed, 59 insertions(+), 58 deletions(-) diff --git a/components/inputs/Stepper.tsx b/components/inputs/Stepper.tsx index eb5032cf..86a4ffa9 100644 --- a/components/inputs/Stepper.tsx +++ b/components/inputs/Stepper.tsx @@ -1,8 +1,10 @@ import {TouchableOpacity, View} from "react-native"; import {Text} from "@/components/common/Text"; +import DisabledSetting from "@/components/settings/DisabledSetting"; interface StepperProps { value: number, + disabled?: boolean, step: number, min: number, max: number, @@ -12,6 +14,7 @@ interface StepperProps { export const Stepper: React.FC = ({ value, + disabled, step, min, max, @@ -19,7 +22,11 @@ export const Stepper: React.FC = ({ appendValue }) => { return ( - + onUpdate(Math.max(min, value - step))} className="w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center" @@ -39,6 +46,6 @@ export const Stepper: React.FC = ({ > + - + ) } \ No newline at end of file diff --git a/components/settings/MediaToggles.tsx b/components/settings/MediaToggles.tsx index 7e4c4346..6283cb03 100644 --- a/components/settings/MediaToggles.tsx +++ b/components/settings/MediaToggles.tsx @@ -1,72 +1,61 @@ -import React from "react"; -import { TouchableOpacity, View, ViewProps } from "react-native"; +import React, {useMemo} from "react"; +import { ViewProps } from "react-native"; import { useSettings } from "@/utils/atoms/settings"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -import { Text } from "../common/Text"; +import DisabledSetting from "@/components/settings/DisabledSetting"; +import {Stepper} from "@/components/inputs/Stepper"; interface Props extends ViewProps {} export const MediaToggles: React.FC = ({ ...props }) => { - const [settings, updateSettings] = useSettings(); + const [settings, updateSettings, pluginSettings] = useSettings(); if (!settings) return null; - const renderSkipControl = ( - value: number, - onDecrease: () => void, - onIncrease: () => void - ) => ( - - - - - - - {value}s - - - + - - - ); + const disabled = useMemo(() => ( + pluginSettings?.forwardSkipTime?.locked === true && + pluginSettings?.rewindSkipTime?.locked === true + ), + [pluginSettings] + ) return ( - + - - {renderSkipControl( - settings.forwardSkipTime, - () => - updateSettings({ - forwardSkipTime: Math.max(0, settings.forwardSkipTime - 5), - }), - () => - updateSettings({ - forwardSkipTime: Math.min(60, settings.forwardSkipTime + 5), - }) - )} + + updateSettings({forwardSkipTime})} + /> - - {renderSkipControl( - settings.rewindSkipTime, - () => - updateSettings({ - rewindSkipTime: Math.max(0, settings.rewindSkipTime - 5), - }), - () => - updateSettings({ - rewindSkipTime: Math.min(60, settings.rewindSkipTime + 5), - }) - )} + + updateSettings({rewindSkipTime})} + /> - + ); }; diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 0ed9199b..993c722d 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -223,11 +223,16 @@ export const useSettings = () => { ) // We do not want to save over users pre-existing settings in case admin ever removes/unlocks a setting. + // If admin sets locked to false but provides a value, + // use user settings first and fallback on admin setting if required. const settings: Settings = useMemo(() => { const overrideSettings = Object.entries(pluginSettings || {}) - .reduce((acc, [key, value]) => { - if (value) { - acc = Object.assign(acc, {[key]: value.value}) + .reduce((acc, [key, setting]) => { + if (setting) { + const {value, locked} = setting + acc = Object.assign(acc, { + [key]: locked ? value : _settings?.[key as keyof Settings] ?? value + }) } return acc }, {} as Settings) From 0f974ef2a3ca2c27ca714ae1a93376f4c1b79bde Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:42:57 -0500 Subject: [PATCH 08/12] feat: [StreamyfinPlugin] Audio Toggles settings --- components/settings/AudioToggles.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/settings/AudioToggles.tsx b/components/settings/AudioToggles.tsx index 62aea437..6afaedf4 100644 --- a/components/settings/AudioToggles.tsx +++ b/components/settings/AudioToggles.tsx @@ -6,11 +6,13 @@ import { Switch } from "react-native-gesture-handler"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; import { Ionicons } from "@expo/vector-icons"; +import {useSettings} from "@/utils/atoms/settings"; interface Props extends ViewProps {} export const AudioToggles: React.FC = ({ ...props }) => { const media = useMedia(); + const [_, __, pluginSettings] = useSettings(); const { settings, updateSettings } = media; const cultures = media.cultures; @@ -26,9 +28,13 @@ export const AudioToggles: React.FC = ({ ...props }) => { } > - + updateSettings({ rememberAudioSelections: value }) } From 455bf08213902b61ab8cae704892583d58d49759 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Fri, 10 Jan 2025 23:16:58 -0500 Subject: [PATCH 09/12] feat: [StreamyfinPlugin] Subtitle Toggles settings - Used stepper & dropdown components to simplify page --- components/common/Dropdown.tsx | 43 ++++--- components/settings/SubtitleToggles.tsx | 146 +++++++++--------------- 2 files changed, 80 insertions(+), 109 deletions(-) diff --git a/components/common/Dropdown.tsx b/components/common/Dropdown.tsx index d9612d48..fec36d2f 100644 --- a/components/common/Dropdown.tsx +++ b/components/common/Dropdown.tsx @@ -1,14 +1,16 @@ import * as DropdownMenu from "zeego/dropdown-menu"; import {TouchableOpacity, View, ViewProps} from "react-native"; import {Text} from "@/components/common/Text"; -import React, {PropsWithChildren, useEffect, useState} from "react"; +import React, {PropsWithChildren, ReactNode, useEffect, useState} from "react"; +import DisabledSetting from "@/components/settings/DisabledSetting"; interface Props { data: T[] + disabled?: boolean placeholderText?: string, keyExtractor: (item: T) => string - titleExtractor: (item: T) => string - title: string, + titleExtractor: (item: T) => string | undefined + title: string | ReactNode, label: string, onSelected: (...item: T[]) => void multi?: boolean @@ -16,6 +18,7 @@ interface Props { const Dropdown = ({ data, + disabled, placeholderText, keyExtractor, titleExtractor, @@ -34,20 +37,30 @@ const Dropdown = ({ }, [selected]); return ( - + - - - {title} - - - - {selected?.length !== undefined ? selected.map(titleExtractor).join(",") : placeholderText} + {typeof title === 'string' ? ( + + + {title} - - + + + {selected?.length !== undefined ? selected.map(titleExtractor).join(",") : placeholderText} + + + + ) : ( + <> + {title} + + )} ({ ))} - + ) }; diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx index 66c514b1..6719171d 100644 --- a/components/settings/SubtitleToggles.tsx +++ b/components/settings/SubtitleToggles.tsx @@ -7,11 +7,15 @@ import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; import { Ionicons } from "@expo/vector-icons"; import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client"; +import {useSettings} from "@/utils/atoms/settings"; +import {Stepper} from "@/components/inputs/Stepper"; +import Dropdown from "@/components/common/Dropdown"; interface Props extends ViewProps {} export const SubtitleToggles: React.FC = ({ ...props }) => { const media = useMedia(); + const [_, __, pluginSettings] = useSettings(); const { settings, updateSettings } = media; const cultures = media.cultures; @@ -36,8 +40,11 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { } > - - + item?.ThreeLetterISOLanguageName ?? "unknown"} + titleExtractor={(item) => item?.DisplayName} + title={ {settings?.defaultSubtitleLanguage?.DisplayName || "None"} @@ -48,48 +55,28 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { color="#5A5960" /> - - - Languages - { - updateSettings({ - defaultSubtitleLanguage: null, - }); - }} - > - None - - {cultures?.map((l) => ( - { - updateSettings({ - defaultSubtitleLanguage: l, - }); - }} - > - - {l.DisplayName} - - - ))} - - + } + label="Languages" + onSelected={(defaultSubtitleLanguage) => + updateSettings({ + defaultSubtitleLanguage: defaultSubtitleLanguage.DisplayName === "None" + ? null + : defaultSubtitleLanguage + }) + } + /> - - - + + {settings?.subtitleMode || "Loading"} @@ -100,68 +87,39 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { color="#5A5960" /> - - - Subtitle Mode - {subtitleModes?.map((l) => ( - { - updateSettings({ - subtitleMode: l, - }); - }} - > - {l} - - ))} - - + } + label="Subtitle Mode" + onSelected={(subtitleMode) => + updateSettings({subtitleMode}) + } + /> - + updateSettings({ rememberSubtitleSelections: value }) } /> - - - - updateSettings({ - subtitleSize: Math.max(0, settings.subtitleSize - 5), - }) - } - className="w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center" - > - - - - - {settings.subtitleSize} - - - updateSettings({ - subtitleSize: Math.min(120, settings.subtitleSize + 5), - }) - } - > - + - - + + updateSettings({subtitleSize})} + /> From dc498d62d8ec95e5758862d243ef226c6f32f1b1 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Fri, 10 Jan 2025 23:44:37 -0500 Subject: [PATCH 10/12] feat: [StreamyfinPlugin] Other settings --- .../(home)/settings/hide-libraries/page.tsx | 10 +- components/settings/OtherSettings.tsx | 245 ++++++++---------- 2 files changed, 113 insertions(+), 142 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx index 6919441e..35200bc1 100644 --- a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx @@ -8,9 +8,10 @@ import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; import { Switch, View } from "react-native"; +import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { - const [settings, updateSettings] = useSettings(); + const [settings, updateSettings, pluginSettings] = useSettings(); const user = useAtomValue(userAtom); const api = useAtomValue(apiAtom); @@ -35,7 +36,10 @@ export default function page() { ); return ( - + {data?.map((view) => ( {}}> @@ -56,6 +60,6 @@ export default function page() { Select the libraries you want to hide from the Library tab and home page sections. - + ); } diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index cbd8fc18..dcea3f26 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -9,19 +9,18 @@ import * as BackgroundFetch from "expo-background-fetch"; import { useRouter } from "expo-router"; import * as ScreenOrientation from "expo-screen-orientation"; import * as TaskManager from "expo-task-manager"; -import React, { useEffect } from "react"; -import { Linking, Switch, TouchableOpacity, ViewProps } from "react-native"; +import React, {useEffect, useMemo} from "react"; +import { Linking, Switch, TouchableOpacity } from "react-native"; import { toast } from "sonner-native"; -import * as DropdownMenu from "zeego/dropdown-menu"; import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; - -interface Props extends ViewProps {} +import DisabledSetting from "@/components/settings/DisabledSetting"; +import Dropdown from "@/components/common/Dropdown"; export const OtherSettings: React.FC = () => { const router = useRouter(); - const [settings, updateSettings] = useSettings(); + const [settings, updateSettings, pluginSettings] = useSettings(); /******************** * Background task @@ -53,146 +52,114 @@ export const OtherSettings: React.FC = () => { /********************** *********************/ + const disabled = useMemo(() => ( + pluginSettings?.autoRotate?.locked === true && + pluginSettings?.defaultVideoOrientation?.locked === true && + pluginSettings?.safeAreaInControlsEnabled?.locked === true && + pluginSettings?.showCustomMenuLinks?.locked === true && + pluginSettings?.hiddenLibraries?.locked === true && + pluginSettings?.disableHapticFeedback?.locked === true + ), [pluginSettings]); + + const orientations = [ + ScreenOrientation.OrientationLock.DEFAULT, + ScreenOrientation.OrientationLock.PORTRAIT_UP, + ScreenOrientation.OrientationLock.LANDSCAPE_LEFT, + ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT + ] + if (!settings) return null; return ( - - - updateSettings({ autoRotate: value })} - /> - + + + + updateSettings({autoRotate: value})} + /> + - - - - - - {ScreenOrientationEnum[settings.defaultVideoOrientation]} - - - - - - Orientation - { - updateSettings({ - defaultVideoOrientation: - ScreenOrientation.OrientationLock.DEFAULT, - }); - }} - > - - { - ScreenOrientationEnum[ - ScreenOrientation.OrientationLock.DEFAULT - ] - } - - - { - updateSettings({ - defaultVideoOrientation: - ScreenOrientation.OrientationLock.PORTRAIT_UP, - }); - }} - > - - { - ScreenOrientationEnum[ - ScreenOrientation.OrientationLock.PORTRAIT_UP - ] - } - - - { - updateSettings({ - defaultVideoOrientation: - ScreenOrientation.OrientationLock.LANDSCAPE_LEFT, - }); - }} - > - - { - ScreenOrientationEnum[ - ScreenOrientation.OrientationLock.LANDSCAPE_LEFT - ] - } - - - { - updateSettings({ - defaultVideoOrientation: - ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT, - }); - }} - > - - { - ScreenOrientationEnum[ - ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT - ] - } - - - - - + + + ScreenOrientationEnum[item] + } + title={ + + + {ScreenOrientationEnum[settings.defaultVideoOrientation]} + + + + } + label="Orientation" + onSelected={(defaultVideoOrientation) => + updateSettings({defaultVideoOrientation}) + } + /> + - - - updateSettings({ safeAreaInControlsEnabled: value }) - } - /> - + + + updateSettings({safeAreaInControlsEnabled: value}) + } + /> + - - Linking.openURL( - "https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links" - ) - } - > - - updateSettings({ showCustomMenuLinks: value }) + + Linking.openURL( + "https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links" + ) } + > + + updateSettings({showCustomMenuLinks: value}) + } + /> + + router.push("/settings/hide-libraries/page")} + title="Hide Libraries" + showArrow /> - - router.push("/settings/hide-libraries/page")} - title="Hide Libraries" - showArrow - /> - - - updateSettings({ disableHapticFeedback: value }) - } - /> - - + + + updateSettings({disableHapticFeedback}) + } + /> + + + ); }; From 1727125ea76ae6c943ccad7f6aa64c404a892161 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Fri, 10 Jan 2025 23:52:58 -0500 Subject: [PATCH 11/12] feat: [StreamyfinPlugin] Popular Plugin settings --- .../(home)/settings/popular-lists/page.tsx | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/settings/popular-lists/page.tsx b/app/(auth)/(tabs)/(home)/settings/popular-lists/page.tsx index 43cf76c4..c7d66e75 100644 --- a/app/(auth)/(tabs)/(home)/settings/popular-lists/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/popular-lists/page.tsx @@ -9,6 +9,8 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useNavigation } from "expo-router"; import { useAtom } from "jotai"; import { Linking, Switch, View } from "react-native"; +import {useMemo} from "react"; +import DisabledSetting from "@/components/settings/DisabledSetting"; export default function page() { const navigation = useNavigation(); @@ -16,7 +18,7 @@ export default function page() { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const [settings, updateSettings] = useSettings(); + const [settings, updateSettings, pluginSettings] = useSettings(); const handleOpenLink = () => { Linking.openURL( @@ -48,13 +50,22 @@ export default function page() { staleTime: 0, }); + const disabled = useMemo(() => ( + pluginSettings?.usePopularPlugin?.locked === true && + pluginSettings?.mediaListCollectionIds?.locked === true + ), [pluginSettings]); + if (!settings) return null; return ( - + { updateSettings({ usePopularPlugin: true }); queryClient.invalidateQueries({ queryKey: ["search"] }); @@ -62,9 +73,10 @@ export default function page() { > { - updateSettings({ usePopularPlugin: value }); - }} + disabled={pluginSettings?.usePopularPlugin?.locked} + onValueChange={(usePopularPlugin) => + updateSettings({ usePopularPlugin }) + } /> @@ -88,11 +100,14 @@ export default function page() { <> {mediaListCollections?.map((mlc) => ( - + { if (!settings.mediaListCollectionIds) { updateSettings({ @@ -130,6 +145,6 @@ export default function page() { )} )} - + ); } From d0ae63235daddc59626b309589a292629ee547bf Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Sat, 11 Jan 2025 00:15:56 -0500 Subject: [PATCH 12/12] feat: [StreamyfinPlugin] Library Options settings --- app/(auth)/(tabs)/(libraries)/_layout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/(auth)/(tabs)/(libraries)/_layout.tsx b/app/(auth)/(tabs)/(libraries)/_layout.tsx index 17813ed1..439e41df 100644 --- a/app/(auth)/(tabs)/(libraries)/_layout.tsx +++ b/app/(auth)/(tabs)/(libraries)/_layout.tsx @@ -6,7 +6,7 @@ import { Platform } from "react-native"; import * as DropdownMenu from "zeego/dropdown-menu"; export default function IndexLayout() { - const [settings, updateSettings] = useSettings(); + const [settings, updateSettings, pluginSettings] = useSettings(); if (!settings?.libraryOptions) return null; @@ -25,6 +25,7 @@ export default function IndexLayout() { headerTransparent: Platform.OS === "ios" ? true : false, headerShadowVisible: false, headerRight: () => ( + !pluginSettings?.libraryOptions?.locked &&