mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-27 01:06:42 +01:00
feat: add "Are you still watching" modal overlay with configurable options (#663)
Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
This commit is contained in:
@@ -66,11 +66,14 @@ export const PlayButton: React.FC<Props> = ({
|
||||
const startColor = useSharedValue(colorAtom);
|
||||
const widthProgress = useSharedValue(0);
|
||||
const colorChangeProgress = useSharedValue(0);
|
||||
const [settings] = useSettings();
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const lightHapticFeedback = useHaptic("light");
|
||||
|
||||
const goToPlayer = useCallback(
|
||||
(q: string) => {
|
||||
if (settings.maxAutoPlayEpisodeCount.value !== -1) {
|
||||
updateSettings({ autoPlayEpisodeCount: 0 });
|
||||
}
|
||||
router.push(`/player/direct-player?${q}`);
|
||||
},
|
||||
[router],
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/utils/background-tasks";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
import i18n, { TFunction } from "i18next";
|
||||
import type React from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -251,7 +252,46 @@ export const OtherSettings: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={t("home.settings.other.max_auto_play_episode_count")}>
|
||||
<Dropdown
|
||||
data={AUTOPLAY_EPISODES_COUNT(t)}
|
||||
keyExtractor={(item) => item.key}
|
||||
titleExtractor={(item) => item.key}
|
||||
title={
|
||||
<TouchableOpacity className='flex flex-row items-center justify-between py-3 pl-3'>
|
||||
<Text className='mr-1 text-[#8E8D91]'>
|
||||
{t(settings?.maxAutoPlayEpisodeCount.key)}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name='chevron-expand-sharp'
|
||||
size={18}
|
||||
color='#5A5960'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
label={t("home.settings.other.max_auto_play_episode_count")}
|
||||
onSelected={(maxAutoPlayEpisodeCount) =>
|
||||
updateSettings({ maxAutoPlayEpisodeCount })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
</DisabledSetting>
|
||||
);
|
||||
};
|
||||
|
||||
const AUTOPLAY_EPISODES_COUNT = (
|
||||
t: TFunction<"translation", undefined>,
|
||||
): {
|
||||
key: string;
|
||||
value: number;
|
||||
}[] => [
|
||||
{ key: t("home.settings.other.disabled"), value: -1 },
|
||||
{ key: "1", value: 1 },
|
||||
{ key: "2", value: 2 },
|
||||
{ key: "3", value: 3 },
|
||||
{ key: "4", value: 4 },
|
||||
{ key: "5", value: 5 },
|
||||
{ key: "6", value: 6 },
|
||||
{ key: "7", value: 7 },
|
||||
];
|
||||
|
||||
49
components/video-player/controls/ContinueWatchingOverlay.tsx
Normal file
49
components/video-player/controls/ContinueWatchingOverlay.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { useRouter } from "expo-router";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
export interface ContinueWatchingOverlayProps {
|
||||
goToNextItem: (options: {
|
||||
isAutoPlay: boolean;
|
||||
resetWatchCount: boolean;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({
|
||||
goToNextItem,
|
||||
}) => {
|
||||
const [settings] = useSettings();
|
||||
const router = useRouter();
|
||||
|
||||
return settings.autoPlayEpisodeCount >=
|
||||
settings.maxAutoPlayEpisodeCount.value ? (
|
||||
<View
|
||||
className={
|
||||
"absolute top-0 bottom-0 left-0 right-0 flex flex-col px-4 items-center justify-center bg-[#000000B3]"
|
||||
}
|
||||
>
|
||||
<Text className='text-2xl font-bold text-white py-4 '>
|
||||
Are you still watching ?
|
||||
</Text>
|
||||
<Button
|
||||
onPress={() => {
|
||||
goToNextItem({ isAutoPlay: false, resetWatchCount: true });
|
||||
}}
|
||||
color={"purple"}
|
||||
className='my-4 w-2/3'
|
||||
>
|
||||
{t("player.continue_watching")}
|
||||
</Button>
|
||||
|
||||
<Button onPress={router.back} color={"transparent"} className='w-2/3'>
|
||||
{t("player.go_back")}
|
||||
</Button>
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default ContinueWatchingOverlay;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
|
||||
import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes";
|
||||
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
@@ -28,7 +29,7 @@ import { Image } from "expo-image";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import { debounce } from "lodash";
|
||||
import {
|
||||
import React, {
|
||||
type Dispatch,
|
||||
type FC,
|
||||
type MutableRefObject,
|
||||
@@ -121,7 +122,7 @@ export const Controls: FC<Props> = ({
|
||||
enableTrickplay = true,
|
||||
isVlc = false,
|
||||
}) => {
|
||||
const [settings] = useSettings();
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [api] = useAtom(apiAtom);
|
||||
@@ -236,15 +237,76 @@ export const Controls: FC<Props> = ({
|
||||
goToItemCommon(previousItem);
|
||||
}, [previousItem, goToItemCommon]);
|
||||
|
||||
const goToNextItem = useCallback(() => {
|
||||
if (!nextItem) return;
|
||||
goToItemCommon(nextItem);
|
||||
}, [nextItem, goToItemCommon]);
|
||||
const goToNextItem = useCallback(
|
||||
({
|
||||
isAutoPlay,
|
||||
resetWatchCount,
|
||||
}: { isAutoPlay?: boolean; resetWatchCount?: boolean }) => {
|
||||
if (!nextItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAutoPlay) {
|
||||
// if we are not autoplaying, we won't update anything, we just go to the next item
|
||||
goToItemCommon(nextItem);
|
||||
if (resetWatchCount) {
|
||||
updateSettings({
|
||||
autoPlayEpisodeCount: 0,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip autoplay logic if maxAutoPlayEpisodeCount is -1
|
||||
if (settings.maxAutoPlayEpisodeCount.value === -1) {
|
||||
goToItemCommon(nextItem);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
settings.autoPlayEpisodeCount + 1 <
|
||||
settings.maxAutoPlayEpisodeCount.value
|
||||
) {
|
||||
goToItemCommon(nextItem);
|
||||
}
|
||||
|
||||
// Check if the autoPlayEpisodeCount is less than maxAutoPlayEpisodeCount for the autoPlay
|
||||
if (
|
||||
settings.autoPlayEpisodeCount < settings.maxAutoPlayEpisodeCount.value
|
||||
) {
|
||||
// update the autoPlayEpisodeCount in settings
|
||||
updateSettings({
|
||||
autoPlayEpisodeCount: settings.autoPlayEpisodeCount + 1,
|
||||
});
|
||||
}
|
||||
},
|
||||
[nextItem, goToItemCommon],
|
||||
);
|
||||
|
||||
// Add a memoized handler for autoplay next episode
|
||||
const handleNextEpisodeAutoPlay = useCallback(() => {
|
||||
goToNextItem({ isAutoPlay: true });
|
||||
}, [goToNextItem]);
|
||||
|
||||
// Add a memoized handler for manual next episode
|
||||
const handleNextEpisodeManual = useCallback(() => {
|
||||
goToNextItem({ isAutoPlay: false });
|
||||
}, [goToNextItem]);
|
||||
|
||||
// Add a memoized handler for ContinueWatchingOverlay
|
||||
const handleContinueWatching = useCallback(
|
||||
(options: { isAutoPlay?: boolean; resetWatchCount?: boolean }) => {
|
||||
goToNextItem(options);
|
||||
},
|
||||
[goToNextItem],
|
||||
);
|
||||
|
||||
const goToItem = useCallback(
|
||||
async (itemId: string) => {
|
||||
const gotoItem = await getItemById(api, itemId);
|
||||
if (!gotoItem) return;
|
||||
if (!gotoItem) {
|
||||
return;
|
||||
}
|
||||
goToItemCommon(gotoItem);
|
||||
},
|
||||
[goToItemCommon, api],
|
||||
@@ -300,7 +362,9 @@ export const Controls: FC<Props> = ({
|
||||
};
|
||||
|
||||
const handleSliderStart = useCallback(() => {
|
||||
if (!showControls) return;
|
||||
if (!showControls) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSliding(true);
|
||||
wasPlayingRef.current = isPlaying;
|
||||
@@ -339,7 +403,9 @@ export const Controls: FC<Props> = ({
|
||||
);
|
||||
|
||||
const handleSkipBackward = useCallback(async () => {
|
||||
if (!settings?.rewindSkipTime) return;
|
||||
if (!settings?.rewindSkipTime) {
|
||||
return;
|
||||
}
|
||||
wasPlayingRef.current = isPlaying;
|
||||
lightHapticFeedback();
|
||||
try {
|
||||
@@ -371,7 +437,9 @@ export const Controls: FC<Props> = ({
|
||||
? curr + secondsToMs(settings.forwardSkipTime)
|
||||
: ticksToSeconds(curr) + settings.forwardSkipTime;
|
||||
seek(Math.max(0, newTime));
|
||||
if (wasPlayingRef.current) play();
|
||||
if (wasPlayingRef.current) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
writeToLog("ERROR", "Error seeking video forwards", error);
|
||||
@@ -546,7 +614,7 @@ export const Controls: FC<Props> = ({
|
||||
|
||||
{nextItem && !offline && (
|
||||
<TouchableOpacity
|
||||
onPress={goToNextItem}
|
||||
onPress={() => goToNextItem({ isAutoPlay: false })}
|
||||
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
||||
>
|
||||
<Ionicons name='play-skip-forward' size={24} color='white' />
|
||||
@@ -741,17 +809,21 @@ export const Controls: FC<Props> = ({
|
||||
onPress={skipCredit}
|
||||
buttonText='Skip Credits'
|
||||
/>
|
||||
<NextEpisodeCountDownButton
|
||||
show={
|
||||
!nextItem
|
||||
? false
|
||||
: isVlc
|
||||
? remainingTime < 10000
|
||||
: remainingTime < 10
|
||||
}
|
||||
onFinish={goToNextItem}
|
||||
onPress={goToNextItem}
|
||||
/>
|
||||
{(settings.maxAutoPlayEpisodeCount.value === -1 ||
|
||||
settings.autoPlayEpisodeCount <
|
||||
settings.maxAutoPlayEpisodeCount.value) && (
|
||||
<NextEpisodeCountDownButton
|
||||
show={
|
||||
!nextItem
|
||||
? false
|
||||
: isVlc
|
||||
? remainingTime < 10000
|
||||
: remainingTime < 10
|
||||
}
|
||||
onFinish={handleNextEpisodeAutoPlay}
|
||||
onPress={handleNextEpisodeManual}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
@@ -799,6 +871,9 @@ export const Controls: FC<Props> = ({
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
{settings.maxAutoPlayEpisodeCount.value !== -1 && (
|
||||
<ContinueWatchingOverlay goToNextItem={handleContinueWatching} />
|
||||
)}
|
||||
</ControlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
|
||||
>
|
||||
<Animated.View style={animatedStyle} />
|
||||
<View className='px-3 py-3'>
|
||||
<Text className='text-center font-bold'>
|
||||
<Text numberOfLines={1} className='text-center font-bold'>
|
||||
{t("player.next_episode")}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user