chore: keep up to date with master

This commit is contained in:
Fredrik Burmester
2024-10-13 19:25:32 +02:00
parent a71832c6e5
commit d41040e6d3
15 changed files with 164 additions and 215 deletions

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
"version": "0.17.0",
"version": "0.18.0",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
@@ -33,7 +33,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 44,
"versionCode": 46,
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive_icon.png"
},
@@ -66,13 +66,6 @@
}
}
],
[
"./plugins/withAndroidMainActivityAttributes",
{
"com.reactnative.googlecast.RNGCExpandedControllerActivity": true
}
],
["./plugins/withExpandedController.js"],
[
"expo-build-properties",
{

View File

@@ -25,11 +25,10 @@ import {
import NetInfo from "@react-native-community/netinfo";
import { QueryFunction, useQuery, useQueryClient } from "@tanstack/react-query";
import { useNavigation, useRouter } from "expo-router";
import { useAtom, useAtomValue } from "jotai";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
Platform,
RefreshControl,
ScrollView,
TouchableOpacity,

View File

@@ -1,12 +1,8 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import {
useFocusEffect,
useLocalSearchParams,
useNavigation,
} from "expo-router";
import { useLocalSearchParams, useNavigation } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation";
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 { Text } from "@/components/common/Text";
@@ -16,6 +12,7 @@ import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { ItemCardText } from "@/components/ItemCardText";
import { Loader } from "@/components/Loader";
import { ItemPoster } from "@/components/posters/ItemPoster";
import { useOrientation } from "@/hooks/useOrientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
genreFilterAtom,
@@ -32,7 +29,6 @@ import {
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
import { orientationAtom } from "@/utils/atoms/orientation";
import {
BaseItemDto,
BaseItemDtoQueryResult,
@@ -60,12 +56,13 @@ const Page = () => {
const [selectedTags, setSelectedTags] = useAtom(tagsFilterAtom);
const [sortBy, _setSortBy] = useAtom(sortByAtom);
const [sortOrder, _setSortOrder] = useAtom(sortOrderAtom);
const [orientation] = useAtom(orientationAtom);
const [sortByPreference, setSortByPreference] = useAtom(sortByPreferenceAtom);
const [sortOrderPreference, setOderByPreference] = useAtom(
sortOrderPreferenceAtom
);
const { orientation } = useOrientation();
useEffect(() => {
const sop = getSortOrderPreference(libraryId, sortOrderPreference);
if (sop) {
@@ -106,11 +103,12 @@ const Page = () => {
[libraryId, sortOrderPreference]
);
const getNumberOfColumns = useCallback(() => {
if (orientation === ScreenOrientation.Orientation.PORTRAIT_UP) return 3;
if (screenWidth < 600) return 5;
if (screenWidth < 960) return 6;
if (screenWidth < 1280) return 7;
const nrOfCols = useMemo(() => {
if (screenWidth < 300) return 2;
if (screenWidth < 500) return 3;
if (screenWidth < 800) return 5;
if (screenWidth < 1000) return 6;
if (screenWidth < 1500) return 7;
return 6;
}, [screenWidth, orientation]);
@@ -219,7 +217,7 @@ const Page = () => {
const renderItem = useCallback(
({ item, index }: { item: BaseItemDto; index: number }) => (
<MemoizedTouchableItemRouter
<TouchableItemRouter
key={item.Id}
style={{
width: "100%",
@@ -230,10 +228,10 @@ const Page = () => {
<View
style={{
alignSelf:
orientation === ScreenOrientation.Orientation.PORTRAIT_UP
? index % 3 === 0
orientation === ScreenOrientation.OrientationLock.PORTRAIT_UP
? index % nrOfCols === 0
? "flex-end"
: (index + 1) % 3 === 0
: (index + 1) % nrOfCols === 0
? "flex-start"
: "center"
: "center",
@@ -244,7 +242,7 @@ const Page = () => {
<ItemPoster item={item} />
<ItemCardText item={item} />
</View>
</MemoizedTouchableItemRouter>
</TouchableItemRouter>
),
[orientation]
);
@@ -429,6 +427,7 @@ const Page = () => {
return (
<FlashList
key={orientation}
ListEmptyComponent={
<View className="flex flex-col items-center justify-center h-full">
<Text className="font-bold text-xl text-neutral-500">No results</Text>
@@ -437,10 +436,10 @@ const Page = () => {
contentInsetAdjustmentBehavior="automatic"
data={flatData}
renderItem={renderItem}
extraData={orientation}
extraData={[orientation, nrOfCols]}
keyExtractor={keyExtractor}
estimatedItemSize={244}
numColumns={getNumberOfColumns()}
numColumns={nrOfCols}
onEndReached={() => {
if (hasNextPage) {
fetchNextPage();

View File

@@ -9,14 +9,8 @@ import {
VlcPlayerViewRef,
} from "@/modules/vlc-player/src/VlcPlayer.types";
import { apiAtom } from "@/providers/JellyfinProvider";
import {
PlaybackType,
usePlaySettings,
} from "@/providers/PlaySettingsProvider";
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { ticksToSeconds } from "@/utils/time";
import { Api } from "@jellyfin/sdk";
import * as Haptics from "expo-haptics";
import { useFocusEffect } from "expo-router";
import { useAtomValue } from "jotai";
@@ -27,7 +21,7 @@ import React, {
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 { SelectedTrackType } from "react-native-video";
@@ -37,7 +31,10 @@ export default function page() {
const [settings] = useSettings();
const videoRef = useRef<VlcPlayerViewRef>(null);
const screenDimensions = Dimensions.get("screen");
const dimensions = useWindowDimensions();
useOrientation();
useOrientationSettings();
useAndroidNavigationBar();
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
const [showControls, setShowControls] = useState(true);
@@ -176,8 +173,8 @@ export default function page() {
return (
<View
style={{
width: screenDimensions.width,
height: screenDimensions.height,
width: dimensions.width,
height: dimensions.height,
position: "relative",
}}
className="flex flex-col items-center justify-center"

View File

@@ -18,7 +18,7 @@ import * as Haptics from "expo-haptics";
import { useFocusEffect } from "expo-router";
import { useAtomValue } from "jotai";
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 Video, {
OnProgressData,
@@ -34,8 +34,7 @@ export default function page() {
const poster = usePoster(playSettings, api);
const videoSource = useVideoSource(playSettings, api, poster, playUrl);
const firstTime = useRef(true);
const screenDimensions = Dimensions.get("screen");
const dimensions = useWindowDimensions();
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
const [showControls, setShowControls] = useState(true);
@@ -170,7 +169,7 @@ export default function page() {
}, [play, stop])
);
const { orientation } = useOrientation();
useOrientation();
useOrientationSettings();
useAndroidNavigationBar();
@@ -214,23 +213,36 @@ export default function page() {
return (
<View
style={{
width: screenDimensions.width,
height: screenDimensions.height,
flex: 1,
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
width: dimensions.width,
height: dimensions.height,
position: "relative",
}}
className="flex flex-col items-center justify-center"
>
<StatusBar hidden />
<Pressable
onPress={() => {
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
ref={videoRef}
source={videoSource}
style={{ width: "100%", height: "100%" }}
style={{
width: dimensions.width,
height: dimensions.height,
}}
resizeMode={ignoreSafeAreas ? "cover" : "contain"}
onProgress={onProgress}
onError={() => {}}

View File

@@ -1,8 +1,9 @@
import { Feather } from "@expo/vector-icons";
import { BlurView } from "expo-blur";
import React, { useEffect } from "react";
import React, { useCallback, useEffect } from "react";
import { Platform, TouchableOpacity, ViewProps } from "react-native";
import GoogleCast, {
CastButton,
CastContext,
useCastDevice,
useDevices,
@@ -39,18 +40,32 @@ export const Chromecast: React.FC<Props> = ({
})();
}, [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")
return (
<TouchableOpacity
onPress={() => {
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
else CastContext.showCastDialog();
}}
className="rounded-full h-10 w-10 flex items-center justify-center b"
{...props}
>
<Feather name="cast" size={22} color={"white"} />
</TouchableOpacity>
<>
<TouchableOpacity
onPress={() => {
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
else CastContext.showCastDialog();
}}
className="rounded-full h-10 w-10 flex items-center justify-center b"
{...props}
>
<Feather name="cast" size={22} color={"white"} />
</TouchableOpacity>
<AndroidCastButton />
</>
);
if (Platform.OS === "android")
@@ -82,6 +97,7 @@ export const Chromecast: React.FC<Props> = ({
>
<Feather name="cast" size={22} color={"white"} />
</BlurView>
<AndroidCastButton />
</TouchableOpacity>
);
};

View File

@@ -246,7 +246,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
</View>
)}
<PlayButton item={item} url={playUrl} className="grow" />
<PlayButton className="grow" />
</View>
{item.Type === "Episode" && (

View File

@@ -1,4 +1,4 @@
import { apiAtom } from "@/providers/JellyfinProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
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 { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useAtom } from "jotai";
import { useEffect, useMemo } from "react";
import { Linking, TouchableOpacity, View } from "react-native";
import { useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo } from "react";
import { Alert, Linking, TouchableOpacity, View } from "react-native";
import CastContext, {
CastButton,
PlayServicesState,
useMediaStatus,
useRemoteMediaClient,
@@ -28,32 +29,31 @@ import { Button } from "./Button";
import { Text } from "./common/Text";
import { useRouter } from "expo-router";
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> {
item?: BaseItemDto | null;
url?: string | null;
}
interface Props extends React.ComponentProps<typeof Button> {}
const ANIMATION_DURATION = 500;
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 client = useRemoteMediaClient();
const mediaStatus = useMediaStatus();
const [colorAtom] = useAtom(itemThemeColorAtom);
const [api] = useAtom(apiAtom);
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
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 targetWidth = useSharedValue(0);
const endColor = useSharedValue(memoizedColor);
const startColor = useSharedValue(memoizedColor);
const endColor = useSharedValue(colorAtom);
const startColor = useSharedValue(colorAtom);
const widthProgress = useSharedValue(0);
const colorChangeProgress = useSharedValue(0);
const [settings] = useSettings();
@@ -62,7 +62,11 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
return !url?.includes("m3u8");
}, [url]);
const onPress = async () => {
const item = useMemo(() => {
return playSettings?.item;
}, [playSettings?.item]);
const onPress = useCallback(async () => {
if (!url || !item) {
console.warn(
"No URL or item provided to PlayButton",
@@ -98,7 +102,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
switch (selectedIndex) {
case 0:
await CastContext.getPlayServicesState().then((state) => {
await CastContext.getPlayServicesState().then(async (state) => {
if (state && state !== PlayServicesState.SUCCESS)
CastContext.showPlayServicesErrorDialog(state);
else {
@@ -108,10 +112,34 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
CastContext.showExpandedControls();
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
.loadMedia({
mediaInfo: {
contentUrl: url,
contentUrl: data?.url,
contentType: "video/mp4",
metadata:
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(() => {
if (!memoizedItem || !memoizedItem.RunTimeTicks) return 0;
const userData = memoizedItem.UserData;
if (!item || !item.RunTimeTicks) return 0;
const userData = item.UserData;
if (userData && userData.PlaybackPositionTicks) {
return userData.PlaybackPositionTicks > 0
? Math.max(
(userData.PlaybackPositionTicks / memoizedItem.RunTimeTicks) * 100,
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
MIN_PLAYBACK_WIDTH
)
: 0;
}
return 0;
}, [memoizedItem]);
}, [item]);
useAnimatedReaction(
() => derivedTargetWidth.value,
@@ -214,7 +253,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
);
useAnimatedReaction(
() => memoizedColor,
() => colorAtom,
(newColor) => {
endColor.value = newColor;
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),
});
},
[memoizedColor]
[colorAtom]
);
useEffect(() => {
const timeout_2 = setTimeout(() => {
startColor.value = memoizedColor;
startColor.value = colorAtom;
startWidth.value = targetWidth.value;
}, ANIMATION_DURATION);
return () => {
clearTimeout(timeout_2);
};
}, [memoizedColor, memoizedItem]);
}, [colorAtom, item]);
/**
* ANIMATED STYLES
@@ -318,6 +357,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
{client && (
<Animated.Text style={animatedTextStyle}>
<Feather name="cast" size={22} />
<CastButton tintColor="transparent" />
</Animated.Text>
)}
{!client && settings?.openInVLC && (

View File

@@ -11,12 +11,9 @@ import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useAtom } from "jotai";
import { useEffect, useMemo, useState } from "react";
import { useMemo } from "react";
import { TouchableOpacityProps, View } from "react-native";
import { getColors } from "react-native-image-colors";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
import { useImageColors } from "@/hooks/useImageColors";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
interface Props extends TouchableOpacityProps {
library: BaseItemDto;
@@ -53,10 +50,6 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
[library]
);
// If we want to use image colors for library cards
// const [color] = useAtom(itemThemeColorAtom)
// useImageColors({ url });
const { data: itemsCount } = useQuery({
queryKey: ["library-count", library.Id],
queryFn: async () => {
@@ -68,6 +61,7 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
});
return response.data.TotalRecordCount;
},
staleTime: 1000 * 60 * 60,
});
if (!url) return null;

View File

@@ -85,47 +85,7 @@ export const Controls: React.FC<Props> = ({
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: offline ? undefined : item,
});
const { previousItem, nextItem } = useAdjacentItems({ item });
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(
item,
!offline
@@ -398,7 +358,7 @@ export const Controls: React.FC<Props> = ({
toggleControls();
}}
>
<Animated.View
<View
style={[
{
position: "absolute",
@@ -406,11 +366,11 @@ export const Controls: React.FC<Props> = ({
left: 0,
width: windowDimensions.width + 100,
height: windowDimensions.height + 100,
opacity: showControls ? 1 : 0,
},
animatedStyles,
]}
className={`bg-black/50 z-0`}
></Animated.View>
></View>
</Pressable>
<View
@@ -429,14 +389,14 @@ export const Controls: React.FC<Props> = ({
<Loader />
</View>
<Animated.View
<View
style={[
{
position: "absolute",
top: insets.top,
right: insets.right,
opacity: showControls ? 1 : 0,
},
animatedTopStyles,
]}
pointerEvents={showControls ? "auto" : "none"}
className={`flex flex-row items-center space-x-2 z-10 p-4 `}
@@ -460,9 +420,9 @@ export const Controls: React.FC<Props> = ({
>
<Ionicons name="close" size={24} color="white" />
</TouchableOpacity>
</Animated.View>
</View>
<Animated.View
<View
style={[
{
position: "absolute",
@@ -470,8 +430,8 @@ export const Controls: React.FC<Props> = ({
maxHeight: windowDimensions.height,
left: insets.left,
bottom: Platform.OS === "ios" ? insets.bottom : insets.bottom,
opacity: showControls ? 1 : 0,
},
animatedBottomStyles,
]}
pointerEvents={showControls ? "auto" : "none"}
className={`flex flex-col p-4 `}
@@ -606,7 +566,7 @@ export const Controls: React.FC<Props> = ({
</View>
</View>
</View>
</Animated.View>
</View>
</View>
);
};

View File

@@ -22,13 +22,13 @@
}
},
"production": {
"channel": "0.17.0",
"channel": "0.18.0",
"android": {
"image": "latest"
}
},
"production-apk": {
"channel": "0.17.0",
"channel": "0.18.0",
"android": {
"buildType": "apk",
"image": "latest"

View File

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

View File

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

View File

@@ -52,7 +52,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setJellyfin(
() =>
new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.17.0" },
clientInfo: { name: "Streamyfin", version: "0.18.0" },
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
})
);
@@ -86,7 +86,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
return {
authorization: `MediaBrowser Client="Streamyfin", Device=${
Platform.OS === "android" ? "Android" : "iOS"
}, DeviceId="${deviceId}", Version="0.17.0"`,
}, DeviceId="${deviceId}", Version="0.18.0"`,
};
}, [deviceId]);

View File

@@ -16,7 +16,8 @@ export const runtimeTicksToMinutes = (
const hours = Math.floor(ticks / ticksPerHour);
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 = (