diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx
index 95cfd856..0675f12f 100644
--- a/app/(auth)/(tabs)/(home)/index.tsx
+++ b/app/(auth)/(tabs)/(home)/index.tsx
@@ -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 (
Oops!
@@ -365,7 +374,7 @@ export default function index() {
);
- if (l1 || l2)
+ if (l1)
return (
diff --git a/app/(auth)/(tabs)/(home)/settings/popular-lists/page.tsx b/app/(auth)/(tabs)/(home)/settings/popular-lists/page.tsx
deleted file mode 100644
index 6dfcb3c8..00000000
--- a/app/(auth)/(tabs)/(home)/settings/popular-lists/page.tsx
+++ /dev/null
@@ -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 (
-
-
- {
- updateSettings({ usePopularPlugin: true });
- queryClient.invalidateQueries({ queryKey: ["search"] });
- }}
- >
-
- updateSettings({ usePopularPlugin })
- }
- />
-
-
-
- Popular Lists is a plugin that enables you to show custom Jellyfin lists
- on the Streamyfin home page.{" "}
-
- Read more about Popular Lists.
-
-
-
- {settings.usePopularPlugin && (
- <>
- {!isLoadingMediaListCollections ? (
- <>
- {mediaListCollections?.length === 0 ? (
-
- No collections found. Add some in Jellyfin.
-
- ) : (
- <>
-
- {mediaListCollections?.map((mlc) => (
-
- {
- 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!,
- ],
- });
- }}
- />
-
- ))}
-
-
- Select the lists you want displayed on the home screen.
-
- >
- )}
- >
- ) : (
-
- )}
- >
- )}
-
- );
-}
diff --git a/components/settings/PluginSettings.tsx b/components/settings/PluginSettings.tsx
index f611d1a6..a4ac9e18 100644
--- a/components/settings/PluginSettings.tsx
+++ b/components/settings/PluginSettings.tsx
@@ -24,11 +24,6 @@ export const PluginSettings = () => {
title="Marlin Search"
showArrow
/>
- router.push("/settings/popular-lists/page")}
- title="Popular Lists"
- showArrow
- />
);
diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts
index d30ccf30..a16c51a6 100644
--- a/utils/atoms/settings.ts
+++ b/utils/atoms/settings.ts
@@ -6,8 +6,13 @@ import { Platform } from "react-native";
import {
CultureDto,
SubtitlePlaybackMode,
+ ItemSortBy,
+ SortOrder,
+ BaseItemKind,
+ ItemFilter,
} from "@jellyfin/sdk/lib/generated-client";
import { apiAtom } from "@/providers/JellyfinProvider";
+import { writeInfoLog } from "@/utils/log";
const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004";
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS";
@@ -68,10 +73,29 @@ export enum DownloadMethod {
Optimized = "optimized",
}
+export type Home = {
+ sections: [Object];
+};
+
+export type HomeSection = {
+ orientation?: "horizontal" | "vertical";
+ items?: HomeSectionItemResolver;
+};
+
+export type HomeSectionItemResolver = {
+ sortBy?: Array;
+ sortOrder?: Array;
+ includeItemTypes?: Array;
+ genres?: Array;
+ parentId?: string;
+ limit?: number;
+ filters?: Array;
+};
+
export type Settings = {
+ home?: Home | null;
autoRotate?: boolean;
forceLandscapeInVideoPlayer?: boolean;
- usePopularPlugin?: boolean;
deviceProfile?: "Expo" | "Native" | "Old";
mediaListCollectionIds?: string[];
searchEngine: "Marlin" | "Jellyfin";
@@ -115,9 +139,9 @@ export type StreamyfinPluginConfig = {
const loadSettings = (): Settings => {
const defaultValues: Settings = {
+ home: null,
autoRotate: true,
forceLandscapeInVideoPlayer: false,
- usePopularPlugin: false,
deviceProfile: "Expo",
mediaListCollectionIds: [],
searchEngine: "Jellyfin",
@@ -201,6 +225,8 @@ export const useSettings = () => {
.getStreamyfinPluginConfig()
.then(({ data }) => data?.settings);
+ writeInfoLog(`Got remote settings: ${JSON.stringify(settings)}`);
+
setPluginSettings(settings);
return settings;
}, [api]);
diff --git a/utils/log.tsx b/utils/log.tsx
index 7c432406..45999062 100644
--- a/utils/log.tsx
+++ b/utils/log.tsx
@@ -1,7 +1,7 @@
import { atomWithStorage, createJSONStorage } from "jotai/utils";
import { storage } from "./mmkv";
-import {useQuery} from "@tanstack/react-query";
-import React, {createContext, useContext} from "react";
+import { useQuery } from "@tanstack/react-query";
+import React, { createContext, useContext } from "react";
type LogLevel = "INFO" | "WARN" | "ERROR";
@@ -19,10 +19,12 @@ const mmkvStorage = createJSONStorage(() => ({
}));
const logsAtom = atomWithStorage("logs", [], mmkvStorage);
-const LogContext = createContext | null>(null);
-const DownloadContext = createContext | null>(null);
+const LogContext = createContext | null>(
+ null
+);
+const DownloadContext = createContext | null>(
+ null
+);
function useLogProvider() {
const { data: logs } = useQuery({
@@ -32,11 +34,10 @@ function useLogProvider() {
});
return {
- logs
- }
+ logs,
+ };
}
-
export const writeToLog = (level: LogLevel, message: string, data?: any) => {
const newEntry: LogEntry = {
timestamp: new Date().toISOString(),
@@ -53,10 +54,13 @@ export const writeToLog = (level: LogLevel, message: string, data?: any) => {
const recentLogs = logs.slice(Math.max(logs.length - maxLogs, 0));
storage.set("logs", JSON.stringify(recentLogs));
+ console.log(message);
};
-export const writeInfoLog = (message: string, data?: any) => writeToLog("INFO", message, data);
-export const writeErrorLog = (message: string, data?: any) => writeToLog("ERROR", message, data);
+export const writeInfoLog = (message: string, data?: any) =>
+ writeToLog("INFO", message, data);
+export const writeErrorLog = (message: string, data?: any) =>
+ writeToLog("ERROR", message, data);
export const readFromLog = (): LogEntry[] => {
const logs = storage.getString("logs");
@@ -75,14 +79,10 @@ export function useLog() {
return context;
}
-export function LogProvider({children}: { children: React.ReactNode }) {
+export function LogProvider({ children }: { children: React.ReactNode }) {
const provider = useLogProvider();
- return (
-
- {children}
-
- )
+ return {children};
}
export default logsAtom;