mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
wip
This commit is contained in:
4
app.json
4
app.json
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "Streamyfin",
|
||||
"slug": "streamyfin",
|
||||
"version": "0.13.1",
|
||||
"version": "0.14.0",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "streamyfin",
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"android": {
|
||||
"jsEngine": "hermes",
|
||||
"versionCode": 39,
|
||||
"versionCode": 40,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/icon.png"
|
||||
},
|
||||
|
||||
@@ -6,10 +6,11 @@ import { writeToLog } from "@/utils/log";
|
||||
import { runtimeTicksToMinutes, runtimeTicksToSeconds } from "@/utils/time";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { useSegments } from "expo-router";
|
||||
import { useRouter, useSegments } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Dimensions,
|
||||
Pressable,
|
||||
@@ -19,7 +20,9 @@ import {
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
import "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
SharedValue,
|
||||
useAnimatedStyle,
|
||||
useDerivedValue,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
@@ -27,9 +30,13 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import Video from "react-native-video";
|
||||
import { Text } from "./common/Text";
|
||||
import { Loader } from "./Loader";
|
||||
import { Image } from "expo-image";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { itemRouter } from "./common/TouchableItemRouter";
|
||||
|
||||
export const CurrentlyPlayingBar: React.FC = () => {
|
||||
const segments = useSegments();
|
||||
const {
|
||||
currentlyPlaying,
|
||||
pauseVideo,
|
||||
@@ -40,41 +47,50 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
isPlaying,
|
||||
videoRef,
|
||||
presentFullscreenPlayer,
|
||||
progressTicks,
|
||||
onProgress,
|
||||
isBuffering: _isBuffering,
|
||||
setIsBuffering,
|
||||
} = usePlayback();
|
||||
const insets = useSafeAreaInsets();
|
||||
const segments = useSegments();
|
||||
const router = useRouter();
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
|
||||
const from = useMemo(() => segments[2], [segments]);
|
||||
|
||||
const [ignoreSafeArea, setIgnoreSafeArea] = useState(false);
|
||||
|
||||
const screenHeight = Dimensions.get("window").height;
|
||||
const screenWidth = Dimensions.get("window").width;
|
||||
|
||||
const controlsOpacity = useSharedValue(1);
|
||||
const progress = useSharedValue(0);
|
||||
|
||||
const progress = useSharedValue(progressTicks || 0);
|
||||
const min = useSharedValue(0);
|
||||
const max = useSharedValue(currentlyPlaying?.item.RunTimeTicks || 0);
|
||||
const sliding = useRef(false);
|
||||
const hideControlsTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const from = useMemo(() => segments[2] || "(home)", [segments]);
|
||||
const localIsBuffering = useSharedValue(false);
|
||||
// const hideControlsTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const toggleIgnoreSafeArea = () => {
|
||||
setIgnoreSafeArea((prev) => !prev);
|
||||
};
|
||||
|
||||
const showControls = () => {
|
||||
controlsOpacity.value = withTiming(1, { duration: 300 });
|
||||
controlsOpacity.value = 1;
|
||||
};
|
||||
|
||||
const hideControls = () => {
|
||||
controlsOpacity.value = withTiming(0, { duration: 300 });
|
||||
controlsOpacity.value = 0;
|
||||
};
|
||||
|
||||
const animatedControlsStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: controlsOpacity.value,
|
||||
opacity: withTiming(controlsOpacity.value > 0 ? 1 : 0, {
|
||||
duration: 300,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -119,29 +135,29 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
|
||||
const showControlsAndResetTimer = () => {
|
||||
showControls();
|
||||
resetHideControlsTimer();
|
||||
// resetHideControlsTimer();
|
||||
};
|
||||
|
||||
const resetHideControlsTimer = () => {
|
||||
if (hideControlsTimerRef.current) {
|
||||
clearTimeout(hideControlsTimerRef.current);
|
||||
}
|
||||
hideControlsTimerRef.current = setTimeout(() => {
|
||||
hideControls();
|
||||
}, 3000);
|
||||
};
|
||||
// const resetHideControlsTimer = () => {
|
||||
// if (hideControlsTimerRef.current) {
|
||||
// clearTimeout(hideControlsTimerRef.current);
|
||||
// }
|
||||
// hideControlsTimerRef.current = setTimeout(() => {
|
||||
// hideControls();
|
||||
// }, 3000);
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
if (controlsOpacity.value > 0) {
|
||||
resetHideControlsTimer();
|
||||
}
|
||||
// useEffect(() => {
|
||||
// if (controlsOpacity.value > 0) {
|
||||
// resetHideControlsTimer();
|
||||
// }
|
||||
|
||||
return () => {
|
||||
if (hideControlsTimerRef.current) {
|
||||
clearTimeout(hideControlsTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [controlsOpacity.value]);
|
||||
// return () => {
|
||||
// if (hideControlsTimerRef.current) {
|
||||
// clearTimeout(hideControlsTimerRef.current);
|
||||
// }
|
||||
// };
|
||||
// }, [controlsOpacity.value]);
|
||||
|
||||
useEffect(() => {
|
||||
max.value = currentlyPlaying?.item.RunTimeTicks || 0;
|
||||
@@ -158,24 +174,181 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
: screenWidth - (insets.left + insets.right),
|
||||
};
|
||||
|
||||
const animatedLoaderStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: withTiming(localIsBuffering.value === true ? 1 : 0, {
|
||||
duration: 300,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const animatedVideoContainerStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: withTiming(
|
||||
controlsOpacity.value > 0 || localIsBuffering.value === true ? 0.5 : 1,
|
||||
{
|
||||
duration: 300,
|
||||
}
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const trickplayInfo = useMemo(() => {
|
||||
if (!currentlyPlaying?.item.Id || !currentlyPlaying?.item.Trickplay) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mediaSourceId = currentlyPlaying.item.Id;
|
||||
const trickplayData = currentlyPlaying.item.Trickplay[mediaSourceId];
|
||||
|
||||
if (!trickplayData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the first available resolution
|
||||
const firstResolution = Object.keys(trickplayData)[0];
|
||||
return firstResolution
|
||||
? {
|
||||
resolution: firstResolution,
|
||||
aspectRatio:
|
||||
trickplayData[firstResolution].Width! /
|
||||
trickplayData[firstResolution].Height!,
|
||||
data: trickplayData[firstResolution],
|
||||
}
|
||||
: null;
|
||||
}, [currentlyPlaying]);
|
||||
|
||||
const [trickPlayUrl, _setTrickPlayUrl] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
url: string;
|
||||
} | null>(null);
|
||||
|
||||
const setTrickplayUrl = (
|
||||
info: typeof trickplayInfo | null,
|
||||
progress: SharedValue<number>,
|
||||
api: Api,
|
||||
id: string
|
||||
) => {
|
||||
if (!info || !id || !api) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data, resolution } = info;
|
||||
const { Interval, TileWidth, TileHeight, Height, Width, ThumbnailCount } =
|
||||
data;
|
||||
|
||||
if (
|
||||
!Interval ||
|
||||
!TileWidth ||
|
||||
!TileHeight ||
|
||||
!Height ||
|
||||
!Width ||
|
||||
!ThumbnailCount ||
|
||||
!resolution
|
||||
) {
|
||||
throw new Error("Invalid trickplay data");
|
||||
}
|
||||
|
||||
const currentSecond = Math.max(0, Math.floor(progress.value / 10000000)); // Convert ticks to seconds
|
||||
|
||||
const cols = TileWidth;
|
||||
const rows = TileHeight;
|
||||
const imagesPerTile = cols * rows;
|
||||
const imageIndex = Math.floor(currentSecond / (Interval / 1000)); // Interval is in ms
|
||||
const tileIndex = Math.floor(imageIndex / imagesPerTile);
|
||||
|
||||
const positionInTile = imageIndex % imagesPerTile;
|
||||
const rowInTile = Math.floor(positionInTile / cols);
|
||||
const colInTile = positionInTile % cols;
|
||||
|
||||
const res = {
|
||||
x: rowInTile,
|
||||
y: colInTile,
|
||||
url: `${api.basePath}/Videos/${id}/Trickplay/${resolution}/${tileIndex}.jpg?api_key=${api.accessToken}`,
|
||||
};
|
||||
|
||||
_setTrickPlayUrl(res);
|
||||
};
|
||||
|
||||
const { data: previousItem } = useQuery({
|
||||
queryKey: [
|
||||
"previousItem",
|
||||
currentlyPlaying?.item.ParentId,
|
||||
currentlyPlaying?.item.IndexNumber,
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (
|
||||
!api ||
|
||||
!currentlyPlaying?.item.ParentId ||
|
||||
currentlyPlaying?.item.IndexNumber === undefined ||
|
||||
currentlyPlaying?.item.IndexNumber === null ||
|
||||
currentlyPlaying.item.IndexNumber - 2 < 0
|
||||
) {
|
||||
console.log("No previous item");
|
||||
return null;
|
||||
}
|
||||
|
||||
const res = await getItemsApi(api).getItems({
|
||||
parentId: currentlyPlaying.item.ParentId!,
|
||||
startIndex: currentlyPlaying.item.IndexNumber! - 2,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Prev: ",
|
||||
res.data.Items?.map((i) => i.Name)
|
||||
);
|
||||
return res.data.Items?.[0];
|
||||
},
|
||||
enabled: currentlyPlaying?.item.Type === "Episode",
|
||||
});
|
||||
|
||||
const { data: nextItem } = useQuery({
|
||||
queryKey: [
|
||||
"nextItem",
|
||||
currentlyPlaying?.item.ParentId,
|
||||
currentlyPlaying?.item.IndexNumber,
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (
|
||||
!api ||
|
||||
!currentlyPlaying?.item.ParentId ||
|
||||
currentlyPlaying?.item.IndexNumber === undefined ||
|
||||
currentlyPlaying?.item.IndexNumber === null
|
||||
) {
|
||||
console.log("No next item");
|
||||
return null;
|
||||
}
|
||||
|
||||
const res = await getItemsApi(api).getItems({
|
||||
parentId: currentlyPlaying.item.ParentId!,
|
||||
startIndex: currentlyPlaying.item.IndexNumber!,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Next: ",
|
||||
res.data.Items?.map((i) => i.Name)
|
||||
);
|
||||
return res.data.Items?.[0];
|
||||
},
|
||||
enabled: currentlyPlaying?.item.Type === "Episode",
|
||||
});
|
||||
|
||||
const prevButtonEnabled = useMemo(() => {
|
||||
return !!previousItem;
|
||||
}, [previousItem]);
|
||||
|
||||
const nextButtonEnabled = useMemo(() => {
|
||||
return !!nextItem;
|
||||
}, [nextItem]);
|
||||
|
||||
if (!api || !currentlyPlaying) return null;
|
||||
|
||||
return (
|
||||
<View style={{ width: screenWidth, height: screenHeight }}>
|
||||
<View style={{ width: "100%", height: "100%", backgroundColor: "black" }}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
position: "absolute",
|
||||
bottom: insets.bottom + 40,
|
||||
left: 32 + insets.left,
|
||||
height: 64,
|
||||
width: 140,
|
||||
zIndex: 10,
|
||||
},
|
||||
]}
|
||||
></View>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
@@ -214,7 +387,9 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
<View style={videoContainerStyle}>
|
||||
<Animated.View
|
||||
style={[videoContainerStyle, animatedVideoContainerStyle]}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (controlsOpacity.value > 0) {
|
||||
@@ -223,7 +398,10 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
showControlsAndResetTimer();
|
||||
}
|
||||
}}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{videoSource && (
|
||||
<Video
|
||||
@@ -240,11 +418,16 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
ignoreSilentSwitch="ignore"
|
||||
controls={false}
|
||||
pictureInPicture={true}
|
||||
debug={{
|
||||
enable: true,
|
||||
thread: true,
|
||||
}}
|
||||
onProgress={(e) => {
|
||||
// Set buffering state
|
||||
if (e.playableDuration === 0) {
|
||||
setIsBuffering(true);
|
||||
localIsBuffering.value = true;
|
||||
} else {
|
||||
setIsBuffering(false);
|
||||
localIsBuffering.value = false;
|
||||
}
|
||||
|
||||
if (sliding.current === true) return;
|
||||
onProgress(e);
|
||||
progress.value = e.currentTime * 10000000;
|
||||
@@ -270,7 +453,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
onVolumeChange={(e) => {
|
||||
setVolume(e.volume);
|
||||
}}
|
||||
progressUpdateInterval={4000}
|
||||
progressUpdateInterval={1000}
|
||||
onError={(e) => {
|
||||
console.log(e);
|
||||
writeToLog(
|
||||
@@ -280,17 +463,10 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
Alert.alert("Error", "Cannot play this video file.");
|
||||
setIsPlaying(false);
|
||||
}}
|
||||
renderLoader={
|
||||
currentlyPlaying.item?.Type !== "Audio" && (
|
||||
<View className="flex flex-col items-center justify-center h-full">
|
||||
<Loader />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
@@ -322,19 +498,32 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<BlurView
|
||||
intensity={100}
|
||||
className="flex flex-row items-center space-x-6 rounded-full py-1.5 pl-4 pr-4 z-10 overflow-hidden"
|
||||
>
|
||||
<View className="flex flex-row items-center space-x-6 rounded-full py-1.5 pl-4 pr-4 z-10 bg-neutral-800">
|
||||
<View className="flex flex-row items-center space-x-2">
|
||||
<Ionicons name="play-skip-back" size={18} color="white" />
|
||||
<TouchableOpacity
|
||||
disabled={prevButtonEnabled === false}
|
||||
style={{
|
||||
opacity: prevButtonEnabled === false ? 0.5 : 1,
|
||||
}}
|
||||
onPress={() => {
|
||||
if (controlsOpacity.value === 0) return;
|
||||
if (prevButtonEnabled === false) return;
|
||||
if (!previousItem || !from) return;
|
||||
const url = itemRouter(previousItem, from);
|
||||
stopPlayback();
|
||||
// @ts-ignore
|
||||
router.push(url);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="play-skip-back" size={18} color="white" />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={async () => {
|
||||
if (controlsOpacity.value === 0) return;
|
||||
const curr = await videoRef.current?.getCurrentPosition();
|
||||
if (!curr) return;
|
||||
videoRef.current?.seek(Math.max(0, curr - 15));
|
||||
resetHideControlsTimer();
|
||||
// resetHideControlsTimer();
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
@@ -351,7 +540,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
if (controlsOpacity.value === 0) return;
|
||||
if (isPlaying) pauseVideo();
|
||||
else playVideo();
|
||||
resetHideControlsTimer();
|
||||
// resetHideControlsTimer();
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
@@ -366,12 +555,28 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
const curr = await videoRef.current?.getCurrentPosition();
|
||||
if (!curr) return;
|
||||
videoRef.current?.seek(Math.max(0, curr + 15));
|
||||
resetHideControlsTimer();
|
||||
// resetHideControlsTimer();
|
||||
}}
|
||||
>
|
||||
<Ionicons name="refresh-outline" size={22} color="white" />
|
||||
</TouchableOpacity>
|
||||
<Ionicons name="play-skip-forward" size={18} color="white" />
|
||||
<TouchableOpacity
|
||||
disabled={nextButtonEnabled === false}
|
||||
style={{
|
||||
opacity: nextButtonEnabled === false ? 0.5 : 1,
|
||||
}}
|
||||
onPress={() => {
|
||||
if (controlsOpacity.value === 0) return;
|
||||
if (nextButtonEnabled === false) return;
|
||||
if (!nextItem || !from) return;
|
||||
const url = itemRouter(nextItem, from);
|
||||
stopPlayback();
|
||||
// @ts-ignore
|
||||
router.push(url);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="play-skip-forward" size={18} color="white" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View className="flex flex-col w-full shrink">
|
||||
<Slider
|
||||
@@ -397,12 +602,52 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
if (controlsOpacity.value === 0) return;
|
||||
const tick = Math.floor(val);
|
||||
progress.value = tick;
|
||||
resetHideControlsTimer();
|
||||
setTrickplayUrl(
|
||||
trickplayInfo,
|
||||
progress,
|
||||
api,
|
||||
currentlyPlaying.item.Id!
|
||||
);
|
||||
// resetHideControlsTimer();
|
||||
}}
|
||||
containerStyle={{
|
||||
borderRadius: 100,
|
||||
}}
|
||||
bubble={(s) => runtimeTicksToMinutes(s)}
|
||||
renderBubble={() => {
|
||||
if (!trickPlayUrl || !trickplayInfo) {
|
||||
return null;
|
||||
}
|
||||
const { x, y, url } = trickPlayUrl;
|
||||
|
||||
const tileWidth = 200;
|
||||
const tileHeight = 200 / trickplayInfo.aspectRatio!;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: tileWidth,
|
||||
height: tileHeight,
|
||||
marginLeft: -tileWidth / 4,
|
||||
marginTop: -tileHeight / 4 - 60,
|
||||
}}
|
||||
className=" bg-neutral-800 overflow-hidden"
|
||||
>
|
||||
<Image
|
||||
style={{
|
||||
width: 200 * trickplayInfo?.data.TileWidth!,
|
||||
height:
|
||||
(200 / trickplayInfo.aspectRatio!) *
|
||||
trickplayInfo?.data.TileHeight!,
|
||||
transform: [
|
||||
{ translateX: -x * tileWidth },
|
||||
{ translateY: -y * tileHeight },
|
||||
],
|
||||
}}
|
||||
source={{ uri: url }}
|
||||
contentFit="cover"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
sliderHeight={8}
|
||||
thumbWidth={0}
|
||||
progress={progress}
|
||||
@@ -418,7 +663,29 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</BlurView>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View
|
||||
pointerEvents="none"
|
||||
style={[
|
||||
{
|
||||
position: "absolute" as const,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: ignoreSafeArea ? 0 : insets.left,
|
||||
right: ignoreSafeArea ? 0 : insets.right,
|
||||
width: ignoreSafeArea
|
||||
? screenWidth
|
||||
: screenWidth - (insets.left + insets.right),
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: 10,
|
||||
},
|
||||
animatedLoaderStyle,
|
||||
]}
|
||||
>
|
||||
<Loader />
|
||||
</Animated.View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -117,6 +117,8 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
|
||||
itemId: id,
|
||||
});
|
||||
|
||||
console.log("itemID", res?.Id);
|
||||
|
||||
return res;
|
||||
},
|
||||
enabled: !!id && !!api,
|
||||
|
||||
@@ -57,7 +57,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
|
||||
const directStream = useMemo(() => {
|
||||
return !url?.includes("m3u8");
|
||||
}, []);
|
||||
}, [url]);
|
||||
|
||||
const onPress = async () => {
|
||||
if (!url || !item) return;
|
||||
|
||||
@@ -8,6 +8,42 @@ interface Props extends TouchableOpacityProps {
|
||||
item: BaseItemDto;
|
||||
}
|
||||
|
||||
export const itemRouter = (item: BaseItemDto, from: string) => {
|
||||
if (item.Type === "Series") {
|
||||
return `/(auth)/(tabs)/${from}/series/${item.Id}`;
|
||||
}
|
||||
|
||||
if (item.Type === "MusicAlbum") {
|
||||
return `/(auth)/(tabs)/${from}/albums/${item.Id}`;
|
||||
}
|
||||
|
||||
if (item.Type === "Audio") {
|
||||
return `/(auth)/(tabs)/${from}/albums/${item.AlbumId}`;
|
||||
}
|
||||
|
||||
if (item.Type === "MusicArtist") {
|
||||
return `/(auth)/(tabs)/${from}/artists/${item.Id}`;
|
||||
}
|
||||
|
||||
if (item.Type === "Person") {
|
||||
return `/(auth)/(tabs)/${from}/actors/${item.Id}`;
|
||||
}
|
||||
|
||||
if (item.Type === "BoxSet") {
|
||||
return `/(auth)/(tabs)/${from}/collections/${item.Id}`;
|
||||
}
|
||||
|
||||
if (item.Type === "UserView") {
|
||||
return `/(auth)/(tabs)/${from}/collections/${item.Id}`;
|
||||
}
|
||||
|
||||
if (item.Type === "CollectionFolder") {
|
||||
return `/(auth)/(tabs)/(libraries)/${item.Id}`;
|
||||
}
|
||||
|
||||
return `/(auth)/(tabs)/${from}/items/page?id=${item.Id}`;
|
||||
};
|
||||
|
||||
export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
item,
|
||||
children,
|
||||
@@ -23,54 +59,9 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
|
||||
if (item.Type === "Series") {
|
||||
router.push(`/(auth)/(tabs)/${from}/series/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "MusicAlbum") {
|
||||
router.push(`/(auth)/(tabs)/${from}/albums/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "Audio") {
|
||||
router.push(`/(auth)/(tabs)/${from}/albums/${item.AlbumId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "MusicArtist") {
|
||||
router.push(`/(auth)/(tabs)/${from}/artists/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "Person") {
|
||||
router.push(`/(auth)/(tabs)/${from}/actors/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "BoxSet") {
|
||||
router.push(`/(auth)/(tabs)/${from}/collections/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "UserView") {
|
||||
router.push(`/(auth)/(tabs)/${from}/collections/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "CollectionFolder") {
|
||||
router.push(`/(auth)/(tabs)/(libraries)/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Same as default
|
||||
// if (item.Type === "Episode") {
|
||||
// router.push(`/items/${item.Id}`);
|
||||
// return;
|
||||
// }
|
||||
|
||||
router.push(`/(auth)/(tabs)/${from}/items/page?id=${item.Id}`);
|
||||
const url = itemRouter(item, from);
|
||||
// @ts-ignore
|
||||
router.push(url);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
|
||||
4
eas.json
4
eas.json
@@ -21,13 +21,13 @@
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"channel": "0.13.1",
|
||||
"channel": "0.14.0",
|
||||
"android": {
|
||||
"image": "latest"
|
||||
}
|
||||
},
|
||||
"production-apk": {
|
||||
"channel": "0.13.1",
|
||||
"channel": "0.14.0",
|
||||
"android": {
|
||||
"buildType": "apk",
|
||||
"image": "latest"
|
||||
|
||||
@@ -63,7 +63,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
setJellyfin(
|
||||
() =>
|
||||
new Jellyfin({
|
||||
clientInfo: { name: "Streamyfin", version: "0.13.1" },
|
||||
clientInfo: { name: "Streamyfin", version: "0.14.0" },
|
||||
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
|
||||
})
|
||||
);
|
||||
@@ -97,7 +97,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
return {
|
||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||
Platform.OS === "android" ? "Android" : "iOS"
|
||||
}, DeviceId="${deviceId}", Version="0.13.1"`,
|
||||
}, DeviceId="${deviceId}", Version="0.14.0"`,
|
||||
};
|
||||
}, [deviceId]);
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ interface PlaybackContextType {
|
||||
dismissFullscreenPlayer: () => void;
|
||||
setIsFullscreen: (isFullscreen: boolean) => void;
|
||||
setIsPlaying: (isPlaying: boolean) => void;
|
||||
isBuffering: boolean;
|
||||
setIsBuffering: (val: boolean) => void;
|
||||
onProgress: (data: OnProgressData) => void;
|
||||
setVolume: (volume: number) => void;
|
||||
setCurrentlyPlayingState: (
|
||||
@@ -70,6 +72,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
|
||||
const previousVolume = useRef<number | null>(null);
|
||||
|
||||
const [isPlaying, _setIsPlaying] = useState<boolean>(false);
|
||||
const [isBuffering, setIsBuffering] = useState<boolean>(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
|
||||
const [progressTicks, setProgressTicks] = useState<number | null>(0);
|
||||
const [volume, _setVolume] = useState<number | null>(null);
|
||||
@@ -254,7 +257,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
|
||||
const onProgress = useCallback(
|
||||
debounce((e: OnProgressData) => {
|
||||
_onProgress(e);
|
||||
}, 1000),
|
||||
}, 500),
|
||||
[_onProgress]
|
||||
);
|
||||
|
||||
@@ -352,6 +355,8 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
|
||||
<PlaybackContext.Provider
|
||||
value={{
|
||||
onProgress,
|
||||
isBuffering,
|
||||
setIsBuffering,
|
||||
progressTicks,
|
||||
setVolume,
|
||||
setIsPlaying,
|
||||
|
||||
Reference in New Issue
Block a user