mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-22 16:54:42 +01:00
feat: Custom homescreen support (#424)
This commit is contained in:
@@ -8,7 +8,7 @@ import { Colors } from "@/constants/Colors";
|
||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { HomeSectionStyle, useSettings } from "@/utils/atoms/settings";
|
||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import {
|
||||
@@ -141,29 +141,6 @@ export default function index() {
|
||||
[data, settings?.hiddenLibraries]
|
||||
);
|
||||
|
||||
const {
|
||||
data: mediaListCollections,
|
||||
isError: e2,
|
||||
isLoading: l2,
|
||||
} = useQuery({
|
||||
queryKey: ["home", "sf_promoted", user?.Id, settings?.usePopularPlugin],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id) return [];
|
||||
|
||||
const response = await getItemsApi(api).getItems({
|
||||
userId: user.Id,
|
||||
tags: ["sf_promoted"],
|
||||
recursive: true,
|
||||
fields: ["Tags"],
|
||||
includeItemTypes: ["BoxSet"],
|
||||
});
|
||||
|
||||
return response.data.Items || [];
|
||||
},
|
||||
enabled: !!api && !!user?.Id && settings?.usePopularPlugin === true,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
const collections = useMemo(() => {
|
||||
const allow = ["movies", "tvshows"];
|
||||
return (
|
||||
@@ -211,107 +188,139 @@ export default function index() {
|
||||
[api, user?.Id]
|
||||
);
|
||||
|
||||
const sections = useMemo(() => {
|
||||
if (!api || !user?.Id) return [];
|
||||
let sections: Section[] = [];
|
||||
if (settings?.home === null || settings?.home?.sections === null) {
|
||||
sections = useMemo(() => {
|
||||
if (!api || !user?.Id) return [];
|
||||
|
||||
const latestMediaViews = collections.map((c) => {
|
||||
const includeItemTypes: BaseItemKind[] =
|
||||
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
|
||||
const title = "Recently Added in " + c.Name;
|
||||
const queryKey = [
|
||||
"home",
|
||||
"recentlyAddedIn" + c.CollectionType,
|
||||
user?.Id!,
|
||||
c.Id!,
|
||||
];
|
||||
return createCollectionConfig(
|
||||
title || "",
|
||||
queryKey,
|
||||
includeItemTypes,
|
||||
c.Id
|
||||
);
|
||||
});
|
||||
const latestMediaViews = collections.map((c) => {
|
||||
const includeItemTypes: BaseItemKind[] =
|
||||
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
|
||||
const title = "Recently Added in " + c.Name;
|
||||
const queryKey = [
|
||||
"home",
|
||||
"recentlyAddedIn" + c.CollectionType,
|
||||
user?.Id!,
|
||||
c.Id!,
|
||||
];
|
||||
return createCollectionConfig(
|
||||
title || "",
|
||||
queryKey,
|
||||
includeItemTypes,
|
||||
c.Id
|
||||
);
|
||||
});
|
||||
|
||||
const ss: Section[] = [
|
||||
{
|
||||
title: "Continue Watching",
|
||||
queryKey: ["home", "resumeItems"],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await getItemsApi(api).getResumeItems({
|
||||
userId: user.Id,
|
||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||
includeItemTypes: ["Movie", "Series", "Episode"],
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "horizontal",
|
||||
},
|
||||
{
|
||||
title: "Next Up",
|
||||
queryKey: ["home", "nextUp-all"],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await getTvShowsApi(api).getNextUp({
|
||||
userId: user?.Id,
|
||||
fields: ["MediaSourceCount"],
|
||||
limit: 20,
|
||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||
enableResumable: false,
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "horizontal",
|
||||
},
|
||||
...latestMediaViews,
|
||||
...(mediaListCollections?.map(
|
||||
(ml) =>
|
||||
({
|
||||
title: ml.Name,
|
||||
queryKey: ["home", "mediaList", ml.Id!],
|
||||
queryFn: async () => ml,
|
||||
type: "MediaListSection",
|
||||
orientation: "vertical",
|
||||
} as Section)
|
||||
) || []),
|
||||
{
|
||||
title: "Suggested Movies",
|
||||
queryKey: ["home", "suggestedMovies", user?.Id],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await getSuggestionsApi(api).getSuggestions({
|
||||
userId: user?.Id,
|
||||
limit: 10,
|
||||
mediaType: ["Video"],
|
||||
type: ["Movie"],
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "vertical",
|
||||
},
|
||||
{
|
||||
title: "Suggested Episodes",
|
||||
queryKey: ["home", "suggestedEpisodes", user?.Id],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const suggestions = await getSuggestions(api, user.Id);
|
||||
const nextUpPromises = suggestions.map((series) =>
|
||||
getNextUp(api, user.Id, series.Id)
|
||||
);
|
||||
const nextUpResults = await Promise.all(nextUpPromises);
|
||||
|
||||
return nextUpResults.filter((item) => item !== null) || [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
return [];
|
||||
}
|
||||
const ss: Section[] = [
|
||||
{
|
||||
title: "Continue Watching",
|
||||
queryKey: ["home", "resumeItems"],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await getItemsApi(api).getResumeItems({
|
||||
userId: user.Id,
|
||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||
includeItemTypes: ["Movie", "Series", "Episode"],
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "horizontal",
|
||||
},
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "horizontal",
|
||||
},
|
||||
];
|
||||
return ss;
|
||||
}, [api, user?.Id, collections, mediaListCollections]);
|
||||
{
|
||||
title: "Next Up",
|
||||
queryKey: ["home", "nextUp-all"],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await getTvShowsApi(api).getNextUp({
|
||||
userId: user?.Id,
|
||||
fields: ["MediaSourceCount"],
|
||||
limit: 20,
|
||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||
enableResumable: false,
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "horizontal",
|
||||
},
|
||||
...latestMediaViews,
|
||||
// ...(mediaListCollections?.map(
|
||||
// (ml) =>
|
||||
// ({
|
||||
// title: ml.Name,
|
||||
// queryKey: ["home", "mediaList", ml.Id!],
|
||||
// queryFn: async () => ml,
|
||||
// type: "MediaListSection",
|
||||
// orientation: "vertical",
|
||||
// } as Section)
|
||||
// ) || []),
|
||||
{
|
||||
title: "Suggested Movies",
|
||||
queryKey: ["home", "suggestedMovies", user?.Id],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await getSuggestionsApi(api).getSuggestions({
|
||||
userId: user?.Id,
|
||||
limit: 10,
|
||||
mediaType: ["Video"],
|
||||
type: ["Movie"],
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "vertical",
|
||||
},
|
||||
{
|
||||
title: "Suggested Episodes",
|
||||
queryKey: ["home", "suggestedEpisodes", user?.Id],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const suggestions = await getSuggestions(api, user.Id);
|
||||
const nextUpPromises = suggestions.map((series) =>
|
||||
getNextUp(api, user.Id, series.Id)
|
||||
);
|
||||
const nextUpResults = await Promise.all(nextUpPromises);
|
||||
|
||||
return nextUpResults.filter((item) => item !== null) || [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "horizontal",
|
||||
},
|
||||
];
|
||||
return ss;
|
||||
}, [api, user?.Id, collections]);
|
||||
} else {
|
||||
sections = useMemo(() => {
|
||||
if (!api || !user?.Id) return [];
|
||||
const ss: Section[] = [];
|
||||
|
||||
for (const key in settings.home?.sections) {
|
||||
const section = settings.home?.sections[key];
|
||||
ss.push({
|
||||
title: key,
|
||||
queryKey: ["home", key, user?.Id],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await getItemsApi(api).getItems({
|
||||
userId: user?.Id,
|
||||
limit: section.items?.limit || 20,
|
||||
recursive: true,
|
||||
includeItemTypes: section.items?.includeItemTypes,
|
||||
sortBy: section.items?.sortBy,
|
||||
sortOrder: section.items?.sortOrder,
|
||||
filters: section.items?.filters,
|
||||
parentId: section.items?.parentId,
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: section?.orientation || "vertical",
|
||||
});
|
||||
}
|
||||
return ss;
|
||||
}, [api, user?.Id, settings.home?.sections]);
|
||||
}
|
||||
|
||||
if (isConnected === false) {
|
||||
return (
|
||||
@@ -355,7 +364,7 @@ export default function index() {
|
||||
);
|
||||
}
|
||||
|
||||
if (e1 || e2)
|
||||
if (e1)
|
||||
return (
|
||||
<View className="flex flex-col items-center justify-center h-full -mt-6">
|
||||
<Text className="text-3xl font-bold mb-2">Oops!</Text>
|
||||
@@ -365,7 +374,7 @@ export default function index() {
|
||||
</View>
|
||||
);
|
||||
|
||||
if (l1 || l2)
|
||||
if (l1)
|
||||
return (
|
||||
<View className="justify-center items-center h-full">
|
||||
<Loader />
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import { useMemo } from "react";
|
||||
import { Linking, Switch } from "react-native";
|
||||
|
||||
export default function page() {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
const [settings, updateSettings, pluginSettings] = useSettings();
|
||||
|
||||
const handleOpenLink = () => {
|
||||
Linking.openURL(
|
||||
"https://github.com/lostb1t/jellyfin-plugin-collection-import"
|
||||
);
|
||||
};
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data: mediaListCollections,
|
||||
isLoading: isLoadingMediaListCollections,
|
||||
} = useQuery({
|
||||
queryKey: ["sf_promoted", user?.Id, settings?.usePopularPlugin],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id) return [];
|
||||
|
||||
const response = await getItemsApi(api).getItems({
|
||||
userId: user.Id,
|
||||
tags: ["sf_promoted"],
|
||||
recursive: true,
|
||||
fields: ["Tags"],
|
||||
includeItemTypes: ["BoxSet"],
|
||||
});
|
||||
|
||||
return response.data.Items ?? [];
|
||||
},
|
||||
enabled: !!api && !!user?.Id && settings?.usePopularPlugin === true,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const disabled = useMemo(
|
||||
() =>
|
||||
pluginSettings?.usePopularPlugin?.locked === true &&
|
||||
pluginSettings?.mediaListCollectionIds?.locked === true,
|
||||
[pluginSettings]
|
||||
);
|
||||
|
||||
if (!settings) return null;
|
||||
|
||||
return (
|
||||
<DisabledSetting disabled={disabled} className="px-4 pt-4">
|
||||
<ListGroup title={"Enable plugin"} className="">
|
||||
<ListItem
|
||||
title={"Enable Popular Lists"}
|
||||
disabled={pluginSettings?.usePopularPlugin?.locked}
|
||||
onPress={() => {
|
||||
updateSettings({ usePopularPlugin: true });
|
||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
value={settings.usePopularPlugin}
|
||||
disabled={pluginSettings?.usePopularPlugin?.locked}
|
||||
onValueChange={(usePopularPlugin) =>
|
||||
updateSettings({ usePopularPlugin })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
||||
Popular Lists is a plugin that enables you to show custom Jellyfin lists
|
||||
on the Streamyfin home page.{" "}
|
||||
<Text className="text-blue-500" onPress={handleOpenLink}>
|
||||
Read more about Popular Lists.
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
{settings.usePopularPlugin && (
|
||||
<>
|
||||
{!isLoadingMediaListCollections ? (
|
||||
<>
|
||||
{mediaListCollections?.length === 0 ? (
|
||||
<Text className="text-xs opacity-50 p-4">
|
||||
No collections found. Add some in Jellyfin.
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<ListGroup title="Media List Collections" className="mt-4">
|
||||
{mediaListCollections?.map((mlc) => (
|
||||
<ListItem
|
||||
key={mlc.Id}
|
||||
title={mlc.Name}
|
||||
disabled={
|
||||
pluginSettings?.mediaListCollectionIds?.locked
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
disabled={
|
||||
pluginSettings?.mediaListCollectionIds?.locked
|
||||
}
|
||||
value={settings.mediaListCollectionIds?.includes(
|
||||
mlc.Id!
|
||||
)}
|
||||
onValueChange={(value) => {
|
||||
if (!settings.mediaListCollectionIds) {
|
||||
updateSettings({
|
||||
mediaListCollectionIds: [mlc.Id!],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateSettings({
|
||||
mediaListCollectionIds:
|
||||
settings.mediaListCollectionIds.includes(
|
||||
mlc.Id!
|
||||
)
|
||||
? settings.mediaListCollectionIds.filter(
|
||||
(id) => id !== mlc.Id
|
||||
)
|
||||
: [
|
||||
...settings.mediaListCollectionIds,
|
||||
mlc.Id!,
|
||||
],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</ListGroup>
|
||||
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
||||
Select the lists you want displayed on the home screen.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DisabledSetting>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user