mirror of
https://github.com/Jellify-Music/App.git
synced 2026-02-21 19:28:59 -06:00
Fix Add to Queue Issues (#457)
* fix issue where items added to queue weren't always queued * On Repeat and Recently Played Sections in CarPlay now load tracks into the queue and start playback
This commit is contained in:
@@ -32,113 +32,129 @@ describe('Queue Index Util', () => {
|
||||
|
||||
describe('findPlayQueueIndexStart', () => {
|
||||
it('should return the index of the first track that is not from selection', async () => {
|
||||
const result = await findPlayQueueIndexStart([
|
||||
{
|
||||
id: '1',
|
||||
index: 0,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '1' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
index: 1,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '2' },
|
||||
QueuingType: QueuingType.PlayingNext,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
index: 2,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '3' },
|
||||
QueuingType: QueuingType.DirectlyQueued,
|
||||
},
|
||||
])
|
||||
const result = await findPlayQueueIndexStart(
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
index: 0,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '1' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
index: 1,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '2' },
|
||||
QueuingType: QueuingType.PlayingNext,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
index: 2,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '3' },
|
||||
QueuingType: QueuingType.DirectlyQueued,
|
||||
},
|
||||
],
|
||||
0,
|
||||
)
|
||||
|
||||
expect(result).toBe(3)
|
||||
})
|
||||
|
||||
it('should return the index of the first track that is not from selection and after other queued tracks', async () => {
|
||||
const result = await findPlayQueueIndexStart([
|
||||
{
|
||||
id: '1',
|
||||
index: 0,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '1' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
index: 1,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '2' },
|
||||
QueuingType: QueuingType.PlayingNext,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
index: 2,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '3' },
|
||||
QueuingType: QueuingType.DirectlyQueued,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
index: 3,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '4' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
index: 4,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '5' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
])
|
||||
const result = await findPlayQueueIndexStart(
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
index: 0,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '1' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
index: 1,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '2' },
|
||||
QueuingType: QueuingType.PlayingNext,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
index: 2,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '3' },
|
||||
QueuingType: QueuingType.DirectlyQueued,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
index: 3,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '4' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
index: 4,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '5' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
],
|
||||
0,
|
||||
)
|
||||
|
||||
expect(result).toBe(3)
|
||||
})
|
||||
|
||||
it('should add in relation to the active track if shuffled, but respect queue priority', async () => {
|
||||
const result = await findPlayQueueIndexStart([
|
||||
{
|
||||
id: '2',
|
||||
index: 0,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '2' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
index: 1,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '1' },
|
||||
QueuingType: QueuingType.PlayingNext,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
index: 2,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '3' },
|
||||
QueuingType: QueuingType.DirectlyQueued,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
index: 3,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '5' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
index: 4,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '4' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
])
|
||||
const result = await findPlayQueueIndexStart(
|
||||
[
|
||||
{
|
||||
id: '2',
|
||||
index: 0,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '2' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
index: 1,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '1' },
|
||||
QueuingType: QueuingType.PlayingNext,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
index: 2,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '3' },
|
||||
QueuingType: QueuingType.DirectlyQueued,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
index: 3,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '5' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
index: 4,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '4' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
index: 5,
|
||||
url: 'https://example.com',
|
||||
item: { Id: '6' },
|
||||
QueuingType: QueuingType.FromSelection,
|
||||
},
|
||||
],
|
||||
0,
|
||||
)
|
||||
|
||||
expect(result).toBe(3)
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"@react-navigation/native-stack": "^7.3.21",
|
||||
"@sentry/react-native": "^6.17.0",
|
||||
"@shopify/flash-list": "^2.0.0-rc.12",
|
||||
"@tamagui/config": "^1.132.11",
|
||||
"@tamagui/config": "^1.132.12",
|
||||
"@tanstack/query-sync-storage-persister": "^5.83.0",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query-persist-client": "^5.83.0",
|
||||
@@ -89,7 +89,7 @@
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-vector-icons": "^10.2.0",
|
||||
"ruby": "^0.6.1",
|
||||
"tamagui": "^1.132.11"
|
||||
"tamagui": "^1.132.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.0",
|
||||
|
||||
@@ -5,11 +5,11 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import TracksTemplate from './Tracks'
|
||||
import ArtistsTemplate from './Artists'
|
||||
import uuid from 'react-native-uuid'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { JellifyUser } from '../../types/JellifyUser'
|
||||
import { InfiniteData } from '@tanstack/react-query'
|
||||
import { QueueMutation } from '../../providers/Player/interfaces'
|
||||
|
||||
const CarPlayHome = (api: Api, user: JellifyUser, sessionId: string) =>
|
||||
const CarPlayHome = (user: JellifyUser, loadQueue: (mutation: QueueMutation) => void) =>
|
||||
new ListTemplate({
|
||||
id: uuid.v4(),
|
||||
title: 'Home',
|
||||
@@ -54,7 +54,7 @@ const CarPlayHome = (api: Api, user: JellifyUser, sessionId: string) =>
|
||||
QueryKeys.RecentlyPlayed,
|
||||
]) ?? { pages: [], pageParams: [] }
|
||||
CarPlay.pushTemplate(
|
||||
TracksTemplate(api, sessionId, items.pages.flat(), 'Recently Played'),
|
||||
TracksTemplate(items.pages.flat(), loadQueue, 'Recently Played'),
|
||||
)
|
||||
break
|
||||
}
|
||||
@@ -73,9 +73,7 @@ const CarPlayHome = (api: Api, user: JellifyUser, sessionId: string) =>
|
||||
const items = queryClient.getQueryData<InfiniteData<BaseItemDto[], unknown>>([
|
||||
QueryKeys.FrequentlyPlayed,
|
||||
]) ?? { pages: [], pageParams: [] }
|
||||
CarPlay.pushTemplate(
|
||||
TracksTemplate(api, sessionId, items.pages.flat(), 'On Repeat'),
|
||||
)
|
||||
CarPlay.pushTemplate(TracksTemplate(items.pages.flat(), loadQueue, 'On Repeat'))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { TabBarTemplate } from 'react-native-carplay'
|
||||
import CarPlayHome from './Home'
|
||||
import CarPlayDiscover from './Discover'
|
||||
import uuid from 'react-native-uuid'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { JellifyUser } from '../../types/JellifyUser'
|
||||
import { QueueMutation } from '../../providers/Player/interfaces'
|
||||
|
||||
const CarPlayNavigation = (api: Api, user: JellifyUser, sessionId: string) =>
|
||||
const CarPlayNavigation = (user: JellifyUser, loadQueue: (mutation: QueueMutation) => void) =>
|
||||
new TabBarTemplate({
|
||||
id: uuid.v4(),
|
||||
title: 'Tabs',
|
||||
templates: [CarPlayHome(api, user, sessionId), CarPlayDiscover],
|
||||
templates: [CarPlayHome(user, loadQueue), CarPlayDiscover],
|
||||
onTemplateSelect(template, e) {},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { CarPlay, ListTemplate } from 'react-native-carplay'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import uuid from 'react-native-uuid'
|
||||
import CarPlayNowPlaying from './NowPlaying'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import React from 'react'
|
||||
import { QueueContext } from '../../providers/Player/queue'
|
||||
import { Queue } from '../../player/types/queue-item'
|
||||
import { QueueMutation } from '../../providers/Player/interfaces'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
|
||||
const TracksTemplate = (api: Api, sessionId: string, items: BaseItemDto[], queuingRef: Queue) =>
|
||||
const TracksTemplate = (
|
||||
items: BaseItemDto[],
|
||||
loadQueue: (mutation: QueueMutation) => void,
|
||||
queuingRef: Queue,
|
||||
) =>
|
||||
new ListTemplate({
|
||||
id: uuid.v4(),
|
||||
sections: [
|
||||
@@ -24,12 +24,16 @@ const TracksTemplate = (api: Api, sessionId: string, items: BaseItemDto[], queui
|
||||
}) ?? [],
|
||||
},
|
||||
],
|
||||
onItemSelect: async (item) => {
|
||||
const { loadQueue } = React.useContext(QueueContext)
|
||||
|
||||
console.debug(`loadQueue ${loadQueue}`)
|
||||
|
||||
await loadQueue(items, queuingRef, 0)
|
||||
onItemSelect: async ({ index }) => {
|
||||
loadQueue({
|
||||
queuingType: QueuingType.FromSelection,
|
||||
index,
|
||||
tracklist: items,
|
||||
queue: queuingRef,
|
||||
shuffled: false,
|
||||
track: items[index],
|
||||
startPlayback: true,
|
||||
})
|
||||
|
||||
CarPlay.pushTemplate(CarPlayNowPlaying)
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Theme, useTheme } from 'tamagui'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import JellifyToastConfig from '../constants/toast.config'
|
||||
import { useColorScheme } from 'react-native'
|
||||
import { CarPlayProvider } from '../providers/CarPlay'
|
||||
/**
|
||||
* The main component for the Jellify app. Children are wrapped in the {@link JellifyProvider}
|
||||
* @returns The {@link Jellify} component
|
||||
@@ -82,6 +83,7 @@ function App(): React.JSX.Element {
|
||||
<NetworkContextProvider>
|
||||
<QueueProvider>
|
||||
<PlayerProvider>
|
||||
<CarPlayProvider />
|
||||
<Root />
|
||||
</PlayerProvider>
|
||||
</QueueProvider>
|
||||
|
||||
56
src/providers/CarPlay/index.tsx
Normal file
56
src/providers/CarPlay/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import CarPlayNavigation from '../../components/CarPlay/Navigation'
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import { CarPlay } from 'react-native-carplay'
|
||||
import { useJellifyContext } from '../index'
|
||||
import { useQueueContext } from '../Player/queue'
|
||||
|
||||
interface CarPlayContext {
|
||||
carplayConnected: boolean
|
||||
}
|
||||
|
||||
const CarPlayContextInitializer = () => {
|
||||
const { user, api, sessionId } = useJellifyContext()
|
||||
const [carplayConnected, setCarPlayConnected] = useState(CarPlay ? CarPlay.connected : false)
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
|
||||
useEffect(() => {
|
||||
function onConnect() {
|
||||
setCarPlayConnected(true)
|
||||
|
||||
if (user && api) {
|
||||
CarPlay.setRootTemplate(CarPlayNavigation(user, useLoadNewQueue))
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
CarPlay.enableNowPlaying(true) // https://github.com/birkir/react-native-carplay/issues/185
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDisconnect() {
|
||||
setCarPlayConnected(false)
|
||||
}
|
||||
|
||||
if (CarPlay) {
|
||||
CarPlay.registerOnConnect(onConnect)
|
||||
CarPlay.registerOnDisconnect(onDisconnect)
|
||||
return () => {
|
||||
CarPlay.unregisterOnConnect(onConnect)
|
||||
CarPlay.unregisterOnDisconnect(onDisconnect)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
carplayConnected,
|
||||
}
|
||||
}
|
||||
|
||||
const CarPlayContext = createContext<CarPlayContext>({
|
||||
carplayConnected: false,
|
||||
})
|
||||
|
||||
export const CarPlayProvider = () => {
|
||||
const value = CarPlayContextInitializer()
|
||||
return <CarPlayContext.Provider value={value} />
|
||||
}
|
||||
@@ -403,7 +403,7 @@ const QueueContextInitailizer = () => {
|
||||
}
|
||||
|
||||
const playInQueue = async (items: BaseItemDto[]) => {
|
||||
const insertIndex = await findPlayQueueIndexStart(playQueue)
|
||||
const insertIndex = await findPlayQueueIndexStart(playQueue, currentIndex)
|
||||
console.debug(`Adding ${items.length} to queue at index ${insertIndex}`)
|
||||
|
||||
const newTracks = items.map((item) =>
|
||||
@@ -847,13 +847,7 @@ export const QueueProvider: ({ children }: { children: ReactNode }) => React.JSX
|
||||
// Memoize the context value to prevent unnecessary re-renders
|
||||
const value = useMemo(
|
||||
() => context,
|
||||
[
|
||||
context.currentIndex,
|
||||
context.shuffled,
|
||||
context.skipping,
|
||||
// Functions are stable since they're defined inside the initializer
|
||||
// Arrays are memoized by length to avoid reference changes
|
||||
],
|
||||
[context.currentIndex, context.shuffled, context.skipping, context.playQueue],
|
||||
)
|
||||
|
||||
return <QueueContext.Provider value={value}>{children}</QueueContext.Provider>
|
||||
|
||||
@@ -24,18 +24,14 @@ export async function findPlayNextIndexStart(playQueue: JellifyTrack[]) {
|
||||
* @param playQueue The current player queue
|
||||
* @returns The index to insert songs to add to the user queue
|
||||
*/
|
||||
export async function findPlayQueueIndexStart(playQueue: JellifyTrack[]) {
|
||||
export async function findPlayQueueIndexStart(playQueue: JellifyTrack[], currentIndex: number) {
|
||||
if (isEmpty(playQueue)) return 0
|
||||
|
||||
const activeTrack = await TrackPlayer.getActiveTrack()
|
||||
|
||||
const activeIndex = playQueue.findIndex((track) => track.item.Id === activeTrack?.item.Id)
|
||||
|
||||
if (isUndefined(activeTrack) || activeIndex === -1) return 0
|
||||
if (currentIndex === -1) return 0
|
||||
|
||||
const insertIndex = playQueue.findIndex(
|
||||
({ QueuingType: queuingType, index: itemIndex }) =>
|
||||
queuingType === QueuingType.FromSelection && itemIndex > activeIndex,
|
||||
({ QueuingType: queuingType }, index) =>
|
||||
queuingType === QueuingType.FromSelection && index > currentIndex,
|
||||
)
|
||||
|
||||
if (insertIndex === -1) return playQueue.length
|
||||
|
||||
@@ -8,9 +8,6 @@ import {
|
||||
useState,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { CarPlay } from 'react-native-carplay'
|
||||
import CarPlayNavigation from '../components/CarPlay/Navigation'
|
||||
import { Platform } from 'react-native'
|
||||
import { JellifyLibrary } from '../types/JellifyLibrary'
|
||||
import { JellifyServer } from '../types/JellifyServer'
|
||||
import { JellifyUser } from '../types/JellifyUser'
|
||||
@@ -50,11 +47,6 @@ interface JellifyContext {
|
||||
*/
|
||||
library: JellifyLibrary | undefined
|
||||
|
||||
/**
|
||||
* Whether CarPlay / Android Auto is connected.
|
||||
*/
|
||||
carPlayConnected: boolean
|
||||
|
||||
/**
|
||||
* The ID for the current session.
|
||||
*/
|
||||
@@ -103,8 +95,6 @@ const JellifyContextInitializer = () => {
|
||||
|
||||
const [loggedIn, setLoggedIn] = useState<boolean>(false)
|
||||
|
||||
const [carPlayConnected, setCarPlayConnected] = useState(CarPlay ? CarPlay.connected : false)
|
||||
|
||||
const signOut = () => {
|
||||
setServer(undefined)
|
||||
setUser(undefined)
|
||||
@@ -144,33 +134,6 @@ const JellifyContextInitializer = () => {
|
||||
else storage.delete(MMKVStorageKeys.Library)
|
||||
}, [library])
|
||||
|
||||
useEffect(() => {
|
||||
function onConnect() {
|
||||
setCarPlayConnected(true)
|
||||
|
||||
if (user && api) {
|
||||
CarPlay.setRootTemplate(CarPlayNavigation(api, user, sessionId))
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
CarPlay.enableNowPlaying(true) // https://github.com/birkir/react-native-carplay/issues/185
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDisconnect() {
|
||||
setCarPlayConnected(false)
|
||||
}
|
||||
|
||||
if (CarPlay) {
|
||||
CarPlay.registerOnConnect(onConnect)
|
||||
CarPlay.registerOnDisconnect(onDisconnect)
|
||||
return () => {
|
||||
CarPlay.unregisterOnConnect(onConnect)
|
||||
CarPlay.unregisterOnDisconnect(onDisconnect)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
api,
|
||||
@@ -181,7 +144,6 @@ const JellifyContextInitializer = () => {
|
||||
setServer,
|
||||
setUser,
|
||||
setLibrary,
|
||||
carPlayConnected,
|
||||
signOut,
|
||||
}
|
||||
}
|
||||
@@ -196,7 +158,6 @@ const JellifyContext = createContext<JellifyContext>({
|
||||
setServer: () => {},
|
||||
setUser: () => {},
|
||||
setLibrary: () => {},
|
||||
carPlayConnected: false,
|
||||
signOut: () => {},
|
||||
})
|
||||
|
||||
@@ -222,7 +183,6 @@ export const JellifyProvider: ({ children }: { children: ReactNode }) => React.J
|
||||
context.server?.url,
|
||||
context.user?.id,
|
||||
context.library?.musicLibraryId,
|
||||
context.carPlayConnected,
|
||||
context.sessionId,
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user