Compare commits

...

10 Commits

Author SHA1 Message Date
Fredrik Burmester
7cab50750f chore: version
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Failing after 6s
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Failing after 4s
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Failing after 5s
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been skipped
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been skipped
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Failing after 4s
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Failing after 4s
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (phone) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (tv) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Successful in 5s
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Failing after 30s
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Failing after 28s
2025-08-20 10:30:57 +02:00
Fredrik Burmester
d795e82581 fix: trickplay and re-rendering issues 2025-08-20 09:59:03 +02:00
Fredrik Burmester
e7161bc9ab fix: revert fade in controls 2025-08-20 08:21:01 +02:00
Fredrik Burmester
8e74363f32 Revert "chore: refactor controls (#946)"
This reverts commit 8389404975.
2025-08-20 08:18:12 +02:00
Alex
1cb28788d6 Fix selecting bit rate on whole series downloads (#956)
Co-authored-by: Alex Kim <alexkim@Alexs-MacBook-Pro.local>
2025-08-20 00:12:51 +10:00
renovate[bot]
ff9f855d4c chore(deps): update amannn/action-semantic-pull-request action to v6.1.0 (#953) 2025-08-19 13:37:47 +02:00
Fredrik Burmester
13df2d1077 chore: version 2025-08-19 10:01:34 +02:00
Fredrik Burmester
8389404975 chore: refactor controls (#946)
Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com>
2025-08-19 09:02:56 +02:00
Fredrik Burmester
cd920e2d84 fix: small design change 2025-08-19 08:10:54 +02:00
Gauvain
92a11c18e0 docs: add new contributors to README (#951) 2025-08-19 04:25:49 +02:00
11 changed files with 261 additions and 187 deletions

View File

@@ -20,7 +20,7 @@ jobs:
pull-requests: write pull-requests: write
contents: read contents: read
steps: steps:
- uses: amannn/action-semantic-pull-request@fdd4d3ddf614fbcd8c29e4b106d3bbe0cb2c605d # v6.0.1 - uses: amannn/action-semantic-pull-request@7f33ba792281b034f64e96f4c0b5496782dd3b37 # v6.1.0
id: lint_pr_title id: lint_pr_title
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -181,6 +181,12 @@ Thanks to the following contributors for their significant contributions:
<br /><sub><b>@topiga</b></sub> <br /><sub><b>@topiga</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/lancechant">
<img src="https://github.com/lancechant.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@lancechant</b></sub>
</a>
</td>
</tr> </tr>
<tr> <tr>
<td align="center"> <td align="center">
@@ -213,6 +219,12 @@ Thanks to the following contributors for their significant contributions:
<br /><sub><b>@whoopsi-daisy</b></sub> <br /><sub><b>@whoopsi-daisy</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/Gauvino">
<img src="https://github.com/Gauvino.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@Gauvino</b></sub>
</a>
</td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Streamyfin", "name": "Streamyfin",
"slug": "streamyfin", "slug": "streamyfin",
"version": "0.31.0", "version": "0.32.1",
"orientation": "default", "orientation": "default",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "streamyfin", "scheme": "streamyfin",
@@ -37,7 +37,7 @@
}, },
"android": { "android": {
"jsEngine": "hermes", "jsEngine": "hermes",
"versionCode": 59, "versionCode": 62,
"adaptiveIcon": { "adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.png", "foregroundImage": "./assets/images/icon-android-plain.png",
"monochromeImage": "./assets/images/icon-android-themed.png", "monochromeImage": "./assets/images/icon-android-themed.png",

View File

@@ -15,7 +15,7 @@ import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Alert, Platform, View } from "react-native"; import { Alert, Platform, View } from "react-native";
import { useSharedValue } from "react-native-reanimated"; import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
import { BITRATES } from "@/components/BitrateSelector"; import { BITRATES } from "@/components/BitrateSelector";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
@@ -98,7 +98,7 @@ export default function page() {
/** Playback position in ticks. */ /** Playback position in ticks. */
playbackPosition?: string; playbackPosition?: string;
}>(); }>();
const [settings] = useSettings(); const [_settings] = useSettings();
const offline = offlineStr === "true"; const offline = offlineStr === "true";
const playbackManager = usePlaybackManager(); const playbackManager = usePlaybackManager();
@@ -280,11 +280,15 @@ export default function page() {
]); ]);
const stop = useCallback(() => { const stop = useCallback(() => {
// Update URL with final playback position before stopping
router.setParams({
playbackPosition: msToTicks(progress.get()).toString(),
});
reportPlaybackStopped(); reportPlaybackStopped();
setIsPlaybackStopped(true); setIsPlaybackStopped(true);
videoRef.current?.stop(); videoRef.current?.stop();
revalidateProgressCache(); revalidateProgressCache();
}, [videoRef, reportPlaybackStopped]); }, [videoRef, reportPlaybackStopped, progress]);
useEffect(() => { useEffect(() => {
const beforeRemoveListener = navigation.addListener("beforeRemove", stop); const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
@@ -293,7 +297,7 @@ export default function page() {
}; };
}, [navigation, stop]); }, [navigation, stop]);
const currentPlayStateInfo = () => { const currentPlayStateInfo = useCallback(() => {
if (!stream) return; if (!stream) return;
return { return {
itemId: item?.Id!, itemId: item?.Id!,
@@ -309,7 +313,32 @@ export default function page() {
repeatMode: RepeatMode.RepeatNone, repeatMode: RepeatMode.RepeatNone,
playbackOrder: PlaybackOrder.Default, playbackOrder: PlaybackOrder.Default,
}; };
}; }, [
stream,
item?.Id,
audioIndex,
subtitleIndex,
mediaSourceId,
progress,
isPlaying,
isMuted,
]);
const lastUrlUpdateTime = useSharedValue(0);
const wasJustSeeking = useSharedValue(false);
const URL_UPDATE_INTERVAL = 30000; // Update URL every 30 seconds instead of every second
// Track when seeking ends to update URL immediately
useAnimatedReaction(
() => isSeeking.get(),
(currentSeeking, previousSeeking) => {
if (previousSeeking && !currentSeeking) {
// Seeking just ended
wasJustSeeking.value = true;
}
},
[],
);
const onProgress = useCallback( const onProgress = useCallback(
async (data: ProgressUpdatePayload) => { async (data: ProgressUpdatePayload) => {
@@ -322,10 +351,20 @@ export default function page() {
progress.set(currentTime); progress.set(currentTime);
// Update the playback position in the URL. // Update URL immediately after seeking, or every 30 seconds during normal playback
router.setParams({ const now = Date.now();
playbackPosition: msToTicks(currentTime).toString(), const shouldUpdateUrl = wasJustSeeking.get();
}); wasJustSeeking.value = false;
if (
shouldUpdateUrl ||
now - lastUrlUpdateTime.get() > URL_UPDATE_INTERVAL
) {
router.setParams({
playbackPosition: msToTicks(currentTime).toString(),
});
lastUrlUpdateTime.value = now;
}
if (!item?.Id) return; if (!item?.Id) return;
@@ -398,6 +437,7 @@ export default function page() {
console.error("Error toggling mute:", error); console.error("Error toggling mute:", error);
} }
}, [previousVolume]); }, [previousVolume]);
const volumeDownCb = useCallback(async () => { const volumeDownCb = useCallback(async () => {
if (Platform.isTV) return; if (Platform.isTV) return;
@@ -512,7 +552,7 @@ export default function page() {
/** Whether the stream we're playing is not transcoding*/ /** Whether the stream we're playing is not transcoding*/
const notTranscoding = !stream?.mediaSource.TranscodingUrl; const notTranscoding = !stream?.mediaSource.TranscodingUrl;
/** The initial options to pass to the VLC Player */ /** The initial options to pass to the VLC Player */
const initOptions = [`--sub-text-scale=${settings.subtitleSize}`]; const initOptions = [``];
if ( if (
chosenSubtitleTrack && chosenSubtitleTrack &&
(notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream) (notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
@@ -537,6 +577,54 @@ export default function page() {
return () => setIsMounted(false); return () => setIsMounted(false);
}, []); }, []);
// Memoize video ref functions to prevent unnecessary re-renders
const startPictureInPicture = useMemo(
() => videoRef.current?.startPictureInPicture,
[isVideoLoaded],
);
const play = useMemo(
() => videoRef.current?.play || (() => {}),
[isVideoLoaded],
);
const pause = useMemo(
() => videoRef.current?.pause || (() => {}),
[isVideoLoaded],
);
const seek = useMemo(
() => videoRef.current?.seekTo || (() => {}),
[isVideoLoaded],
);
const getAudioTracks = useMemo(
() => videoRef.current?.getAudioTracks,
[isVideoLoaded],
);
const getSubtitleTracks = useMemo(
() => videoRef.current?.getSubtitleTracks,
[isVideoLoaded],
);
const setSubtitleTrack = useMemo(
() => videoRef.current?.setSubtitleTrack,
[isVideoLoaded],
);
const setSubtitleURL = useMemo(
() => videoRef.current?.setSubtitleURL,
[isVideoLoaded],
);
const setAudioTrack = useMemo(
() => videoRef.current?.setAudioTrack,
[isVideoLoaded],
);
const setVideoAspectRatio = useMemo(
() => videoRef.current?.setVideoAspectRatio,
[isVideoLoaded],
);
const setVideoScaleFactor = useMemo(
() => videoRef.current?.setVideoScaleFactor,
[isVideoLoaded],
);
console.log("Debug: component render"); // Uncomment to debug re-renders
// Show error UI first, before checking loading/missingdata // Show error UI first, before checking loading/missingdata
if (itemStatus.isError || streamStatus.isError) { if (itemStatus.isError || streamStatus.isError) {
return ( return (
@@ -567,7 +655,7 @@ export default function page() {
<View <View
style={{ style={{
flex: 1, flex: 1,
backgroundColor: "blue", backgroundColor: "black",
height: "100%", height: "100%",
width: "100%", width: "100%",
}} }}
@@ -624,19 +712,19 @@ export default function page() {
showControls={showControls} showControls={showControls}
setShowControls={setShowControls} setShowControls={setShowControls}
isVideoLoaded={isVideoLoaded} isVideoLoaded={isVideoLoaded}
startPictureInPicture={videoRef.current?.startPictureInPicture} startPictureInPicture={startPictureInPicture}
play={videoRef.current?.play || (() => {})} play={play}
pause={videoRef.current?.pause || (() => {})} pause={pause}
seek={videoRef.current?.seekTo || (() => {})} seek={seek}
enableTrickplay={true} enableTrickplay={true}
getAudioTracks={videoRef.current?.getAudioTracks} getAudioTracks={getAudioTracks}
getSubtitleTracks={videoRef.current?.getSubtitleTracks} getSubtitleTracks={getSubtitleTracks}
offline={offline} offline={offline}
setSubtitleTrack={videoRef.current?.setSubtitleTrack} setSubtitleTrack={setSubtitleTrack}
setSubtitleURL={videoRef.current?.setSubtitleURL} setSubtitleURL={setSubtitleURL}
setAudioTrack={videoRef.current?.setAudioTrack} setAudioTrack={setAudioTrack}
setVideoAspectRatio={videoRef.current?.setVideoAspectRatio} setVideoAspectRatio={setVideoAspectRatio}
setVideoScaleFactor={videoRef.current?.setVideoScaleFactor} setVideoScaleFactor={setVideoScaleFactor}
aspectRatio={aspectRatio} aspectRatio={aspectRatio}
scaleFactor={scaleFactor} scaleFactor={scaleFactor}
setAspectRatio={setAspectRatio} setAspectRatio={setAspectRatio}

View File

@@ -106,20 +106,17 @@ export const DownloadItems: React.FC<DownloadProps> = ({
// Initialize selectedOptions with default values // Initialize selectedOptions with default values
useEffect(() => { useEffect(() => {
if (itemsNotDownloaded.length === 1) { setSelectedOptions(() => ({
setSelectedOptions(() => ({ bitrate: defaultBitrate,
bitrate: defaultBitrate, mediaSource: defaultMediaSource,
mediaSource: defaultMediaSource, subtitleIndex: defaultSubtitleIndex ?? -1,
subtitleIndex: defaultSubtitleIndex ?? -1, audioIndex: defaultAudioIndex,
audioIndex: defaultAudioIndex, }));
}));
}
}, [ }, [
defaultAudioIndex, defaultAudioIndex,
defaultBitrate, defaultBitrate,
defaultSubtitleIndex, defaultSubtitleIndex,
defaultMediaSource, defaultMediaSource,
itemsNotDownloaded.length,
]); ]);
const itemsToDownload = useMemo(() => { const itemsToDownload = useMemo(() => {

View File

@@ -186,7 +186,7 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
> >
<View className='flex flex-col bg-transparent shrink'> <View className='flex flex-col bg-transparent shrink'>
<View className='flex flex-col px-4 w-full space-y-2 pt-2 mb-2 shrink'> <View className='flex flex-col px-4 w-full space-y-2 pt-2 mb-2 shrink'>
<ItemHeader item={item} className='mb-4' /> <ItemHeader item={item} className='mb-2' />
{item.Type !== "Program" && !Platform.isTV && !isOffline && ( {item.Type !== "Program" && !Platform.isTV && !isOffline && (
<View className='flex flex-row items-center justify-start w-full h-16'> <View className='flex flex-row items-center justify-start w-full h-16'>
<BitrateSelector <BitrateSelector

View File

@@ -24,13 +24,11 @@ import {
View, View,
} from "react-native"; } from "react-native";
import { Slider } from "react-native-awesome-slider"; import { Slider } from "react-native-awesome-slider";
import Animated, { import {
runOnJS, runOnJS,
type SharedValue, type SharedValue,
useAnimatedReaction, useAnimatedReaction,
useAnimatedStyle,
useSharedValue, useSharedValue,
withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
@@ -163,45 +161,11 @@ export const Controls: FC<Props> = ({
const min = useSharedValue(0); const min = useSharedValue(0);
const max = useSharedValue(item.RunTimeTicks || 0); const max = useSharedValue(item.RunTimeTicks || 0);
// Animated opacity for smooth transitions
const controlsOpacity = useSharedValue(showControls ? 1 : 0);
// Animated scale for slider
const sliderScale = useSharedValue(1);
const wasPlayingRef = useRef(false); const wasPlayingRef = useRef(false);
const lastProgressRef = useRef<number>(0); const lastProgressRef = useRef<number>(0);
const lightHapticFeedback = useHaptic("light"); const lightHapticFeedback = useHaptic("light");
// Animate controls opacity when showControls changes
useEffect(() => {
controlsOpacity.value = withTiming(showControls ? 1 : 0, {
duration: 300,
});
}, [showControls, controlsOpacity]);
// Animated styles for controls
const animatedControlsStyle = useAnimatedStyle(() => {
return {
opacity: controlsOpacity.value,
};
});
// Animated style for black overlay (75% opacity when visible)
const animatedOverlayStyle = useAnimatedStyle(() => {
return {
opacity: controlsOpacity.value * 0.75,
};
});
// Animated style for slider scale
const animatedSliderStyle = useAnimatedStyle(() => {
return {
transform: [{ scaleY: sliderScale.value }],
};
});
useEffect(() => { useEffect(() => {
prefetchAllTrickplayImages(); prefetchAllTrickplayImages();
}, []); }, []);
@@ -318,18 +282,31 @@ export const Controls: FC<Props> = ({
const effectiveProgress = useSharedValue(0); const effectiveProgress = useSharedValue(0);
// Recompute progress whenever remote scrubbing is active // Recompute progress whenever remote scrubbing is active or when progress significantly changes
useAnimatedReaction( useAnimatedReaction(
() => ({ () => ({
isScrubbing: isRemoteScrubbing.value, isScrubbing: isRemoteScrubbing.value,
scrub: remoteScrubProgress.value, scrub: remoteScrubProgress.value,
actual: progress.value, actual: progress.value,
}), }),
(current) => { (current, previous) => {
effectiveProgress.value = // Always update if scrubbing state changed or we're currently scrubbing
current.isScrubbing && current.scrub != null if (
? current.scrub current.isScrubbing !== previous?.isScrubbing ||
: current.actual; current.isScrubbing
) {
effectiveProgress.value =
current.isScrubbing && current.scrub != null
? current.scrub
: current.actual;
} else {
// When not scrubbing, only update if progress changed significantly (1 second)
const progressUnit = isVlc ? 1000 : 10000000; // 1 second in ms or ticks
const progressDiff = Math.abs(current.actual - effectiveProgress.value);
if (progressDiff >= progressUnit) {
effectiveProgress.value = current.actual;
}
}
}, },
[], [],
); );
@@ -486,6 +463,9 @@ export const Controls: FC<Props> = ({
[goToNextItem], [goToNextItem],
); );
const lastCurrentTimeRef = useRef(0);
const lastRemainingTimeRef = useRef(0);
const updateTimes = useCallback( const updateTimes = useCallback(
(currentProgress: number, maxValue: number) => { (currentProgress: number, maxValue: number) => {
const current = isVlc ? currentProgress : ticksToSeconds(currentProgress); const current = isVlc ? currentProgress : ticksToSeconds(currentProgress);
@@ -493,8 +473,25 @@ export const Controls: FC<Props> = ({
? maxValue - currentProgress ? maxValue - currentProgress
: ticksToSeconds(maxValue - currentProgress); : ticksToSeconds(maxValue - currentProgress);
setCurrentTime(current); // Only update state if the displayed time actually changed (avoid sub-second updates)
setRemainingTime(remaining); const currentSeconds = Math.floor(current / (isVlc ? 1000 : 1));
const remainingSeconds = Math.floor(remaining / (isVlc ? 1000 : 1));
const lastCurrentSeconds = Math.floor(
lastCurrentTimeRef.current / (isVlc ? 1000 : 1),
);
const lastRemainingSeconds = Math.floor(
lastRemainingTimeRef.current / (isVlc ? 1000 : 1),
);
if (
currentSeconds !== lastCurrentSeconds ||
remainingSeconds !== lastRemainingSeconds
) {
setCurrentTime(current);
setRemainingTime(remaining);
lastCurrentTimeRef.current = current;
lastRemainingTimeRef.current = remaining;
}
}, },
[goToNextItem, isVlc], [goToNextItem, isVlc],
); );
@@ -552,31 +549,19 @@ export const Controls: FC<Props> = ({
if (!showControls) { if (!showControls) {
return; return;
} }
// Scale up the slider immediately on touch
sliderScale.value = withTiming(1.4, { duration: 300 });
}, [showControls]); }, [showControls]);
const handleTouchEnd = useCallback(() => { const handleTouchEnd = useCallback(() => {
if (!showControls) { if (!showControls) {
return; return;
} }
// Scale down the slider on touch end (only if not sliding, to avoid conflict with onSlidingComplete)
if (!isSliding) {
sliderScale.value = withTiming(1.0, { duration: 300 });
}
}, [showControls, isSliding]); }, [showControls, isSliding]);
const handleSliderComplete = useCallback( const handleSliderComplete = useCallback(
async (value: number) => { async (value: number) => {
setIsSliding(false);
isSeeking.value = false; isSeeking.value = false;
progress.value = value; progress.value = value;
setIsSliding(false);
// Scale down the slider
sliderScale.value = withTiming(1.0, { duration: 200 });
seek(Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value)))); seek(Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value))));
if (wasPlayingRef.current) { if (wasPlayingRef.current) {
play(); play();
@@ -791,10 +776,10 @@ export const Controls: FC<Props> = ({
<VideoTouchOverlay <VideoTouchOverlay
screenWidth={screenWidth} screenWidth={screenWidth}
screenHeight={screenHeight} screenHeight={screenHeight}
showControls={showControls}
onToggleControls={toggleControls} onToggleControls={toggleControls}
animatedStyle={animatedOverlayStyle}
/> />
<Animated.View <View
style={[ style={[
{ {
position: "absolute", position: "absolute",
@@ -803,8 +788,8 @@ export const Controls: FC<Props> = ({
width: settings?.safeAreaInControlsEnabled width: settings?.safeAreaInControlsEnabled
? screenWidth - insets.left - insets.right ? screenWidth - insets.left - insets.right
: screenWidth, : screenWidth,
opacity: showControls ? 1 : 0,
}, },
animatedControlsStyle,
]} ]}
pointerEvents={showControls ? "auto" : "none"} pointerEvents={showControls ? "auto" : "none"}
className={"flex flex-row w-full pt-2"} className={"flex flex-row w-full pt-2"}
@@ -883,39 +868,33 @@ export const Controls: FC<Props> = ({
<Ionicons name='close' size={24} color='white' /> <Ionicons name='close' size={24} color='white' />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</Animated.View> </View>
<View
<Animated.View style={{
style={[ position: "absolute",
{ top: "50%", // Center vertically
position: "absolute", left: settings?.safeAreaInControlsEnabled ? insets.left : 0,
top: "50%", // Center vertically right: settings?.safeAreaInControlsEnabled ? insets.right : 0,
left: settings?.safeAreaInControlsEnabled ? insets.left : 0, flexDirection: "row",
right: settings?.safeAreaInControlsEnabled ? insets.right : 0, justifyContent: "space-between",
flexDirection: "row", alignItems: "center",
justifyContent: "space-between", transform: [{ translateY: -22.5 }], // Adjust for the button's height (half of 45)
alignItems: "center", paddingHorizontal: "28%", // Add some padding to the left and right
transform: [{ translateY: -22.5 }], // Adjust for the button's height (half of 45) }}
paddingHorizontal: 17,
},
animatedControlsStyle,
]}
pointerEvents={showControls ? "box-none" : "none"} pointerEvents={showControls ? "box-none" : "none"}
> >
{/* Brightness Control */}
<View <View
style={{ style={{
width: 50, position: "absolute",
height: 50,
alignItems: "center", alignItems: "center",
justifyContent: "center", transform: [{ rotate: "270deg" }], // Rotate the slider to make it vertical
transform: [{ rotate: "270deg" }], left: 0,
bottom: 30,
opacity: showControls ? 1 : 0,
}} }}
> >
<BrightnessSlider /> <BrightnessSlider />
</View> </View>
{/* Skip Backward */}
{!Platform.isTV && ( {!Platform.isTV && (
<TouchableOpacity onPress={handleSkipBackward}> <TouchableOpacity onPress={handleSkipBackward}>
<View <View
@@ -923,6 +902,7 @@ export const Controls: FC<Props> = ({
position: "relative", position: "relative",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
opacity: showControls ? 1 : 0,
}} }}
> >
<Ionicons <Ionicons
@@ -948,8 +928,9 @@ export const Controls: FC<Props> = ({
</TouchableOpacity> </TouchableOpacity>
)} )}
{/* Play/Pause Button */} <View
<View style={{ alignItems: "center" }}> style={Platform.isTV ? { flex: 1, alignItems: "center" } : {}}
>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
togglePlay(); togglePlay();
@@ -960,6 +941,9 @@ export const Controls: FC<Props> = ({
name={isPlaying ? "pause" : "play"} name={isPlaying ? "pause" : "play"}
size={50} size={50}
color='white' color='white'
style={{
opacity: showControls ? 1 : 0,
}}
/> />
) : ( ) : (
<Loader size={"large"} /> <Loader size={"large"} />
@@ -967,7 +951,6 @@ export const Controls: FC<Props> = ({
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Skip Forward */}
{!Platform.isTV && ( {!Platform.isTV && (
<TouchableOpacity onPress={handleSkipForward}> <TouchableOpacity onPress={handleSkipForward}>
<View <View
@@ -975,6 +958,7 @@ export const Controls: FC<Props> = ({
position: "relative", position: "relative",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
opacity: showControls ? 1 : 0,
}} }}
> >
<Ionicons name='refresh-outline' size={50} color='white' /> <Ionicons name='refresh-outline' size={50} color='white' />
@@ -992,23 +976,21 @@ export const Controls: FC<Props> = ({
</View> </View>
</TouchableOpacity> </TouchableOpacity>
)} )}
{/* Volume/Audio Control */}
<View <View
style={{ style={{
width: 50, position: "absolute",
height: 50,
alignItems: "center", alignItems: "center",
justifyContent: "center", transform: [{ rotate: "270deg" }], // Rotate the slider to make it vertical
transform: [{ rotate: "270deg" }], bottom: 30,
right: 0,
opacity: showAudioSlider || showControls ? 1 : 0, opacity: showAudioSlider || showControls ? 1 : 0,
}} }}
> >
<AudioSlider setVisibility={setShowAudioSlider} /> <AudioSlider setVisibility={setShowAudioSlider} />
</View> </View>
</Animated.View> </View>
<Animated.View <View
style={[ style={[
{ {
position: "absolute", position: "absolute",
@@ -1018,7 +1000,6 @@ export const Controls: FC<Props> = ({
? Math.max(insets.bottom - 17, 0) ? Math.max(insets.bottom - 17, 0)
: 0, : 0,
}, },
animatedControlsStyle,
]} ]}
className={"flex flex-col px-2"} className={"flex flex-col px-2"}
onTouchStart={handleControlsInteraction} onTouchStart={handleControlsInteraction}
@@ -1034,6 +1015,7 @@ export const Controls: FC<Props> = ({
style={{ style={{
flexDirection: "column", flexDirection: "column",
alignSelf: "flex-end", // Shrink height based on content alignSelf: "flex-end", // Shrink height based on content
opacity: showControls ? 1 : 0,
}} }}
pointerEvents={showControls ? "box-none" : "none"} pointerEvents={showControls ? "box-none" : "none"}
> >
@@ -1082,6 +1064,9 @@ export const Controls: FC<Props> = ({
</View> </View>
<View <View
className={"flex flex-col-reverse rounded-lg items-center my-2"} className={"flex flex-col-reverse rounded-lg items-center my-2"}
style={{
opacity: showControls ? 1 : 0,
}}
pointerEvents={showControls ? "box-none" : "none"} pointerEvents={showControls ? "box-none" : "none"}
> >
<View className={"flex flex-col w-full shrink"}> <View className={"flex flex-col w-full shrink"}>
@@ -1094,35 +1079,32 @@ export const Controls: FC<Props> = ({
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd} onTouchEnd={handleTouchEnd}
> >
<Animated.View style={animatedSliderStyle}> <Slider
<Slider theme={{
theme={{ maximumTrackTintColor: "rgba(255,255,255,0.2)",
maximumTrackTintColor: "rgba(255,255,255,0.2)", minimumTrackTintColor: "#fff",
minimumTrackTintColor: "#fff", cacheTrackTintColor: "rgba(255,255,255,0.3)",
cacheTrackTintColor: "rgba(255,255,255,0.3)", bubbleBackgroundColor: "#fff",
bubbleBackgroundColor: "#fff", bubbleTextColor: "#666",
bubbleTextColor: "#666", heartbeatColor: "#999",
heartbeatColor: "#999", }}
}} renderThumb={() => null}
renderThumb={() => null} cache={cacheProgress}
cache={cacheProgress} onSlidingStart={handleSliderStart}
onSlidingStart={handleSliderStart} onSlidingComplete={handleSliderComplete}
onSlidingComplete={handleSliderComplete} onValueChange={handleSliderChange}
onValueChange={handleSliderChange} containerStyle={{
containerStyle={{ borderRadius: 100,
borderRadius: 100, }}
}} renderBubble={() =>
renderBubble={() => (isSliding || showRemoteBubble) && memoizedRenderBubble()
(isSliding || showRemoteBubble) && }
memoizedRenderBubble() sliderHeight={10}
} thumbWidth={0}
sliderHeight={10} progress={effectiveProgress}
thumbWidth={0} minimumValue={min}
progress={effectiveProgress} maximumValue={max}
minimumValue={min} />
maximumValue={max}
/>
</Animated.View>
</View> </View>
<View className='flex flex-row items-center justify-between mt-2'> <View className='flex flex-row items-center justify-between mt-2'>
<Text className='text-[12px] text-neutral-400'> <Text className='text-[12px] text-neutral-400'>
@@ -1152,7 +1134,7 @@ export const Controls: FC<Props> = ({
</View> </View>
</View> </View>
</View> </View>
</Animated.View> </View>
</> </>
)} )}
{settings.maxAutoPlayEpisodeCount.value !== -1 && ( {settings.maxAutoPlayEpisodeCount.value !== -1 && (

View File

@@ -1,43 +1,38 @@
import { Pressable } from "react-native"; import { Pressable } from "react-native";
import Animated, { type AnimatedStyle } from "react-native-reanimated";
import { useTapDetection } from "./useTapDetection"; import { useTapDetection } from "./useTapDetection";
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
interface Props { interface Props {
screenWidth: number; screenWidth: number;
screenHeight: number; screenHeight: number;
showControls: boolean;
onToggleControls: () => void; onToggleControls: () => void;
animatedStyle: AnimatedStyle;
} }
export const VideoTouchOverlay = ({ export const VideoTouchOverlay = ({
screenWidth, screenWidth,
screenHeight, screenHeight,
showControls,
onToggleControls, onToggleControls,
animatedStyle,
}: Props) => { }: Props) => {
const { handleTouchStart, handleTouchEnd } = useTapDetection({ const { handleTouchStart, handleTouchEnd } = useTapDetection({
onValidTap: onToggleControls, onValidTap: onToggleControls,
}); });
return ( return (
<AnimatedPressable <Pressable
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd} onTouchEnd={handleTouchEnd}
style={[ style={{
{ position: "absolute",
position: "absolute", width: screenWidth,
width: screenWidth, height: screenHeight,
height: screenHeight, backgroundColor: "black",
backgroundColor: "black", left: 0,
left: 0, right: 0,
right: 0, top: 0,
top: 0, bottom: 0,
bottom: 0, opacity: showControls ? 0.75 : 0,
}, }}
animatedStyle,
]}
/> />
); );
}; };

View File

@@ -46,14 +46,14 @@
}, },
"production": { "production": {
"environment": "production", "environment": "production",
"channel": "0.31.0", "channel": "0.32.1",
"android": { "android": {
"image": "latest" "image": "latest"
} }
}, },
"production-apk": { "production-apk": {
"environment": "production", "environment": "production",
"channel": "0.31.0", "channel": "0.32.1",
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest" "image": "latest"
@@ -61,7 +61,7 @@
}, },
"production-apk-tv": { "production-apk-tv": {
"environment": "production", "environment": "production",
"channel": "0.31.0", "channel": "0.32.1",
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest" "image": "latest"

View File

@@ -70,7 +70,7 @@ export const usePlaybackManager = ({
useDownload(); useDownload();
/** Whether the device is online. actually it's connected to the internet. */ /** Whether the device is online. actually it's connected to the internet. */
const isOnline = netInfo.isConnected; const isOnline = useMemo(() => netInfo.isConnected, [netInfo.isConnected]);
// Adjacent episodes logic // Adjacent episodes logic
const { data: adjacentItems } = useQuery({ const { data: adjacentItems } = useQuery({

View File

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