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 4s
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Failing after 14s
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Failing after 14s
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
contents: read
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
env:
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>
</a>
</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>
<td align="center">
@@ -213,6 +219,12 @@ Thanks to the following contributors for their significant contributions:
<br /><sub><b>@whoopsi-daisy</b></sub>
</a>
</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>
</table>
</div>

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
"version": "0.31.0",
"version": "0.32.1",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
@@ -37,7 +37,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 59,
"versionCode": 62,
"adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.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 { useTranslation } from "react-i18next";
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 { Text } from "@/components/common/Text";
@@ -98,7 +98,7 @@ export default function page() {
/** Playback position in ticks. */
playbackPosition?: string;
}>();
const [settings] = useSettings();
const [_settings] = useSettings();
const offline = offlineStr === "true";
const playbackManager = usePlaybackManager();
@@ -280,11 +280,15 @@ export default function page() {
]);
const stop = useCallback(() => {
// Update URL with final playback position before stopping
router.setParams({
playbackPosition: msToTicks(progress.get()).toString(),
});
reportPlaybackStopped();
setIsPlaybackStopped(true);
videoRef.current?.stop();
revalidateProgressCache();
}, [videoRef, reportPlaybackStopped]);
}, [videoRef, reportPlaybackStopped, progress]);
useEffect(() => {
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
@@ -293,7 +297,7 @@ export default function page() {
};
}, [navigation, stop]);
const currentPlayStateInfo = () => {
const currentPlayStateInfo = useCallback(() => {
if (!stream) return;
return {
itemId: item?.Id!,
@@ -309,7 +313,32 @@ export default function page() {
repeatMode: RepeatMode.RepeatNone,
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(
async (data: ProgressUpdatePayload) => {
@@ -322,10 +351,20 @@ export default function page() {
progress.set(currentTime);
// Update the playback position in the URL.
router.setParams({
playbackPosition: msToTicks(currentTime).toString(),
});
// Update URL immediately after seeking, or every 30 seconds during normal playback
const now = Date.now();
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;
@@ -398,6 +437,7 @@ export default function page() {
console.error("Error toggling mute:", error);
}
}, [previousVolume]);
const volumeDownCb = useCallback(async () => {
if (Platform.isTV) return;
@@ -512,7 +552,7 @@ export default function page() {
/** Whether the stream we're playing is not transcoding*/
const notTranscoding = !stream?.mediaSource.TranscodingUrl;
/** The initial options to pass to the VLC Player */
const initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
const initOptions = [``];
if (
chosenSubtitleTrack &&
(notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
@@ -537,6 +577,54 @@ export default function page() {
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
if (itemStatus.isError || streamStatus.isError) {
return (
@@ -567,7 +655,7 @@ export default function page() {
<View
style={{
flex: 1,
backgroundColor: "blue",
backgroundColor: "black",
height: "100%",
width: "100%",
}}
@@ -624,19 +712,19 @@ export default function page() {
showControls={showControls}
setShowControls={setShowControls}
isVideoLoaded={isVideoLoaded}
startPictureInPicture={videoRef.current?.startPictureInPicture}
play={videoRef.current?.play || (() => {})}
pause={videoRef.current?.pause || (() => {})}
seek={videoRef.current?.seekTo || (() => {})}
startPictureInPicture={startPictureInPicture}
play={play}
pause={pause}
seek={seek}
enableTrickplay={true}
getAudioTracks={videoRef.current?.getAudioTracks}
getSubtitleTracks={videoRef.current?.getSubtitleTracks}
getAudioTracks={getAudioTracks}
getSubtitleTracks={getSubtitleTracks}
offline={offline}
setSubtitleTrack={videoRef.current?.setSubtitleTrack}
setSubtitleURL={videoRef.current?.setSubtitleURL}
setAudioTrack={videoRef.current?.setAudioTrack}
setVideoAspectRatio={videoRef.current?.setVideoAspectRatio}
setVideoScaleFactor={videoRef.current?.setVideoScaleFactor}
setSubtitleTrack={setSubtitleTrack}
setSubtitleURL={setSubtitleURL}
setAudioTrack={setAudioTrack}
setVideoAspectRatio={setVideoAspectRatio}
setVideoScaleFactor={setVideoScaleFactor}
aspectRatio={aspectRatio}
scaleFactor={scaleFactor}
setAspectRatio={setAspectRatio}

View File

@@ -106,20 +106,17 @@ export const DownloadItems: React.FC<DownloadProps> = ({
// Initialize selectedOptions with default values
useEffect(() => {
if (itemsNotDownloaded.length === 1) {
setSelectedOptions(() => ({
bitrate: defaultBitrate,
mediaSource: defaultMediaSource,
subtitleIndex: defaultSubtitleIndex ?? -1,
audioIndex: defaultAudioIndex,
}));
}
setSelectedOptions(() => ({
bitrate: defaultBitrate,
mediaSource: defaultMediaSource,
subtitleIndex: defaultSubtitleIndex ?? -1,
audioIndex: defaultAudioIndex,
}));
}, [
defaultAudioIndex,
defaultBitrate,
defaultSubtitleIndex,
defaultMediaSource,
itemsNotDownloaded.length,
]);
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 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 && (
<View className='flex flex-row items-center justify-start w-full h-16'>
<BitrateSelector

View File

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

View File

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

View File

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

View File

@@ -70,7 +70,7 @@ export const usePlaybackManager = ({
useDownload();
/** 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
const { data: adjacentItems } = useQuery({

View File

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