mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-01 11:38:26 +01:00
chore: Apply linting rules and add git hok (#611)
Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { View, StyleSheet, Platform } from "react-native";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import type React from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
const VolumeManager = Platform.isTV ? null : require("react-native-volume-manager");
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
const VolumeManager = Platform.isTV
|
||||
? null
|
||||
: require("react-native-volume-manager");
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { VolumeResult } from "react-native-volume-manager";
|
||||
import type { VolumeResult } from "react-native-volume-manager";
|
||||
|
||||
interface AudioSliderProps {
|
||||
setVisibility: (show: boolean) => void;
|
||||
@@ -50,20 +53,22 @@ const AudioSlider: React.FC<AudioSliderProps> = ({ setVisibility }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const volumeListener = VolumeManager.addVolumeListener((result: VolumeResult) => {
|
||||
volume.value = result.volume * 100;
|
||||
setVisibility(true);
|
||||
const volumeListener = VolumeManager.addVolumeListener(
|
||||
(result: VolumeResult) => {
|
||||
volume.value = result.volume * 100;
|
||||
setVisibility(true);
|
||||
|
||||
// Clear any existing timeout
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
// Clear any existing timeout
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
// Set a new timeout to hide the visibility after 2 seconds
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setVisibility(false);
|
||||
}, 1000);
|
||||
});
|
||||
// Set a new timeout to hide the visibility after 2 seconds
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setVisibility(false);
|
||||
}, 1000);
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
volumeListener.remove();
|
||||
@@ -92,9 +97,9 @@ const AudioSlider: React.FC<AudioSliderProps> = ({ setVisibility }) => {
|
||||
}}
|
||||
/>
|
||||
<Ionicons
|
||||
name="volume-high"
|
||||
name='volume-high'
|
||||
size={20}
|
||||
color="#FDFDFD"
|
||||
color='#FDFDFD'
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { View, StyleSheet, Platform } from "react-native";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
// import * as Brightness from "expo-brightness";
|
||||
const Brightness = !Platform.isTV ? require("expo-brightness") : null;
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
@@ -46,9 +46,9 @@ const BrightnessSlider = () => {
|
||||
}}
|
||||
/>
|
||||
<Ionicons
|
||||
name="sunny"
|
||||
name='sunny'
|
||||
size={20}
|
||||
color="#FDFDFD"
|
||||
color='#FDFDFD'
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
}}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import {
|
||||
HorizontalScroll,
|
||||
HorizontalScrollRef,
|
||||
} from "@/components/common/HorrizontalScroll";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
|
||||
import { DownloadSingleItem } from "@/components/DownloadItem";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import {
|
||||
HorizontalScroll,
|
||||
type HorizontalScrollRef,
|
||||
} from "@/components/common/HorrizontalScroll";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import {
|
||||
SeasonDropdown,
|
||||
SeasonIndexState,
|
||||
type SeasonIndexState,
|
||||
} from "@/components/series/SeasonDropdown";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
||||
import { runtimeTicksToSeconds } from "@/utils/time";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { atom, useAtom } from "jotai";
|
||||
@@ -59,7 +59,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
||||
getUserItemData({ api, userId: user?.Id, itemId: item.SeriesId }).then(
|
||||
(res) => {
|
||||
setSeriesItem(res);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [item.SeriesId]);
|
||||
@@ -80,7 +80,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
||||
headers: {
|
||||
Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
return response.data.Items;
|
||||
},
|
||||
@@ -90,7 +90,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
||||
const selectedSeasonId: string | null = useMemo(
|
||||
() =>
|
||||
seasons?.find((season: any) => season.IndexNumber === seasonIndex)?.Id,
|
||||
[seasons, seasonIndex]
|
||||
[seasons, seasonIndex],
|
||||
);
|
||||
|
||||
const { data: episodes, isFetching } = useQuery({
|
||||
@@ -123,7 +123,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
useEffect(() => {
|
||||
for (let e of episodes || []) {
|
||||
for (const e of episodes || []) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["item", e.Id],
|
||||
queryFn: async () => {
|
||||
@@ -187,9 +187,9 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
||||
onPress={async () => {
|
||||
close();
|
||||
}}
|
||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
||||
className='aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2'
|
||||
>
|
||||
<Ionicons name="close" size={24} color="white" />
|
||||
<Ionicons name='close' size={24} color='white' />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -216,7 +216,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
||||
showPlayButton={_item.Id !== item.Id}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View className="shrink">
|
||||
<View className='shrink'>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
@@ -226,19 +226,19 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
||||
>
|
||||
{_item.Name}
|
||||
</Text>
|
||||
<Text numberOfLines={1} className="text-xs text-neutral-475">
|
||||
<Text numberOfLines={1} className='text-xs text-neutral-475'>
|
||||
{`S${_item.ParentIndexNumber?.toString()}:E${_item.IndexNumber?.toString()}`}
|
||||
</Text>
|
||||
<Text className="text-xs text-neutral-500">
|
||||
<Text className='text-xs text-neutral-500'>
|
||||
{runtimeTicksToSeconds(_item.RunTimeTicks)}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="self-start mt-2">
|
||||
<View className='self-start mt-2'>
|
||||
<DownloadSingleItem item={_item} />
|
||||
</View>
|
||||
<Text
|
||||
numberOfLines={5}
|
||||
className="text-xs text-neutral-500 shrink"
|
||||
className='text-xs text-neutral-500 shrink'
|
||||
>
|
||||
{_item.Overview}
|
||||
</Text>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import type React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TouchableOpacity,
|
||||
type TouchableOpacityProps,
|
||||
View,
|
||||
} from "react-native";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
@@ -8,8 +15,6 @@ import Animated, {
|
||||
Easing,
|
||||
runOnJS,
|
||||
} from "react-native-reanimated";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface NextEpisodeCountDownButtonProps extends TouchableOpacityProps {
|
||||
onFinish?: () => void;
|
||||
@@ -38,7 +43,7 @@ const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
|
||||
if (finished && onFinish) {
|
||||
runOnJS(onFinish)();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [show, onFinish]);
|
||||
@@ -68,13 +73,15 @@ const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="w-32 overflow-hidden rounded-md bg-black/60 border border-neutral-900"
|
||||
className='w-32 overflow-hidden rounded-md bg-black/60 border border-neutral-900'
|
||||
{...props}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<Animated.View style={animatedStyle} />
|
||||
<View className="px-3 py-3">
|
||||
<Text className="text-center font-bold">{t("player.next_episode")}</Text>
|
||||
<View className='px-3 py-3'>
|
||||
<Text className='text-center font-bold'>
|
||||
{t("player.next_episode")}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { View, TouchableOpacity, Text, ViewProps } from "react-native";
|
||||
import type React from "react";
|
||||
import { Text, TouchableOpacity, View, type ViewProps } from "react-native";
|
||||
|
||||
interface SkipButtonProps extends ViewProps {
|
||||
onPress: () => void;
|
||||
@@ -17,9 +17,9 @@ const SkipButton: React.FC<SkipButtonProps> = ({
|
||||
<View className={showButton ? "flex" : "hidden"} {...props}>
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
className="bg-black/60 rounded-md px-3 py-3 border border-neutral-900"
|
||||
className='bg-black/60 rounded-md px-3 py-3 border border-neutral-900'
|
||||
>
|
||||
<Text className="text-white font-bold">{buttonText}</Text>
|
||||
<Text className='text-white font-bold'>{buttonText}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useTrickplay } from '@/hooks/useTrickplay';
|
||||
import { formatTimeString, msToTicks, ticksToSeconds } from '@/utils/time';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
||||
import { formatTimeString, msToTicks, ticksToSeconds } from "@/utils/time";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { Image } from "expo-image";
|
||||
import type React from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
import { SharedValue, useSharedValue } from 'react-native-reanimated';
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { type SharedValue, useSharedValue } from "react-native-reanimated";
|
||||
|
||||
interface SliderScrubberProps {
|
||||
cacheProgress: SharedValue<number>;
|
||||
@@ -30,12 +31,9 @@ const SliderScrubber: React.FC<SliderScrubberProps> = ({
|
||||
remainingTime,
|
||||
item,
|
||||
}) => {
|
||||
|
||||
|
||||
const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 0 });
|
||||
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(
|
||||
item,
|
||||
);
|
||||
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } =
|
||||
useTrickplay(item);
|
||||
|
||||
const handleSliderChange = (value: number) => {
|
||||
const progressInTicks = msToTicks(value);
|
||||
@@ -86,7 +84,7 @@ const SliderScrubber: React.FC<SliderScrubberProps> = ({
|
||||
marginTop: -tileHeight / 4 - 60,
|
||||
zIndex: 10,
|
||||
}}
|
||||
className=" bg-neutral-800 overflow-hidden"
|
||||
className=' bg-neutral-800 overflow-hidden'
|
||||
>
|
||||
<Image
|
||||
cachePolicy={"memory-disk"}
|
||||
@@ -101,7 +99,7 @@ const SliderScrubber: React.FC<SliderScrubberProps> = ({
|
||||
],
|
||||
}}
|
||||
source={{ uri: url }}
|
||||
contentFit="cover"
|
||||
contentFit='cover'
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
@@ -116,9 +114,7 @@ const SliderScrubber: React.FC<SliderScrubberProps> = ({
|
||||
>
|
||||
{`${time.hours > 0 ? `${time.hours}:` : ""}${
|
||||
time.minutes < 10 ? `0${time.minutes}` : time.minutes
|
||||
}:${
|
||||
time.seconds < 10 ? `0${time.seconds}` : time.seconds
|
||||
}`}
|
||||
}:${time.seconds < 10 ? `0${time.seconds}` : time.seconds}`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
@@ -129,11 +125,11 @@ const SliderScrubber: React.FC<SliderScrubberProps> = ({
|
||||
minimumValue={min}
|
||||
maximumValue={max}
|
||||
/>
|
||||
<View className="flex flex-row items-center justify-between mt-0.5">
|
||||
<Text className="text-[12px] text-neutral-400">
|
||||
<View className='flex flex-row items-center justify-between mt-0.5'>
|
||||
<Text className='text-[12px] text-neutral-400'>
|
||||
{formatTimeString(currentTime, "ms")}
|
||||
</Text>
|
||||
<Text className="text-[12px] text-neutral-400">
|
||||
<Text className='text-[12px] text-neutral-400'>
|
||||
-{formatTimeString(remainingTime, "ms")}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -141,4 +137,4 @@ const SliderScrubber: React.FC<SliderScrubberProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SliderScrubber;
|
||||
export default SliderScrubber;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
import type {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import React, { createContext, useContext, useState, ReactNode } from "react";
|
||||
import type React from "react";
|
||||
import { type ReactNode, createContext, useContext, useState } from "react";
|
||||
|
||||
interface ControlContextProps {
|
||||
item: BaseItemDto;
|
||||
@@ -11,7 +12,7 @@ interface ControlContextProps {
|
||||
}
|
||||
|
||||
const ControlContext = createContext<ControlContextProps | undefined>(
|
||||
undefined
|
||||
undefined,
|
||||
);
|
||||
|
||||
interface ControlProviderProps {
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { TrackInfo } from "@/modules/VlcPlayer.types";
|
||||
import React, { createContext, useContext, useState, ReactNode, useEffect, useMemo } from "react";
|
||||
import { useControlContext } from "./ControlContext";
|
||||
import { Track } from "../types";
|
||||
import type { TrackInfo } from "@/modules/VlcPlayer.types";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import type React from "react";
|
||||
import {
|
||||
type ReactNode,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { Track } from "../types";
|
||||
import { useControlContext } from "./ControlContext";
|
||||
|
||||
interface VideoContextProps {
|
||||
audioTracks: Track[] | null;
|
||||
@@ -16,8 +24,14 @@ const VideoContext = createContext<VideoContextProps | undefined>(undefined);
|
||||
|
||||
interface VideoProviderProps {
|
||||
children: ReactNode;
|
||||
getAudioTracks: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]) | undefined;
|
||||
getSubtitleTracks: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]) | undefined;
|
||||
getAudioTracks:
|
||||
| (() => Promise<TrackInfo[] | null>)
|
||||
| (() => TrackInfo[])
|
||||
| undefined;
|
||||
getSubtitleTracks:
|
||||
| (() => Promise<TrackInfo[] | null>)
|
||||
| (() => TrackInfo[])
|
||||
| undefined;
|
||||
setAudioTrack: ((index: number) => void) | undefined;
|
||||
setSubtitleTrack: ((index: number) => void) | undefined;
|
||||
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
|
||||
@@ -38,20 +52,24 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
||||
const mediaSource = ControlContext?.mediaSource;
|
||||
|
||||
const allSubs = mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || [];
|
||||
const allSubs =
|
||||
mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || [];
|
||||
|
||||
const { itemId, audioIndex, bitrateValue, subtitleIndex } = useLocalSearchParams<{
|
||||
itemId: string;
|
||||
audioIndex: string;
|
||||
subtitleIndex: string;
|
||||
mediaSourceId: string;
|
||||
bitrateValue: string;
|
||||
}>();
|
||||
const { itemId, audioIndex, bitrateValue, subtitleIndex } =
|
||||
useLocalSearchParams<{
|
||||
itemId: string;
|
||||
audioIndex: string;
|
||||
subtitleIndex: string;
|
||||
mediaSourceId: string;
|
||||
bitrateValue: string;
|
||||
}>();
|
||||
|
||||
const onTextBasedSubtitle = useMemo(
|
||||
() =>
|
||||
allSubs.find((s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream) || subtitleIndex === "-1",
|
||||
[allSubs, subtitleIndex]
|
||||
allSubs.find(
|
||||
(s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream,
|
||||
) || subtitleIndex === "-1",
|
||||
[allSubs, subtitleIndex],
|
||||
);
|
||||
|
||||
const setPlayerParams = ({
|
||||
@@ -74,14 +92,21 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
router.replace(`player/direct-player?${queryParams}`);
|
||||
};
|
||||
|
||||
const setTrackParams = (type: "audio" | "subtitle", index: number, serverIndex: number) => {
|
||||
const setTrackParams = (
|
||||
type: "audio" | "subtitle",
|
||||
index: number,
|
||||
serverIndex: number,
|
||||
) => {
|
||||
const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack;
|
||||
const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex";
|
||||
|
||||
// If we're transcoding and we're going from a image based subtitle
|
||||
// to a text based subtitle, we need to change the player params.
|
||||
|
||||
const shouldChangePlayerParams = type === "subtitle" && mediaSource?.TranscodingUrl && !onTextBasedSubtitle;
|
||||
const shouldChangePlayerParams =
|
||||
type === "subtitle" &&
|
||||
mediaSource?.TranscodingUrl &&
|
||||
!onTextBasedSubtitle;
|
||||
|
||||
console.log("Set player params", index, serverIndex);
|
||||
if (shouldChangePlayerParams) {
|
||||
@@ -102,16 +127,19 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
const subtitleData = await getSubtitleTracks();
|
||||
|
||||
// Step 1: Move external subs to the end, because VLC puts external subs at the end
|
||||
const sortedSubs = allSubs.sort((a, b) => Number(a.IsExternal) - Number(b.IsExternal));
|
||||
const sortedSubs = allSubs.sort(
|
||||
(a, b) => Number(a.IsExternal) - Number(b.IsExternal),
|
||||
);
|
||||
|
||||
// Step 2: Apply VLC indexing logic
|
||||
let textSubIndex = 0;
|
||||
const processedSubs: Track[] = sortedSubs?.map((sub) => {
|
||||
// Always increment for non-transcoding subtitles
|
||||
// Only increment for text-based subtitles when transcoding
|
||||
const shouldIncrement = !mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream;
|
||||
const shouldIncrement =
|
||||
!mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream;
|
||||
const vlcIndex = subtitleData?.at(textSubIndex)?.index ?? -1;
|
||||
const finalIndex = shouldIncrement ? vlcIndex : sub.Index ?? -1;
|
||||
const finalIndex = shouldIncrement ? vlcIndex : (sub.Index ?? -1);
|
||||
|
||||
if (shouldIncrement) textSubIndex++;
|
||||
return {
|
||||
@@ -127,7 +155,9 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
});
|
||||
|
||||
// Step 3: Restore the original order
|
||||
const subtitles: Track[] = processedSubs.sort((a, b) => a.index - b.index);
|
||||
const subtitles: Track[] = processedSubs.sort(
|
||||
(a, b) => a.index - b.index,
|
||||
);
|
||||
|
||||
// Add a "Disable Subtitles" option
|
||||
subtitles.unshift({
|
||||
@@ -143,20 +173,23 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
if (getAudioTracks) {
|
||||
const audioData = await getAudioTracks();
|
||||
|
||||
const allAudio = mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || [];
|
||||
const allAudio =
|
||||
mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || [];
|
||||
const audioTracks: Track[] = allAudio?.map((audio, idx) => {
|
||||
if (!mediaSource?.TranscodingUrl) {
|
||||
const vlcIndex = audioData?.at(idx)?.index ?? -1;
|
||||
return {
|
||||
name: audio.DisplayTitle ?? "Undefined Audio",
|
||||
index: audio.Index ?? -1,
|
||||
setTrack: () => setTrackParams("audio", vlcIndex, audio.Index ?? -1),
|
||||
setTrack: () =>
|
||||
setTrackParams("audio", vlcIndex, audio.Index ?? -1),
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: audio.DisplayTitle ?? "Undefined Audio",
|
||||
index: audio.Index ?? -1,
|
||||
setTrack: () => setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }),
|
||||
setTrack: () =>
|
||||
setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }),
|
||||
};
|
||||
});
|
||||
setAudioTracks(audioTracks);
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { TouchableOpacity, Platform } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React, { useCallback } from "react";
|
||||
import { Platform, TouchableOpacity } from "react-native";
|
||||
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||
import { useVideoContext } from "../contexts/VideoContext";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { BITRATES } from "@/components/BitrateSelector";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useControlContext } from "../contexts/ControlContext";
|
||||
import { useVideoContext } from "../contexts/VideoContext";
|
||||
|
||||
const DropdownView = () => {
|
||||
const videoContext = useVideoContext();
|
||||
const { subtitleTracks, audioTracks } = videoContext;
|
||||
const ControlContext = useControlContext();
|
||||
const [item, mediaSource] = [ControlContext?.item, ControlContext?.mediaSource];
|
||||
const [item, mediaSource] = [
|
||||
ControlContext?.item,
|
||||
ControlContext?.mediaSource,
|
||||
];
|
||||
const router = useRouter();
|
||||
|
||||
const { subtitleIndex, audioIndex, bitrateValue } = useLocalSearchParams<{
|
||||
@@ -34,27 +37,29 @@ const DropdownView = () => {
|
||||
// @ts-expect-error
|
||||
router.replace(`player/direct-player?${queryParams}`);
|
||||
},
|
||||
[item, mediaSource, subtitleIndex, audioIndex]
|
||||
[item, mediaSource, subtitleIndex, audioIndex],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<TouchableOpacity className="aspect-square flex flex-col rounded-xl items-center justify-center p-2">
|
||||
<Ionicons name="ellipsis-horizontal" size={24} color={"white"} />
|
||||
<TouchableOpacity className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'>
|
||||
<Ionicons name='ellipsis-horizontal' size={24} color={"white"} />
|
||||
</TouchableOpacity>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
loop={true}
|
||||
side="bottom"
|
||||
align="start"
|
||||
side='bottom'
|
||||
align='start'
|
||||
alignOffset={0}
|
||||
avoidCollisions={true}
|
||||
collisionPadding={8}
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger key="qualitytrigger">Quality</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubTrigger key='qualitytrigger'>
|
||||
Quality
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
alignOffset={-10}
|
||||
avoidCollisions={true}
|
||||
@@ -66,15 +71,21 @@ const DropdownView = () => {
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={`quality-item-${idx}`}
|
||||
value={bitrateValue === (bitrate.value?.toString() ?? "")}
|
||||
onValueChange={() => changeBitrate(bitrate.value?.toString() ?? "")}
|
||||
onValueChange={() =>
|
||||
changeBitrate(bitrate.value?.toString() ?? "")
|
||||
}
|
||||
>
|
||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>{bitrate.key}</DropdownMenu.ItemTitle>
|
||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
||||
{bitrate.key}
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger key="subtitle-trigger">Subtitle</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubTrigger key='subtitle-trigger'>
|
||||
Subtitle
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
alignOffset={-10}
|
||||
avoidCollisions={true}
|
||||
@@ -88,13 +99,17 @@ const DropdownView = () => {
|
||||
value={subtitleIndex === sub.index.toString()}
|
||||
onValueChange={() => sub.setTrack()}
|
||||
>
|
||||
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>{sub.name}</DropdownMenu.ItemTitle>
|
||||
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>
|
||||
{sub.name}
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger key="audio-trigger">Audio</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubTrigger key='audio-trigger'>
|
||||
Audio
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
alignOffset={-10}
|
||||
avoidCollisions={true}
|
||||
@@ -108,7 +123,9 @@ const DropdownView = () => {
|
||||
value={audioIndex === track.index.toString()}
|
||||
onValueChange={() => track.setTrack()}
|
||||
>
|
||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>{track.name}</DropdownMenu.ItemTitle>
|
||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
||||
{track.name}
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.SubContent>
|
||||
|
||||
@@ -23,4 +23,4 @@ type Track = {
|
||||
setTrack: () => void;
|
||||
};
|
||||
|
||||
export { EmbeddedSubtitle, ExternalSubtitle, TranscodedSubtitle, Track };
|
||||
export type { EmbeddedSubtitle, ExternalSubtitle, TranscodedSubtitle, Track };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef } from "react";
|
||||
import { GestureResponderEvent } from "react-native";
|
||||
import type { GestureResponderEvent } from "react-native";
|
||||
|
||||
interface TapDetectionOptions {
|
||||
maxDuration?: number;
|
||||
@@ -33,7 +33,7 @@ export const useTapDetection = ({
|
||||
const touchDuration = touchEndTime - touchStartTime.current;
|
||||
const touchDistance = Math.sqrt(
|
||||
Math.pow(touchEndPosition.x - touchStartPosition.current.x, 2) +
|
||||
Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2)
|
||||
Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2),
|
||||
);
|
||||
|
||||
if (touchDuration < maxDuration && touchDistance < maxDistance) {
|
||||
|
||||
Reference in New Issue
Block a user