diff --git a/components/casting/CastingMiniPlayer.tsx b/components/casting/CastingMiniPlayer.tsx
index 573f04319..f5be0f13c 100644
--- a/components/casting/CastingMiniPlayer.tsx
+++ b/components/casting/CastingMiniPlayer.tsx
@@ -9,7 +9,7 @@ import { Image } from "expo-image";
import { router } from "expo-router";
import { useAtomValue } from "jotai";
import React, { useEffect, useMemo, useRef, useState } from "react";
-import { Dimensions, Pressable, View } from "react-native";
+import { Pressable, View } from "react-native";
import { Slider } from "react-native-awesome-slider";
import {
MediaPlayerState,
@@ -23,14 +23,11 @@ import Animated, {
useSharedValue,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { CastTrickplayBubble } from "@/components/casting/player/CastTrickplayBubble";
import { Text } from "@/components/common/Text";
import { useTrickplay } from "@/hooks/useTrickplay";
import { apiAtom } from "@/providers/JellyfinProvider";
-import {
- formatTime,
- formatTrickplayTime,
- getPosterUrl,
-} from "@/utils/casting/helpers";
+import { formatTime, getPosterUrl } from "@/utils/casting/helpers";
import { CASTING_CONSTANTS } from "@/utils/casting/types";
import { msToTicks, ticksToSeconds } from "@/utils/time";
@@ -54,7 +51,6 @@ export const CastingMiniPlayer: React.FC = () => {
minutes: 0,
seconds: 0,
});
- const [scrubPercentage, setScrubPercentage] = useState(0);
const isScrubbing = useRef(false);
// Slider shared values
@@ -223,11 +219,6 @@ export const CastingMiniPlayer: React.FC = () => {
const minutes = Math.floor((progressInSeconds % 3600) / 60);
const seconds = progressInSeconds % 60;
setTrickplayTime({ hours, minutes, seconds });
-
- // Track scrub percentage for bubble positioning
- if (duration > 0) {
- setScrubPercentage(value / duration);
- }
}}
onSlidingComplete={(value) => {
isScrubbing.current = false;
@@ -241,119 +232,15 @@ export const CastingMiniPlayer: React.FC = () => {
});
}
}}
- renderBubble={() => {
- // Calculate bubble position with edge clamping
- const screenWidth = Dimensions.get("window").width;
- const sliderPadding = 8;
- const thumbWidth = 10; // matches thumbWidth prop on Slider
- const sliderWidth = screenWidth - sliderPadding * 2;
- // Adjust thumb position to account for thumb width affecting travel range
- const effectiveTrackWidth = sliderWidth - thumbWidth;
- const thumbPosition =
- thumbWidth / 2 + scrubPercentage * effectiveTrackWidth;
-
- if (!trickPlayUrl || !trickplayInfo) {
- // Show simple time bubble when no trickplay
- const timeBubbleWidth = 70;
- const minLeft = -thumbPosition;
- const maxLeft = sliderWidth - thumbPosition - timeBubbleWidth;
- const centeredLeft = -timeBubbleWidth / 2;
- const clampedLeft = Math.max(
- minLeft,
- Math.min(maxLeft, centeredLeft),
- );
-
- return (
-
-
- {formatTrickplayTime(trickplayTime)}
-
-
- );
- }
-
- const { x, y, url } = trickPlayUrl;
- const tileWidth = 140; // Smaller preview for mini player
- const tileHeight = tileWidth / (trickplayInfo.aspectRatio ?? 1.78);
-
- // Calculate clamped position for trickplay preview
- const minLeft = -thumbPosition;
- const maxLeft = sliderWidth - thumbPosition - tileWidth;
- const centeredLeft = -tileWidth / 2;
- const clampedLeft = Math.max(
- minLeft,
- Math.min(maxLeft, centeredLeft),
- );
-
- return (
-
- {/* Trickplay image preview */}
-
-
-
- {/* Time overlay */}
-
-
- {formatTrickplayTime(trickplayTime)}
-
-
-
- );
- }}
+ renderBubble={() => (
+
+ )}
+ bubbleWidth={trickPlayUrl && trickplayInfo ? 140 : 60}
sliderHeight={3}
thumbWidth={10}
panHitSlop={{ top: 20, bottom: 20, left: 5, right: 5 }}
@@ -436,6 +323,19 @@ export const CastingMiniPlayer: React.FC = () => {
+ {/* Stop button */}
+ {
+ e.stopPropagation();
+ remoteMediaClient?.stop()?.catch((error: unknown) => {
+ console.error("[CastingMiniPlayer] Stop error:", error);
+ });
+ }}
+ style={{ padding: 8 }}
+ >
+
+
+
{/* Play/Pause button */}
{