Fix Android? Queue and Player Control Improvements (#293)

I hope this fixes android - restored a dependency that wasnt present in previous versions (.5, .6, .7)

Tightening up player refactor, namely around indexes and skipping tracks
This commit is contained in:
Violet Caulfield
2025-04-27 01:13:14 -05:00
committed by GitHub
parent 9840d4d9fa
commit 3cffc623e4
22 changed files with 286 additions and 265 deletions

View File

@@ -5,4 +5,7 @@ node_modules/
*.yaml
# Ignore Markdown files
*.md
*.md
# Ignore iOS Pods directory
ios/**/*

View File

@@ -4,7 +4,6 @@ import Icon from '../helpers/icon'
import { useQuery } from '@tanstack/react-query'
import { isUndefined } from 'lodash'
import { getTokens, Spinner } from 'tamagui'
import { usePlayerContext } from '../../..//player/provider'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchUserData } from '../../../api/queries/functions/favorites'
import { useJellifyUserDataContext } from '../../../components/user-data-provider'
@@ -20,8 +19,6 @@ export default function FavoriteButton({
item: BaseItemDto
onToggle?: () => void
}): React.JSX.Element {
usePlayerContext()
const [isFavorite, setFavorite] = useState<boolean>(isFavoriteItem(item))
const { toggleFavorite } = useJellifyUserDataContext()

View File

@@ -9,8 +9,7 @@ import Icon from '../helpers/icon'
import { QueuingType } from '../../../enums/queuing-type'
import { RunTimeTicks } from '../helpers/time-codes'
import { useQueueContext } from '../../../player/queue-provider'
import { usePlayerContext } from '../../../player/provider'
import { State } from 'react-native-track-player'
import { usePlayerContext } from '../../../player/player-provider'
export default function Item({
item,
@@ -60,9 +59,7 @@ export default function Item({
queuingType: QueuingType.FromSelection,
},
{
onSuccess: () => {
useStartPlayback.mutate()
},
onSuccess: () => useStartPlayback.mutate(),
},
)
break

View File

@@ -1,4 +1,4 @@
import { usePlayerContext } from '../../../player/provider'
import { usePlayerContext } from '../../../player/player-provider'
import React from 'react'
import { getToken, getTokens, Theme, useTheme, XStack, YStack } from 'tamagui'
import { Text } from '../helpers/text'
@@ -19,7 +19,6 @@ import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchMediaInfo } from '../../../api/queries/functions/media'
import { useQueueContext } from '../../../player/queue-provider'
import { State } from 'react-native-track-player'
interface TrackProps {
track: BaseItemDto

View File

@@ -9,7 +9,7 @@ import { trigger } from 'react-native-haptic-feedback'
import { H2 } from '../../../components/Global/helpers/text'
import Icon from '../../../components/Global/helpers/icon'
import { useQueueContext } from '../../../player/queue-provider'
import { usePlayerContext } from '../../../player/provider'
import { usePlayerContext } from '../../../player/player-provider'
export default function FrequentlyPlayedTracks({
navigation,

View File

@@ -3,7 +3,7 @@ import { View, XStack } from 'tamagui'
import { useHomeContext } from '../provider'
import { H2 } from '../../Global/helpers/text'
import { ItemCard } from '../../Global/components/item-card'
import { usePlayerContext } from '../../../player/provider'
import { usePlayerContext } from '../../../player/player-provider'
import { StackParamList } from '../../../components/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { trigger } from 'react-native-haptic-feedback'

View File

@@ -173,7 +173,8 @@ export default function TrackOptions({
name={isDownloaded ? 'delete' : 'download'}
title={isDownloaded ? 'Remove Download' : 'Download'}
onPress={() => {
(isDownloaded ? useRemoveDownload : useDownload).mutate(track)
if (isDownloaded) useRemoveDownload.mutate(track)
else useDownload.mutate(track)
}}
size={width / 6}
/>

View File

@@ -1,7 +1,7 @@
import { State } from 'react-native-track-player'
import { Colors } from 'react-native/Libraries/NewAppScreen'
import { Circle, Spinner, View } from 'tamagui'
import { usePlayerContext } from '../../../player/provider'
import { usePlayerContext } from '../../../player/player-provider'
import IconButton from '../../../components/Global/helpers/icon-button'
export default function PlayPauseButton({

View File

@@ -2,24 +2,23 @@ import React from 'react'
import { XStack, getToken } from 'tamagui'
import PlayPauseButton from './buttons'
import Icon from '../../../components/Global/helpers/icon'
import { getProgress, seekBy, skipToNext } from 'react-native-track-player/lib/src/trackPlayer'
import { usePlayerContext } from '../../../player/provider'
import { usePlayerContext } from '../../../player/player-provider'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { useQueueContext } from '../../../player/queue-provider'
export default function Controls(): React.JSX.Element {
const { width } = useSafeAreaFrame()
const { useSeekTo } = usePlayerContext()
const { useSeekBy } = usePlayerContext()
const { usePrevious } = useQueueContext()
const { usePrevious, useSkip } = useQueueContext()
return (
<XStack alignItems='center' justifyContent='space-evenly' marginVertical={'$2'}>
<Icon
color={getToken('$color.amethyst')}
name='rewind-15'
onPress={() => seekBy(-15)}
onPress={() => useSeekBy.mutate(-15)}
/>
<Icon
@@ -35,14 +34,14 @@ export default function Controls(): React.JSX.Element {
<Icon
color={getToken('$color.amethyst')}
name='skip-next'
onPress={() => skipToNext()}
onPress={() => useSkip.mutate(undefined)}
large
/>
<Icon
color={getToken('$color.amethyst')}
name='fast-forward-15'
onPress={() => seekBy(15)}
onPress={() => useSeekBy.mutate(15)}
/>
</XStack>
)

View File

@@ -5,7 +5,7 @@ import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import { trigger } from 'react-native-haptic-feedback'
import { XStack, YStack } from 'tamagui'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { usePlayerContext } from '../../../player/provider'
import { usePlayerContext } from '../../../player/player-provider'
import { RunTimeSeconds } from '../../../components/Global/helpers/time-codes'
import { UPDATE_INTERVAL } from '../../../player/config'
import { ProgressMultiplier } from '../component.config'

View File

@@ -1,6 +1,6 @@
import React from 'react'
import { getToken, getTokens, useTheme, View, XStack, YStack } from 'tamagui'
import { usePlayerContext } from '../../player/provider'
import { usePlayerContext } from '../../player/player-provider'
import { BottomTabNavigationEventMap } from '@react-navigation/bottom-tabs'
import { NavigationHelpers, ParamListBase } from '@react-navigation/native'
import Icon from '../Global/helpers/icon'

View File

@@ -1,5 +1,5 @@
import { StackParamList } from '../../../components/types'
import { usePlayerContext } from '../../../player/provider'
import { usePlayerContext } from '../../../player/player-provider'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import React, { useMemo } from 'react'
import { SafeAreaView, useSafeAreaFrame } from 'react-native-safe-area-context'

View File

@@ -1,7 +1,7 @@
import Icon from '../../../components/Global/helpers/icon'
import Track from '../../../components/Global/components/track'
import { StackParamList } from '../../../components/types'
import { usePlayerContext } from '../../../player/provider'
import { usePlayerContext } from '../../../player/player-provider'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import DraggableFlatList from 'react-native-draggable-flatlist'
@@ -9,6 +9,7 @@ import { trigger } from 'react-native-haptic-feedback'
import { Separator } from 'tamagui'
import { useQueueContext } from '../../../player/queue-provider'
import Animated from 'react-native-reanimated'
import { isUndefined } from 'lodash'
export default function Queue({
navigation,
@@ -77,7 +78,9 @@ export default function Queue({
index={getIndex() ?? 0}
showArtwork
onPress={() => {
useSkip.mutate(getIndex() ?? 0)
const index = getIndex()
console.debug(`Skip triggered on index ${index}`)
useSkip.mutate(index)
}}
onLongPress={() => {
trigger('impactLight')

View File

@@ -23,13 +23,13 @@ export default function TracksScreen({ route, navigation }: TracksProps): React.
<Track
navigation={navigation}
showArtwork
index={index}
index={0}
track={track}
tracklist={
route.params.tracks
? route.params.tracks.slice(index, index + 50)
: favoriteTracks
? favoriteTracks
? favoriteTracks.slice(index, index + 50)
: []
}
queue={route.params.queue}

View File

@@ -3,7 +3,7 @@ import React from 'react'
import Navigation from './navigation'
import Login from './Login/component'
import { JellyfinAuthenticationProvider } from './Login/provider'
import { PlayerProvider } from '../player/provider'
import { PlayerProvider } from '../player/player-provider'
import { useColorScheme } from 'react-native'
import { PortalProvider } from '@tamagui/portal'
import { JellifyProvider, useJellifyContext } from './provider'

View File

@@ -7,7 +7,7 @@ import Settings from './Settings/stack'
import { Discover } from './Discover/stack'
import { Miniplayer } from './Player/mini-player'
import { getToken, getTokens, Separator } from 'tamagui'
import { usePlayerContext } from '../player/provider'
import { usePlayerContext } from '../player/player-provider'
import SearchStack from './Search/stack'
import LibraryStack from './Library/stack'
import { useColorScheme, View } from 'react-native'

View File

@@ -519,10 +519,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n";
@@ -536,10 +540,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n";
@@ -839,10 +847,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -928,10 +933,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;

View File

@@ -0,0 +1,5 @@
import { QueueContext, QueueProvider } from '../player/queue-provider'
describe(QueueProvider.name, () => {
test('Skips to the correct track index')
})

View File

@@ -1,121 +1,122 @@
{
"name": "jellify",
"version": "0.11.7",
"private": true,
"scripts": {
"init:ios": "yarn install && yarn pod:install",
"reinstall": "rm -rf ./node_modules && yarn install",
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
"clean:ios": "cd ios && pod deintegrate",
"clean:android": "cd android && rm -rf app/ build/",
"pod:install": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=0 bundle exec pod install",
"pod:install-new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
"fastlane:ios:match": "cd ios && bundle exec fastlane match development",
"fastlane:ios:beta": "cd ios && bundle exec fastlane beta",
"fastlane:android:build": "cd android && bundle install && bundle exec fastlane build",
"androidBuild": "cd android && ./gradlew clean && ./gradlew assembleRelease && cd .. && echo 'find apk in android/app/build/outputs/apk/release'",
"prepare": "husky",
"format:check": "prettier --check .",
"format": "prettier --write .",
"postinstall": "patch-package"
},
"dependencies": {
"@jellyfin/sdk": "^0.11.0",
"@react-native-community/blur": "^4.4.1",
"@react-native-community/cli": "^15.1.3",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/material-top-tabs": "^7.2.10",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"@react-navigation/stack": "^7.2.10",
"@tamagui/config": "^1.126.1",
"@tamagui/toast": "^1.126.1",
"@tanstack/query-sync-storage-persister": "^5.74.6",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-persist-client": "^5.74.6",
"axios": "^1.8.4",
"bundle": "^2.1.0",
"burnt": "^0.13.0",
"expo": "^52.0.46",
"expo-image": "^2.0.7",
"gem": "^2.4.3",
"invert-color": "^2.0.0",
"jest-expo": "^52.0.6",
"lodash": "^4.17.21",
"patch-package": "^8.0.0",
"react": "18.3.1",
"react-freeze": "^1.0.4",
"react-native": "0.77.0",
"react-native-background-actions": "^4.0.1",
"react-native-blurhash": "^2.1.1",
"react-native-boost": "^0.5.6",
"react-native-carplay": "^2.4.1-beta.0",
"react-native-device-info": "^14.0.4",
"react-native-draggable-flatlist": "^4.0.2",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.25.0",
"react-native-haptic-feedback": "^2.3.3",
"react-native-mmkv": "^2.12.2",
"react-native-pager-view": "^6.7.1",
"react-native-reanimated": "^3.17.5",
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "^4.10.0",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.14.0",
"react-native-track-player": "^4.1.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.3",
"react-native-vector-icons": "^10.2.0",
"ruby": "^0.6.1",
"tamagui": "^1.126.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli-platform-android": "15.1.3",
"@react-native-community/cli-platform-ios": "15.1.3",
"@react-native/babel-preset": "0.77.0",
"@react-native/eslint-config": "0.77.0",
"@react-native/metro-config": "0.77.0",
"@react-native/typescript-config": "0.77.0",
"@types/jest": "^29.5.13",
"@types/lodash": "^4.17.10",
"@types/react": "^18.2.6",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"babel-plugin-module-resolver": "^5.0.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-native": "^5.0.0",
"husky": "^9.1.7",
"jest": "^29.6.3",
"jscodeshift": "^0.15.2",
"lint-staged": "^15.5.0",
"prettier": "^2.8.8",
"react-native-cli-bump-version": "^1.5.1",
"react-test-renderer": "18.3.1",
"typescript": "5.7.3"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix"
]
},
"engines": {
"node": ">=18"
}
}
"name": "jellify",
"version": "0.11.7",
"private": true,
"scripts": {
"init:ios": "yarn install && yarn pod:install",
"reinstall": "rm -rf ./node_modules && yarn install",
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
"clean:ios": "cd ios && pod deintegrate",
"clean:android": "cd android && rm -rf app/ build/",
"pod:install": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=0 bundle exec pod install",
"pod:install-new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
"fastlane:ios:match": "cd ios && bundle exec fastlane match development",
"fastlane:ios:beta": "cd ios && bundle exec fastlane beta",
"fastlane:android:build": "cd android && bundle install && bundle exec fastlane build",
"androidBuild": "cd android && ./gradlew clean && ./gradlew assembleRelease && cd .. && echo 'find apk in android/app/build/outputs/apk/release'",
"prepare": "husky",
"format:check": "prettier --check .",
"format": "prettier --write .",
"postinstall": "patch-package"
},
"dependencies": {
"@jellyfin/sdk": "^0.11.0",
"@react-native-community/blur": "^4.4.1",
"@react-native-community/cli": "^15.1.3",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/material-top-tabs": "^7.2.10",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"@react-navigation/stack": "^7.2.10",
"@tamagui/config": "^1.126.1",
"@tamagui/toast": "^1.126.1",
"@tanstack/query-sync-storage-persister": "^5.74.6",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-persist-client": "^5.74.6",
"axios": "^1.8.4",
"bundle": "^2.1.0",
"burnt": "^0.13.0",
"expo": "^52.0.46",
"expo-image": "^2.0.7",
"gem": "^2.4.3",
"invert-color": "^2.0.0",
"jest-expo": "^52.0.6",
"lodash": "^4.17.21",
"npm-bundle": "^3.0.3",
"patch-package": "^8.0.0",
"react": "18.3.1",
"react-freeze": "^1.0.4",
"react-native": "0.77.0",
"react-native-background-actions": "^4.0.1",
"react-native-blurhash": "^2.1.1",
"react-native-boost": "^0.5.6",
"react-native-carplay": "^2.4.1-beta.0",
"react-native-device-info": "^14.0.4",
"react-native-draggable-flatlist": "^4.0.2",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.25.0",
"react-native-haptic-feedback": "^2.3.3",
"react-native-mmkv": "^2.12.2",
"react-native-pager-view": "^6.7.1",
"react-native-reanimated": "^3.17.5",
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "^4.10.0",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.14.0",
"react-native-track-player": "^4.1.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.3",
"react-native-vector-icons": "^10.2.0",
"ruby": "^0.6.1",
"tamagui": "^1.126.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli-platform-android": "15.1.3",
"@react-native-community/cli-platform-ios": "15.1.3",
"@react-native/babel-preset": "0.77.0",
"@react-native/eslint-config": "0.77.0",
"@react-native/metro-config": "0.77.0",
"@react-native/typescript-config": "0.77.0",
"@types/jest": "^29.5.13",
"@types/lodash": "^4.17.10",
"@types/react": "^18.2.6",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"babel-plugin-module-resolver": "^5.0.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-native": "^5.0.0",
"husky": "^9.1.7",
"jest": "^29.6.3",
"jscodeshift": "^0.15.2",
"lint-staged": "^15.5.0",
"prettier": "^2.8.8",
"react-native-cli-bump-version": "^1.5.1",
"react-test-renderer": "18.3.1",
"typescript": "5.7.3"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix"
]
},
"engines": {
"node": ">=18"
}
}

View File

@@ -8,12 +8,10 @@ import TrackPlayer, {
usePlaybackState,
useTrackPlayerEvents,
} from 'react-native-track-player'
import { isEqual, isUndefined } from 'lodash'
import { handlePlaybackProgressUpdated, handlePlaybackState } from './handlers'
import { useUpdateOptions } from '../player/hooks'
import { useMutation, UseMutationResult } from '@tanstack/react-query'
import { trigger } from 'react-native-haptic-feedback'
import { pause, seekTo } from 'react-native-track-player/lib/src/trackPlayer'
import { pause, play, seekBy, seekTo } from 'react-native-track-player/lib/src/trackPlayer'
import { convertRunTimeTicksToSeconds } from '../helpers/runtimeticks'
import Client from '../api/client'
@@ -22,13 +20,11 @@ import { useNetworkContext } from '../components/Network/provider'
import { useQueueContext } from './queue-provider'
interface PlayerContext {
initialized: boolean
nowPlayingIsFavorite: boolean
setNowPlayingIsFavorite: React.Dispatch<SetStateAction<boolean>>
nowPlaying: JellifyTrack | undefined
useStartPlayback: UseMutationResult<void, Error, void, unknown>
useTogglePlayback: UseMutationResult<void, Error, void, unknown>
useSeekTo: UseMutationResult<void, Error, number, unknown>
useSeekBy: UseMutationResult<void, Error, number, unknown>
}
const PlayerContextInitializer = () => {
@@ -37,9 +33,6 @@ const PlayerContextInitializer = () => {
const playStateApi = getPlaystateApi(Client.api!)
//#region State
const [initialized, setInitialized] = useState<boolean>(false)
const [nowPlayingIsFavorite, setNowPlayingIsFavorite] = useState<boolean>(false)
const [nowPlaying, setNowPlaying] = useState<JellifyTrack | undefined>(
nowPlayingJson ? JSON.parse(nowPlayingJson) : undefined,
)
@@ -48,13 +41,6 @@ const PlayerContextInitializer = () => {
//#endregion State
//#region Functions
const play = async () => {
await TrackPlayer.play()
}
//#endregion Functions
//#region Hooks
const useStartPlayback = useMutation({
mutationFn: play,
@@ -80,6 +66,14 @@ const PlayerContextInitializer = () => {
})
},
})
const useSeekBy = useMutation({
mutationFn: async (seekSeconds: number) => {
trigger('clockTick')
await seekBy(seekSeconds)
},
})
//#endregion
//#region RNTP Setup
@@ -88,22 +82,14 @@ const PlayerContextInitializer = () => {
const { useDownload, downloadedTracks, networkStatus } = useNetworkContext()
useTrackPlayerEvents(
[
Event.RemoteLike,
Event.RemoteDislike,
Event.PlaybackProgressUpdated,
Event.PlaybackState,
Event.PlaybackActiveTrackChanged,
],
[Event.RemoteLike, Event.RemoteDislike, Event.PlaybackProgressUpdated, Event.PlaybackState],
async (event) => {
switch (event.type) {
case Event.RemoteLike: {
setNowPlayingIsFavorite(true)
break
}
case Event.RemoteDislike: {
setNowPlayingIsFavorite(false)
break
}
@@ -135,36 +121,6 @@ const PlayerContextInitializer = () => {
break
}
case Event.PlaybackActiveTrackChanged: {
if (initialized) {
const activeTrack = (await TrackPlayer.getActiveTrack()) as
| JellifyTrack
| undefined
if (activeTrack && !isEqual(activeTrack, nowPlaying)) {
setNowPlaying(activeTrack)
// Set player favorite state to user data IsFavorite
// This is super nullish so we need to do a lot of
// checks on the fields
// TODO: Turn this check into a helper function
setNowPlayingIsFavorite(
isUndefined(activeTrack)
? false
: isUndefined(activeTrack!.item.UserData)
? false
: activeTrack.item.UserData.IsFavorite ?? false,
)
await useUpdateOptions(nowPlayingIsFavorite)
} else if (!activeTrack) {
setNowPlaying(undefined)
setNowPlayingIsFavorite(false)
} else {
// Do nothing
}
}
}
}
},
)
@@ -174,34 +130,24 @@ const PlayerContextInitializer = () => {
//#region useEffects
useEffect(() => {
if (initialized && nowPlaying)
storage.set(MMKVStorageKeys.NowPlaying, JSON.stringify(nowPlaying))
if (nowPlaying) storage.set(MMKVStorageKeys.NowPlaying, JSON.stringify(nowPlaying))
}, [nowPlaying])
useEffect(() => {
if (!initialized && playQueue.length > 0 && nowPlaying) {
TrackPlayer.skip(playQueue.findIndex((track) => track.item.Id! === nowPlaying.item.Id!))
}
setInitialized(true)
}, [playQueue, nowPlaying])
useEffect(() => {
if (currentIndex > -1 && playQueue.length > currentIndex)
if (currentIndex > -1 && playQueue.length > currentIndex) {
console.debug(`Setting now playing to queue index ${currentIndex}`)
setNowPlaying(playQueue[currentIndex])
setNowPlaying(playQueue[currentIndex])
}
}, [currentIndex])
//#endregion useEffects
//#region return
return {
initialized,
nowPlayingIsFavorite,
setNowPlayingIsFavorite,
nowPlaying,
useStartPlayback,
useTogglePlayback,
useSeekTo,
useSeekBy,
playbackState,
}
//#endregion return
@@ -209,9 +155,6 @@ const PlayerContextInitializer = () => {
//#region Create PlayerContext
export const PlayerContext = createContext<PlayerContext>({
initialized: false,
nowPlayingIsFavorite: false,
setNowPlayingIsFavorite: () => {},
nowPlaying: undefined,
useStartPlayback: {
mutate: () => {},
@@ -267,6 +210,24 @@ export const PlayerContext = createContext<PlayerContext>({
failureReason: null,
submittedAt: 0,
},
useSeekBy: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
})
//#endregion Create PlayerContext

View File

@@ -11,14 +11,15 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { mapDtoToTrack } from '../helpers/mappings'
import { useNetworkContext } from '../components/Network/provider'
import { QueuingType } from '../enums/queuing-type'
import TrackPlayer from 'react-native-track-player'
import TrackPlayer, { Event, useTrackPlayerEvents } from 'react-native-track-player'
import { findPlayQueueIndexStart } from './helpers'
import { getQueue, seekTo } from 'react-native-track-player/lib/src/trackPlayer'
import { getQueue, play, seekTo } from 'react-native-track-player/lib/src/trackPlayer'
import { trigger } from 'react-native-haptic-feedback'
import * as Burnt from 'burnt'
import { markItemPlayed } from '../api/mutations/functions/item'
import { filterTracksOnNetworkStatus } from './helpers/queue'
import { SKIP_TO_PREVIOUS_THRESHOLD } from './config'
import { isNull, isUndefined } from 'lodash'
interface QueueContext {
queueRef: Queue
@@ -50,6 +51,13 @@ const QueueContextInitailizer = () => {
const { downloadedTracks, networkStatus } = useNetworkContext()
useTrackPlayerEvents([Event.PlaybackActiveTrackChanged], ({ lastIndex, index }) => {
const lastTrackFinished =
!isUndefined(index) && !isUndefined(lastIndex) && index - lastIndex === 1
if (lastTrackFinished) setCurrentIndex(currentIndex + 1)
})
//#region Functions
const fetchQueueSectionData: () => Section[] = () => {
return Object.keys(QueuingType).map((type) => {
@@ -97,6 +105,13 @@ const QueueContextInitailizer = () => {
) => {
console.debug(`Queuing ${audioItems.length} items`)
/**
* If the start index matches the current index,
* then our useEffect won't fire - this ensures
* it does
*/
setCurrentIndex(-1)
const availableAudioItems = filterTracksOnNetworkStatus(
networkStatus,
audioItems,
@@ -118,6 +133,8 @@ const QueueContextInitailizer = () => {
setCurrentIndex(startIndex)
console.debug(`Queued ${queue.length} tracks, starting at ${startIndex}`)
await play()
}
const playNextInQueue = async (item: BaseItemDto) => {
@@ -146,6 +163,31 @@ const QueueContextInitailizer = () => {
console.debug(`Queue has ${playQueue.length} tracks`)
}
const previous = async () => {
trigger('impactMedium')
const { position } = await TrackPlayer.getProgress()
console.debug(`Skip to previous triggered. Index is ${currentIndex}`)
if (currentIndex > 0 && position < SKIP_TO_PREVIOUS_THRESHOLD) {
setCurrentIndex(currentIndex - 1)
} else await seekTo(0)
}
const skip = async (index?: number | undefined) => {
trigger('impactMedium')
console.debug(
`Skip to next triggered. Index is ${`using ${
!isUndefined(index) ? index : currentIndex
} as index ${!isUndefined(index) ? 'since it was provided' : ''}`}`,
)
if (isUndefined(index)) setCurrentIndex(currentIndex + 1)
else if (playQueue.length > index) setCurrentIndex(index)
}
//#endregion Functions
//#region Hooks
@@ -215,32 +257,11 @@ const QueueContextInitailizer = () => {
})
const useSkip = useMutation({
mutationFn: async (index?: number | undefined) => {
trigger('impactMedium')
console.debug(
`Skip to next triggered. Index is ${`using ${
index ? index : currentIndex
} as index ${index ? 'since it was provided' : ''}`}`,
)
if (index && index < playQueue.length - 1) setCurrentIndex(index)
else if (playQueue.length - 1 > currentIndex) setCurrentIndex(currentIndex + 1)
},
mutationFn: skip,
})
const usePrevious = useMutation({
mutationFn: async () => {
trigger('impactMedium')
const { position } = await TrackPlayer.getProgress()
console.debug(`Skip to previous triggered. Index is ${currentIndex}`)
if (currentIndex > 0 && position < SKIP_TO_PREVIOUS_THRESHOLD) {
setCurrentIndex(currentIndex - 1)
} else await seekTo(0)
},
mutationFn: previous,
})
const useUpdateRntpQueue = useMutation({

View File

@@ -6539,6 +6539,17 @@ glob@^10.2.2, glob@^10.3.10, glob@^10.4.2:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
glob@^6.0.1:
version "6.0.4"
resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==
dependencies:
inflight "^1.0.4"
inherits "2"
minimatch "2 || 3"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.1.1, glob@^7.1.3, glob@^7.1.4:
version "7.2.3"
resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz"
@@ -6835,6 +6846,11 @@ ini@~1.3.0:
resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
insync@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/insync/-/insync-2.1.1.tgz#22e26c61121303c06f51d35a3ccf6d8fc1e914c4"
integrity sha512-UzUhOZFpCMM22Xlig9iUPqalf8n7c4eYScamce1C+jN3ad8FtmVm42ryMwVq0hAxHbwUhWFhPvTFQQpFdDUKkw==
internal-ip@^4.3.0:
version "4.3.0"
resolved "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz"
@@ -8462,6 +8478,13 @@ mimic-function@^5.0.0:
resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz"
integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==
"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimatch@^10.0.1:
version "10.0.1"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz"
@@ -8469,13 +8492,6 @@ minimatch@^10.0.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimatch@^8.0.2:
version "8.0.4"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz"
@@ -8594,6 +8610,11 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
ncp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
@@ -8663,6 +8684,17 @@ normalize-path@^3.0.0:
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
npm-bundle@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/npm-bundle/-/npm-bundle-3.0.3.tgz#464e613f21f14e4918ed93e810b5a9cdeb35acc1"
integrity sha512-fHF7FR32YNgjqi0MQMLnE78Ff9/wYd4/7/Cke3dLLi2QzETKotIiWGCxwDoXAZDWVoTuVRYQa2ZdiZPuBL7QnA==
dependencies:
glob "^6.0.1"
insync "^2.1.1"
mkdirp "^0.5.1"
ncp "^2.0.0"
rimraf "^2.4.4"
npm-package-arg@^11.0.0:
version "11.0.3"
resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz"
@@ -9879,7 +9911,7 @@ rfdc@^1.4.1:
resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz"
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
rimraf@^2.6.3:
rimraf@^2.4.4, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==