mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-21 00:04:42 +01:00
fix: tv playback (#820)
Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> Signed-off-by: lancechant <13349722+lancechant@users.noreply.github.com> Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com> Co-authored-by: Uruk <contact@uruk.dev> Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { Platform, type ViewProps } from "react-native";
|
||||
import { Platform } from "react-native";
|
||||
import GoogleCast, {
|
||||
CastButton,
|
||||
CastContext,
|
||||
@@ -11,12 +11,6 @@ import GoogleCast, {
|
||||
} from "react-native-google-cast";
|
||||
import { RoundButton } from "./RoundButton";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
background?: "blur" | "transparent";
|
||||
}
|
||||
|
||||
export function Chromecast({
|
||||
width = 48,
|
||||
height = 48,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "zeego/context-menu";
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Animated, {
|
||||
Easing,
|
||||
@@ -17,7 +15,6 @@ import Animated, {
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||
@@ -37,12 +34,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
selectedOptions,
|
||||
...props
|
||||
}: Props) => {
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [colorAtom] = useAtom(itemThemeColorAtom);
|
||||
const _api = useAtomValue(apiAtom);
|
||||
const _user = useAtomValue(userAtom);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export function InfiniteHorizontalScroll({
|
||||
};
|
||||
});
|
||||
|
||||
const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
||||
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
||||
queryKey,
|
||||
queryFn,
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRouter, useSegments } from "expo-router";
|
||||
import type React from "react";
|
||||
import { type PropsWithChildren, useCallback, useMemo } from "react";
|
||||
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
|
||||
import * as ContextMenu from "@/components/ContextMenu";
|
||||
import * as ContextMenu from "zeego/context-menu";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||
import {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Platform, Text as RNText, type TextProps } from "react-native";
|
||||
import { UITextView } from "react-native-uitextview";
|
||||
export function Text(
|
||||
props: TextProps & {
|
||||
uiTextView?: boolean;
|
||||
},
|
||||
) {
|
||||
export function Text(props: TextProps) {
|
||||
const { style, ...otherProps } = props;
|
||||
if (Platform.isTV)
|
||||
return (
|
||||
@@ -16,7 +11,7 @@ export function Text(
|
||||
);
|
||||
|
||||
return (
|
||||
<UITextView
|
||||
<RNText
|
||||
allowFontScaling={false}
|
||||
style={[{ color: "white" }, style]}
|
||||
{...otherProps}
|
||||
|
||||
@@ -60,9 +60,9 @@ interface DownloadCardProps extends TouchableOpacityProps {
|
||||
}
|
||||
|
||||
const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
||||
const { processes, startDownload } = useDownload();
|
||||
const { startDownload } = useDownload();
|
||||
const router = useRouter();
|
||||
const { removeProcess, setProcesses } = useDownload();
|
||||
const { removeProcess } = useDownload();
|
||||
const [settings] = useSettings();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ interface EpisodeCardProps extends TouchableOpacityProps {
|
||||
item: BaseItemDto;
|
||||
}
|
||||
|
||||
export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item, ...props }) => {
|
||||
export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item }) => {
|
||||
const { deleteFile } = useDownload();
|
||||
const { openFile } = useDownloadedFileOpener();
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
|
||||
@@ -72,7 +72,6 @@ export const FilterSheet = <T,>({
|
||||
renderItemLabel,
|
||||
showSearch = true,
|
||||
multiple = false,
|
||||
...props
|
||||
}: Props<T>) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const snapPoints = useMemo(() => ["80%"], []);
|
||||
|
||||
@@ -50,11 +50,8 @@ const Fact: React.FC<{ title: string; fact?: string | null } & ViewProps> = ({
|
||||
const DetailFacts: React.FC<
|
||||
{ details?: MovieDetails | TvDetails } & ViewProps
|
||||
> = ({ details, className, ...props }) => {
|
||||
const {
|
||||
jellyseerrUser,
|
||||
jellyseerrRegion: region,
|
||||
jellyseerrLocale: locale,
|
||||
} = useJellyseerr();
|
||||
const { jellyseerrRegion: region, jellyseerrLocale: locale } =
|
||||
useJellyseerr();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const releases = useMemo(
|
||||
|
||||
@@ -40,7 +40,6 @@ const ParallaxSlideShow = <T,>({
|
||||
renderItem,
|
||||
keyExtractor,
|
||||
onEndReached,
|
||||
...props
|
||||
}: PropsWithChildren<Props<T> & ViewProps>) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
|
||||
@@ -38,16 +38,7 @@ const RequestModal = forwardRef<
|
||||
Props & Omit<ViewProps, "id">
|
||||
>(
|
||||
(
|
||||
{
|
||||
id,
|
||||
title,
|
||||
requestBody,
|
||||
type,
|
||||
isAnime = false,
|
||||
onRequested,
|
||||
onDismiss,
|
||||
...props
|
||||
},
|
||||
{ id, title, requestBody, type, isAnime = false, onRequested, onDismiss },
|
||||
ref,
|
||||
) => {
|
||||
const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
|
||||
|
||||
@@ -24,7 +24,7 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||
[slide],
|
||||
);
|
||||
|
||||
const { data, isFetching, isLoading } = useQuery({
|
||||
const { data } = useQuery({
|
||||
queryKey: ["jellyseerr", "discover", slide.type, slide.id],
|
||||
queryFn: async () => {
|
||||
return jellyseerrApi?.getGenreSliders(
|
||||
|
||||
@@ -11,11 +11,7 @@ import type { NonFunctionProperties } from "@/utils/jellyseerr/server/interfaces
|
||||
const RequestCard: React.FC<{ request: MediaRequest }> = ({ request }) => {
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
|
||||
const {
|
||||
data: details,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
const { data: details } = useQuery({
|
||||
queryKey: [
|
||||
"jellyseerr",
|
||||
"detail",
|
||||
@@ -57,11 +53,7 @@ const RecentRequestsSlide: React.FC<SlideProps & ViewProps> = ({
|
||||
}) => {
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
|
||||
const {
|
||||
data: requests,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
const { data: requests } = useQuery({
|
||||
queryKey: ["jellyseerr", "recent_requests"],
|
||||
queryFn: async () => jellyseerrApi?.requests(),
|
||||
enabled: !!jellyseerrApi,
|
||||
|
||||
@@ -82,7 +82,6 @@ const ListItemContent = ({
|
||||
showArrow,
|
||||
iconAfter,
|
||||
children,
|
||||
...props
|
||||
}: Props) => {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props extends ViewProps {
|
||||
export const MoviesTitleHeader: React.FC<Props> = ({ item, ...props }) => {
|
||||
return (
|
||||
<View {...props}>
|
||||
<Text uiTextView selectable className='font-bold text-2xl mb-1'>
|
||||
<Text selectable className='font-bold text-2xl mb-1'>
|
||||
{item?.Name}
|
||||
</Text>
|
||||
<Text className='opacity-50'>{item?.ProductionYear}</Text>
|
||||
|
||||
@@ -38,7 +38,6 @@ const JellyseerrPoster: React.FC<Props> = ({
|
||||
horizontal,
|
||||
showDownloadInfo,
|
||||
mediaRequest,
|
||||
...props
|
||||
}) => {
|
||||
const { jellyseerrApi, getTitle, getYear, getMediaType } = useJellyseerr();
|
||||
const loadingOpacity = useSharedValue(1);
|
||||
|
||||
@@ -40,7 +40,7 @@ export const SearchItemWrapper = <T,>({
|
||||
onEndReachedThreshold={1}
|
||||
onEndReached={onEndReached}
|
||||
//@ts-ignore
|
||||
renderItem={({ item, index }) => (item ? renderItem(item) : <></>)}
|
||||
renderItem={({ item }) => (item ? renderItem(item) : null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ export const EpisodeTitleHeader: React.FC<Props> = ({ item, ...props }) => {
|
||||
|
||||
return (
|
||||
<View {...props}>
|
||||
<Text uiTextView className='font-bold text-2xl' selectable>
|
||||
<Text className='font-bold text-2xl' selectable>
|
||||
{item?.Name}
|
||||
</Text>
|
||||
<View className='flex flex-row items-center mb-1'>
|
||||
|
||||
@@ -57,7 +57,7 @@ const JellyseerrSeasonEpisodes: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const RenderItem = ({ item, index }: any) => {
|
||||
const RenderItem = ({ item }: any) => {
|
||||
const {
|
||||
jellyseerrApi,
|
||||
jellyseerrRegion: region,
|
||||
|
||||
@@ -27,7 +27,7 @@ type Props = {
|
||||
|
||||
export const seasonIndexAtom = atom<SeasonIndexState>({});
|
||||
|
||||
export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
|
||||
export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ListItem } from "../list/ListItem";
|
||||
|
||||
interface Props extends ViewProps {}
|
||||
|
||||
export const AppLanguageSelector: React.FC<Props> = ({ ...props }) => {
|
||||
export const AppLanguageSelector: React.FC<Props> = () => {
|
||||
const isTv = Platform.isTV;
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ListItem } from "../list/ListItem";
|
||||
|
||||
export const Dashboard = () => {
|
||||
const [settings, _updateSettings] = useSettings();
|
||||
const { sessions = [], isLoading } = useSessions({} as useSessionsProps);
|
||||
const { sessions = [] } = useSessions({} as useSessionsProps);
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function DownloadSettings({ ...props }) {
|
||||
export default function DownloadSettings() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -14,12 +14,8 @@ import { ListGroup } from "../list/ListGroup";
|
||||
import { ListItem } from "../list/ListItem";
|
||||
|
||||
export const JellyseerrSettings = () => {
|
||||
const {
|
||||
jellyseerrApi,
|
||||
jellyseerrUser,
|
||||
setJellyseerrUser,
|
||||
clearAllJellyseerData,
|
||||
} = useJellyseerr();
|
||||
const { jellyseerrUser, setJellyseerrUser, clearAllJellyseerData } =
|
||||
useJellyseerr();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export const StorageSettings = () => {
|
||||
const successHapticFeedback = useHaptic("success");
|
||||
const errorHapticFeedback = useHaptic("error");
|
||||
|
||||
const { data: size, isLoading: appSizeLoading } = useQuery({
|
||||
const { data: size } = useQuery({
|
||||
queryKey: ["appSize", appSizeUsage],
|
||||
queryFn: async () => {
|
||||
const app = await appSizeUsage;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type React from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import type { VolumeResult } from "react-native-volume-manager";
|
||||
|
||||
const VolumeManager = Platform.isTV
|
||||
? null
|
||||
: require("react-native-volume-manager");
|
||||
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type { VolumeResult } from "react-native-volume-manager";
|
||||
|
||||
interface AudioSliderProps {
|
||||
setVisibility: (show: boolean) => void;
|
||||
}
|
||||
@@ -47,7 +46,7 @@ const AudioSlider: React.FC<AudioSliderProps> = ({ setVisibility }) => {
|
||||
|
||||
const handleValueChange = async (value: number) => {
|
||||
volume.value = value;
|
||||
await VolumeManager.setVolume(value / 100);
|
||||
// await VolumeManager.setVolume(value / 100);
|
||||
|
||||
// Re-call showNativeVolumeUI to ensure the setting is applied on iOS
|
||||
VolumeManager.showNativeVolumeUI({ enabled: false });
|
||||
@@ -55,7 +54,7 @@ const AudioSlider: React.FC<AudioSliderProps> = ({ setVisibility }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (isTv) return;
|
||||
const volumeListener = VolumeManager.addVolumeListener(
|
||||
const _volumeListener = VolumeManager.addVolumeListener(
|
||||
(result: VolumeResult) => {
|
||||
volume.value = result.volume * 100;
|
||||
setVisibility(true);
|
||||
@@ -73,7 +72,7 @@ const AudioSlider: React.FC<AudioSliderProps> = ({ setVisibility }) => {
|
||||
);
|
||||
|
||||
return () => {
|
||||
volumeListener.remove();
|
||||
// volumeListener.remove();
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import {
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
useTVEventHandler,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from "react-native";
|
||||
@@ -156,6 +157,134 @@ export const Controls: FC<Props> = ({
|
||||
prefetchAllTrickplayImages();
|
||||
}, []);
|
||||
|
||||
const remoteScrubProgress = useSharedValue<number | null>(null);
|
||||
const isRemoteScrubbing = useSharedValue(false);
|
||||
const SCRUB_INTERVAL = isVlc ? secondsToMs(10) : msToTicks(secondsToMs(10));
|
||||
const [showRemoteBubble, setShowRemoteBubble] = useState(false);
|
||||
|
||||
const [longPressScrubMode, setLongPressScrubMode] = useState<
|
||||
"FF" | "RW" | null
|
||||
>(null);
|
||||
|
||||
useTVEventHandler((evt) => {
|
||||
if (!evt) return;
|
||||
|
||||
switch (evt.eventType) {
|
||||
case "longLeft": {
|
||||
setLongPressScrubMode((prev) => (!prev ? "RW" : null));
|
||||
break;
|
||||
}
|
||||
case "longRight": {
|
||||
setLongPressScrubMode((prev) => (!prev ? "FF" : null));
|
||||
break;
|
||||
}
|
||||
case "left":
|
||||
case "right": {
|
||||
isRemoteScrubbing.value = true;
|
||||
setShowRemoteBubble(true);
|
||||
|
||||
const direction = evt.eventType === "left" ? -1 : 1;
|
||||
const base = remoteScrubProgress.value ?? progress.value;
|
||||
const updated = Math.max(
|
||||
min.value,
|
||||
Math.min(max.value, base + direction * SCRUB_INTERVAL),
|
||||
);
|
||||
remoteScrubProgress.value = updated;
|
||||
const progressInTicks = isVlc ? msToTicks(updated) : updated;
|
||||
calculateTrickplayUrl(progressInTicks);
|
||||
const progressInSeconds = Math.floor(ticksToSeconds(progressInTicks));
|
||||
const hours = Math.floor(progressInSeconds / 3600);
|
||||
const minutes = Math.floor((progressInSeconds % 3600) / 60);
|
||||
const seconds = progressInSeconds % 60;
|
||||
setTime({ hours, minutes, seconds });
|
||||
break;
|
||||
}
|
||||
case "select": {
|
||||
if (isRemoteScrubbing.value && remoteScrubProgress.value != null) {
|
||||
progress.value = remoteScrubProgress.value;
|
||||
|
||||
const seekTarget = isVlc
|
||||
? Math.max(0, remoteScrubProgress.value)
|
||||
: Math.max(0, ticksToSeconds(remoteScrubProgress.value));
|
||||
|
||||
seek(seekTarget);
|
||||
if (isPlaying) play();
|
||||
|
||||
isRemoteScrubbing.value = false;
|
||||
remoteScrubProgress.value = null;
|
||||
setShowRemoteBubble(false);
|
||||
} else {
|
||||
togglePlay();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "down":
|
||||
case "up":
|
||||
// cancel scrubbing on other directions
|
||||
isRemoteScrubbing.value = false;
|
||||
remoteScrubProgress.value = null;
|
||||
setShowRemoteBubble(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!showControls) toggleControls();
|
||||
});
|
||||
|
||||
const longPressTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
let seekTime = 10;
|
||||
|
||||
const scrubWithLongPress = () => {
|
||||
if (!isActive || !longPressScrubMode) return;
|
||||
|
||||
setIsSliding(true);
|
||||
const scrubFn =
|
||||
longPressScrubMode === "FF" ? handleSeekForward : handleSeekBackward;
|
||||
scrubFn(seekTime);
|
||||
seekTime *= 1.1;
|
||||
|
||||
longPressTimeoutRef.current = setTimeout(scrubWithLongPress, 300);
|
||||
};
|
||||
|
||||
if (longPressScrubMode) {
|
||||
isActive = true;
|
||||
scrubWithLongPress();
|
||||
}
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
setIsSliding(false);
|
||||
if (longPressTimeoutRef.current) {
|
||||
clearTimeout(longPressTimeoutRef.current);
|
||||
longPressTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [longPressScrubMode]);
|
||||
|
||||
const effectiveProgress = useSharedValue(0);
|
||||
|
||||
// Recompute progress whenever remote scrubbing is active
|
||||
useAnimatedReaction(
|
||||
() => ({
|
||||
isScrubbing: isRemoteScrubbing.value,
|
||||
scrub: remoteScrubProgress.value,
|
||||
actual: progress.value,
|
||||
}),
|
||||
(current) => {
|
||||
effectiveProgress.value =
|
||||
current.isScrubbing && current.scrub != null
|
||||
? current.scrub
|
||||
: current.actual;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
progress.value = isVlc
|
||||
@@ -374,20 +503,19 @@ export const Controls: FC<Props> = ({
|
||||
|
||||
pause();
|
||||
isSeeking.value = true;
|
||||
}, [showControls, isPlaying]);
|
||||
}, [showControls, isPlaying, pause]);
|
||||
|
||||
const handleSliderComplete = useCallback(
|
||||
async (value: number) => {
|
||||
isSeeking.value = false;
|
||||
progress.value = value;
|
||||
setIsSliding(false);
|
||||
|
||||
seek(Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value))));
|
||||
if (wasPlayingRef.current) {
|
||||
play();
|
||||
}
|
||||
},
|
||||
[isVlc],
|
||||
[isVlc, seek, play],
|
||||
);
|
||||
|
||||
const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 0 });
|
||||
@@ -424,7 +552,43 @@ export const Controls: FC<Props> = ({
|
||||
} catch (error) {
|
||||
writeToLog("ERROR", "Error seeking video backwards", error);
|
||||
}
|
||||
}, [settings, isPlaying, isVlc]);
|
||||
}, [settings, isPlaying, isVlc, play, seek]);
|
||||
|
||||
const handleSeekBackward = useCallback(
|
||||
async (seconds: number) => {
|
||||
wasPlayingRef.current = isPlaying;
|
||||
try {
|
||||
const curr = progress.value;
|
||||
if (curr !== undefined) {
|
||||
const newTime = isVlc
|
||||
? Math.max(0, curr - secondsToMs(seconds))
|
||||
: Math.max(0, ticksToSeconds(curr) - seconds);
|
||||
seek(newTime);
|
||||
}
|
||||
} catch (error) {
|
||||
writeToLog("ERROR", "Error seeking video backwards", error);
|
||||
}
|
||||
},
|
||||
[isPlaying, isVlc, seek],
|
||||
);
|
||||
|
||||
const handleSeekForward = useCallback(
|
||||
async (seconds: number) => {
|
||||
wasPlayingRef.current = isPlaying;
|
||||
try {
|
||||
const curr = progress.value;
|
||||
if (curr !== undefined) {
|
||||
const newTime = isVlc
|
||||
? curr + secondsToMs(seconds)
|
||||
: ticksToSeconds(curr) + seconds;
|
||||
seek(Math.max(0, newTime));
|
||||
}
|
||||
} catch (error) {
|
||||
writeToLog("ERROR", "Error seeking video forwards", error);
|
||||
}
|
||||
},
|
||||
[isPlaying, isVlc, seek],
|
||||
);
|
||||
|
||||
const handleSkipForward = useCallback(async () => {
|
||||
if (!settings?.forwardSkipTime) {
|
||||
@@ -446,7 +610,7 @@ export const Controls: FC<Props> = ({
|
||||
} catch (error) {
|
||||
writeToLog("ERROR", "Error seeking video forwards", error);
|
||||
}
|
||||
}, [settings, isPlaying, isVlc]);
|
||||
}, [settings, isPlaying, isVlc, play, seek]);
|
||||
|
||||
const toggleIgnoreSafeAreas = useCallback(() => {
|
||||
setIgnoreSafeAreas((prev) => !prev);
|
||||
@@ -667,80 +831,87 @@ export const Controls: FC<Props> = ({
|
||||
>
|
||||
<BrightnessSlider />
|
||||
</View>
|
||||
<TouchableOpacity onPress={handleSkipBackward}>
|
||||
<View
|
||||
style={{
|
||||
position: "relative",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
opacity: showControls ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name='refresh-outline'
|
||||
size={50}
|
||||
color='white'
|
||||
style={{
|
||||
transform: [{ scaleY: -1 }, { rotate: "180deg" }],
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
position: "absolute",
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
bottom: 10,
|
||||
}}
|
||||
>
|
||||
{settings?.rewindSkipTime}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
togglePlay();
|
||||
}}
|
||||
>
|
||||
{!isBuffering ? (
|
||||
<Ionicons
|
||||
name={isPlaying ? "pause" : "play"}
|
||||
size={50}
|
||||
color='white'
|
||||
{!Platform.isTV && (
|
||||
<TouchableOpacity onPress={handleSkipBackward}>
|
||||
<View
|
||||
style={{
|
||||
position: "relative",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
opacity: showControls ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Loader size={"large"} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
>
|
||||
<Ionicons
|
||||
name='refresh-outline'
|
||||
size={50}
|
||||
color='white'
|
||||
style={{
|
||||
transform: [{ scaleY: -1 }, { rotate: "180deg" }],
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
position: "absolute",
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
bottom: 10,
|
||||
}}
|
||||
>
|
||||
{settings?.rewindSkipTime}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity onPress={handleSkipForward}>
|
||||
<View
|
||||
style={{
|
||||
position: "relative",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
opacity: showControls ? 1 : 0,
|
||||
<View
|
||||
style={Platform.isTV ? { flex: 1, alignItems: "center" } : {}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
togglePlay();
|
||||
}}
|
||||
>
|
||||
<Ionicons name='refresh-outline' size={50} color='white' />
|
||||
<Text
|
||||
{!isBuffering ? (
|
||||
<Ionicons
|
||||
name={isPlaying ? "pause" : "play"}
|
||||
size={50}
|
||||
color='white'
|
||||
style={{
|
||||
opacity: showControls ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Loader size={"large"} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{!Platform.isTV && (
|
||||
<TouchableOpacity onPress={handleSkipForward}>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
bottom: 10,
|
||||
position: "relative",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
opacity: showControls ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{settings?.forwardSkipTime}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Ionicons name='refresh-outline' size={50} color='white' />
|
||||
<Text
|
||||
style={{
|
||||
position: "absolute",
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
bottom: 10,
|
||||
}}
|
||||
>
|
||||
{settings?.forwardSkipTime}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -850,10 +1021,12 @@ export const Controls: FC<Props> = ({
|
||||
containerStyle={{
|
||||
borderRadius: 100,
|
||||
}}
|
||||
renderBubble={() => isSliding && memoizedRenderBubble()}
|
||||
renderBubble={() =>
|
||||
(isSliding || showRemoteBubble) && memoizedRenderBubble()
|
||||
}
|
||||
sliderHeight={10}
|
||||
thumbWidth={0}
|
||||
progress={progress}
|
||||
progress={effectiveProgress}
|
||||
minimumValue={min}
|
||||
maximumValue={max}
|
||||
/>
|
||||
|
||||
@@ -93,7 +93,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
||||
[seasons, seasonIndex],
|
||||
);
|
||||
|
||||
const { data: episodes, isFetching } = useQuery({
|
||||
const { data: episodes } = useQuery({
|
||||
queryKey: ["episodes", item.SeriesId, selectedSeasonId],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id || !item.Id || !selectedSeasonId) return [];
|
||||
|
||||
Reference in New Issue
Block a user