forked from Ninjalama/streamyfin_mirror
Compare commits
23 Commits
refactor/s
...
v0.18.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbd12c540a | ||
|
|
d6ee1807f3 | ||
|
|
0d7c3cb9da | ||
|
|
fd252247aa | ||
|
|
c12af2efe9 | ||
|
|
04b24ee86b | ||
|
|
43d251fcda | ||
|
|
fed3725733 | ||
|
|
f5be204ac8 | ||
|
|
093fdcda45 | ||
|
|
eeaa027579 | ||
|
|
a4c20981cf | ||
|
|
63965c9e64 | ||
|
|
c5f39f6f8a | ||
|
|
eb841601f6 | ||
|
|
3f5ce6dc43 | ||
|
|
b73a33b05b | ||
|
|
e3baa2f58b | ||
|
|
ef7fbc985f | ||
|
|
381c6701f2 | ||
|
|
71da79ee6a | ||
|
|
5cff323871 | ||
|
|
39b7c66d34 |
11
app.json
11
app.json
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Streamyfin",
|
"name": "Streamyfin",
|
||||||
"slug": "streamyfin",
|
"slug": "streamyfin",
|
||||||
"version": "0.17.0",
|
"version": "0.18.0",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "streamyfin",
|
"scheme": "streamyfin",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"jsEngine": "hermes",
|
"jsEngine": "hermes",
|
||||||
"versionCode": 43,
|
"versionCode": 46,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/images/adaptive_icon.png"
|
"foregroundImage": "./assets/images/adaptive_icon.png"
|
||||||
},
|
},
|
||||||
@@ -66,13 +66,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
|
||||||
"./plugins/withAndroidMainActivityAttributes",
|
|
||||||
{
|
|
||||||
"com.reactnative.googlecast.RNGCExpandedControllerActivity": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
["./plugins/withExpandedController.js"],
|
|
||||||
[
|
[
|
||||||
"expo-build-properties",
|
"expo-build-properties",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,11 +25,10 @@ import {
|
|||||||
import NetInfo from "@react-native-community/netinfo";
|
import NetInfo from "@react-native-community/netinfo";
|
||||||
import { QueryFunction, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { QueryFunction, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useNavigation, useRouter } from "expo-router";
|
import { useNavigation, useRouter } from "expo-router";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Platform,
|
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
useFocusEffect,
|
|
||||||
useLocalSearchParams,
|
|
||||||
useNavigation,
|
|
||||||
} from "expo-router";
|
|
||||||
import * as ScreenOrientation from "expo-screen-orientation";
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo } from "react";
|
import React, { useCallback, useEffect, useMemo } from "react";
|
||||||
import { FlatList, useWindowDimensions, View } from "react-native";
|
import { FlatList, useWindowDimensions, View } from "react-native";
|
||||||
|
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
@@ -16,6 +12,7 @@ import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
|
|||||||
import { ItemCardText } from "@/components/ItemCardText";
|
import { ItemCardText } from "@/components/ItemCardText";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
import { ItemPoster } from "@/components/posters/ItemPoster";
|
import { ItemPoster } from "@/components/posters/ItemPoster";
|
||||||
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import {
|
import {
|
||||||
genreFilterAtom,
|
genreFilterAtom,
|
||||||
@@ -32,7 +29,6 @@ import {
|
|||||||
tagsFilterAtom,
|
tagsFilterAtom,
|
||||||
yearFilterAtom,
|
yearFilterAtom,
|
||||||
} from "@/utils/atoms/filters";
|
} from "@/utils/atoms/filters";
|
||||||
import { orientationAtom } from "@/utils/atoms/orientation";
|
|
||||||
import {
|
import {
|
||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
BaseItemDtoQueryResult,
|
BaseItemDtoQueryResult,
|
||||||
@@ -60,12 +56,13 @@ const Page = () => {
|
|||||||
const [selectedTags, setSelectedTags] = useAtom(tagsFilterAtom);
|
const [selectedTags, setSelectedTags] = useAtom(tagsFilterAtom);
|
||||||
const [sortBy, _setSortBy] = useAtom(sortByAtom);
|
const [sortBy, _setSortBy] = useAtom(sortByAtom);
|
||||||
const [sortOrder, _setSortOrder] = useAtom(sortOrderAtom);
|
const [sortOrder, _setSortOrder] = useAtom(sortOrderAtom);
|
||||||
const [orientation] = useAtom(orientationAtom);
|
|
||||||
const [sortByPreference, setSortByPreference] = useAtom(sortByPreferenceAtom);
|
const [sortByPreference, setSortByPreference] = useAtom(sortByPreferenceAtom);
|
||||||
const [sortOrderPreference, setOderByPreference] = useAtom(
|
const [sortOrderPreference, setOderByPreference] = useAtom(
|
||||||
sortOrderPreferenceAtom
|
sortOrderPreferenceAtom
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { orientation } = useOrientation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sop = getSortOrderPreference(libraryId, sortOrderPreference);
|
const sop = getSortOrderPreference(libraryId, sortOrderPreference);
|
||||||
if (sop) {
|
if (sop) {
|
||||||
@@ -106,11 +103,12 @@ const Page = () => {
|
|||||||
[libraryId, sortOrderPreference]
|
[libraryId, sortOrderPreference]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getNumberOfColumns = useCallback(() => {
|
const nrOfCols = useMemo(() => {
|
||||||
if (orientation === ScreenOrientation.Orientation.PORTRAIT_UP) return 3;
|
if (screenWidth < 300) return 2;
|
||||||
if (screenWidth < 600) return 5;
|
if (screenWidth < 500) return 3;
|
||||||
if (screenWidth < 960) return 6;
|
if (screenWidth < 800) return 5;
|
||||||
if (screenWidth < 1280) return 7;
|
if (screenWidth < 1000) return 6;
|
||||||
|
if (screenWidth < 1500) return 7;
|
||||||
return 6;
|
return 6;
|
||||||
}, [screenWidth, orientation]);
|
}, [screenWidth, orientation]);
|
||||||
|
|
||||||
@@ -219,7 +217,7 @@ const Page = () => {
|
|||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ item, index }: { item: BaseItemDto; index: number }) => (
|
({ item, index }: { item: BaseItemDto; index: number }) => (
|
||||||
<MemoizedTouchableItemRouter
|
<TouchableItemRouter
|
||||||
key={item.Id}
|
key={item.Id}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@@ -230,10 +228,10 @@ const Page = () => {
|
|||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
alignSelf:
|
alignSelf:
|
||||||
orientation === ScreenOrientation.Orientation.PORTRAIT_UP
|
orientation === ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||||
? index % 3 === 0
|
? index % nrOfCols === 0
|
||||||
? "flex-end"
|
? "flex-end"
|
||||||
: (index + 1) % 3 === 0
|
: (index + 1) % nrOfCols === 0
|
||||||
? "flex-start"
|
? "flex-start"
|
||||||
: "center"
|
: "center"
|
||||||
: "center",
|
: "center",
|
||||||
@@ -244,7 +242,7 @@ const Page = () => {
|
|||||||
<ItemPoster item={item} />
|
<ItemPoster item={item} />
|
||||||
<ItemCardText item={item} />
|
<ItemCardText item={item} />
|
||||||
</View>
|
</View>
|
||||||
</MemoizedTouchableItemRouter>
|
</TouchableItemRouter>
|
||||||
),
|
),
|
||||||
[orientation]
|
[orientation]
|
||||||
);
|
);
|
||||||
@@ -429,6 +427,7 @@ const Page = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
|
key={orientation}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View className="flex flex-col items-center justify-center h-full">
|
<View className="flex flex-col items-center justify-center h-full">
|
||||||
<Text className="font-bold text-xl text-neutral-500">No results</Text>
|
<Text className="font-bold text-xl text-neutral-500">No results</Text>
|
||||||
@@ -437,10 +436,10 @@ const Page = () => {
|
|||||||
contentInsetAdjustmentBehavior="automatic"
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
data={flatData}
|
data={flatData}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
extraData={orientation}
|
extraData={[orientation, nrOfCols]}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
estimatedItemSize={244}
|
estimatedItemSize={244}
|
||||||
numColumns={getNumberOfColumns()}
|
numColumns={nrOfCols}
|
||||||
onEndReached={() => {
|
onEndReached={() => {
|
||||||
if (hasNextPage) {
|
if (hasNextPage) {
|
||||||
fetchNextPage();
|
fetchNextPage();
|
||||||
|
|||||||
@@ -7,25 +7,13 @@ import {
|
|||||||
PlaybackType,
|
PlaybackType,
|
||||||
usePlaySettings,
|
usePlaySettings,
|
||||||
} from "@/providers/PlaySettingsProvider";
|
} from "@/providers/PlaySettingsProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
|
||||||
import orientationToOrientationLock from "@/utils/OrientationLockConverter";
|
|
||||||
import { secondsToTicks } from "@/utils/secondsToTicks";
|
import { secondsToTicks } from "@/utils/secondsToTicks";
|
||||||
import { Api } from "@jellyfin/sdk";
|
import { Api } from "@jellyfin/sdk";
|
||||||
import * as Haptics from "expo-haptics";
|
import * as Haptics from "expo-haptics";
|
||||||
import * as NavigationBar from "expo-navigation-bar";
|
|
||||||
import { useFocusEffect } from "expo-router";
|
import { useFocusEffect } from "expo-router";
|
||||||
import * as ScreenOrientation from "expo-screen-orientation";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, {
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||||
useCallback,
|
import { Pressable, StatusBar, useWindowDimensions, View } from "react-native";
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { Dimensions, Platform, Pressable, StatusBar, View } from "react-native";
|
|
||||||
import { useSharedValue } from "react-native-reanimated";
|
import { useSharedValue } from "react-native-reanimated";
|
||||||
import Video, { OnProgressData, VideoRef } from "react-native-video";
|
import Video, { OnProgressData, VideoRef } from "react-native-video";
|
||||||
|
|
||||||
@@ -37,7 +25,10 @@ export default function page() {
|
|||||||
const videoSource = useVideoSource(playSettings, api, playUrl);
|
const videoSource = useVideoSource(playSettings, api, playUrl);
|
||||||
const firstTime = useRef(true);
|
const firstTime = useRef(true);
|
||||||
|
|
||||||
const screenDimensions = Dimensions.get("screen");
|
const dimensions = useWindowDimensions();
|
||||||
|
useOrientation();
|
||||||
|
useOrientationSettings();
|
||||||
|
useAndroidNavigationBar();
|
||||||
|
|
||||||
const [showControls, setShowControls] = useState(true);
|
const [showControls, setShowControls] = useState(true);
|
||||||
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
|
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
|
||||||
@@ -77,10 +68,6 @@ export default function page() {
|
|||||||
}, [play, stop])
|
}, [play, stop])
|
||||||
);
|
);
|
||||||
|
|
||||||
const { orientation } = useOrientation();
|
|
||||||
useOrientationSettings();
|
|
||||||
useAndroidNavigationBar();
|
|
||||||
|
|
||||||
const onProgress = useCallback(async (data: OnProgressData) => {
|
const onProgress = useCallback(async (data: OnProgressData) => {
|
||||||
if (isSeeking.value === true) return;
|
if (isSeeking.value === true) return;
|
||||||
progress.value = secondsToTicks(data.currentTime);
|
progress.value = secondsToTicks(data.currentTime);
|
||||||
@@ -94,8 +81,8 @@ export default function page() {
|
|||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
width: screenDimensions.width,
|
width: dimensions.width,
|
||||||
height: screenDimensions.height,
|
height: dimensions.height,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
className="flex flex-col items-center justify-center"
|
className="flex flex-col items-center justify-center"
|
||||||
|
|||||||
@@ -18,15 +18,13 @@ import * as Haptics from "expo-haptics";
|
|||||||
import { useFocusEffect } from "expo-router";
|
import { useFocusEffect } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { Dimensions, Pressable, StatusBar, View } from "react-native";
|
import { Pressable, StatusBar, useWindowDimensions, View } from "react-native";
|
||||||
import { useSharedValue } from "react-native-reanimated";
|
import { useSharedValue } from "react-native-reanimated";
|
||||||
import Video, {
|
import Video, {
|
||||||
OnProgressData,
|
OnProgressData,
|
||||||
VideoRef,
|
|
||||||
SelectedTrack,
|
|
||||||
SelectedTrackType,
|
SelectedTrackType,
|
||||||
|
VideoRef,
|
||||||
} from "react-native-video";
|
} from "react-native-video";
|
||||||
import { WithDefault } from "react-native/Libraries/Types/CodegenTypes";
|
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const { playSettings, playUrl, playSessionId } = usePlaySettings();
|
const { playSettings, playUrl, playSessionId } = usePlaySettings();
|
||||||
@@ -36,8 +34,7 @@ export default function page() {
|
|||||||
const poster = usePoster(playSettings, api);
|
const poster = usePoster(playSettings, api);
|
||||||
const videoSource = useVideoSource(playSettings, api, poster, playUrl);
|
const videoSource = useVideoSource(playSettings, api, poster, playUrl);
|
||||||
const firstTime = useRef(true);
|
const firstTime = useRef(true);
|
||||||
|
const dimensions = useWindowDimensions();
|
||||||
const screenDimensions = Dimensions.get("screen");
|
|
||||||
|
|
||||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
||||||
const [showControls, setShowControls] = useState(true);
|
const [showControls, setShowControls] = useState(true);
|
||||||
@@ -172,7 +169,7 @@ export default function page() {
|
|||||||
}, [play, stop])
|
}, [play, stop])
|
||||||
);
|
);
|
||||||
|
|
||||||
const { orientation } = useOrientation();
|
useOrientation();
|
||||||
useOrientationSettings();
|
useOrientationSettings();
|
||||||
useAndroidNavigationBar();
|
useAndroidNavigationBar();
|
||||||
|
|
||||||
@@ -216,23 +213,36 @@ export default function page() {
|
|||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
width: screenDimensions.width,
|
flex: 1,
|
||||||
height: screenDimensions.height,
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: dimensions.width,
|
||||||
|
height: dimensions.height,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
className="flex flex-col items-center justify-center"
|
|
||||||
>
|
>
|
||||||
<StatusBar hidden />
|
<StatusBar hidden />
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShowControls(!showControls);
|
setShowControls(!showControls);
|
||||||
}}
|
}}
|
||||||
className="absolute z-0 h-full w-full"
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: dimensions.width,
|
||||||
|
height: dimensions.height,
|
||||||
|
zIndex: 0,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Video
|
<Video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
source={videoSource}
|
source={videoSource}
|
||||||
style={{ width: "100%", height: "100%" }}
|
style={{
|
||||||
|
width: dimensions.width,
|
||||||
|
height: dimensions.height,
|
||||||
|
}}
|
||||||
resizeMode={ignoreSafeAreas ? "cover" : "contain"}
|
resizeMode={ignoreSafeAreas ? "cover" : "contain"}
|
||||||
onProgress={onProgress}
|
onProgress={onProgress}
|
||||||
onError={() => {}}
|
onError={() => {}}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import React, { useEffect } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
import { Platform, TouchableOpacity, ViewProps } from "react-native";
|
import { Platform, TouchableOpacity, ViewProps } from "react-native";
|
||||||
import GoogleCast, {
|
import GoogleCast, {
|
||||||
|
CastButton,
|
||||||
CastContext,
|
CastContext,
|
||||||
useCastDevice,
|
useCastDevice,
|
||||||
useDevices,
|
useDevices,
|
||||||
@@ -39,18 +40,32 @@ export const Chromecast: React.FC<Props> = ({
|
|||||||
})();
|
})();
|
||||||
}, [client, devices, castDevice, sessionManager, discoveryManager]);
|
}, [client, devices, castDevice, sessionManager, discoveryManager]);
|
||||||
|
|
||||||
|
// Android requires the cast button to be present for startDiscovery to work
|
||||||
|
const AndroidCastButton = useCallback(
|
||||||
|
() =>
|
||||||
|
Platform.OS === "android" ? (
|
||||||
|
<CastButton tintColor="transparent" />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
),
|
||||||
|
[Platform.OS]
|
||||||
|
);
|
||||||
|
|
||||||
if (background === "transparent")
|
if (background === "transparent")
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<>
|
||||||
onPress={() => {
|
<TouchableOpacity
|
||||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
onPress={() => {
|
||||||
else CastContext.showCastDialog();
|
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
||||||
}}
|
else CastContext.showCastDialog();
|
||||||
className="rounded-full h-10 w-10 flex items-center justify-center b"
|
}}
|
||||||
{...props}
|
className="rounded-full h-10 w-10 flex items-center justify-center b"
|
||||||
>
|
{...props}
|
||||||
<Feather name="cast" size={22} color={"white"} />
|
>
|
||||||
</TouchableOpacity>
|
<Feather name="cast" size={22} color={"white"} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<AndroidCastButton />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (Platform.OS === "android")
|
if (Platform.OS === "android")
|
||||||
@@ -82,6 +97,7 @@ export const Chromecast: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<Feather name="cast" size={22} color={"white"} />
|
<Feather name="cast" size={22} color={"white"} />
|
||||||
</BlurView>
|
</BlurView>
|
||||||
|
<AndroidCastButton />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { useFocusEffect, useNavigation } from "expo-router";
|
|||||||
import * as ScreenOrientation from "expo-screen-orientation";
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { View } from "react-native";
|
import { Alert, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Chromecast } from "./Chromecast";
|
import { Chromecast } from "./Chromecast";
|
||||||
import { ItemHeader } from "./ItemHeader";
|
import { ItemHeader } from "./ItemHeader";
|
||||||
@@ -59,6 +59,11 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
audioIndex,
|
audioIndex,
|
||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!mediaSource) {
|
||||||
|
Alert.alert("Error", "No media source found for this item.");
|
||||||
|
navigation.goBack();
|
||||||
|
}
|
||||||
}, [item, settings])
|
}, [item, settings])
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -241,7 +246,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PlayButton item={item} url={playUrl} className="grow" />
|
<PlayButton className="grow" />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{item.Type === "Episode" && (
|
{item.Type === "Episode" && (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||||
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
|
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||||
@@ -6,10 +6,11 @@ import { runtimeTicksToMinutes } from "@/utils/time";
|
|||||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||||
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { Linking, TouchableOpacity, View } from "react-native";
|
import { Alert, Linking, TouchableOpacity, View } from "react-native";
|
||||||
import CastContext, {
|
import CastContext, {
|
||||||
|
CastButton,
|
||||||
PlayServicesState,
|
PlayServicesState,
|
||||||
useMediaStatus,
|
useMediaStatus,
|
||||||
useRemoteMediaClient,
|
useRemoteMediaClient,
|
||||||
@@ -28,32 +29,31 @@ import { Button } from "./Button";
|
|||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
|
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
||||||
|
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof Button> {
|
interface Props extends React.ComponentProps<typeof Button> {}
|
||||||
item?: BaseItemDto | null;
|
|
||||||
url?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ANIMATION_DURATION = 500;
|
const ANIMATION_DURATION = 500;
|
||||||
const MIN_PLAYBACK_WIDTH = 15;
|
const MIN_PLAYBACK_WIDTH = 15;
|
||||||
|
|
||||||
export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
export const PlayButton: React.FC<Props> = ({ ...props }) => {
|
||||||
|
const { playSettings, playUrl: url } = usePlaySettings();
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const client = useRemoteMediaClient();
|
const client = useRemoteMediaClient();
|
||||||
const mediaStatus = useMediaStatus();
|
const mediaStatus = useMediaStatus();
|
||||||
|
|
||||||
const [colorAtom] = useAtom(itemThemeColorAtom);
|
const [colorAtom] = useAtom(itemThemeColorAtom);
|
||||||
const [api] = useAtom(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
|
const user = useAtomValue(userAtom);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const memoizedItem = useMemo(() => item, [item?.Id]); // Memoize the item
|
|
||||||
const memoizedColor = useMemo(() => colorAtom, [colorAtom]); // Memoize the color
|
|
||||||
|
|
||||||
const startWidth = useSharedValue(0);
|
const startWidth = useSharedValue(0);
|
||||||
const targetWidth = useSharedValue(0);
|
const targetWidth = useSharedValue(0);
|
||||||
const endColor = useSharedValue(memoizedColor);
|
const endColor = useSharedValue(colorAtom);
|
||||||
const startColor = useSharedValue(memoizedColor);
|
const startColor = useSharedValue(colorAtom);
|
||||||
const widthProgress = useSharedValue(0);
|
const widthProgress = useSharedValue(0);
|
||||||
const colorChangeProgress = useSharedValue(0);
|
const colorChangeProgress = useSharedValue(0);
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
@@ -62,7 +62,11 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
|||||||
return !url?.includes("m3u8");
|
return !url?.includes("m3u8");
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
const onPress = async () => {
|
const item = useMemo(() => {
|
||||||
|
return playSettings?.item;
|
||||||
|
}, [playSettings?.item]);
|
||||||
|
|
||||||
|
const onPress = useCallback(async () => {
|
||||||
if (!url || !item) {
|
if (!url || !item) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"No URL or item provided to PlayButton",
|
"No URL or item provided to PlayButton",
|
||||||
@@ -98,7 +102,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
|||||||
|
|
||||||
switch (selectedIndex) {
|
switch (selectedIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
await CastContext.getPlayServicesState().then((state) => {
|
await CastContext.getPlayServicesState().then(async (state) => {
|
||||||
if (state && state !== PlayServicesState.SUCCESS)
|
if (state && state !== PlayServicesState.SUCCESS)
|
||||||
CastContext.showPlayServicesErrorDialog(state);
|
CastContext.showPlayServicesErrorDialog(state);
|
||||||
else {
|
else {
|
||||||
@@ -108,10 +112,34 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
|||||||
CastContext.showExpandedControls();
|
CastContext.showExpandedControls();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a new URL with the Chromecast device profile:
|
||||||
|
const data = await getStreamUrl({
|
||||||
|
api,
|
||||||
|
deviceProfile: chromecastProfile,
|
||||||
|
item,
|
||||||
|
mediaSourceId: playSettings?.mediaSource?.Id,
|
||||||
|
startTimeTicks: 0,
|
||||||
|
maxStreamingBitrate: playSettings?.bitrate?.value,
|
||||||
|
audioStreamIndex: playSettings?.audioIndex ?? 0,
|
||||||
|
subtitleStreamIndex: playSettings?.subtitleIndex ?? -1,
|
||||||
|
userId: user?.Id,
|
||||||
|
forceDirectPlay: settings?.forceDirectPlay,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data?.url) {
|
||||||
|
console.warn("No URL returned from getStreamUrl", data);
|
||||||
|
Alert.alert(
|
||||||
|
"Client error",
|
||||||
|
"Could not create stream for Chromecast"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
client
|
client
|
||||||
.loadMedia({
|
.loadMedia({
|
||||||
mediaInfo: {
|
mediaInfo: {
|
||||||
contentUrl: url,
|
contentUrl: data?.url,
|
||||||
contentType: "video/mp4",
|
contentType: "video/mp4",
|
||||||
metadata:
|
metadata:
|
||||||
item.Type === "Episode"
|
item.Type === "Episode"
|
||||||
@@ -184,21 +212,32 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
}, [
|
||||||
|
url,
|
||||||
|
item,
|
||||||
|
client,
|
||||||
|
settings,
|
||||||
|
api,
|
||||||
|
user,
|
||||||
|
playSettings,
|
||||||
|
router,
|
||||||
|
showActionSheetWithOptions,
|
||||||
|
mediaStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
const derivedTargetWidth = useDerivedValue(() => {
|
const derivedTargetWidth = useDerivedValue(() => {
|
||||||
if (!memoizedItem || !memoizedItem.RunTimeTicks) return 0;
|
if (!item || !item.RunTimeTicks) return 0;
|
||||||
const userData = memoizedItem.UserData;
|
const userData = item.UserData;
|
||||||
if (userData && userData.PlaybackPositionTicks) {
|
if (userData && userData.PlaybackPositionTicks) {
|
||||||
return userData.PlaybackPositionTicks > 0
|
return userData.PlaybackPositionTicks > 0
|
||||||
? Math.max(
|
? Math.max(
|
||||||
(userData.PlaybackPositionTicks / memoizedItem.RunTimeTicks) * 100,
|
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
||||||
MIN_PLAYBACK_WIDTH
|
MIN_PLAYBACK_WIDTH
|
||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}, [memoizedItem]);
|
}, [item]);
|
||||||
|
|
||||||
useAnimatedReaction(
|
useAnimatedReaction(
|
||||||
() => derivedTargetWidth.value,
|
() => derivedTargetWidth.value,
|
||||||
@@ -214,7 +253,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useAnimatedReaction(
|
useAnimatedReaction(
|
||||||
() => memoizedColor,
|
() => colorAtom,
|
||||||
(newColor) => {
|
(newColor) => {
|
||||||
endColor.value = newColor;
|
endColor.value = newColor;
|
||||||
colorChangeProgress.value = 0;
|
colorChangeProgress.value = 0;
|
||||||
@@ -223,19 +262,19 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
|||||||
easing: Easing.bezier(0.9, 0, 0.31, 0.99),
|
easing: Easing.bezier(0.9, 0, 0.31, 0.99),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[memoizedColor]
|
[colorAtom]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout_2 = setTimeout(() => {
|
const timeout_2 = setTimeout(() => {
|
||||||
startColor.value = memoizedColor;
|
startColor.value = colorAtom;
|
||||||
startWidth.value = targetWidth.value;
|
startWidth.value = targetWidth.value;
|
||||||
}, ANIMATION_DURATION);
|
}, ANIMATION_DURATION);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timeout_2);
|
clearTimeout(timeout_2);
|
||||||
};
|
};
|
||||||
}, [memoizedColor, memoizedItem]);
|
}, [colorAtom, item]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ANIMATED STYLES
|
* ANIMATED STYLES
|
||||||
@@ -318,6 +357,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
|||||||
{client && (
|
{client && (
|
||||||
<Animated.Text style={animatedTextStyle}>
|
<Animated.Text style={animatedTextStyle}>
|
||||||
<Feather name="cast" size={22} />
|
<Feather name="cast" size={22} />
|
||||||
|
<CastButton tintColor="transparent" />
|
||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
)}
|
)}
|
||||||
{!client && settings?.openInVLC && (
|
{!client && settings?.openInVLC && (
|
||||||
|
|||||||
@@ -11,12 +11,9 @@ import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { TouchableOpacityProps, View } from "react-native";
|
import { TouchableOpacityProps, View } from "react-native";
|
||||||
import { getColors } from "react-native-image-colors";
|
|
||||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||||
import { useImageColors } from "@/hooks/useImageColors";
|
|
||||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
library: BaseItemDto;
|
library: BaseItemDto;
|
||||||
@@ -53,10 +50,6 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
|||||||
[library]
|
[library]
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we want to use image colors for library cards
|
|
||||||
// const [color] = useAtom(itemThemeColorAtom)
|
|
||||||
// useImageColors({ url });
|
|
||||||
|
|
||||||
const { data: itemsCount } = useQuery({
|
const { data: itemsCount } = useQuery({
|
||||||
queryKey: ["library-count", library.Id],
|
queryKey: ["library-count", library.Id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -68,6 +61,7 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
|||||||
});
|
});
|
||||||
return response.data.TotalRecordCount;
|
return response.data.TotalRecordCount;
|
||||||
},
|
},
|
||||||
|
staleTime: 1000 * 60 * 60,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
|
|||||||
@@ -71,44 +71,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
const windowDimensions = Dimensions.get("window");
|
const windowDimensions = Dimensions.get("window");
|
||||||
|
|
||||||
const op = useSharedValue<number>(1);
|
|
||||||
const tr = useSharedValue<number>(10);
|
|
||||||
const animatedStyles = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
opacity: op.value,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const animatedTopStyles = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
opacity: op.value,
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
translateY: -tr.value,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const animatedBottomStyles = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
opacity: op.value,
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
translateY: tr.value,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showControls || isBuffering) {
|
|
||||||
op.value = withTiming(1, { duration: 200 });
|
|
||||||
tr.value = withTiming(0, { duration: 200 });
|
|
||||||
} else {
|
|
||||||
op.value = withTiming(0, { duration: 200 });
|
|
||||||
tr.value = withTiming(10, { duration: 200 });
|
|
||||||
}
|
|
||||||
}, [showControls, isBuffering]);
|
|
||||||
|
|
||||||
const { previousItem, nextItem } = useAdjacentItems({ item });
|
const { previousItem, nextItem } = useAdjacentItems({ item });
|
||||||
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(
|
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(
|
||||||
item,
|
item,
|
||||||
@@ -123,17 +85,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
const wasPlayingRef = useRef(false);
|
const wasPlayingRef = useRef(false);
|
||||||
|
|
||||||
const updateTimes = useCallback(
|
|
||||||
(currentProgress: number, maxValue: number) => {
|
|
||||||
const current = ticksToSeconds(currentProgress);
|
|
||||||
const remaining = ticksToSeconds(maxValue - currentProgress);
|
|
||||||
|
|
||||||
setCurrentTime(current);
|
|
||||||
setRemainingTime(remaining);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { showSkipButton, skipIntro } = useIntroSkipper(
|
const { showSkipButton, skipIntro } = useIntroSkipper(
|
||||||
item.Id,
|
item.Id,
|
||||||
currentTime,
|
currentTime,
|
||||||
@@ -180,6 +131,23 @@ export const Controls: React.FC<Props> = ({
|
|||||||
router.replace("/play-video");
|
router.replace("/play-video");
|
||||||
}, [nextItem, settings]);
|
}, [nextItem, settings]);
|
||||||
|
|
||||||
|
const updateTimes = useCallback(
|
||||||
|
(currentProgress: number, maxValue: number) => {
|
||||||
|
const current = ticksToSeconds(currentProgress);
|
||||||
|
const remaining = ticksToSeconds(maxValue - currentProgress);
|
||||||
|
|
||||||
|
setCurrentTime(current);
|
||||||
|
setRemainingTime(remaining);
|
||||||
|
|
||||||
|
if (currentProgress === maxValue) {
|
||||||
|
setShowControls(true);
|
||||||
|
// Automatically play the next item if it exists
|
||||||
|
goToNextItem();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[goToNextItem]
|
||||||
|
);
|
||||||
|
|
||||||
useAnimatedReaction(
|
useAnimatedReaction(
|
||||||
() => ({
|
() => ({
|
||||||
progress: progress.value,
|
progress: progress.value,
|
||||||
@@ -316,7 +284,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
toggleControls();
|
toggleControls();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Animated.View
|
<View
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -324,11 +292,11 @@ export const Controls: React.FC<Props> = ({
|
|||||||
left: 0,
|
left: 0,
|
||||||
width: windowDimensions.width + 100,
|
width: windowDimensions.width + 100,
|
||||||
height: windowDimensions.height + 100,
|
height: windowDimensions.height + 100,
|
||||||
|
opacity: showControls ? 1 : 0,
|
||||||
},
|
},
|
||||||
animatedStyles,
|
|
||||||
]}
|
]}
|
||||||
className={`bg-black/50 z-0`}
|
className={`bg-black/50 z-0`}
|
||||||
></Animated.View>
|
></View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -347,14 +315,14 @@ export const Controls: React.FC<Props> = ({
|
|||||||
<Loader />
|
<Loader />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Animated.View
|
<View
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: insets.top,
|
top: insets.top,
|
||||||
right: insets.right,
|
right: insets.right,
|
||||||
|
opacity: showControls ? 1 : 0,
|
||||||
},
|
},
|
||||||
animatedTopStyles,
|
|
||||||
]}
|
]}
|
||||||
pointerEvents={showControls ? "auto" : "none"}
|
pointerEvents={showControls ? "auto" : "none"}
|
||||||
className={`flex flex-row items-center space-x-2 z-10 p-4`}
|
className={`flex flex-row items-center space-x-2 z-10 p-4`}
|
||||||
@@ -377,9 +345,9 @@ export const Controls: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<Ionicons name="close" size={24} color="white" />
|
<Ionicons name="close" size={24} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</View>
|
||||||
|
|
||||||
<Animated.View
|
<View
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -387,8 +355,8 @@ export const Controls: React.FC<Props> = ({
|
|||||||
maxHeight: windowDimensions.height,
|
maxHeight: windowDimensions.height,
|
||||||
left: insets.left,
|
left: insets.left,
|
||||||
bottom: Platform.OS === "ios" ? insets.bottom : insets.bottom,
|
bottom: Platform.OS === "ios" ? insets.bottom : insets.bottom,
|
||||||
|
opacity: showControls ? 1 : 0,
|
||||||
},
|
},
|
||||||
animatedBottomStyles,
|
|
||||||
]}
|
]}
|
||||||
pointerEvents={showControls ? "auto" : "none"}
|
pointerEvents={showControls ? "auto" : "none"}
|
||||||
className={`flex flex-col p-4 `}
|
className={`flex flex-col p-4 `}
|
||||||
@@ -523,7 +491,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
4
eas.json
4
eas.json
@@ -22,13 +22,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"channel": "0.17.0",
|
"channel": "0.18.0",
|
||||||
"android": {
|
"android": {
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production-apk": {
|
"production-apk": {
|
||||||
"channel": "0.17.0",
|
"channel": "0.18.0",
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk",
|
"buildType": "apk",
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
const { withAndroidManifest } = require("@expo/config-plugins");
|
|
||||||
|
|
||||||
function addAttributesToMainActivity(androidManifest, attributes) {
|
|
||||||
const { manifest } = androidManifest;
|
|
||||||
|
|
||||||
if (!Array.isArray(manifest["application"])) {
|
|
||||||
console.warn("withAndroidMainActivityAttributes: No application array in manifest?");
|
|
||||||
return androidManifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
const application = manifest["application"].find(
|
|
||||||
(item) => item.$["android:name"] === ".MainApplication"
|
|
||||||
);
|
|
||||||
if (!application) {
|
|
||||||
console.warn("withAndroidMainActivityAttributes: No .MainApplication?");
|
|
||||||
return androidManifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(application["activity"])) {
|
|
||||||
console.warn("withAndroidMainActivityAttributes: No activity array in .MainApplication?");
|
|
||||||
return androidManifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activity = application["activity"].find(
|
|
||||||
(item) => item.$["android:name"] === ".MainActivity"
|
|
||||||
);
|
|
||||||
if (!activity) {
|
|
||||||
console.warn("withAndroidMainActivityAttributes: No .MainActivity?");
|
|
||||||
return androidManifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.$ = { ...activity.$, ...attributes };
|
|
||||||
|
|
||||||
return androidManifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function withAndroidMainActivityAttributes(config, attributes) {
|
|
||||||
return withAndroidManifest(config, (config) => {
|
|
||||||
config.modResults = addAttributesToMainActivity(config.modResults, attributes);
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
const { withAppDelegate } = require("@expo/config-plugins");
|
|
||||||
|
|
||||||
const withExpandedController = (config) => {
|
|
||||||
return withAppDelegate(config, async (config) => {
|
|
||||||
const contents = config.modResults.contents;
|
|
||||||
|
|
||||||
// Looking for the initialProps string inside didFinishLaunchingWithOptions,
|
|
||||||
// and injecting expanded controller config.
|
|
||||||
// Should be updated once there is an expo config option - see https://github.com/react-native-google-cast/react-native-google-cast/discussions/537
|
|
||||||
const injectionIndex = contents.indexOf("self.initialProps = @{};");
|
|
||||||
config.modResults.contents =
|
|
||||||
contents.substring(0, injectionIndex) +
|
|
||||||
`\n [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = true; \n` +
|
|
||||||
contents.substring(injectionIndex);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = withExpandedController;
|
|
||||||
@@ -52,7 +52,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
setJellyfin(
|
setJellyfin(
|
||||||
() =>
|
() =>
|
||||||
new Jellyfin({
|
new Jellyfin({
|
||||||
clientInfo: { name: "Streamyfin", version: "0.17.0" },
|
clientInfo: { name: "Streamyfin", version: "0.18.0" },
|
||||||
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
|
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -86,7 +86,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
return {
|
return {
|
||||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||||
Platform.OS === "android" ? "Android" : "iOS"
|
Platform.OS === "android" ? "Android" : "iOS"
|
||||||
}, DeviceId="${deviceId}", Version="0.17.0"`,
|
}, DeviceId="${deviceId}", Version="0.18.0"`,
|
||||||
};
|
};
|
||||||
}, [deviceId]);
|
}, [deviceId]);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Settings } from "../atoms/settings";
|
|||||||
interface PlaySettings {
|
interface PlaySettings {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
bitrate: (typeof BITRATES)[0];
|
bitrate: (typeof BITRATES)[0];
|
||||||
mediaSource: MediaSourceInfo | undefined;
|
mediaSource?: MediaSourceInfo | null;
|
||||||
audioIndex?: number | null;
|
audioIndex?: number | null;
|
||||||
subtitleIndex?: number | null;
|
subtitleIndex?: number | null;
|
||||||
}
|
}
|
||||||
@@ -29,9 +29,8 @@ export function getDefaultPlaySettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. Get first media source
|
// 1. Get first media source
|
||||||
const mediaSource = item.MediaSources?.[0];
|
|
||||||
|
|
||||||
if (!mediaSource) throw new Error("No media source found");
|
const mediaSource = item.MediaSources?.[0];
|
||||||
|
|
||||||
// 2. Get default or preferred audio
|
// 2. Get default or preferred audio
|
||||||
const defaultAudioIndex = mediaSource?.DefaultAudioStreamIndex;
|
const defaultAudioIndex = mediaSource?.DefaultAudioStreamIndex;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const runtimeTicksToMinutes = (
|
|||||||
const hours = Math.floor(ticks / ticksPerHour);
|
const hours = Math.floor(ticks / ticksPerHour);
|
||||||
const minutes = Math.floor((ticks % ticksPerHour) / ticksPerMinute);
|
const minutes = Math.floor((ticks % ticksPerHour) / ticksPerMinute);
|
||||||
|
|
||||||
return `${hours}h ${minutes}m`;
|
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||||
|
else return `${minutes}m`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const runtimeTicksToSeconds = (
|
export const runtimeTicksToSeconds = (
|
||||||
|
|||||||
Reference in New Issue
Block a user