From 1e3311fea9245ab11a4ee3887ecb01eb34e0babb Mon Sep 17 00:00:00 2001 From: Uruk Date: Fri, 22 May 2026 11:05:10 +0200 Subject: [PATCH] fix(casting): trickplay bubble positioning and mini-player preview Position the trickplay/scrub bubble above the progress bar and let the slider own horizontal placement (bubbleMaxWidth/bubbleWidth = tile width) so the preview tracks the cursor and is clamped at the track edges. Wire the mini-player trickplay to the fetched full item and size its tile/thumb. --- components/casting/CastingMiniPlayer.tsx | 17 ++++----- .../casting/player/CastPlayerProgressBar.tsx | 4 ++- .../casting/player/CastTrickplayBubble.tsx | 36 +++++++++++++++---- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/components/casting/CastingMiniPlayer.tsx b/components/casting/CastingMiniPlayer.tsx index f5be0f13c..0f01bead5 100644 --- a/components/casting/CastingMiniPlayer.tsx +++ b/components/casting/CastingMiniPlayer.tsx @@ -4,7 +4,6 @@ */ import { Ionicons } from "@expo/vector-icons"; -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; import { router } from "expo-router"; import { useAtomValue } from "jotai"; @@ -25,22 +24,22 @@ import Animated, { import { useSafeAreaInsets } from "react-native-safe-area-context"; import { CastTrickplayBubble } from "@/components/casting/player/CastTrickplayBubble"; import { Text } from "@/components/common/Text"; +import { useCastPlayerItem } from "@/hooks/useCastPlayerItem"; import { useTrickplay } from "@/hooks/useTrickplay"; -import { apiAtom } from "@/providers/JellyfinProvider"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { formatTime, getPosterUrl } from "@/utils/casting/helpers"; import { CASTING_CONSTANTS } from "@/utils/casting/types"; import { msToTicks, ticksToSeconds } from "@/utils/time"; export const CastingMiniPlayer: React.FC = () => { const api = useAtomValue(apiAtom); + const user = useAtomValue(userAtom); const insets = useSafeAreaInsets(); const castDevice = useCastDevice(); const mediaStatus = useMediaStatus(); const remoteMediaClient = useRemoteMediaClient(); - const currentItem = useMemo(() => { - return mediaStatus?.mediaInfo?.customData as BaseItemDto | undefined; - }, [mediaStatus?.mediaInfo?.customData]); + const { currentItem } = useCastPlayerItem({ api, user, mediaStatus }); // Trickplay support - pass currentItem as BaseItemDto or null const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay( @@ -237,12 +236,14 @@ export const CastingMiniPlayer: React.FC = () => { trickPlayUrl={trickPlayUrl} trickplayInfo={trickplayInfo} trickplayTime={trickplayTime} - tileWidth={140} + tileWidth={190} /> )} - bubbleWidth={trickPlayUrl && trickplayInfo ? 140 : 60} + bubbleMaxWidth={190} + bubbleWidth={190} + bubbleTranslateY={-20} sliderHeight={3} - thumbWidth={10} + thumbWidth={14} panHitSlop={{ top: 20, bottom: 20, left: 5, right: 5 }} /> diff --git a/components/casting/player/CastPlayerProgressBar.tsx b/components/casting/player/CastPlayerProgressBar.tsx index f98a89d97..c389a0772 100644 --- a/components/casting/player/CastPlayerProgressBar.tsx +++ b/components/casting/player/CastPlayerProgressBar.tsx @@ -119,7 +119,9 @@ export function CastPlayerProgressBar({ tileWidth={220} /> )} - bubbleWidth={trickPlayUrl && trickplayInfo ? 220 : 64} + bubbleMaxWidth={220} + bubbleWidth={220} + bubbleTranslateY={-20} sliderHeight={6} thumbWidth={16} panHitSlop={{ top: 12, bottom: 12, left: 10, right: 10 }} diff --git a/components/casting/player/CastTrickplayBubble.tsx b/components/casting/player/CastTrickplayBubble.tsx index 17acf3edd..9400b0d01 100644 --- a/components/casting/player/CastTrickplayBubble.tsx +++ b/components/casting/player/CastTrickplayBubble.tsx @@ -1,9 +1,10 @@ /** * Shared scrub-preview bubble for the casting progress bars. * - * Renders the trickplay tile (when trickplay data is available) with the scrub - * time as plain text above it, or just the scrub time as plain text. It does NO - * positioning of its own — the slider places it via its `bubbleWidth` prop. + * The slider (`react-native-awesome-slider`) sizes, centres and clamps this + * bubble on the thumb via its `bubbleMaxWidth` / `bubbleWidth` props. This + * component therefore does NO horizontal positioning — it only anchors itself + * vertically (`bottom: 0`, growing upward) so it sits above the progress bar. */ import { Image } from "expo-image"; @@ -47,16 +48,39 @@ export function CastTrickplayBubble({ ); - // No trickplay: just the plain time text. + // Anchored to the bottom of the slider-positioned container, growing upward, + // and filling the container width (left/right: 0) so it stays centred on the + // thumb. No horizontal maths here — the slider owns horizontal placement. if (!trickPlayUrl || !trickplayInfo) { - return timeText; + return ( + + {timeText} + + ); } const { x, y, url } = trickPlayUrl; const tileHeight = tileWidth / (trickplayInfo.aspectRatio ?? 1.78); return ( - + {timeText}