From 3cffc623e4327a42a5c2d4a32abbbc8b20c66f2f Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sun, 27 Apr 2025 01:13:14 -0500 Subject: [PATCH] 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 --- .prettierignore | 5 +- .../Global/components/favorite-button.tsx | 3 - components/Global/components/item.tsx | 7 +- components/Global/components/track.tsx | 3 +- components/Home/helpers/frequent-tracks.tsx | 2 +- components/Home/helpers/recently-played.tsx | 2 +- .../ItemDetail/helpers/TrackOptions.tsx | 3 +- components/Player/helpers/buttons.tsx | 2 +- components/Player/helpers/controls.tsx | 13 +- components/Player/helpers/scrubber.tsx | 2 +- components/Player/mini-player.tsx | 2 +- components/Player/screens/index.tsx | 2 +- components/Player/screens/queue.tsx | 7 +- components/Tracks/screen.tsx | 4 +- components/jellify.tsx | 2 +- components/tabs.tsx | 2 +- ios/Jellify.xcodeproj/project.pbxproj | 18 +- jest/queue-provider.test.js | 5 + package.json | 241 +++++++++--------- player/{provider.tsx => player-provider.tsx} | 107 +++----- player/queue-provider.tsx | 71 ++++-- yarn.lock | 48 +++- 22 files changed, 286 insertions(+), 265 deletions(-) create mode 100644 jest/queue-provider.test.js rename player/{provider.tsx => player-provider.tsx} (68%) diff --git a/.prettierignore b/.prettierignore index 69dee407..b7e7a392 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,7 @@ node_modules/ *.yaml # Ignore Markdown files -*.md \ No newline at end of file +*.md + +# Ignore iOS Pods directory +ios/**/* \ No newline at end of file diff --git a/components/Global/components/favorite-button.tsx b/components/Global/components/favorite-button.tsx index e791abcc..81a63bd7 100644 --- a/components/Global/components/favorite-button.tsx +++ b/components/Global/components/favorite-button.tsx @@ -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(isFavoriteItem(item)) const { toggleFavorite } = useJellifyUserDataContext() diff --git a/components/Global/components/item.tsx b/components/Global/components/item.tsx index 3d85615b..42c50599 100644 --- a/components/Global/components/item.tsx +++ b/components/Global/components/item.tsx @@ -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 diff --git a/components/Global/components/track.tsx b/components/Global/components/track.tsx index c5f5aaf6..4e4f4b05 100644 --- a/components/Global/components/track.tsx +++ b/components/Global/components/track.tsx @@ -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 diff --git a/components/Home/helpers/frequent-tracks.tsx b/components/Home/helpers/frequent-tracks.tsx index 1d9dfb3f..1314a166 100644 --- a/components/Home/helpers/frequent-tracks.tsx +++ b/components/Home/helpers/frequent-tracks.tsx @@ -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, diff --git a/components/Home/helpers/recently-played.tsx b/components/Home/helpers/recently-played.tsx index 7f420cb1..c1d90b3e 100644 --- a/components/Home/helpers/recently-played.tsx +++ b/components/Home/helpers/recently-played.tsx @@ -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' diff --git a/components/ItemDetail/helpers/TrackOptions.tsx b/components/ItemDetail/helpers/TrackOptions.tsx index d9076ad9..c4241e9a 100644 --- a/components/ItemDetail/helpers/TrackOptions.tsx +++ b/components/ItemDetail/helpers/TrackOptions.tsx @@ -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} /> diff --git a/components/Player/helpers/buttons.tsx b/components/Player/helpers/buttons.tsx index 066ab8d1..4902f2eb 100644 --- a/components/Player/helpers/buttons.tsx +++ b/components/Player/helpers/buttons.tsx @@ -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({ diff --git a/components/Player/helpers/controls.tsx b/components/Player/helpers/controls.tsx index e89785a7..0c51b397 100644 --- a/components/Player/helpers/controls.tsx +++ b/components/Player/helpers/controls.tsx @@ -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 ( seekBy(-15)} + onPress={() => useSeekBy.mutate(-15)} /> skipToNext()} + onPress={() => useSkip.mutate(undefined)} large /> seekBy(15)} + onPress={() => useSeekBy.mutate(15)} /> ) diff --git a/components/Player/helpers/scrubber.tsx b/components/Player/helpers/scrubber.tsx index 9367820f..80276b96 100644 --- a/components/Player/helpers/scrubber.tsx +++ b/components/Player/helpers/scrubber.tsx @@ -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' diff --git a/components/Player/mini-player.tsx b/components/Player/mini-player.tsx index 4db3b388..b804b45d 100644 --- a/components/Player/mini-player.tsx +++ b/components/Player/mini-player.tsx @@ -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' diff --git a/components/Player/screens/index.tsx b/components/Player/screens/index.tsx index 21727221..2ad0eec6 100644 --- a/components/Player/screens/index.tsx +++ b/components/Player/screens/index.tsx @@ -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' diff --git a/components/Player/screens/queue.tsx b/components/Player/screens/queue.tsx index 7ea7a9ee..744aee67 100644 --- a/components/Player/screens/queue.tsx +++ b/components/Player/screens/queue.tsx @@ -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') diff --git a/components/Tracks/screen.tsx b/components/Tracks/screen.tsx index 05a04968..ac77bdc4 100644 --- a/components/Tracks/screen.tsx +++ b/components/Tracks/screen.tsx @@ -23,13 +23,13 @@ export default function TracksScreen({ route, navigation }: TracksProps): React. { + test('Skips to the correct track index') +}) diff --git a/package.json b/package.json index 6e3187f9..225f1eae 100644 --- a/package.json +++ b/package.json @@ -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" - } -} \ No newline at end of file + "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" + } +} diff --git a/player/provider.tsx b/player/player-provider.tsx similarity index 68% rename from player/provider.tsx rename to player/player-provider.tsx index ad25873d..eb9591d3 100644 --- a/player/provider.tsx +++ b/player/player-provider.tsx @@ -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> nowPlaying: JellifyTrack | undefined useStartPlayback: UseMutationResult useTogglePlayback: UseMutationResult useSeekTo: UseMutationResult + useSeekBy: UseMutationResult } const PlayerContextInitializer = () => { @@ -37,9 +33,6 @@ const PlayerContextInitializer = () => { const playStateApi = getPlaystateApi(Client.api!) //#region State - const [initialized, setInitialized] = useState(false) - - const [nowPlayingIsFavorite, setNowPlayingIsFavorite] = useState(false) const [nowPlaying, setNowPlaying] = useState( 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({ - initialized: false, - nowPlayingIsFavorite: false, - setNowPlayingIsFavorite: () => {}, nowPlaying: undefined, useStartPlayback: { mutate: () => {}, @@ -267,6 +210,24 @@ export const PlayerContext = createContext({ 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 diff --git a/player/queue-provider.tsx b/player/queue-provider.tsx index eff0e52d..cfded22d 100644 --- a/player/queue-provider.tsx +++ b/player/queue-provider.tsx @@ -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({ diff --git a/yarn.lock b/yarn.lock index cee04f00..eb2bea24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==