mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
294 lines
8.2 KiB
TypeScript
294 lines
8.2 KiB
TypeScript
import { atom, useAtom } from "jotai";
|
|
import { useCallback, useEffect, useMemo } from "react";
|
|
import * as ScreenOrientation from "expo-screen-orientation";
|
|
import { storage } from "../mmkv";
|
|
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";
|
|
|
|
export type DownloadQuality = "original" | "high" | "low";
|
|
|
|
export type DownloadOption = {
|
|
label: string;
|
|
value: DownloadQuality;
|
|
};
|
|
|
|
export const ScreenOrientationEnum: Record<
|
|
ScreenOrientation.OrientationLock,
|
|
string
|
|
> = {
|
|
[ScreenOrientation.OrientationLock.DEFAULT]: "Default",
|
|
[ScreenOrientation.OrientationLock.ALL]: "All",
|
|
[ScreenOrientation.OrientationLock.PORTRAIT]: "Portrait",
|
|
[ScreenOrientation.OrientationLock.PORTRAIT_UP]: "Portrait Up",
|
|
[ScreenOrientation.OrientationLock.PORTRAIT_DOWN]: "Portrait Down",
|
|
[ScreenOrientation.OrientationLock.LANDSCAPE]: "Landscape",
|
|
[ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: "Landscape Left",
|
|
[ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: "Landscape Right",
|
|
[ScreenOrientation.OrientationLock.OTHER]: "Other",
|
|
[ScreenOrientation.OrientationLock.UNKNOWN]: "Unknown",
|
|
};
|
|
|
|
export const DownloadOptions: DownloadOption[] = [
|
|
{
|
|
label: "Original quality",
|
|
value: "original",
|
|
},
|
|
{
|
|
label: "High quality",
|
|
value: "high",
|
|
},
|
|
{
|
|
label: "Small file size",
|
|
value: "low",
|
|
},
|
|
];
|
|
|
|
export type LibraryOptions = {
|
|
display: "row" | "list";
|
|
cardStyle: "compact" | "detailed";
|
|
imageStyle: "poster" | "cover";
|
|
showTitles: boolean;
|
|
showStats: boolean;
|
|
};
|
|
|
|
export type DefaultLanguageOption = {
|
|
value: string;
|
|
label: string;
|
|
};
|
|
|
|
export enum DownloadMethod {
|
|
Remux = "remux",
|
|
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;
|
|
deviceProfile?: "Expo" | "Native" | "Old";
|
|
mediaListCollectionIds?: string[];
|
|
preferedLanguage?: string;
|
|
searchEngine: "Marlin" | "Jellyfin";
|
|
marlinServerUrl?: string;
|
|
openInVLC?: boolean;
|
|
downloadQuality?: DownloadOption;
|
|
libraryOptions: LibraryOptions;
|
|
defaultAudioLanguage: CultureDto | null;
|
|
playDefaultAudioTrack: boolean;
|
|
rememberAudioSelections: boolean;
|
|
defaultSubtitleLanguage: CultureDto | null;
|
|
subtitleMode: SubtitlePlaybackMode;
|
|
rememberSubtitleSelections: boolean;
|
|
showHomeTitles: boolean;
|
|
defaultVideoOrientation: ScreenOrientation.OrientationLock;
|
|
forwardSkipTime: number;
|
|
rewindSkipTime: number;
|
|
optimizedVersionsServerUrl?: string | null;
|
|
downloadMethod: DownloadMethod;
|
|
autoDownload: boolean;
|
|
showCustomMenuLinks: boolean;
|
|
disableHapticFeedback: boolean;
|
|
subtitleSize: number;
|
|
remuxConcurrentLimit: 1 | 2 | 3 | 4;
|
|
safeAreaInControlsEnabled: boolean;
|
|
jellyseerrServerUrl?: string;
|
|
hiddenLibraries?: string[];
|
|
};
|
|
|
|
export interface Lockable<T> {
|
|
locked: boolean;
|
|
value: T;
|
|
}
|
|
|
|
export type PluginLockableSettings = {
|
|
[K in keyof Settings]: Lockable<Settings[K]>;
|
|
};
|
|
export type StreamyfinPluginConfig = {
|
|
settings: PluginLockableSettings;
|
|
};
|
|
|
|
const loadSettings = (): Settings => {
|
|
const defaultValues: Settings = {
|
|
home: null,
|
|
autoRotate: true,
|
|
forceLandscapeInVideoPlayer: false,
|
|
deviceProfile: "Expo",
|
|
mediaListCollectionIds: [],
|
|
preferedLanguage: undefined,
|
|
searchEngine: "Jellyfin",
|
|
marlinServerUrl: "",
|
|
openInVLC: false,
|
|
downloadQuality: DownloadOptions[0],
|
|
libraryOptions: {
|
|
display: "list",
|
|
cardStyle: "detailed",
|
|
imageStyle: "cover",
|
|
showTitles: true,
|
|
showStats: true,
|
|
},
|
|
defaultAudioLanguage: null,
|
|
playDefaultAudioTrack: true,
|
|
rememberAudioSelections: true,
|
|
defaultSubtitleLanguage: null,
|
|
subtitleMode: SubtitlePlaybackMode.Default,
|
|
rememberSubtitleSelections: true,
|
|
showHomeTitles: true,
|
|
defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT,
|
|
forwardSkipTime: 30,
|
|
rewindSkipTime: 10,
|
|
optimizedVersionsServerUrl: null,
|
|
downloadMethod: DownloadMethod.Remux,
|
|
autoDownload: false,
|
|
showCustomMenuLinks: false,
|
|
disableHapticFeedback: false,
|
|
subtitleSize: Platform.OS === "ios" ? 60 : 100,
|
|
remuxConcurrentLimit: 1,
|
|
safeAreaInControlsEnabled: true,
|
|
jellyseerrServerUrl: undefined,
|
|
hiddenLibraries: [],
|
|
};
|
|
|
|
try {
|
|
const jsonValue = storage.getString("settings");
|
|
const loadedValues: Partial<Settings> =
|
|
jsonValue != null ? JSON.parse(jsonValue) : {};
|
|
|
|
return { ...defaultValues, ...loadedValues };
|
|
} catch (error) {
|
|
console.error("Failed to load settings:", error);
|
|
return defaultValues;
|
|
}
|
|
};
|
|
|
|
const saveSettings = (settings: Settings) => {
|
|
const jsonValue = JSON.stringify(settings);
|
|
storage.set("settings", jsonValue);
|
|
};
|
|
|
|
export const settingsAtom = atom<Settings | null>(null);
|
|
export const pluginSettingsAtom = atom(
|
|
storage.get<PluginLockableSettings>(STREAMYFIN_PLUGIN_SETTINGS)
|
|
);
|
|
|
|
export const useSettings = () => {
|
|
const [api] = useAtom(apiAtom);
|
|
const [_settings, setSettings] = useAtom(settingsAtom);
|
|
const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom);
|
|
|
|
useEffect(() => {
|
|
if (_settings === null) {
|
|
const loadedSettings = loadSettings();
|
|
setSettings(loadedSettings);
|
|
}
|
|
}, [_settings, setSettings]);
|
|
|
|
const setPluginSettings = useCallback(
|
|
(settings: PluginLockableSettings | undefined) => {
|
|
storage.setAny(STREAMYFIN_PLUGIN_SETTINGS, settings);
|
|
_setPluginSettings(settings);
|
|
},
|
|
[_setPluginSettings]
|
|
);
|
|
|
|
const refreshStreamyfinPluginSettings = useCallback(async () => {
|
|
if (!api) return;
|
|
const settings = await api
|
|
.getStreamyfinPluginConfig()
|
|
.then(({ data }) => data?.settings);
|
|
|
|
writeInfoLog(`Got remote settings: ${JSON.stringify(settings)}`);
|
|
|
|
setPluginSettings(settings);
|
|
return settings;
|
|
}, [api]);
|
|
|
|
const updateSettings = (update: Partial<Settings>) => {
|
|
if (settings) {
|
|
const newSettings = { ...settings, ...update };
|
|
|
|
setSettings(newSettings);
|
|
saveSettings(newSettings);
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
};
|