mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-04 21:18:31 +01:00
Compare commits
3 Commits
v0.25.0
...
chore/expo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09189e125e | ||
|
|
1ac10d8f34 | ||
|
|
d5fe354986 |
@@ -18,7 +18,6 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Exp
|
|||||||
- 🔊 **Background audio**: Stream music in the background, even when locking the phone.
|
- 🔊 **Background audio**: Stream music in the background, even when locking the phone.
|
||||||
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.
|
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.
|
||||||
- 📡 **Chromecast** (Experimental): Cast your media to any Chromecast-enabled device.
|
- 📡 **Chromecast** (Experimental): Cast your media to any Chromecast-enabled device.
|
||||||
- 📡 **Settings management** (Experimental): Manage app settings for all your users with a JF plugin.
|
|
||||||
- 🤖 **Jellyseerr integration**: Request media directly in the app.
|
- 🤖 **Jellyseerr integration**: Request media directly in the app.
|
||||||
|
|
||||||
## 🧪 Experimental Features
|
## 🧪 Experimental Features
|
||||||
|
|||||||
24
app.json
24
app.json
@@ -7,6 +7,10 @@
|
|||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "streamyfin",
|
"scheme": "streamyfin",
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/images/splash.png",
|
||||||
|
"resizeMode": "contain"
|
||||||
|
},
|
||||||
"jsEngine": "hermes",
|
"jsEngine": "hermes",
|
||||||
"assetBundlePatterns": [
|
"assetBundlePatterns": [
|
||||||
"**/*"
|
"**/*"
|
||||||
@@ -73,10 +77,11 @@
|
|||||||
"useFrameworks": "static"
|
"useFrameworks": "static"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"compileSdkVersion": 35,
|
"android": {
|
||||||
"targetSdkVersion": 35,
|
"compileSdkVersion": 34,
|
||||||
"buildToolsVersion": "35.0.0",
|
"targetSdkVersion": 34,
|
||||||
"kotlinVersion": "2.0.21",
|
"buildToolsVersion": "34.0.0"
|
||||||
|
},
|
||||||
"minSdkVersion": 24,
|
"minSdkVersion": 24,
|
||||||
"usesCleartextTraffic": true,
|
"usesCleartextTraffic": true,
|
||||||
"packagingOptions": {
|
"packagingOptions": {
|
||||||
@@ -122,17 +127,6 @@
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"./plugins/withTrustLocalCerts.js"
|
"./plugins/withTrustLocalCerts.js"
|
||||||
],
|
|
||||||
[
|
|
||||||
"./plugins/withGradleProperties.js"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"expo-splash-screen",
|
|
||||||
{
|
|
||||||
"backgroundColor": "#2e2e2e",
|
|
||||||
"image": "./assets/images/StreamyFinFinal.png",
|
|
||||||
"imageWidth": 100
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { Feather } from "@expo/vector-icons";
|
|||||||
import { Stack, useRouter } from "expo-router";
|
import { Stack, useRouter } from "expo-router";
|
||||||
import { Platform, TouchableOpacity, View } from "react-native";
|
import { Platform, TouchableOpacity, View } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
const Chromecast = !Platform.isTV ? require("@/components/Chromecast") : null;
|
import { lazy } from "react";
|
||||||
|
// const Chromecast = !Platform.isTV ? require("@/components/Chromecast") : null;
|
||||||
|
const Chromecast = lazy(() => import("@/components/Chromecast"));
|
||||||
|
|
||||||
export default function IndexLayout() {
|
export default function IndexLayout() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -26,7 +28,7 @@ export default function IndexLayout() {
|
|||||||
<View className="flex flex-row items-center space-x-2">
|
<View className="flex flex-row items-center space-x-2">
|
||||||
{!Platform.isTV && (
|
{!Platform.isTV && (
|
||||||
<>
|
<>
|
||||||
<Chromecast.Chromecast />
|
<Chromecast />
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.push("/(auth)/settings");
|
router.push("/(auth)/settings");
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default function IndexLayout() {
|
|||||||
disabled={settings.libraryOptions.imageStyle === "poster"}
|
disabled={settings.libraryOptions.imageStyle === "poster"}
|
||||||
key="show-titles-option"
|
key="show-titles-option"
|
||||||
value={settings.libraryOptions.showTitles}
|
value={settings.libraryOptions.showTitles}
|
||||||
onValueChange={(newValue: string) => {
|
onValueChange={(newValue) => {
|
||||||
if (settings.libraryOptions.imageStyle === "poster")
|
if (settings.libraryOptions.imageStyle === "poster")
|
||||||
return;
|
return;
|
||||||
updateSettings({
|
updateSettings({
|
||||||
@@ -172,7 +172,7 @@ export default function IndexLayout() {
|
|||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
key="show-stats-option"
|
key="show-stats-option"
|
||||||
value={settings.libraryOptions.showStats}
|
value={settings.libraryOptions.showStats}
|
||||||
onValueChange={(newValue: string) => {
|
onValueChange={(newValue) => {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
libraryOptions: {
|
libraryOptions: {
|
||||||
...settings.libraryOptions,
|
...settings.libraryOptions,
|
||||||
|
|||||||
@@ -1,28 +1,8 @@
|
|||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import { SystemBars } from "react-native-edge-to-edge";
|
import { SystemBars } from "react-native-edge-to-edge";
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const [settings] = useSettings();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (settings.defaultVideoOrientation) {
|
|
||||||
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (settings.autoRotate === true) {
|
|
||||||
ScreenOrientation.unlockAsync();
|
|
||||||
} else {
|
|
||||||
ScreenOrientation.lockAsync(
|
|
||||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [settings]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SystemBars hidden />
|
<SystemBars hidden />
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
console.log("Direct Player");
|
|
||||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
@@ -71,9 +70,9 @@ export default function page() {
|
|||||||
const progress = useSharedValue(0);
|
const progress = useSharedValue(0);
|
||||||
const isSeeking = useSharedValue(false);
|
const isSeeking = useSharedValue(false);
|
||||||
const cacheProgress = useSharedValue(0);
|
const cacheProgress = useSharedValue(0);
|
||||||
let getDownloadedItem = null;
|
|
||||||
if (!Platform.isTV) {
|
if (!Platform.isTV) {
|
||||||
getDownloadedItem = downloadProvider.useDownload();
|
const getDownloadedItem = downloadProvider.useDownload();
|
||||||
}
|
}
|
||||||
|
|
||||||
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
||||||
@@ -117,7 +116,7 @@ export default function page() {
|
|||||||
queryKey: ["item", itemId],
|
queryKey: ["item", itemId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (offline && !Platform.isTV) {
|
if (offline && !Platform.isTV) {
|
||||||
const item = await getDownloadedItem.getDownloadedItem(itemId);
|
const item = await getDownloadedItem(itemId);
|
||||||
if (item) return item.item;
|
if (item) return item.item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +139,7 @@ export default function page() {
|
|||||||
queryKey: ["stream-url", itemId, mediaSourceId, bitrateValue],
|
queryKey: ["stream-url", itemId, mediaSourceId, bitrateValue],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (offline && !Platform.isTV) {
|
if (offline && !Platform.isTV) {
|
||||||
const data = await getDownloadedItem.getDownloadedItem(itemId);
|
const data = await getDownloadedItem(itemId);
|
||||||
if (!data?.mediaSource) return null;
|
if (!data?.mediaSource) return null;
|
||||||
|
|
||||||
const url = await getDownloadedFileUrl(data.item.Id!);
|
const url = await getDownloadedFileUrl(data.item.Id!);
|
||||||
@@ -304,6 +303,9 @@ export default function page() {
|
|||||||
[item?.Id, isPlaying, api, isPlaybackStopped, audioIndex, subtitleIndex]
|
[item?.Id, isPlaying, api, isPlaybackStopped, audioIndex, subtitleIndex]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useOrientation();
|
||||||
|
useOrientationSettings();
|
||||||
|
|
||||||
useWebSocket({
|
useWebSocket({
|
||||||
isPlaying: isPlaying,
|
isPlaying: isPlaying,
|
||||||
togglePlay: togglePlay,
|
togglePlay: togglePlay,
|
||||||
@@ -384,18 +386,16 @@ export default function page() {
|
|||||||
|
|
||||||
const allSubs =
|
const allSubs =
|
||||||
stream?.mediaSource.MediaStreams?.filter(
|
stream?.mediaSource.MediaStreams?.filter(
|
||||||
(sub: { Type: string }) => sub.Type === "Subtitle"
|
(sub) => sub.Type === "Subtitle"
|
||||||
) || [];
|
) || [];
|
||||||
const chosenSubtitleTrack = allSubs.find(
|
const chosenSubtitleTrack = allSubs.find(
|
||||||
(sub: { Index: number }) => sub.Index === subtitleIndex
|
(sub) => sub.Index === subtitleIndex
|
||||||
);
|
);
|
||||||
const allAudio =
|
const allAudio =
|
||||||
stream?.mediaSource.MediaStreams?.filter(
|
stream?.mediaSource.MediaStreams?.filter(
|
||||||
(audio: { Type: string }) => audio.Type === "Audio"
|
(audio) => audio.Type === "Audio"
|
||||||
) || [];
|
) || [];
|
||||||
const chosenAudioTrack = allAudio.find(
|
const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex);
|
||||||
(audio: { Index: number | undefined }) => audio.Index === audioIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
// Direct playback CASE
|
// Direct playback CASE
|
||||||
if (!bitrateValue) {
|
if (!bitrateValue) {
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ import { SubtitleHelper } from "@/utils/SubtitleHelper";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const Player = () => {
|
const Player = () => {
|
||||||
console.log("Transcoding Player");
|
|
||||||
|
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
@@ -297,6 +295,9 @@ const Player = () => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useOrientation();
|
||||||
|
useOrientationSettings();
|
||||||
|
|
||||||
useWebSocket({
|
useWebSocket({
|
||||||
isPlaying: isPlaying,
|
isPlaying: isPlaying,
|
||||||
togglePlay: togglePlay,
|
togglePlay: togglePlay,
|
||||||
|
|||||||
@@ -64,24 +64,22 @@ function useNotificationObserver() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
function redirect(notification: typeof Notifications.Notification) {
|
function redirect(notification: Notifications.Notification) {
|
||||||
const url = notification.request.content.data?.url;
|
const url = notification.request.content.data?.url;
|
||||||
if (url) {
|
if (url) {
|
||||||
router.push(url);
|
router.push(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifications.getLastNotificationResponseAsync().then(
|
Notifications.getLastNotificationResponseAsync().then((response) => {
|
||||||
(response: { notification: any }) => {
|
if (!isMounted || !response?.notification) {
|
||||||
if (!isMounted || !response?.notification) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
redirect(response?.notification);
|
|
||||||
}
|
}
|
||||||
);
|
redirect(response?.notification);
|
||||||
|
});
|
||||||
|
|
||||||
const subscription = Notifications.addNotificationResponseReceivedListener(
|
const subscription = Notifications.addNotificationResponseReceivedListener(
|
||||||
(response: { notification: any }) => {
|
(response) => {
|
||||||
redirect(response.notification);
|
redirect(response.notification);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -129,7 +127,7 @@ if (!Platform.isTV) {
|
|||||||
const downloadUrl = url + "download/" + job.id;
|
const downloadUrl = url + "download/" + job.id;
|
||||||
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
||||||
|
|
||||||
if (tasks.find((task: { id: string }) => task.id === job.id)) {
|
if (tasks.find((task) => task.id === job.id)) {
|
||||||
console.log("TaskManager ~ Download already in progress: ", job.id);
|
console.log("TaskManager ~ Download already in progress: ", job.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -165,9 +163,9 @@ if (!Platform.isTV) {
|
|||||||
trigger: null,
|
trigger: null,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.error((error: any) => {
|
.error((error) => {
|
||||||
console.log("TaskManager ~ Download error: ", job.id, error);
|
console.log("TaskManager ~ Download error: ", job.id, error);
|
||||||
BackGroundDownloader.completeHandler(job.id);
|
completeHandler(job.id);
|
||||||
Notifications.scheduleNotificationAsync({
|
Notifications.scheduleNotificationAsync({
|
||||||
content: {
|
content: {
|
||||||
title: job.item.Name,
|
title: job.item.Name,
|
||||||
@@ -271,15 +269,12 @@ function Layout() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If the user has auto rotate enabled, unlock the orientation
|
if (settings?.autoRotate === true)
|
||||||
if (settings.autoRotate === true) {
|
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT);
|
||||||
ScreenOrientation.unlockAsync();
|
else
|
||||||
} else {
|
|
||||||
// If the user has auto rotate disabled, lock the orientation to portrait
|
|
||||||
ScreenOrientation.lockAsync(
|
ScreenOrientation.lockAsync(
|
||||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}, [settings]);
|
}, [settings]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface Props extends ViewProps {
|
|||||||
background?: "blur" | "transparent";
|
background?: "blur" | "transparent";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chromecast({
|
export default function Chromecast({
|
||||||
width = 48,
|
width = 48,
|
||||||
height = 48,
|
height = 48,
|
||||||
background = "transparent",
|
background = "transparent",
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ import { Image } from "expo-image";
|
|||||||
import { useNavigation } from "expo-router";
|
import { useNavigation } from "expo-router";
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { lazy, useEffect, useMemo, useState } from "react";
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
|
// const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
|
||||||
|
const Chromecast = lazy(() => import("./Chromecast"));
|
||||||
import { ItemHeader } from "./ItemHeader";
|
import { ItemHeader } from "./ItemHeader";
|
||||||
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
|
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
|
||||||
import { MediaSourceSelector } from "./MediaSourceSelector";
|
import { MediaSourceSelector } from "./MediaSourceSelector";
|
||||||
@@ -88,11 +89,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
headerRight: () =>
|
headerRight: () =>
|
||||||
item && (
|
item && (
|
||||||
<View className="flex flex-row items-center space-x-2">
|
<View className="flex flex-row items-center space-x-2">
|
||||||
<Chromecast.Chromecast
|
<Chromecast background="blur" width={22} height={22} />
|
||||||
background="blur"
|
|
||||||
width={22}
|
|
||||||
height={22}
|
|
||||||
/>
|
|
||||||
{item.Type !== "Program" && (
|
{item.Type !== "Program" && (
|
||||||
<View className="flex flex-row items-center space-x-2">
|
<View className="flex flex-row items-center space-x-2">
|
||||||
<DownloadSingleItem item={item} size="large" />
|
<DownloadSingleItem item={item} size="large" />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const BackGroundDownloader = !Platform.isTV
|
|||||||
: null;
|
: null;
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
const FFmpegKitProvider = !Platform.isTV ? require("ffmpeg-kit-react-native") : null;
|
const FFmpegKit = !Platform.isTV ? require("ffmpeg-kit-react-native") : null;
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -42,7 +42,7 @@ export const ActiveDownloads: React.FC<Props> = ({ ...props }) => {
|
|||||||
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
|
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
|
||||||
<Text className="text-lg font-bold mb-2">{t("home.downloads.active_downloads")}</Text>
|
<Text className="text-lg font-bold mb-2">{t("home.downloads.active_downloads")}</Text>
|
||||||
<View className="space-y-2">
|
<View className="space-y-2">
|
||||||
{processes?.map((p: JobStatus) => (
|
{processes?.map((p) => (
|
||||||
<DownloadCard key={p.item.Id} process={p} />
|
<DownloadCard key={p.item.Id} process={p} />
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -80,8 +80,8 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
|||||||
await queryClient.refetchQueries({ queryKey: ["jobs"] });
|
await queryClient.refetchQueries({ queryKey: ["jobs"] });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
FFmpegKitProvider.FFmpegKit.cancel(Number(id));
|
FFmpegKit.cancel(Number(id));
|
||||||
setProcesses((prev: any[]) => prev.filter((p: { id: string; }) => p.id !== id));
|
setProcesses((prev) => prev.filter((p) => p.id !== id));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
|||||||
className="w-28 rounded-lg overflow-hidden border border-neutral-900"
|
className="w-28 rounded-lg overflow-hidden border border-neutral-900"
|
||||||
id={item.id.toString()}
|
id={item.id.toString()}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
colors={['transparent', 'transparent']}
|
colors={[]}
|
||||||
contentFit={"cover"}
|
contentFit={"cover"}
|
||||||
url={jellyseerrApi?.imageProxy(
|
url={jellyseerrApi?.imageProxy(
|
||||||
item.backdrops?.[0],
|
item.backdrops?.[0],
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createHapticHandler = useCallback(
|
const createHapticHandler = useCallback(
|
||||||
(type: typeof Haptics.ImpactFeedbackStyle) => {
|
(type: Haptics.ImpactFeedbackStyle) => {
|
||||||
return Platform.OS === "web" || Platform.isTV
|
return Platform.OS === "web" || Platform.isTV
|
||||||
? () => {}
|
? () => {}
|
||||||
: () => Haptics.impactAsync(type);
|
: () => Haptics.impactAsync(type);
|
||||||
@@ -28,7 +28,7 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const createNotificationFeedback = useCallback(
|
const createNotificationFeedback = useCallback(
|
||||||
(type: typeof Haptics.NotificationFeedbackType) => {
|
(type: Haptics.NotificationFeedbackType) => {
|
||||||
return Platform.OS === "web" || Platform.isTV
|
return Platform.OS === "web" || Platform.isTV
|
||||||
? () => {}
|
? () => {}
|
||||||
: () => Haptics.notificationAsync(type);
|
: () => Haptics.notificationAsync(type);
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const useImageColors = ({
|
|||||||
fallback: "#fff",
|
fallback: "#fff",
|
||||||
cache: false,
|
cache: false,
|
||||||
})
|
})
|
||||||
.then((colors: { platform: string; dominant: string; vibrant: string; detail: string; primary: string; }) => {
|
.then((colors) => {
|
||||||
let primary: string = "#fff";
|
let primary: string = "#fff";
|
||||||
let text: string = "#000";
|
let text: string = "#000";
|
||||||
let backup: string = "#fff";
|
let backup: string = "#fff";
|
||||||
@@ -104,7 +104,7 @@ export const useImageColors = ({
|
|||||||
storage.set(`${source.uri}-text`, text);
|
storage.set(`${source.uri}-text`, text);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error: any) => {
|
.catch((error) => {
|
||||||
console.error("Error getting colors", error);
|
console.error("Error getting colors", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ import {
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import * as FileSystem from "expo-file-system";
|
import * as FileSystem from "expo-file-system";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
// import { FFmpegKit, FFmpegSession, Statistics } from "ffmpeg-kit-react-native";
|
// import { FFmpegKit, FFmpegSession, Statistics } from "ffmpeg-kit-react-native";
|
||||||
const FFMPEGKitReactNative = !Platform.isTV ? require("ffmpeg-kit-react-native") : null;
|
const FFmpegKit = !Platform.isTV ? require("ffmpeg-kit-react-native") : null;
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
@@ -23,9 +22,6 @@ import { JobStatus } from "@/utils/optimize-server";
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession;
|
|
||||||
type Statistics = typeof FFMPEGKitReactNative.Statistics
|
|
||||||
const FFmpegKit = FFMPEGKitReactNative.FFmpegKit;
|
|
||||||
const createFFmpegCommand = (url: string, output: string) => [
|
const createFFmpegCommand = (url: string, output: string) => [
|
||||||
"-y", // overwrite output files without asking
|
"-y", // overwrite output files without asking
|
||||||
"-thread_queue_size 512", // https://ffmpeg.org/ffmpeg.html#toc-Advanced-options
|
"-thread_queue_size 512", // https://ffmpeg.org/ffmpeg.html#toc-Advanced-options
|
||||||
@@ -100,8 +96,8 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
toast.success(t("home.downloads.toasts.download_completed"));
|
toast.success(t("home.downloads.toasts.download_completed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
setProcesses((prev: any[]) => {
|
setProcesses((prev) => {
|
||||||
return prev.filter((process: { itemId: string | undefined; }) => process.itemId !== item.Id);
|
return prev.filter((process) => process.itemId !== item.Id);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -125,8 +121,8 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
totalFrames > 0 ? Math.floor((processedFrames / totalFrames) * 100) : 0;
|
totalFrames > 0 ? Math.floor((processedFrames / totalFrames) * 100) : 0;
|
||||||
|
|
||||||
if (!item.Id) throw new Error("Item is undefined");
|
if (!item.Id) throw new Error("Item is undefined");
|
||||||
setProcesses((prev: any[]) => {
|
setProcesses((prev) => {
|
||||||
return prev.map((process: { itemId: string | undefined; }) => {
|
return prev.map((process) => {
|
||||||
if (process.itemId === item.Id) {
|
if (process.itemId === item.Id) {
|
||||||
return {
|
return {
|
||||||
...process,
|
...process,
|
||||||
@@ -185,13 +181,13 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
writeInfoLog(`useRemuxHlsToMp4 ~ startRemuxing for item ${item.Name}`);
|
writeInfoLog(`useRemuxHlsToMp4 ~ startRemuxing for item ${item.Name}`);
|
||||||
setProcesses((prev: any) => [...prev, job]);
|
setProcesses((prev) => [...prev, job]);
|
||||||
|
|
||||||
await FFmpegKit.executeAsync(
|
await FFmpegKit.executeAsync(
|
||||||
createFFmpegCommand(url, output).join(" "),
|
createFFmpegCommand(url, output).join(" "),
|
||||||
(session: any) => completeCallback(session, item),
|
(session) => completeCallback(session, item),
|
||||||
undefined,
|
undefined,
|
||||||
(s: any) => statisticsCallback(s, item)
|
(s) => statisticsCallback(s, item)
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e as Error;
|
const error = e as Error;
|
||||||
@@ -200,8 +196,8 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
`useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name},
|
`useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name},
|
||||||
Error: ${error.message}, Stack: ${error.stack}`
|
Error: ${error.message}, Stack: ${error.stack}`
|
||||||
);
|
);
|
||||||
setProcesses((prev: any[]) => {
|
setProcesses((prev) => {
|
||||||
return prev.filter((process: { itemId: string | undefined; }) => process.itemId !== item.Id);
|
return prev.filter((process) => process.itemId !== item.Id);
|
||||||
});
|
});
|
||||||
throw error; // Re-throw the error to propagate it to the caller
|
throw error; // Re-throw the error to propagate it to the caller
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
plugins {
|
apply plugin: 'com.android.library'
|
||||||
id 'com.android.library'
|
apply plugin: 'kotlin-android'
|
||||||
id 'kotlin-android'
|
apply plugin: 'kotlin-kapt'
|
||||||
id 'kotlin-kapt'
|
|
||||||
}
|
|
||||||
|
|
||||||
group = 'expo.modules.vlcplayer'
|
group = 'expo.modules.vlcplayer'
|
||||||
version = '0.6.0'
|
version = '0.6.0'
|
||||||
|
|
||||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||||
def kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
|
|
||||||
|
|
||||||
apply from: expoModulesCorePlugin
|
apply from: expoModulesCorePlugin
|
||||||
|
|
||||||
applyKotlinExpoModulesCorePlugin()
|
applyKotlinExpoModulesCorePlugin()
|
||||||
useCoreDependencies()
|
useCoreDependencies()
|
||||||
useExpoPublishing()
|
useExpoPublishing()
|
||||||
@@ -42,8 +37,8 @@ if (useManagedAndroidSdkVersions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.videolan.android:libvlc-all:3.6.0'
|
implementation 'org.videolan.android:libvlc-all:3.6.0-eap12'
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.31"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
29
package.json
29
package.json
@@ -10,9 +10,9 @@
|
|||||||
"ios:tv": "EXPO_TV=1 expo run:ios",
|
"ios:tv": "EXPO_TV=1 expo run:ios",
|
||||||
"android": "EXPO_TV=0 expo run:android",
|
"android": "EXPO_TV=0 expo run:android",
|
||||||
"android:tv": "EXPO_TV=1 expo run:android",
|
"android:tv": "EXPO_TV=1 expo run:android",
|
||||||
"prebuild": "EXPO_TV=0 bun run clean",
|
"prebuild": "EXPO_TV=0 expo prebuild --clean",
|
||||||
"prebuild:tv": "EXPO_TV=1 bun run clean",
|
"prebuild:tv": "EXPO_TV=1 expo prebuild --clean",
|
||||||
"prebuild:tv-new": "EXPO_TV=1 node ./scripts/symlink-native-dirs.js; bun run prebuild:tv",
|
"prebuild:tv-new": "EXPO_TV=1 node ./scripts/symlink-native-dirs.js; EXPO_TV=1 expo prebuild --clean",
|
||||||
"test": "jest --watchAll",
|
"test": "jest --watchAll",
|
||||||
"lint": "expo lint",
|
"lint": "expo lint",
|
||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package"
|
||||||
@@ -27,13 +27,13 @@
|
|||||||
"@gorhom/bottom-sheet": "^5.1.0",
|
"@gorhom/bottom-sheet": "^5.1.0",
|
||||||
"@jellyfin/sdk": "^0.11.0",
|
"@jellyfin/sdk": "^0.11.0",
|
||||||
"@kesha-antonov/react-native-background-downloader": "3.2.6",
|
"@kesha-antonov/react-native-background-downloader": "3.2.6",
|
||||||
"@react-native-async-storage/async-storage": "2.1.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/netinfo": "11.4.1",
|
"@react-native-community/netinfo": "11.4.1",
|
||||||
"@react-native-menu/menu": "^1.2.2",
|
"@react-native-menu/menu": "^1.2.2",
|
||||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||||
"@react-navigation/material-top-tabs": "^7.1.0",
|
"@react-navigation/material-top-tabs": "^7.1.0",
|
||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@shopify/flash-list": "1.7.3",
|
"@shopify/flash-list": "1.7.1",
|
||||||
"@tanstack/react-query": "^5.66.0",
|
"@tanstack/react-query": "^5.66.0",
|
||||||
"@types/lodash": "^4.17.15",
|
"@types/lodash": "^4.17.15",
|
||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
@@ -70,13 +70,14 @@
|
|||||||
"expo-web-browser": "~14.0.2",
|
"expo-web-browser": "~14.0.2",
|
||||||
"ffmpeg-kit-react-native": "^6.0.2",
|
"ffmpeg-kit-react-native": "^6.0.2",
|
||||||
"i18next": "^24.2.2",
|
"i18next": "^24.2.2",
|
||||||
|
"install": "^0.13.0",
|
||||||
"jotai": "^2.11.3",
|
"jotai": "^2.11.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nativewind": "^2.0.11",
|
"nativewind": "^2.0.11",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
|
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
"react-native-bottom-tabs": "0.8.6",
|
"react-native-bottom-tabs": "0.8.6",
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
@@ -84,20 +85,20 @@
|
|||||||
"react-native-country-flag": "^2.0.2",
|
"react-native-country-flag": "^2.0.2",
|
||||||
"react-native-device-info": "^14.0.4",
|
"react-native-device-info": "^14.0.4",
|
||||||
"react-native-edge-to-edge": "^1.4.3",
|
"react-native-edge-to-edge": "^1.4.3",
|
||||||
"react-native-gesture-handler": "~2.23.0",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"react-native-google-cast": "^4.8.3",
|
"react-native-google-cast": "^4.8.3",
|
||||||
"react-native-image-colors": "^2.4.0",
|
"react-native-image-colors": "^2.4.0",
|
||||||
"react-native-ios-context-menu": "^3.1.0",
|
"react-native-ios-context-menu": "^3.1.0",
|
||||||
"react-native-ios-utilities": "5.1.1",
|
"react-native-ios-utilities": "5.1.1",
|
||||||
"react-native-mmkv": "^2.12.2",
|
"react-native-mmkv": "^2.12.2",
|
||||||
"react-native-pager-view": "6.7.0",
|
"react-native-pager-view": "6.5.1",
|
||||||
"react-native-progress": "^5.0.1",
|
"react-native-progress": "^5.0.1",
|
||||||
"react-native-reanimated": "~3.16.7",
|
"react-native-reanimated": "~3.16.7",
|
||||||
"react-native-reanimated-carousel": "3.5.1",
|
"react-native-reanimated-carousel": "3.5.1",
|
||||||
"react-native-safe-area-context": "5.2.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "4.6.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-svg": "15.11.1",
|
"react-native-svg": "15.8.0",
|
||||||
"react-native-tab-view": "^4.0.5",
|
"react-native-tab-view": "^4.0.5",
|
||||||
"react-native-udp": "^4.1.7",
|
"react-native-udp": "^4.1.7",
|
||||||
"react-native-uitextview": "^1.4.0",
|
"react-native-uitextview": "^1.4.0",
|
||||||
@@ -106,7 +107,7 @@
|
|||||||
"react-native-video": "6.10.0",
|
"react-native-video": "6.10.0",
|
||||||
"react-native-volume-manager": "^2.0.8",
|
"react-native-volume-manager": "^2.0.8",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-webview": "13.13.2",
|
"react-native-webview": "13.12.5",
|
||||||
"sonner-native": "^0.17.0",
|
"sonner-native": "^0.17.0",
|
||||||
"tailwindcss": "3.3.2",
|
"tailwindcss": "3.3.2",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
@@ -115,11 +116,11 @@
|
|||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.26.8",
|
||||||
"@react-native-community/cli": "15.1.3",
|
"@react-native-community/cli": "15.1.3",
|
||||||
"@react-native-tvos/config-tv": "^0.1.1",
|
"@react-native-tvos/config-tv": "^0.1.1",
|
||||||
"@babel/core": "^7.26.8",
|
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/react": "~19.0.8",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-test-renderer": "^19.0.0",
|
"@types/react-test-renderer": "^19.0.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
const { withGradleProperties } = require('expo/config-plugins');
|
|
||||||
|
|
||||||
function setGradlePropertiesValue(config, key, value) {
|
|
||||||
return withGradleProperties(config, exportedConfig => {
|
|
||||||
const props = exportedConfig.modResults;
|
|
||||||
const keyIdx = props.findIndex(item => item.type === 'property' && item.key === key);
|
|
||||||
const property = {
|
|
||||||
type: 'property',
|
|
||||||
key,
|
|
||||||
value
|
|
||||||
};
|
|
||||||
|
|
||||||
if (keyIdx >= 0) {
|
|
||||||
props.splice(keyIdx, 1, property);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
props.push(property);
|
|
||||||
}
|
|
||||||
|
|
||||||
return exportedConfig;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function withCustomPlugin(config) {
|
|
||||||
// Expo 52 is not setting this
|
|
||||||
// https://github.com/expo/expo/issues/32558
|
|
||||||
config = setGradlePropertiesValue(
|
|
||||||
config,
|
|
||||||
'android.enableJetifier',
|
|
||||||
'true',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Increase memory
|
|
||||||
config = setGradlePropertiesValue(
|
|
||||||
config,
|
|
||||||
'org.gradle.jvmargs',
|
|
||||||
'-Xmx4096m -XX:MaxMetaspaceSize=1024m',
|
|
||||||
);
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
@@ -157,53 +157,53 @@ export type StreamyfinPluginConfig = {
|
|||||||
settings: PluginLockableSettings;
|
settings: PluginLockableSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Settings = {
|
const loadSettings = (): Settings => {
|
||||||
home: null,
|
const defaultValues: Settings = {
|
||||||
autoRotate: true,
|
home: null,
|
||||||
forceLandscapeInVideoPlayer: false,
|
autoRotate: true,
|
||||||
deviceProfile: "Expo",
|
forceLandscapeInVideoPlayer: false,
|
||||||
mediaListCollectionIds: [],
|
deviceProfile: "Expo",
|
||||||
preferedLanguage: undefined,
|
mediaListCollectionIds: [],
|
||||||
searchEngine: "Jellyfin",
|
preferedLanguage: undefined,
|
||||||
marlinServerUrl: "",
|
searchEngine: "Jellyfin",
|
||||||
openInVLC: false,
|
marlinServerUrl: "",
|
||||||
downloadQuality: DownloadOptions[0],
|
openInVLC: false,
|
||||||
libraryOptions: {
|
downloadQuality: DownloadOptions[0],
|
||||||
display: "list",
|
libraryOptions: {
|
||||||
cardStyle: "detailed",
|
display: "list",
|
||||||
imageStyle: "cover",
|
cardStyle: "detailed",
|
||||||
showTitles: true,
|
imageStyle: "cover",
|
||||||
showStats: true,
|
showTitles: true,
|
||||||
},
|
showStats: true,
|
||||||
defaultAudioLanguage: null,
|
},
|
||||||
playDefaultAudioTrack: true,
|
defaultAudioLanguage: null,
|
||||||
rememberAudioSelections: true,
|
playDefaultAudioTrack: true,
|
||||||
defaultSubtitleLanguage: null,
|
rememberAudioSelections: true,
|
||||||
subtitleMode: SubtitlePlaybackMode.Default,
|
defaultSubtitleLanguage: null,
|
||||||
rememberSubtitleSelections: true,
|
subtitleMode: SubtitlePlaybackMode.Default,
|
||||||
showHomeTitles: true,
|
rememberSubtitleSelections: true,
|
||||||
defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT,
|
showHomeTitles: true,
|
||||||
forwardSkipTime: 30,
|
defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT,
|
||||||
rewindSkipTime: 10,
|
forwardSkipTime: 30,
|
||||||
optimizedVersionsServerUrl: null,
|
rewindSkipTime: 10,
|
||||||
downloadMethod: DownloadMethod.Remux,
|
optimizedVersionsServerUrl: null,
|
||||||
autoDownload: false,
|
downloadMethod: DownloadMethod.Remux,
|
||||||
showCustomMenuLinks: false,
|
autoDownload: false,
|
||||||
disableHapticFeedback: false,
|
showCustomMenuLinks: false,
|
||||||
subtitleSize: Platform.OS === "ios" ? 60 : 100,
|
disableHapticFeedback: false,
|
||||||
remuxConcurrentLimit: 1,
|
subtitleSize: Platform.OS === "ios" ? 60 : 100,
|
||||||
safeAreaInControlsEnabled: true,
|
remuxConcurrentLimit: 1,
|
||||||
jellyseerrServerUrl: undefined,
|
safeAreaInControlsEnabled: true,
|
||||||
hiddenLibraries: [],
|
jellyseerrServerUrl: undefined,
|
||||||
};
|
hiddenLibraries: [],
|
||||||
|
};
|
||||||
|
|
||||||
const loadSettings = (): Partial<Settings> => {
|
|
||||||
try {
|
try {
|
||||||
const jsonValue = storage.getString("settings");
|
const jsonValue = storage.getString("settings");
|
||||||
const loadedValues: Partial<Settings> =
|
const loadedValues: Partial<Settings> =
|
||||||
jsonValue != null ? JSON.parse(jsonValue) : {};
|
jsonValue != null ? JSON.parse(jsonValue) : {};
|
||||||
|
|
||||||
return loadedValues;
|
return { ...defaultValues, ...loadedValues };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load settings:", error);
|
console.error("Failed to load settings:", error);
|
||||||
return defaultValues;
|
return defaultValues;
|
||||||
@@ -222,7 +222,7 @@ const saveSettings = (settings: Settings) => {
|
|||||||
storage.set("settings", jsonValue);
|
storage.set("settings", jsonValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const settingsAtom = atom<Partial<Settings> | null>(null);
|
export const settingsAtom = atom<Settings | null>(null);
|
||||||
export const pluginSettingsAtom = atom(
|
export const pluginSettingsAtom = atom(
|
||||||
storage.get<PluginLockableSettings>(STREAMYFIN_PLUGIN_SETTINGS)
|
storage.get<PluginLockableSettings>(STREAMYFIN_PLUGIN_SETTINGS)
|
||||||
);
|
);
|
||||||
@@ -262,7 +262,7 @@ export const useSettings = () => {
|
|||||||
|
|
||||||
const updateSettings = (update: Partial<Settings>) => {
|
const updateSettings = (update: Partial<Settings>) => {
|
||||||
if (settings) {
|
if (settings) {
|
||||||
const newSettings = { ..._settings, ...update };
|
const newSettings = { ...settings, ...update };
|
||||||
|
|
||||||
setSettings(newSettings);
|
setSettings(newSettings);
|
||||||
saveSettings(newSettings);
|
saveSettings(newSettings);
|
||||||
@@ -271,7 +271,7 @@ export const useSettings = () => {
|
|||||||
|
|
||||||
// We do not want to save over users pre-existing settings in case admin ever removes/unlocks a setting.
|
// 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,
|
// If admin sets locked to false but provides a value,
|
||||||
// use user settings first and fallback on admin setting if required.
|
// use user settings first and fallback on admin setting if required.
|
||||||
const settings: Settings = useMemo(() => {
|
const settings: Settings = useMemo(() => {
|
||||||
let unlockedPluginDefaults = {} as Settings;
|
let unlockedPluginDefaults = {} as Settings;
|
||||||
const overrideSettings = Object.entries(pluginSettings || {}).reduce(
|
const overrideSettings = Object.entries(pluginSettings || {}).reduce(
|
||||||
@@ -300,8 +300,12 @@ export const useSettings = () => {
|
|||||||
{} as Settings
|
{} as Settings
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update settings with plugin defined defaults
|
||||||
|
if (Object.keys(unlockedPluginDefaults).length > 0) {
|
||||||
|
updateSettings(unlockedPluginDefaults);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...defaultValues,
|
|
||||||
..._settings,
|
..._settings,
|
||||||
...overrideSettings,
|
...overrideSettings,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user