Merge develop

This commit is contained in:
Simon Caron
2025-01-13 21:18:37 -05:00
parent cd8aba32d8
commit 5703279b46
9 changed files with 258 additions and 386 deletions

View File

@@ -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;
};

View File

@@ -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;