hot fix for player screen not displaying on iOS

This commit is contained in:
Violet Caulfield
2025-12-17 07:18:37 -06:00
parent a6c527a0f6
commit 05bbf02152
4 changed files with 109 additions and 131 deletions

View File

@@ -170,7 +170,15 @@ function AlbumTrackListHeader({ album }: { album: BaseItemDto }): React.JSX.Elem
return (
<YStack alignContent='center' flex={1} marginTop={'$4'}>
<ItemImage item={album} width={'$20'} height={'$20'} />
<ItemImage
item={album}
width={'$20'}
height={'$20'}
imageOptions={{
maxHeight: 500,
maxWidth: 500,
}}
/>
<YStack marginTop={'$2'} alignContent='center' justifyContent='center' gap={'$2'}>
<H5 lineBreakStrategyIOS='standard' textAlign='center' numberOfLines={5}>

View File

@@ -1,11 +1,8 @@
import React, { useCallback, useMemo, useState } from 'react'
import React from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { YStack, useTheme, ZStack, useWindowDimensions, View, getTokenValue } from 'tamagui'
import { YStack, ZStack, useWindowDimensions, View, getTokenValue } from 'tamagui'
import Scrubber from './components/scrubber'
import Controls from './components/controls'
import Toast from 'react-native-toast-message'
import JellifyToastConfig from '../../configs/toast.config'
import { useFocusEffect } from '@react-navigation/native'
import Footer from './components/footer'
import BlurredBackground from './components/blurred-background'
import PlayerHeader from './components/header'
@@ -29,22 +26,11 @@ import { useCurrentTrack } from '../../stores/player/queue'
export default function PlayerScreen(): React.JSX.Element {
usePerformanceMonitor('PlayerScreen', 5)
const [showToast, setShowToast] = useState(true)
const skip = useSkip()
const previous = usePrevious()
const trigger = useHapticFeedback()
const nowPlaying = useCurrentTrack()
const theme = useTheme()
useFocusEffect(
useCallback(() => {
setShowToast(true)
return () => setShowToast(false)
}, []),
)
const isAndroid = Platform.OS === 'android'
const { width, height } = useWindowDimensions()
@@ -63,49 +49,43 @@ export default function PlayerScreen(): React.JSX.Element {
}))
// Let the native sheet gesture handle vertical dismissals; we only own horizontal swipes
const sheetDismissGesture = useMemo(() => Gesture.Native(), [])
const sheetDismissGesture = Gesture.Native()
// Gesture logic for central big swipe area
const swipeGesture = useMemo(
() =>
Gesture.Pan()
.activeOffsetX([-12, 12])
// Bail on vertical intent so native sheet dismiss keeps working
.failOffsetY([-8, 8])
.simultaneousWithExternalGesture(sheetDismissGesture)
.onUpdate((e) => {
if (Math.abs(e.translationY) < 40) {
translateX.value = Math.max(-160, Math.min(160, e.translationX))
}
})
.onEnd((e) => {
const threshold = 120
const minVelocity = 600
const isHorizontal = Math.abs(e.translationY) < 40
if (
isHorizontal &&
(Math.abs(e.translationX) > threshold ||
Math.abs(e.velocityX) > minVelocity)
) {
if (e.translationX > 0) {
// Inverted: swipe right = previous
translateX.value = withSpring(220)
runOnJS(trigger)('notificationSuccess')
runOnJS(previous)()
} else {
// Inverted: swipe left = next
translateX.value = withSpring(-220)
runOnJS(trigger)('notificationSuccess')
runOnJS(skip)(undefined)
}
translateX.value = withDelay(160, withSpring(0))
} else {
translateX.value = withSpring(0)
}
}),
[previous, skip, trigger, translateX, sheetDismissGesture],
)
const swipeGesture = Gesture.Pan()
.activeOffsetX([-12, 12])
// Bail on vertical intent so native sheet dismiss keeps working
.failOffsetY([-8, 8])
.simultaneousWithExternalGesture(sheetDismissGesture)
.onUpdate((e) => {
if (Math.abs(e.translationY) < 40) {
translateX.value = Math.max(-160, Math.min(160, e.translationX))
}
})
.onEnd((e) => {
const threshold = 120
const minVelocity = 600
const isHorizontal = Math.abs(e.translationY) < 40
if (
isHorizontal &&
(Math.abs(e.translationX) > threshold || Math.abs(e.velocityX) > minVelocity)
) {
if (e.translationX > 0) {
// Inverted: swipe right = previous
translateX.value = withSpring(220)
runOnJS(trigger)('notificationSuccess')
runOnJS(previous)()
} else {
// Inverted: swipe left = next
translateX.value = withSpring(-220)
runOnJS(trigger)('notificationSuccess')
runOnJS(skip)(undefined)
}
translateX.value = withDelay(160, withSpring(0))
} else {
translateX.value = withSpring(0)
}
})
/**
* Styling for the top layer of Player ZStack
*
@@ -114,81 +94,69 @@ export default function PlayerScreen(): React.JSX.Element {
*
* Apple devices get a small amount of margin
*/
const mainContainerStyle = useMemo(
() => ({
marginTop: isAndroid ? top : getTokenValue('$4'),
marginBottom: bottom + getTokenValue('$10'),
}),
[top, bottom, isAndroid],
)
const mainContainerStyle = {
marginTop: isAndroid ? top : getTokenValue('$4'),
marginBottom: bottom + getTokenValue('$12'),
}
return (
<View flex={1}>
{nowPlaying && (
<ZStack fullscreen>
<BlurredBackground width={width} height={height} />
return nowPlaying ? (
<ZStack width={width} height={height}>
<BlurredBackground width={width} height={height} />
{/* Swipe feedback icons (topmost overlay) */}
<Animated.View
pointerEvents='none'
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: 9999,
}}
>
<YStack flex={1} justifyContent='center'>
<Animated.View
style={[{ position: 'absolute', left: 12 }, leftIconStyle]}
>
<Icon name='skip-next' color='$primary' large />
</Animated.View>
<Animated.View
style={[{ position: 'absolute', right: 12 }, rightIconStyle]}
>
<Icon name='skip-previous' color='$primary' large />
</Animated.View>
</YStack>
{/* Swipe feedback icons (topmost overlay) */}
<Animated.View
pointerEvents='none'
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: 9999,
}}
>
<YStack flex={1} justifyContent='center'>
<Animated.View style={[{ position: 'absolute', left: 12 }, leftIconStyle]}>
<Icon name='skip-next' color='$primary' large />
</Animated.View>
<Animated.View style={[{ position: 'absolute', right: 12 }, rightIconStyle]}>
<Icon name='skip-previous' color='$primary' large />
</Animated.View>
</YStack>
</Animated.View>
{/* Central large swipe area overlay (captures swipe like big album art) */}
<GestureDetector
gesture={Gesture.Simultaneous(sheetDismissGesture, swipeGesture)}
>
<View
style={{
position: 'absolute',
top: height * 0.18,
left: width * 0.06,
right: width * 0.06,
height: height * 0.36,
zIndex: 9998,
}}
/>
</GestureDetector>
{/* Central large swipe area overlay (captures swipe like big album art) */}
<GestureDetector gesture={Gesture.Simultaneous(sheetDismissGesture, swipeGesture)}>
<View
style={{
position: 'absolute',
top: height * 0.18,
left: width * 0.06,
right: width * 0.06,
height: height * 0.36,
zIndex: 9998,
}}
/>
</GestureDetector>
<YStack
justifyContent='center'
flex={1}
marginHorizontal={'$5'}
{...mainContainerStyle}
>
{/* flexGrow 1 */}
<PlayerHeader />
<YStack
justifyContent='center'
flex={1}
marginHorizontal={'$5'}
{...mainContainerStyle}
>
{/* flexGrow 1 */}
<PlayerHeader />
<YStack justifyContent='flex-start' gap={'$4'} flexShrink={1}>
<SongInfo />
<Scrubber />
<Controls />
<Footer />
</YStack>
</YStack>
</ZStack>
)}
{showToast && <Toast config={JellifyToastConfig(theme)} />}
</View>
<YStack justifyContent='flex-start' gap={'$4'} flexShrink={1}>
<SongInfo />
<Scrubber />
<Controls />
<Footer />
</YStack>
</YStack>
</ZStack>
) : (
<></>
)
}

View File

@@ -6,7 +6,7 @@ import MultipleArtistsSheet from '../Context/multiple-artists'
import { PlayerParamList } from './types'
import Lyrics from '../../components/Player/components/lyrics'
export const PlayerStack = createNativeStackNavigator<PlayerParamList>()
const PlayerStack = createNativeStackNavigator<PlayerParamList>()
export default function Player(): React.JSX.Element {
return (

View File

@@ -14,6 +14,7 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import AudioSpecsSheet from './Stats'
import { useApi, useJellifyLibrary } from '../stores'
import DeletePlaylist from './Library/delete-playlist'
import { Platform } from 'react-native'
const RootStack = createNativeStackNavigator<RootStackParamList>()
@@ -38,8 +39,9 @@ export default function Root(): React.JSX.Element {
name='PlayerRoot'
component={Player}
options={{
presentation: 'formSheet',
sheetAllowedDetents: [1.0],
// Form Sheet gives swipe to dismiss for Android, but royally fucks up the display on iOS
presentation: Platform.OS === 'android' ? 'formSheet' : 'modal',
sheetAllowedDetents: Platform.OS === 'android' ? [1.0] : undefined,
headerShown: false,
}}
/>