Merge branch 'develop' into feat/i18n

This commit is contained in:
Simon Caron
2025-01-16 20:38:00 -05:00
7 changed files with 113 additions and 53 deletions

View File

@@ -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. 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. [Streamyfin Plugin](https://github.com/streamyfin/jellyfin-plugin-streamyfin)
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.
### Jellysearch ### Jellysearch

View File

@@ -62,7 +62,13 @@ export default function index() {
const user = useAtomValue(userAtom); const user = useAtomValue(userAtom);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [settings, _] = useSettings(); const [
settings,
updateSettings,
pluginSettings,
setPluginSettings,
refreshStreamyfinPluginSettings,
] = useSettings();
const [isConnected, setIsConnected] = useState<boolean | null>(null); const [isConnected, setIsConnected] = useState<boolean | null>(null);
const [loadingRetry, setLoadingRetry] = useState(false); const [loadingRetry, setLoadingRetry] = useState(false);
@@ -113,6 +119,7 @@ export default function index() {
cleanCacheDirectory().catch((e) => cleanCacheDirectory().catch((e) =>
console.error("Something went wrong cleaning cache directory") console.error("Something went wrong cleaning cache directory")
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
@@ -157,6 +164,7 @@ export default function index() {
const refetch = useCallback(async () => { const refetch = useCallback(async () => {
setLoading(true); setLoading(true);
await refreshStreamyfinPluginSettings();
await invalidateCache(); await invalidateCache();
setLoading(false); setLoading(false);
}, []); }, []);
@@ -192,7 +200,7 @@ export default function index() {
); );
let sections: Section[] = []; let sections: Section[] = [];
if (settings?.home === null || settings?.home?.sections === null) { if (!settings?.home || !settings?.home?.sections) {
sections = useMemo(() => { sections = useMemo(() => {
if (!api || !user?.Id) return []; if (!api || !user?.Id) return [];
@@ -303,20 +311,33 @@ export default function index() {
const section = settings.home?.sections[key]; const section = settings.home?.sections[key];
ss.push({ ss.push({
title: key, title: key,
queryKey: ["home", key, user?.Id], queryKey: ["home", key],
queryFn: async () => queryFn: async () => {
( if (section.items) {
await getItemsApi(api).getItems({ const response = await getItemsApi(api).getItems({
userId: user?.Id, userId: user?.Id,
limit: section.items?.limit || 20, limit: section.items?.limit || 25,
recursive: true, recursive: true,
includeItemTypes: section.items?.includeItemTypes, includeItemTypes: section.items?.includeItemTypes,
sortBy: section.items?.sortBy, sortBy: section.items?.sortBy,
sortOrder: section.items?.sortOrder, sortOrder: section.items?.sortOrder,
filters: section.items?.filters, filters: section.items?.filters,
parentId: section.items?.parentId, 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", type: "ScrollingCollectionList",
orientation: section?.orientation || "vertical", orientation: section?.orientation || "vertical",
}); });

View File

@@ -49,6 +49,11 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
else else
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`; 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]); }, [item]);
const progress = useMemo(() => { const progress = useMemo(() => {

View File

@@ -107,7 +107,12 @@ export const ScrollingCollectionList: React.FC<Props> = ({
{item.Type === "Movie" && orientation === "vertical" && ( {item.Type === "Movie" && orientation === "vertical" && (
<MoviePoster item={item} /> <MoviePoster item={item} />
)} )}
{item.Type === "Series" && <SeriesPoster item={item} />} {item.Type === "Series" && orientation === "vertical" && (
<SeriesPoster item={item} />
)}
{item.Type === "Series" && orientation === "horizontal" && (
<ContinueWatchingPoster item={item} />
)}
{item.Type === "Program" && ( {item.Type === "Program" && (
<ContinueWatchingPoster item={item} /> <ContinueWatchingPoster item={item} />
)} )}

View File

@@ -9,7 +9,7 @@ import * as BackgroundFetch from "expo-background-fetch";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation"; import * as ScreenOrientation from "expo-screen-orientation";
import * as TaskManager from "expo-task-manager"; 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 { Linking, Switch, TouchableOpacity } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
@@ -55,28 +55,28 @@ export const OtherSettings: React.FC = () => {
/********************** /**********************
*********************/ *********************/
const disabled = useMemo(() => ( const disabled = useMemo(
pluginSettings?.autoRotate?.locked === true && () =>
pluginSettings?.defaultVideoOrientation?.locked === true && pluginSettings?.autoRotate?.locked === true &&
pluginSettings?.safeAreaInControlsEnabled?.locked === true && pluginSettings?.defaultVideoOrientation?.locked === true &&
pluginSettings?.showCustomMenuLinks?.locked === true && pluginSettings?.safeAreaInControlsEnabled?.locked === true &&
pluginSettings?.hiddenLibraries?.locked === true && pluginSettings?.showCustomMenuLinks?.locked === true &&
pluginSettings?.disableHapticFeedback?.locked === true pluginSettings?.hiddenLibraries?.locked === true &&
), [pluginSettings]); pluginSettings?.disableHapticFeedback?.locked === true,
[pluginSettings]
);
const orientations = [ const orientations = [
ScreenOrientation.OrientationLock.DEFAULT, ScreenOrientation.OrientationLock.DEFAULT,
ScreenOrientation.OrientationLock.PORTRAIT_UP, ScreenOrientation.OrientationLock.PORTRAIT_UP,
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT, ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
] ];
if (!settings) return null; if (!settings) return null;
return ( return (
<DisabledSetting <DisabledSetting disabled={disabled}>
disabled={disabled}
>
<ListGroup title={t("home.settings.other.other_title")} className=""> <ListGroup title={t("home.settings.other.other_title")} className="">
<ListItem <ListItem
title={t("home.settings.other.auto_rotate")} title={t("home.settings.other.auto_rotate")}
@@ -85,32 +85,40 @@ export const OtherSettings: React.FC = () => {
<Switch <Switch
value={settings.autoRotate} value={settings.autoRotate}
disabled={pluginSettings?.autoRotate?.locked} disabled={pluginSettings?.autoRotate?.locked}
onValueChange={(value) => updateSettings({autoRotate: value})} onValueChange={(value) => updateSettings({ autoRotate: value })}
/> />
</ListItem> </ListItem>
<ListItem <ListItem
title={t("home.settings.other.video_orientation")} title={t("home.settings.other.video_orientation")}
disabled={pluginSettings?.defaultVideoOrientation?.locked || settings.autoRotate} disabled={
pluginSettings?.defaultVideoOrientation?.locked ||
settings.autoRotate
}
> >
<Dropdown <Dropdown
data={orientations} data={orientations}
disabled={pluginSettings?.defaultVideoOrientation?.locked || settings.autoRotate} disabled={
keyExtractor={String} pluginSettings?.defaultVideoOrientation?.locked ||
titleExtractor={(item) => settings.autoRotate
t(ScreenOrientationEnum[item])
} }
keyExtractor={String}
titleExtractor={(item) => ScreenOrientationEnum[item]}
title={ title={
<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]">
{t(ScreenOrientationEnum[settings.defaultVideoOrientation])} {t(ScreenOrientationEnum[settings.defaultVideoOrientation])}
</Text> </Text>
<Ionicons name="chevron-expand-sharp" size={18} color="#5A5960"/> <Ionicons
name="chevron-expand-sharp"
size={18}
color="#5A5960"
/>
</TouchableOpacity> </TouchableOpacity>
} }
label={t("home.settings.other.orientation")} label={t("home.settings.other.orientation")}
onSelected={(defaultVideoOrientation) => onSelected={(defaultVideoOrientation) =>
updateSettings({defaultVideoOrientation}) updateSettings({ defaultVideoOrientation })
} }
/> />
</ListItem> </ListItem>
@@ -123,7 +131,7 @@ export const OtherSettings: React.FC = () => {
value={settings.safeAreaInControlsEnabled} value={settings.safeAreaInControlsEnabled}
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked} disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
onValueChange={(value) => onValueChange={(value) =>
updateSettings({safeAreaInControlsEnabled: value}) updateSettings({ safeAreaInControlsEnabled: value })
} }
/> />
</ListItem> </ListItem>
@@ -141,7 +149,7 @@ export const OtherSettings: React.FC = () => {
value={settings.showCustomMenuLinks} value={settings.showCustomMenuLinks}
disabled={pluginSettings?.showCustomMenuLinks?.locked} disabled={pluginSettings?.showCustomMenuLinks?.locked}
onValueChange={(value) => onValueChange={(value) =>
updateSettings({showCustomMenuLinks: value}) updateSettings({ showCustomMenuLinks: value })
} }
/> />
</ListItem> </ListItem>
@@ -158,7 +166,7 @@ export const OtherSettings: React.FC = () => {
value={settings.disableHapticFeedback} value={settings.disableHapticFeedback}
disabled={pluginSettings?.disableHapticFeedback?.locked} disabled={pluginSettings?.disableHapticFeedback?.locked}
onValueChange={(disableHapticFeedback) => onValueChange={(disableHapticFeedback) =>
updateSettings({disableHapticFeedback}) updateSettings({ disableHapticFeedback })
} }
/> />
</ListItem> </ListItem>

View File

@@ -177,6 +177,14 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
useInterval(pollQuickConnect, isPolling ? 1000 : null); useInterval(pollQuickConnect, isPolling ? 1000 : null);
useEffect(() => {
(async () => {
await refreshStreamyfinPluginSettings();
})();
}, []);
useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min
const discoverServers = async (url: string): Promise<Server[]> => { const discoverServers = async (url: string): Promise<Server[]> => {
const servers = await jellyfin?.discovery.getRecommendedServerCandidates( const servers = await jellyfin?.discovery.getRecommendedServerCandidates(
url url

View File

@@ -80,6 +80,7 @@ export type Home = {
export type HomeSection = { export type HomeSection = {
orientation?: "horizontal" | "vertical"; orientation?: "horizontal" | "vertical";
items?: HomeSectionItemResolver; items?: HomeSectionItemResolver;
nextUp?: HomeSectionNextUpResolver;
}; };
export type HomeSectionItemResolver = { export type HomeSectionItemResolver = {
@@ -92,6 +93,13 @@ export type HomeSectionItemResolver = {
filters?: Array<ItemFilter>; filters?: Array<ItemFilter>;
}; };
export type HomeSectionNextUpResolver = {
parentId?: string;
limit?: number;
enableResumable?: boolean;
enableRewatching?: boolean;
};
export type Settings = { export type Settings = {
home?: Home | null; home?: Home | null;
autoRotate?: boolean; autoRotate?: boolean;
@@ -191,7 +199,14 @@ const loadSettings = (): Settings => {
} }
}; };
const EXCLUDE_FROM_SAVE = ["home"];
const saveSettings = (settings: Settings) => { 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); const jsonValue = JSON.stringify(settings);
storage.set("settings", jsonValue); storage.set("settings", jsonValue);
}; };
@@ -223,11 +238,13 @@ export const useSettings = () => {
const refreshStreamyfinPluginSettings = useCallback(async () => { const refreshStreamyfinPluginSettings = useCallback(async () => {
if (!api) return; if (!api) return;
const settings = await api const settings = await api.getStreamyfinPluginConfig().then(
.getStreamyfinPluginConfig() ({ data }) => {
.then(({ data }) => data?.settings); writeInfoLog(`Got remote settings`);
return data?.settings;
writeInfoLog(`Got remote settings: ${JSON.stringify(settings)}`); },
(err) => undefined
);
setPluginSettings(settings); setPluginSettings(settings);
return settings; return settings;
@@ -277,6 +294,7 @@ export const useSettings = () => {
if (Object.keys(unlockedPluginDefaults).length > 0) { if (Object.keys(unlockedPluginDefaults).length > 0) {
updateSettings(unlockedPluginDefaults); updateSettings(unlockedPluginDefaults);
} }
return { return {
..._settings, ..._settings,
...overrideSettings, ...overrideSettings,