Compare commits

..

3 Commits

Author SHA1 Message Date
Fredrik Burmester
09189e125e fix: change to yarn 2025-02-09 13:05:18 +01:00
Fredrik Burmester
1ac10d8f34 fix: expo doctor issues 2025-02-09 13:05:07 +01:00
sarendsen
d5fe354986 wip 2025-02-08 16:29:12 +01:00
21 changed files with 1995 additions and 2450 deletions

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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(() => {

BIN
bun.lockb

Binary file not shown.

View File

@@ -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",

View File

@@ -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" />

View File

@@ -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: () => {

View File

@@ -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],

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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

4097
yarn.lock

File diff suppressed because it is too large Load Diff