mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
Merge develop
This commit is contained in:
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -46,11 +46,8 @@ body:
|
||||
- 0.25.0
|
||||
- 0.24.0
|
||||
- 0.23.0
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
- 0.22.0
|
||||
- 0.21.0
|
||||
>>>>>>> Stashed changes
|
||||
- older
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -85,6 +85,12 @@ export default function IndexLayout() {
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="settings/logs/page"
|
||||
options={{
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="intro/page"
|
||||
options={{
|
||||
|
||||
@@ -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 {
|
||||
@@ -144,29 +144,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 (
|
||||
@@ -214,107 +191,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 = t("home.recently_added_in", {libraryName: 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 = t("home.recently_added_in", {libraryName: c.Name});
|
||||
const queryKey = [
|
||||
"home",
|
||||
"recentlyAddedIn" + c.CollectionType,
|
||||
user?.Id!,
|
||||
c.Id!,
|
||||
];
|
||||
return createCollectionConfig(
|
||||
title || "",
|
||||
queryKey,
|
||||
includeItemTypes,
|
||||
c.Id
|
||||
);
|
||||
});
|
||||
|
||||
const ss: Section[] = [
|
||||
{
|
||||
title: t("home.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: t("home.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: t("home.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: t("home.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: t("home.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: t("home.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: t("home.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: t("home.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 (
|
||||
@@ -358,7 +367,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">{t("home.oops")}</Text>
|
||||
@@ -366,7 +375,7 @@ export default function index() {
|
||||
</View>
|
||||
);
|
||||
|
||||
if (l1 || l2)
|
||||
if (l1)
|
||||
return (
|
||||
<View className="justify-center items-center h-full">
|
||||
<Loader />
|
||||
|
||||
@@ -1,154 +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 { useTranslation } from "react-i18next";
|
||||
import { Linking, Switch } from "react-native";
|
||||
|
||||
export default function page() {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
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={t("home.settings.plugins.popular_lists.enable_plugin")} className="">
|
||||
<ListItem
|
||||
title={t("home.settings.plugins.popular_lists.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">
|
||||
{t("home.settings.plugins.popular_lists.enable_popular_hint")}{" "}
|
||||
<Text className="text-blue-500" onPress={handleOpenLink}>
|
||||
{t("home.settings.plugins.popular_lists.read_more_about_popular_lists")}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
{settings.usePopularPlugin && (
|
||||
<>
|
||||
{!isLoadingMediaListCollections ? (
|
||||
<>
|
||||
{mediaListCollections?.length === 0 ? (
|
||||
<Text className="text-xs opacity-50 p-4">
|
||||
{t("home.settings.plugins.popular_lists.no_collections_found")}
|
||||
</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">
|
||||
{t("home.settings.plugins.popular_lists.select_the_lists_you_want_to_display")}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DisabledSetting>
|
||||
);
|
||||
}
|
||||
@@ -27,11 +27,6 @@ export const PluginSettings = () => {
|
||||
title="Marlin Search"
|
||||
showArrow
|
||||
/>
|
||||
<ListItem
|
||||
onPress={() => router.push("/settings/popular-lists/page")}
|
||||
title="Popular Lists"
|
||||
showArrow
|
||||
/>
|
||||
</ListGroup>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -159,14 +159,6 @@
|
||||
"toasts": {
|
||||
"saved": "Saved"
|
||||
}
|
||||
},
|
||||
"popular_lists": {
|
||||
"enable_plugin": "Enable plugin",
|
||||
"enable_popular_lists": "Enable Popular Lists",
|
||||
"enable_popular_hint": "Popular Lists is a plugin that enables you to show custom Jellyfin lists on the Streamyfin home page.",
|
||||
"read_more_about_popular_lists": "Read more about Popular Lists.",
|
||||
"no_collections_found": "No collections found. Add some in Jellyfin.",
|
||||
"select_the_lists_you_want_to_display": "Select the lists you want displayed on the home screen."
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
|
||||
@@ -159,14 +159,6 @@
|
||||
"toasts": {
|
||||
"saved": "Enregistré"
|
||||
}
|
||||
},
|
||||
"popular_lists": {
|
||||
"enable_plugin": "Activer le plugiciel",
|
||||
"enable_popular_lists": "Activer Popular Lists",
|
||||
"enable_popular_hint": "Popular Lists est un plugiciel qui affiche des listes populaires sur l'écran d'accueil.",
|
||||
"read_more_about_popular_lists": "Lisez-en plus sur Popular Lists.",
|
||||
"no_collections_found": "Aucune collection trouvée. Ajoutez-en dans Jellyfin.",
|
||||
"select_the_lists_you_want_to_display": "Sélectionnez les listes que vous voulez afficher sur l'écran d'accueil."
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { atom, useAtom } from "jotai";
|
||||
import {useCallback, useEffect, useMemo} from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import * as ScreenOrientation from "expo-screen-orientation";
|
||||
import { storage } from "../mmkv";
|
||||
import { Platform } from "react-native";
|
||||
import {
|
||||
CultureDto,
|
||||
PluginStatus,
|
||||
SubtitlePlaybackMode,
|
||||
ItemSortBy,
|
||||
SortOrder,
|
||||
BaseItemKind,
|
||||
ItemFilter,
|
||||
} 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 { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { writeInfoLog } from "@/utils/log";
|
||||
|
||||
const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004"
|
||||
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS"
|
||||
const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004";
|
||||
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS";
|
||||
|
||||
export type DownloadQuality = "original" | "high" | "low";
|
||||
|
||||
@@ -68,13 +70,32 @@ export type DefaultLanguageOption = {
|
||||
|
||||
export enum DownloadMethod {
|
||||
Remux = "remux",
|
||||
Optimized = "optimized"
|
||||
Optimized = "optimized",
|
||||
}
|
||||
|
||||
export type Home = {
|
||||
sections: [Object];
|
||||
};
|
||||
|
||||
export type HomeSection = {
|
||||
orientation?: "horizontal" | "vertical";
|
||||
items?: HomeSectionItemResolver;
|
||||
};
|
||||
|
||||
export type HomeSectionItemResolver = {
|
||||
sortBy?: Array<ItemSortBy>;
|
||||
sortOrder?: Array<SortOrder>;
|
||||
includeItemTypes?: Array<BaseItemKind>;
|
||||
genres?: Array<string>;
|
||||
parentId?: string;
|
||||
limit?: number;
|
||||
filters?: Array<ItemFilter>;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
home?: Home | null;
|
||||
autoRotate?: boolean;
|
||||
forceLandscapeInVideoPlayer?: boolean;
|
||||
usePopularPlugin?: boolean;
|
||||
deviceProfile?: "Expo" | "Native" | "Old";
|
||||
mediaListCollectionIds?: string[];
|
||||
preferedLanguage?: string;
|
||||
@@ -107,19 +128,21 @@ export type Settings = {
|
||||
|
||||
export interface Lockable<T> {
|
||||
locked: boolean;
|
||||
value: T
|
||||
value: T;
|
||||
}
|
||||
|
||||
export type PluginLockableSettings = { [K in keyof Settings]: Lockable<Settings[K]> };
|
||||
export type PluginLockableSettings = {
|
||||
[K in keyof Settings]: Lockable<Settings[K]>;
|
||||
};
|
||||
export type StreamyfinPluginConfig = {
|
||||
settings: PluginLockableSettings
|
||||
}
|
||||
settings: PluginLockableSettings;
|
||||
};
|
||||
|
||||
const loadSettings = (): Settings => {
|
||||
const defaultValues: Settings = {
|
||||
home: null,
|
||||
autoRotate: true,
|
||||
forceLandscapeInVideoPlayer: false,
|
||||
usePopularPlugin: false,
|
||||
deviceProfile: "Expo",
|
||||
mediaListCollectionIds: [],
|
||||
preferedLanguage: undefined,
|
||||
@@ -174,7 +197,9 @@ const saveSettings = (settings: Settings) => {
|
||||
};
|
||||
|
||||
export const settingsAtom = atom<Settings | null>(null);
|
||||
export const pluginSettingsAtom = atom(storage.get<PluginLockableSettings>(STREAMYFIN_PLUGIN_SETTINGS));
|
||||
export const pluginSettingsAtom = atom(
|
||||
storage.get<PluginLockableSettings>(STREAMYFIN_PLUGIN_SETTINGS)
|
||||
);
|
||||
|
||||
export const useSettings = () => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
@@ -188,62 +213,25 @@ export const useSettings = () => {
|
||||
}
|
||||
}, [_settings, setSettings]);
|
||||
|
||||
const setPluginSettings = useCallback((settings: PluginLockableSettings | undefined) => {
|
||||
storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings)
|
||||
_setPluginSettings(settings)
|
||||
const setPluginSettings = useCallback(
|
||||
(settings: PluginLockableSettings | undefined) => {
|
||||
storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings);
|
||||
_setPluginSettings(settings);
|
||||
},
|
||||
[_setPluginSettings]
|
||||
)
|
||||
);
|
||||
|
||||
const refreshStreamyfinPluginSettings = useCallback(
|
||||
async () => {
|
||||
if (!api)
|
||||
return
|
||||
const refreshStreamyfinPluginSettings = useCallback(async () => {
|
||||
if (!api) return;
|
||||
const settings = await api
|
||||
.getStreamyfinPluginConfig()
|
||||
.then(({ data }) => data?.settings);
|
||||
|
||||
const plugins = await getPluginsApi(api).getPlugins().then(({data}) => data);
|
||||
writeInfoLog(`Got remote settings: ${JSON.stringify(settings)}`);
|
||||
|
||||
if (plugins && plugins.length > 0) {
|
||||
const streamyfinPlugin = plugins.find(plugin => plugin.Id === STREAMYFIN_PLUGIN_ID);
|
||||
|
||||
if (!streamyfinPlugin || streamyfinPlugin.Status != PluginStatus.Active) {
|
||||
writeErrorLog(
|
||||
"Streamyfin plugin is currently not active.\n" +
|
||||
`Current status is: ${streamyfinPlugin?.Status}`
|
||||
);
|
||||
setPluginSettings(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = await api.getStreamyfinPluginConfig()
|
||||
.then(({data}) => data.settings)
|
||||
|
||||
setPluginSettings(settings);
|
||||
return settings;
|
||||
}
|
||||
},
|
||||
[api]
|
||||
)
|
||||
|
||||
// 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, setting]) => {
|
||||
if (setting) {
|
||||
const {value, locked} = setting
|
||||
acc = Object.assign(acc, {
|
||||
[key]: locked ? value : _settings?.[key as keyof Settings] ?? value
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, {} as Settings)
|
||||
|
||||
return {
|
||||
..._settings,
|
||||
...overrideSettings
|
||||
}
|
||||
}, [_settings, setSettings, pluginSettings, _setPluginSettings, setPluginSettings])
|
||||
setPluginSettings(settings);
|
||||
return settings;
|
||||
}, [api]);
|
||||
|
||||
const updateSettings = (update: Partial<Settings>) => {
|
||||
if (settings) {
|
||||
@@ -254,5 +242,52 @@ export const useSettings = () => {
|
||||
}
|
||||
};
|
||||
|
||||
return [settings, updateSettings, pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings] as const;
|
||||
// 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(() => {
|
||||
let unlockedPluginDefaults = {} as Settings;
|
||||
const overrideSettings = Object.entries(pluginSettings || {}).reduce(
|
||||
(acc, [key, setting]) => {
|
||||
if (setting) {
|
||||
const { value, locked } = setting;
|
||||
|
||||
// Make sure we override default settings with plugin settings when they are not locked.
|
||||
// Admin decided what users defaults should be and grants them the ability to change them too.
|
||||
if (
|
||||
locked === false &&
|
||||
value &&
|
||||
_settings?.[key as keyof Settings] !== value
|
||||
) {
|
||||
unlockedPluginDefaults = Object.assign(unlockedPluginDefaults, {
|
||||
[key as keyof Settings]: value,
|
||||
});
|
||||
}
|
||||
|
||||
acc = Object.assign(acc, {
|
||||
[key]: locked ? value : _settings?.[key as keyof Settings] ?? value,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Settings
|
||||
);
|
||||
|
||||
// Update settings with plugin defined defaults
|
||||
if (Object.keys(unlockedPluginDefaults).length > 0) {
|
||||
updateSettings(unlockedPluginDefaults);
|
||||
}
|
||||
return {
|
||||
..._settings,
|
||||
...overrideSettings,
|
||||
};
|
||||
}, [_settings, pluginSettings]);
|
||||
|
||||
return [
|
||||
settings,
|
||||
updateSettings,
|
||||
pluginSettings,
|
||||
setPluginSettings,
|
||||
refreshStreamyfinPluginSettings,
|
||||
] as const;
|
||||
};
|
||||
|
||||
@@ -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<ReturnType<typeof useLogProvider> | null>(null);
|
||||
const DownloadContext = createContext<ReturnType<
|
||||
typeof useLogProvider
|
||||
> | null>(null);
|
||||
const LogContext = createContext<ReturnType<typeof useLogProvider> | null>(
|
||||
null
|
||||
);
|
||||
const DownloadContext = createContext<ReturnType<typeof useLogProvider> | 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 (
|
||||
<LogContext.Provider value={provider}>
|
||||
{children}
|
||||
</LogContext.Provider>
|
||||
)
|
||||
return <LogContext.Provider value={provider}>{children}</LogContext.Provider>;
|
||||
}
|
||||
|
||||
export default logsAtom;
|
||||
|
||||
Reference in New Issue
Block a user