diff --git a/README.md b/README.md index c1abe08e..533dd65f 100644 --- a/README.md +++ b/README.md @@ -32,22 +32,17 @@ Downloading works by using ffmpeg to convert an HLS stream into a video file on Chromecast support is still in development, and we're working on improving it. Currently, it supports casting videos and audio, but we're working on adding support for subtitles and other features. -## Plugins +### Streamyfin Plugin -In Streamyfin we have built-in support for a few plugins. These plugins are not required to use Streamyfin, but they add some extra functionality. +The Jellyfin Plugin for Streamyfin is a plugin you install into Jellyfin that hold all settings for the client Streamyfin. This allows you to syncronize settings accross all your users, like: -### Collection rows +- Auto log in to Jellyseerr without the user having to do anythin +- Choose the default languages +- Set download method and search provider +- Customize homescreen +- And more... -Jellyfin collections can be shown as rows or carousel on the home screen. -The following tags can be added to a collection to provide this functionality. - -Available tags: - -- sf_promoted: will make the collection a row at home -- sf_carousel: will make the collection a carousel on home. - -A plugin exists to create collections based on external sources like mdblist. This make the automatic process of managing collections such as trending, most watched, etc. -See [Collection Import Plugin](https://github.com/lostb1t/jellyfin-plugin-collection-import) for more info. +[Streamyfin Plugin](https://github.com/streamyfin/jellyfin-plugin-streamyfin) ### Jellysearch diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index a9644f27..23f8872f 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -62,7 +62,13 @@ export default function index() { const user = useAtomValue(userAtom); const [loading, setLoading] = useState(false); - const [settings, _] = useSettings(); + const [ + settings, + updateSettings, + pluginSettings, + setPluginSettings, + refreshStreamyfinPluginSettings, + ] = useSettings(); const [isConnected, setIsConnected] = useState(null); const [loadingRetry, setLoadingRetry] = useState(false); @@ -113,6 +119,7 @@ export default function index() { cleanCacheDirectory().catch((e) => console.error("Something went wrong cleaning cache directory") ); + return () => { unsubscribe(); }; @@ -157,6 +164,7 @@ export default function index() { const refetch = useCallback(async () => { setLoading(true); + await refreshStreamyfinPluginSettings(); await invalidateCache(); setLoading(false); }, []); @@ -192,7 +200,7 @@ export default function index() { ); let sections: Section[] = []; - if (settings?.home === null || settings?.home?.sections === null) { + if (!settings?.home || !settings?.home?.sections) { sections = useMemo(() => { if (!api || !user?.Id) return []; @@ -303,20 +311,33 @@ export default function index() { const section = settings.home?.sections[key]; ss.push({ title: key, - queryKey: ["home", key, user?.Id], - queryFn: async () => - ( - await getItemsApi(api).getItems({ + queryKey: ["home", key], + queryFn: async () => { + if (section.items) { + const response = await getItemsApi(api).getItems({ userId: user?.Id, - limit: section.items?.limit || 20, + limit: section.items?.limit || 25, recursive: true, includeItemTypes: section.items?.includeItemTypes, sortBy: section.items?.sortBy, sortOrder: section.items?.sortOrder, filters: section.items?.filters, parentId: section.items?.parentId, - }) - ).data.Items || [], + }); + return response.data.Items || []; + } else if (section.nextUp) { + const response = await getTvShowsApi(api).getNextUp({ + userId: user?.Id, + fields: ["MediaSourceCount"], + limit: section.items?.limit || 25, + enableImageTypes: ["Primary", "Backdrop", "Thumb"], + enableResumable: section.items?.enableResumable || false, + enableRewatching: section.items?.enableRewatching || false, + }); + return response.data.Items || []; + } + return []; + }, type: "ScrollingCollectionList", orientation: section?.orientation || "vertical", }); diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx index eb000d45..a011d23e 100644 --- a/components/ContinueWatchingPoster.tsx +++ b/components/ContinueWatchingPoster.tsx @@ -49,6 +49,11 @@ const ContinueWatchingPoster: React.FC = ({ else return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`; } + + if (item.ImageTags?.["Thumb"]) + return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.["Thumb"]}`; + else + return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`; }, [item]); const progress = useMemo(() => { diff --git a/components/home/ScrollingCollectionList.tsx b/components/home/ScrollingCollectionList.tsx index be286463..6b4ef40c 100644 --- a/components/home/ScrollingCollectionList.tsx +++ b/components/home/ScrollingCollectionList.tsx @@ -107,7 +107,12 @@ export const ScrollingCollectionList: React.FC = ({ {item.Type === "Movie" && orientation === "vertical" && ( )} - {item.Type === "Series" && } + {item.Type === "Series" && orientation === "vertical" && ( + + )} + {item.Type === "Series" && orientation === "horizontal" && ( + + )} {item.Type === "Program" && ( )} diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index 51c92717..8ddbca48 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -9,7 +9,7 @@ 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, useMemo} from "react"; +import React, { useEffect, useMemo } from "react"; import { Linking, Switch, TouchableOpacity } from "react-native"; import { toast } from "sonner-native"; import { Text } from "../common/Text"; @@ -55,28 +55,28 @@ 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 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 - ] + ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT, + ]; if (!settings) return null; return ( - + { updateSettings({autoRotate: value})} + onValueChange={(value) => updateSettings({ autoRotate: value })} /> - t(ScreenOrientationEnum[item]) + disabled={ + pluginSettings?.defaultVideoOrientation?.locked || + settings.autoRotate } + keyExtractor={String} + titleExtractor={(item) => ScreenOrientationEnum[item]} title={ {t(ScreenOrientationEnum[settings.defaultVideoOrientation])} - + } label={t("home.settings.other.orientation")} onSelected={(defaultVideoOrientation) => - updateSettings({defaultVideoOrientation}) + updateSettings({ defaultVideoOrientation }) } /> @@ -123,7 +131,7 @@ export const OtherSettings: React.FC = () => { value={settings.safeAreaInControlsEnabled} disabled={pluginSettings?.safeAreaInControlsEnabled?.locked} onValueChange={(value) => - updateSettings({safeAreaInControlsEnabled: value}) + updateSettings({ safeAreaInControlsEnabled: value }) } /> @@ -141,7 +149,7 @@ export const OtherSettings: React.FC = () => { value={settings.showCustomMenuLinks} disabled={pluginSettings?.showCustomMenuLinks?.locked} onValueChange={(value) => - updateSettings({showCustomMenuLinks: value}) + updateSettings({ showCustomMenuLinks: value }) } /> @@ -158,7 +166,7 @@ export const OtherSettings: React.FC = () => { value={settings.disableHapticFeedback} disabled={pluginSettings?.disableHapticFeedback?.locked} onValueChange={(disableHapticFeedback) => - updateSettings({disableHapticFeedback}) + updateSettings({ disableHapticFeedback }) } /> diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 30bea359..dddebb10 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -177,6 +177,14 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ useInterval(pollQuickConnect, isPolling ? 1000 : null); + useEffect(() => { + (async () => { + await refreshStreamyfinPluginSettings(); + })(); + }, []); + + useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min + const discoverServers = async (url: string): Promise => { const servers = await jellyfin?.discovery.getRecommendedServerCandidates( url diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 8e70072f..a3eac716 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -80,6 +80,7 @@ export type Home = { export type HomeSection = { orientation?: "horizontal" | "vertical"; items?: HomeSectionItemResolver; + nextUp?: HomeSectionNextUpResolver; }; export type HomeSectionItemResolver = { @@ -92,6 +93,13 @@ export type HomeSectionItemResolver = { filters?: Array; }; +export type HomeSectionNextUpResolver = { + parentId?: string; + limit?: number; + enableResumable?: boolean; + enableRewatching?: boolean; +}; + export type Settings = { home?: Home | null; autoRotate?: boolean; @@ -191,7 +199,14 @@ const loadSettings = (): Settings => { } }; +const EXCLUDE_FROM_SAVE = ["home"]; + const saveSettings = (settings: Settings) => { + Object.keys(settings).forEach((key) => { + if (EXCLUDE_FROM_SAVE.includes(key)) { + delete settings[key as keyof Settings]; + } + }); const jsonValue = JSON.stringify(settings); storage.set("settings", jsonValue); }; @@ -223,11 +238,13 @@ export const useSettings = () => { const refreshStreamyfinPluginSettings = useCallback(async () => { if (!api) return; - const settings = await api - .getStreamyfinPluginConfig() - .then(({ data }) => data?.settings); - - writeInfoLog(`Got remote settings: ${JSON.stringify(settings)}`); + const settings = await api.getStreamyfinPluginConfig().then( + ({ data }) => { + writeInfoLog(`Got remote settings`); + return data?.settings; + }, + (err) => undefined + ); setPluginSettings(settings); return settings; @@ -277,6 +294,7 @@ export const useSettings = () => { if (Object.keys(unlockedPluginDefaults).length > 0) { updateSettings(unlockedPluginDefaults); } + return { ..._settings, ...overrideSettings,