diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 6d36f734..a38e5f84 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -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
diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx
index 4ed55a24..8ee52292 100644
--- a/app/(auth)/(tabs)/(home)/_layout.tsx
+++ b/app/(auth)/(tabs)/(home)/_layout.tsx
@@ -85,6 +85,12 @@ export default function IndexLayout() {
title: "",
}}
/>
+
{
- 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 (
{t("home.oops")}
@@ -366,7 +375,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 c44146db..00000000
--- a/app/(auth)/(tabs)/(home)/settings/popular-lists/page.tsx
+++ /dev/null
@@ -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 (
-
-
- {
- updateSettings({ usePopularPlugin: true });
- queryClient.invalidateQueries({ queryKey: ["search"] });
- }}
- >
-
- updateSettings({ usePopularPlugin })
- }
- />
-
-
-
- {t("home.settings.plugins.popular_lists.enable_popular_hint")}{" "}
-
- {t("home.settings.plugins.popular_lists.read_more_about_popular_lists")}
-
-
-
- {settings.usePopularPlugin && (
- <>
- {!isLoadingMediaListCollections ? (
- <>
- {mediaListCollections?.length === 0 ? (
-
- {t("home.settings.plugins.popular_lists.no_collections_found")}
-
- ) : (
- <>
-
- {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!,
- ],
- });
- }}
- />
-
- ))}
-
-
- {t("home.settings.plugins.popular_lists.select_the_lists_you_want_to_display")}
-
- >
- )}
- >
- ) : (
-
- )}
- >
- )}
-
- );
-}
diff --git a/components/settings/PluginSettings.tsx b/components/settings/PluginSettings.tsx
index deb30cf4..caac1e33 100644
--- a/components/settings/PluginSettings.tsx
+++ b/components/settings/PluginSettings.tsx
@@ -27,11 +27,6 @@ export const PluginSettings = () => {
title="Marlin Search"
showArrow
/>
- router.push("/settings/popular-lists/page")}
- title="Popular Lists"
- showArrow
- />
);
diff --git a/translations/en.json b/translations/en.json
index e35b55b6..c73ebe23 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -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": {
diff --git a/translations/fr.json b/translations/fr.json
index c3b4001b..aa0ae115 100644
--- a/translations/fr.json
+++ b/translations/fr.json
@@ -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": {
diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts
index 7f9bba1e..527b3d63 100644
--- a/utils/atoms/settings.ts
+++ b/utils/atoms/settings.ts
@@ -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;
+ 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[];
preferedLanguage?: string;
@@ -107,19 +128,21 @@ export type Settings = {
export interface Lockable {
locked: boolean;
- value: T
+ value: T;
}
-export type PluginLockableSettings = { [K in keyof Settings]: Lockable };
+export type PluginLockableSettings = {
+ [K in keyof Settings]: Lockable;
+};
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(null);
-export const pluginSettingsAtom = atom(storage.get(STREAMYFIN_PLUGIN_SETTINGS));
+export const pluginSettingsAtom = atom(
+ storage.get(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) => {
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;
};
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;