mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-21 05:20:06 -06:00
clean up jest folder between setup, contextual, and functional files (#451)
Clean up Jest and Maestro Folders Reduction of the number of calls being made by components Fix issue where a different track would display than the one that is currently playing Update OTA dependency to fix an issue on iOS
This commit is contained in:
@@ -1788,7 +1788,7 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-netinfo (11.4.1):
|
||||
- React-Core
|
||||
- react-native-ota-hot-update (2.3.0):
|
||||
- react-native-ota-hot-update (2.3.1):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -2718,7 +2718,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNScreens (4.12.0):
|
||||
- RNScreens (4.13.1):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -2746,10 +2746,10 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNScreens/common (= 4.12.0)
|
||||
- RNScreens/common (= 4.13.1)
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNScreens/common (4.12.0):
|
||||
- RNScreens/common (4.13.1):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -3213,7 +3213,7 @@ SPEC CHECKSUMS:
|
||||
react-native-config: 644074ab88db883fcfaa584f03520ec29589d7df
|
||||
react-native-mmkv: 7fb4729ad5cb787a4394e6c4bd48e4b8ec30f25c
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-ota-hot-update: fe5bbc1e656018b0cf526adfcc92788e7f098110
|
||||
react-native-ota-hot-update: 2242f369e2a38ddf44256ea68aed576a96fb8a0a
|
||||
react-native-pager-view: 6e60acfd433ace1a7a1af75bd80b619a41478640
|
||||
react-native-safe-area-context: 68d1363b8354472a961aa6861ba8451beaf9a810
|
||||
react-native-track-player: 6dc2e2633265704b8ab6d8124b80239d4ed1f911
|
||||
@@ -3258,7 +3258,7 @@ SPEC CHECKSUMS:
|
||||
RNGestureHandler: 5e1a1605659c22098719fc2e8aee453fe728f52e
|
||||
RNReactNativeHapticFeedback: 8eb91a6f48567d02ec8026e515102e18c41030cf
|
||||
RNReanimated: bc1ddb7a5352648bcf0d592256069833bf935a46
|
||||
RNScreens: ab490a252dd536fca261c72ab7e5538e656dcb2b
|
||||
RNScreens: c63849403489bd068ea160f276fbc8416f19f2f7
|
||||
RNSentry: 2b690575f638f588e51b6817e5f77c8ab62de2cf
|
||||
RNVectorIcons: ef9b4b0b786053ebdd63ee2972f48de9633ba166
|
||||
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||
|
||||
@@ -4,17 +4,17 @@ module.exports = {
|
||||
testTimeout: 10000,
|
||||
setupFiles: ['./node_modules/react-native-gesture-handler/jestSetup.js'],
|
||||
setupFilesAfterEnv: [
|
||||
'./jest/setup.ts',
|
||||
'./jest/setup-blur.ts',
|
||||
'./jest/setup-carplay.ts',
|
||||
'./jest/setup-device-info.js', // JS to prevent Typescript implicit any warning
|
||||
'./jest/setup-reanimated.ts',
|
||||
'./jest/setup-rnfs.ts',
|
||||
'./jest/setup-rntp.ts',
|
||||
'./jest/setup-sentry.ts',
|
||||
'./jest/setup-nitro-image.ts',
|
||||
'./jest/setup/setup.ts',
|
||||
'./jest/setup/blur.ts',
|
||||
'./jest/setup/carplay.ts',
|
||||
'./jest/setup/device-info.js', // JS to prevent Typescript implicit any warning
|
||||
'./jest/setup/reanimated.ts',
|
||||
'./jest/setup/rnfs.ts',
|
||||
'./jest/setup/rntp.ts',
|
||||
'./jest/setup/sentry.ts',
|
||||
'./jest/setup/nitro-image.ts',
|
||||
'./tamagui.config.ts',
|
||||
'./jest/setup-native-modules.ts',
|
||||
'./jest/setup/native-modules.ts',
|
||||
],
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
transformIgnorePatterns: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'react-native'
|
||||
import React from 'react'
|
||||
import App from '../App'
|
||||
import App from '../../App'
|
||||
|
||||
import { render } from '@testing-library/react-native'
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react-native'
|
||||
import { JellifyProvider, useJellifyContext } from '../src/providers'
|
||||
import { JellifyProvider, useJellifyContext } from '../../src/providers'
|
||||
import { Text, View } from 'react-native'
|
||||
import { MMKVStorageKeys } from '../src/enums/mmkv-storage-keys'
|
||||
import { storage } from '../src/constants/storage'
|
||||
import { useEffect } from 'react'
|
||||
import { MMKVStorageKeys } from '../../src/enums/mmkv-storage-keys'
|
||||
import { storage } from '../../src/constants/storage'
|
||||
|
||||
const JellifyConsumer = () => {
|
||||
const { server, user, library } = useJellifyContext()
|
||||
@@ -3,8 +3,8 @@ import React from 'react'
|
||||
import { render } from '@testing-library/react-native'
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { QueueProvider } from '../src/providers/Player/queue'
|
||||
import { PlayerProvider } from '../src/providers/Player'
|
||||
import { QueueProvider } from '../../src/providers/Player/queue'
|
||||
import { PlayerProvider } from '../../src/providers/Player'
|
||||
import { View } from 'react-native'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
@@ -5,9 +5,9 @@ import { Event } from 'react-native-track-player'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Button, Text } from 'react-native'
|
||||
|
||||
import { QueueProvider, useQueueContext } from '../src/providers/Player/queue'
|
||||
import { eventHandler } from './setup-rntp'
|
||||
import JellifyTrack from '../src/types/JellifyTrack'
|
||||
import { QueueProvider, useQueueContext } from '../../src/providers/Player/queue'
|
||||
import { eventHandler } from '../setup/rntp'
|
||||
import JellifyTrack from '../../src/types/JellifyTrack'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import 'react-native'
|
||||
import React from 'react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react-native'
|
||||
import { Event } from 'react-native-track-player'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Button, Text } from 'react-native'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
import { QueueProvider, useQueueContext } from '../src/providers/Player/queue'
|
||||
import { PlayerProvider, usePlayerContext } from '../src/providers/Player'
|
||||
import { eventHandler } from './setup-rntp'
|
||||
import JellifyTrack from '../src/types/JellifyTrack'
|
||||
import { QueuingType } from '../src/enums/queuing-type'
|
||||
import { storage } from '../src/constants/storage'
|
||||
import { MMKVStorageKeys } from '../src/enums/mmkv-storage-keys'
|
||||
import { QueueProvider, useQueueContext } from '../../src/providers/Player/queue'
|
||||
import { PlayerProvider, usePlayerContext } from '../../src/providers/Player'
|
||||
import JellifyTrack from '../../src/types/JellifyTrack'
|
||||
import { QueuingType } from '../../src/enums/queuing-type'
|
||||
import { storage } from '../../src/constants/storage'
|
||||
import { MMKVStorageKeys } from '../../src/enums/mmkv-storage-keys'
|
||||
|
||||
// Mock the JellifyProvider to avoid dependency issues
|
||||
jest.mock('../src/providers', () => ({
|
||||
...jest.requireActual('../src/providers'),
|
||||
jest.mock('../../src/providers', () => ({
|
||||
...jest.requireActual('../../src/providers'),
|
||||
useJellifyContext: () => ({
|
||||
api: {},
|
||||
sessionId: 'test-session',
|
||||
@@ -26,7 +23,7 @@ jest.mock('../src/providers', () => ({
|
||||
}))
|
||||
|
||||
// Mock the NetworkProvider to avoid dependency issues
|
||||
jest.mock('../src/providers/Network', () => ({
|
||||
jest.mock('../../src/providers/Network', () => ({
|
||||
useNetworkContext: () => ({
|
||||
downloadedTracks: [],
|
||||
networkStatus: 'ONLINE',
|
||||
@@ -34,7 +31,7 @@ jest.mock('../src/providers/Network', () => ({
|
||||
}))
|
||||
|
||||
// Mock the SettingsProvider to avoid dependency issues
|
||||
jest.mock('../src/providers/Settings', () => ({
|
||||
jest.mock('../../src/providers/Settings', () => ({
|
||||
useSettingsContext: () => ({
|
||||
autoDownload: false,
|
||||
}),
|
||||
42
jest/functional/Move-Track.test.ts
Normal file
42
jest/functional/Move-Track.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import move from '../../src/providers/Player/utils/move'
|
||||
|
||||
const playQueue = [
|
||||
{ id: '1', index: 0, url: 'https://example.com', item: { Id: '1' } },
|
||||
{ id: '2', index: 1, url: 'https://example.com', item: { Id: '2' } },
|
||||
{ id: '3', index: 2, url: 'https://example.com', item: { Id: '3' } },
|
||||
]
|
||||
|
||||
/**
|
||||
* Tests the move track utility function
|
||||
*
|
||||
* Doesn't inspect the RNTP queue, only the play queue
|
||||
*
|
||||
* Doesn't inspect the track indexes, but rather the track IDs to ensure the correct track is moved
|
||||
*/
|
||||
describe('Move Track Util', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('moveTrack', () => {
|
||||
it('should move the first track to the second index', () => {
|
||||
const result = move(playQueue, 0, 1)
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: '2', index: 1, url: 'https://example.com', item: { Id: '2' } },
|
||||
{ id: '1', index: 0, url: 'https://example.com', item: { Id: '1' } },
|
||||
{ id: '3', index: 2, url: 'https://example.com', item: { Id: '3' } },
|
||||
])
|
||||
})
|
||||
|
||||
it('should move the last track to the first index', () => {
|
||||
const result = move(playQueue, 2, 0)
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: '3', index: 2, url: 'https://example.com', item: { Id: '3' } },
|
||||
{ id: '1', index: 0, url: 'https://example.com', item: { Id: '1' } },
|
||||
{ id: '2', index: 1, url: 'https://example.com', item: { Id: '2' } },
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
146
jest/functional/Player-Index.test.ts
Normal file
146
jest/functional/Player-Index.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { QueuingType } from '../../src/enums/queuing-type'
|
||||
import { findPlayNextIndexStart, findPlayQueueIndexStart } from '../../src/providers/Player/utils'
|
||||
|
||||
describe('Queue Index Util', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('findPlayNextIndexStart', () => {
|
||||
it('should return 0 if the queue is empty', async () => {
|
||||
const result = await findPlayNextIndexStart([])
|
||||
|
||||
expect(result).toBe(0)
|
||||
})
|
||||
|
||||
it('should return the index of the active track + 1', async () => {
|
||||
const result = await findPlayNextIndexStart([
|
||||
{ id: '1', index: 0, url: 'https://example.com', item: { Id: '1' } },
|
||||
])
|
||||
|
||||
expect(result).toBe(1)
|
||||
})
|
||||
|
||||
it('should return 0 if the active track is not in the queue', async () => {
|
||||
const result = await findPlayNextIndexStart([
|
||||
{ id: '1', index: 0, url: 'https://example.com', item: { Id: '2' } },
|
||||
])
|
||||
|
||||
expect(result).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
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,
|
||||
},
|
||||
])
|
||||
|
||||
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,
|
||||
},
|
||||
])
|
||||
|
||||
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,
|
||||
},
|
||||
])
|
||||
|
||||
expect(result).toBe(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'react-native'
|
||||
import { shuffleJellifyTracks } from '../src/providers/Player/utils/shuffle'
|
||||
import { QueuingType } from '../src/enums/queuing-type'
|
||||
import JellifyTrack from '../src/types/JellifyTrack'
|
||||
import { shuffleJellifyTracks } from '../../src/providers/Player/utils/shuffle'
|
||||
import { QueuingType } from '../../src/enums/queuing-type'
|
||||
import JellifyTrack from '../../src/types/JellifyTrack'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
// Mock the network status types to avoid dependency issues
|
||||
jest.mock('../src/components/Network/internetConnectionWatcher', () => ({
|
||||
jest.mock('../../src/components/Network/internetConnectionWatcher', () => ({
|
||||
networkStatusTypes: {
|
||||
ONLINE: 'ONLINE',
|
||||
OFFLINE: 'OFFLINE',
|
||||
@@ -35,8 +35,8 @@ jest.mock('react-native-track-player', () => {
|
||||
// player getters
|
||||
getQueue: jest.fn(),
|
||||
getTrack: jest.fn(),
|
||||
getActiveTrackIndex: jest.fn(),
|
||||
getActiveTrack: jest.fn(),
|
||||
getActiveTrackIndex: jest.fn().mockResolvedValue(0),
|
||||
getActiveTrack: jest.fn().mockResolvedValue({ id: '1', index: 0, item: { Id: '1' } }),
|
||||
getCurrentTrack: jest.fn(),
|
||||
getVolume: jest.fn(),
|
||||
getDuration: jest.fn(),
|
||||
@@ -103,10 +103,16 @@ jest.mock('react-native-track-player', () => {
|
||||
Track: 1,
|
||||
Queue: 2,
|
||||
},
|
||||
TrackType: {
|
||||
Default: 'default',
|
||||
HLS: 'hls',
|
||||
SmoothStreaming: 'smoothStreaming',
|
||||
Dash: 'dash',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Mock the gapless helper to avoid dynamic import issues in tests
|
||||
jest.mock('../src/player/helpers/gapless', () => ({
|
||||
jest.mock('../../src/player/helpers/gapless', () => ({
|
||||
ensureUpcomingTracksInQueue: jest.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
@@ -1,4 +1,4 @@
|
||||
jest.mock('../src/api/info', () => {
|
||||
jest.mock('../../src/api/info', () => {
|
||||
return {
|
||||
JellyfinInfo: {
|
||||
clientInfo: {
|
||||
@@ -33,7 +33,7 @@ jest.mock('react-native-haptic-feedback', () => {
|
||||
|
||||
jest.mock('react-native/Libraries/Components/RefreshControl/RefreshControl', () => ({
|
||||
__esModule: true,
|
||||
default: require('./setup-refresh-control'),
|
||||
default: require('./refresh-control'),
|
||||
}))
|
||||
|
||||
jest.mock('react-native-toast-message', () => {
|
||||
@@ -3,6 +3,5 @@ appId: com.jellify
|
||||
- clearState # clears the state of the current app
|
||||
- launchApp
|
||||
- runFlow: ../tests/login.yaml
|
||||
- launchApp
|
||||
- runFlow: ../tests/musicplayer.yaml
|
||||
- runFlow: ../tests/search.yaml
|
||||
@@ -25,7 +25,4 @@ appId: com.jellify
|
||||
text: "Royalty Free Music"
|
||||
- tapOn:
|
||||
id: "let_s_go_button"
|
||||
# Close the app to ensure app is logged in
|
||||
# before next start
|
||||
- stopApp
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
appId: com.jellify
|
||||
---
|
||||
# Navigate to the home screen
|
||||
- assertVisible:
|
||||
id: "home-tab-icon"
|
||||
|
||||
- tapOn:
|
||||
id: "home-tab-icon"
|
||||
|
||||
@@ -23,8 +23,6 @@ appId: com.jellify
|
||||
# Test Player (Playback) Tab
|
||||
- tapOn:
|
||||
text: "Player"
|
||||
- assertVisible:
|
||||
text: "Gapless Playback"
|
||||
- assertVisible:
|
||||
text: "Streaming Quality"
|
||||
|
||||
10
package.json
10
package.json
@@ -45,7 +45,7 @@
|
||||
"@react-navigation/native-stack": "^7.3.21",
|
||||
"@sentry/react-native": "^6.17.0",
|
||||
"@shopify/flash-list": "^2.0.0-rc.11",
|
||||
"@tamagui/config": "^1.132.7",
|
||||
"@tamagui/config": "^1.132.10",
|
||||
"@tanstack/query-sync-storage-persister": "^5.83.0",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query-persist-client": "^5.83.0",
|
||||
@@ -76,7 +76,7 @@
|
||||
"react-native-haptic-feedback": "^2.3.3",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-mmkv": "3.3.0",
|
||||
"react-native-ota-hot-update": "^2.3.0",
|
||||
"react-native-ota-hot-update": "^2.3.1",
|
||||
"react-native-pager-view": "^6.8.1",
|
||||
"react-native-reanimated": "^3.18.0",
|
||||
"react-native-safe-area-context": "^5.5.2",
|
||||
@@ -89,7 +89,7 @@
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-vector-icons": "^10.2.0",
|
||||
"ruby": "^0.6.1",
|
||||
"tamagui": "^1.132.7"
|
||||
"tamagui": "^1.132.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.0",
|
||||
@@ -110,9 +110,9 @@
|
||||
"@types/react-test-renderer": "19.1.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-native": "^5.0.0",
|
||||
"globals": "^16.3.0",
|
||||
|
||||
@@ -11,7 +11,7 @@ if (!serverAddress || !username) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Function to recursively find all YAML files in maestro-tests directory
|
||||
// Function to recursively find all YAML files in maestro/tests directory
|
||||
function findYamlFiles(dir) {
|
||||
const files = []
|
||||
|
||||
@@ -34,8 +34,8 @@ function findYamlFiles(dir) {
|
||||
return files.sort() // Sort for consistent ordering
|
||||
}
|
||||
|
||||
// Get all YAML files from maestro-tests directory
|
||||
const MAESTRO_TESTS_DIR = './maestro-tests/flows'
|
||||
// Get all YAML files from maestro/tests directory
|
||||
const MAESTRO_TESTS_DIR = './maestro/tests'
|
||||
const FLOW_FILES = findYamlFiles(MAESTRO_TESTS_DIR)
|
||||
|
||||
console.log(`🔍 Found ${FLOW_FILES.length} YAML test files:`)
|
||||
@@ -44,7 +44,7 @@ FLOW_FILES.forEach((file, index) => {
|
||||
})
|
||||
|
||||
if (FLOW_FILES.length === 0) {
|
||||
console.error('❌ No YAML test files found in maestro-tests directory')
|
||||
console.error('❌ No YAML test files found in maestro/testsdirectory')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -68,19 +68,15 @@ export function AlbumScreen({ route, navigation }: HomeAlbumProps): React.JSX.El
|
||||
const allTracks = discs.flatMap((disc) => disc.data)
|
||||
if (allTracks.length === 0) return
|
||||
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: allTracks[0],
|
||||
index: 0,
|
||||
tracklist: allTracks,
|
||||
queue: album,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track: allTracks[0],
|
||||
index: 0,
|
||||
tracklist: allTracks,
|
||||
queue: album,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
startPlayback: true,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,6 @@ import { StackParamList } from '../types'
|
||||
import React from 'react'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { useQueueContext } from '../../providers/Player/queue'
|
||||
import { usePlayerContext } from '../../providers/Player'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { fetchAlbumDiscs } from '../../api/queries/item'
|
||||
|
||||
@@ -26,7 +25,6 @@ export default function ArtistTabBar(
|
||||
const { api } = useJellifyContext()
|
||||
const { artist, scroll, albums } = useArtistContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const { useStartPlayback } = usePlayerContext()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
@@ -47,19 +45,15 @@ export default function ArtistTabBar(
|
||||
|
||||
if (allTracks.length === 0) return
|
||||
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: allTracks[0],
|
||||
index: 0,
|
||||
tracklist: allTracks,
|
||||
queue: artist,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track: allTracks[0],
|
||||
index: 0,
|
||||
tracklist: allTracks,
|
||||
queue: artist,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
startPlayback: true,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to play artist tracks:', error)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Text } from '../helpers/text'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchMediaInfo } from '../../../api/queries/media'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
interface CardProps extends TamaguiCardProps {
|
||||
caption?: string | null | undefined
|
||||
@@ -25,14 +22,7 @@ interface CardProps extends TamaguiCardProps {
|
||||
* @param props
|
||||
*/
|
||||
export function ItemCard(props: CardProps) {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const mediaInfo = useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, props.item.Id!],
|
||||
queryFn: () => fetchMediaInfo(api, user, props.item),
|
||||
staleTime: Infinity,
|
||||
enabled: props.item.Type === 'Audio',
|
||||
})
|
||||
const { api } = useJellifyContext()
|
||||
|
||||
return (
|
||||
<View alignItems='center' margin={'$1.5'}>
|
||||
|
||||
@@ -37,24 +37,19 @@ export default function ItemRow({
|
||||
onPress?: () => void
|
||||
circular?: boolean
|
||||
}): React.JSX.Element {
|
||||
const { useStartPlayback } = usePlayerContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
|
||||
const gestureCallback = () => {
|
||||
switch (item.Type) {
|
||||
case 'Audio': {
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: item,
|
||||
tracklist: [item],
|
||||
index: 0,
|
||||
queue: 'Search',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track: item,
|
||||
tracklist: [item],
|
||||
index: 0,
|
||||
queue: 'Search',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
startPlayback: true,
|
||||
})
|
||||
break
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -16,7 +16,6 @@ import { networkStatusTypes } from '../../../components/Network/internetConnecti
|
||||
import { useNetworkContext } from '../../../providers/Network'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchMediaInfo } from '../../../api/queries/media'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { fetchItem } from '../../../api/queries/item'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
@@ -67,22 +66,6 @@ export default function Track({
|
||||
|
||||
const isOffline = networkStatus === networkStatusTypes.DISCONNECTED
|
||||
|
||||
// Fetch media info so it's available in the player
|
||||
const mediaInfo = useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, track.Id!],
|
||||
queryFn: () => fetchMediaInfo(api, user, track),
|
||||
staleTime: Infinity,
|
||||
enabled: track.Type === 'Audio',
|
||||
})
|
||||
|
||||
// Fetch album so it's available in the Details screen
|
||||
const { data: album } = useQuery({
|
||||
queryKey: [QueryKeys.Item, track.Id!], // Different key
|
||||
queryFn: () => fetchItem(api, track.Id!),
|
||||
staleTime: 60 * 60 * 1000 * 24, // 24 hours
|
||||
enabled: !!track.Id, // Add proper enabled condition
|
||||
})
|
||||
|
||||
return (
|
||||
<Theme name={invertedColors ? 'inverted_purple' : undefined}>
|
||||
<XStack
|
||||
@@ -95,18 +78,14 @@ export default function Track({
|
||||
if (onPress) {
|
||||
onPress()
|
||||
} else {
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track,
|
||||
index,
|
||||
tracklist: tracklist ?? playQueue.map((track) => track.item),
|
||||
queue,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track,
|
||||
index,
|
||||
tracklist: tracklist ?? playQueue.map((track) => track.item),
|
||||
queue,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
startPlayback: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
onLongPress={
|
||||
|
||||
@@ -8,7 +8,6 @@ import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { usePlayerContext } from '../../../providers/Player'
|
||||
import { H4 } from '../../../components/Global/helpers/text'
|
||||
import { useDisplayContext } from '../../../providers/Display/display-provider'
|
||||
export default function FrequentlyPlayedTracks({
|
||||
@@ -23,7 +22,6 @@ export default function FrequentlyPlayedTracks({
|
||||
isFetchingFrequentlyPlayed,
|
||||
} = useHomeContext()
|
||||
|
||||
const { useStartPlayback } = usePlayerContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const { horizontalItems } = useDisplayContext()
|
||||
|
||||
@@ -58,20 +56,16 @@ export default function FrequentlyPlayedTracks({
|
||||
subCaption={`${track.Artists?.join(', ')}`}
|
||||
squared
|
||||
onPress={() => {
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
useLoadNewQueue({
|
||||
track,
|
||||
index,
|
||||
tracklist: frequentlyPlayed?.pages.flatMap((page) => page) ?? [
|
||||
track,
|
||||
index,
|
||||
tracklist: frequentlyPlayed?.pages.flatMap((page) => page) ?? [
|
||||
track,
|
||||
],
|
||||
queue: 'On Repeat',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
],
|
||||
queue: 'On Repeat',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
startPlayback: true,
|
||||
})
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactMedium')
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function RecentlyPlayed({
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { nowPlaying, useStartPlayback } = usePlayerContext()
|
||||
const { nowPlaying } = usePlayerContext()
|
||||
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
|
||||
@@ -59,20 +59,16 @@ export default function RecentlyPlayed({
|
||||
testId={`recently-played-${index}`}
|
||||
item={recentlyPlayedTrack}
|
||||
onPress={() => {
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: recentlyPlayedTrack,
|
||||
index: index,
|
||||
tracklist: recentTracks?.pages.flatMap((page) => page) ?? [
|
||||
recentlyPlayedTrack,
|
||||
],
|
||||
queue: 'Recently Played',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track: recentlyPlayedTrack,
|
||||
index: index,
|
||||
tracklist: recentTracks?.pages.flatMap((page) => page) ?? [
|
||||
recentlyPlayedTrack,
|
||||
],
|
||||
queue: 'Recently Played',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
startPlayback: true,
|
||||
})
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactMedium')
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function Scrubber(): React.JSX.Element {
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
// Get progress from the track player with the specified update interval
|
||||
const progress = useProgress(UPDATE_INTERVAL, false)
|
||||
const { position, duration } = useProgress(UPDATE_INTERVAL)
|
||||
|
||||
// Single source of truth for the current position
|
||||
const [displayPosition, setDisplayPosition] = useState<number>(0)
|
||||
@@ -33,16 +33,13 @@ export default function Scrubber(): React.JSX.Element {
|
||||
|
||||
// Calculate maximum track duration in slider units
|
||||
const maxDuration = useMemo(() => {
|
||||
return progress?.duration
|
||||
? Math.round(progress.duration * ProgressMultiplier)
|
||||
: ProgressMultiplier
|
||||
}, [progress?.duration])
|
||||
return Math.round(duration * ProgressMultiplier)
|
||||
}, [duration])
|
||||
|
||||
// Calculate current position in slider units
|
||||
const calculatedPosition = useMemo(() => {
|
||||
if (!progress?.position) return 0
|
||||
return Math.round(progress.position * ProgressMultiplier)
|
||||
}, [progress?.position])
|
||||
return Math.round(position * ProgressMultiplier)
|
||||
}, [position])
|
||||
|
||||
// Update display position from playback progress
|
||||
useEffect(() => {
|
||||
@@ -93,8 +90,8 @@ export default function Scrubber(): React.JSX.Element {
|
||||
|
||||
// Get total duration in seconds
|
||||
const totalSeconds = useMemo(() => {
|
||||
return progress?.duration ? Math.round(progress.duration) : 0
|
||||
}, [progress?.duration])
|
||||
return Math.round(duration)
|
||||
}, [duration])
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={scrubGesture}>
|
||||
|
||||
@@ -10,6 +10,7 @@ import Animated from 'react-native-reanimated'
|
||||
import { Gesture } from 'react-native-gesture-handler'
|
||||
import { useState } from 'react'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
const gesture = Gesture.Pan().runOnJS(true)
|
||||
|
||||
@@ -95,7 +96,8 @@ export default function Queue({
|
||||
showArtwork
|
||||
testID={`queue-item-${getIndex()}`}
|
||||
onPress={() => {
|
||||
useSkip.mutate(getIndex())
|
||||
const index = getIndex()
|
||||
if (!isUndefined(index)) useSkip.mutate(index)
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactLight')
|
||||
@@ -104,7 +106,8 @@ export default function Queue({
|
||||
isNested
|
||||
showRemove
|
||||
onRemove={() => {
|
||||
if (getIndex()) useRemoveFromQueue.mutate(getIndex()!)
|
||||
const index = getIndex()
|
||||
if (!isUndefined(index)) useRemoveFromQueue.mutate(index)
|
||||
}}
|
||||
/>
|
||||
</XStack>
|
||||
|
||||
@@ -17,7 +17,6 @@ import { useSettingsContext } from '../../../../src/providers/Settings'
|
||||
import { ActivityIndicator } from 'react-native'
|
||||
import { mapDtoToTrack } from '../../../utils/mappings'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { usePlayerContext } from '../../../providers/Player'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
|
||||
export default function PlayliistTracklistHeader(
|
||||
@@ -150,7 +149,6 @@ function PlaylistHeaderControls({
|
||||
const { useDownloadMultiple, pendingDownloads } = useNetworkContext()
|
||||
const { downloadQuality, streamingQuality } = useSettingsContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const { useStartPlayback } = usePlayerContext()
|
||||
const isDownloading = pendingDownloads.length != 0
|
||||
const { sessionId, api } = useJellifyContext()
|
||||
|
||||
@@ -165,19 +163,15 @@ function PlaylistHeaderControls({
|
||||
const playPlaylist = (shuffled: boolean = false) => {
|
||||
if (!playlistTracks || playlistTracks.length === 0) return
|
||||
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: playlistTracks[0],
|
||||
index: 0,
|
||||
tracklist: playlistTracks,
|
||||
queue: playlist,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track: playlistTracks[0],
|
||||
index: 0,
|
||||
tracklist: playlistTracks,
|
||||
queue: playlist,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
startPlayback: true,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function InfoTabIndex({ navigation }: InfoTabNativeStackNavigatio
|
||||
title: `Jellify`,
|
||||
subTitle: version,
|
||||
iconName: 'jellyfish',
|
||||
iconColor: '$borderColor',
|
||||
iconColor: '$secondary',
|
||||
children: (
|
||||
<YStack gap={'$2'}>
|
||||
<Text
|
||||
|
||||
@@ -14,22 +14,11 @@ export default function PlaybackTab(): React.JSX.Element {
|
||||
return (
|
||||
<SettingsListGroup
|
||||
settingsList={[
|
||||
{
|
||||
title: 'Gapless Playback',
|
||||
subTitle: 'Seamless transitions between tracks',
|
||||
iconName: 'skip-next',
|
||||
iconColor: '$borderColor',
|
||||
children: (
|
||||
<Text fontSize='$3' color='$color10' padding='$3'>
|
||||
Gapless playback is automatically enabled for smooth music transitions.
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Streaming Quality',
|
||||
subTitle: `Current: ${getQualityLabel(streamingQuality)} • ${getBandwidthEstimate(streamingQuality)}`,
|
||||
iconName: 'wifi',
|
||||
iconColor: '$primary',
|
||||
iconName: 'sine-wave',
|
||||
iconColor: getStreamingQualityIconColor(streamingQuality),
|
||||
children: (
|
||||
<YStack gap='$2' paddingVertical='$2'>
|
||||
<Text bold fontSize='$4'>
|
||||
@@ -72,3 +61,18 @@ export default function PlaybackTab(): React.JSX.Element {
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function getStreamingQualityIconColor(streamingQuality: StreamingQuality): string {
|
||||
switch (streamingQuality) {
|
||||
case 'original':
|
||||
return '$success'
|
||||
case 'high':
|
||||
return '$success'
|
||||
case 'medium':
|
||||
return '$secondary'
|
||||
case 'low':
|
||||
return '$danger'
|
||||
default:
|
||||
return '$borderColor'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ interface PlayerContext {
|
||||
shuffled: boolean
|
||||
useToggleRepeatMode: UseMutationResult<void, Error, void, unknown>
|
||||
useToggleShuffle: UseMutationResult<void, Error, void, unknown>
|
||||
useStartPlayback: UseMutationResult<void, Error, void, unknown>
|
||||
useStartPlayback: () => void
|
||||
useTogglePlayback: UseMutationResult<void, Error, void, unknown>
|
||||
useSeekTo: UseMutationResult<void, Error, number, unknown>
|
||||
useSeekBy: UseMutationResult<void, Error, number, unknown>
|
||||
@@ -358,7 +358,7 @@ const PlayerContextInitializer = () => {
|
||||
/**
|
||||
* A mutation to handle starting playback
|
||||
*/
|
||||
const useStartPlayback = useMutation({
|
||||
const { mutate: useStartPlayback } = useMutation({
|
||||
mutationFn: TrackPlayer.play,
|
||||
})
|
||||
|
||||
@@ -661,24 +661,7 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
submittedAt: 0,
|
||||
},
|
||||
playbackState: undefined,
|
||||
useStartPlayback: {
|
||||
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,
|
||||
},
|
||||
useStartPlayback: () => {},
|
||||
useTogglePlayback: {
|
||||
mutate: () => {},
|
||||
mutateAsync: async () => {},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import JellifyTrack from '../types/JellifyTrack'
|
||||
import { QueuingType } from '../enums/queuing-type'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Queue } from './types/queue-item'
|
||||
import { Queue } from '../../player/types/queue-item'
|
||||
|
||||
/**
|
||||
* A mutation to handle loading a new queue.
|
||||
@@ -34,6 +33,11 @@ export interface QueueMutation {
|
||||
* Whether the queue should be shuffled.
|
||||
*/
|
||||
shuffled?: boolean | undefined
|
||||
|
||||
/**
|
||||
* Whether to start playback immediately.
|
||||
*/
|
||||
startPlayback?: boolean | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3,7 +3,7 @@ import { createContext } from 'react'
|
||||
import { Queue } from '../../player/types/queue-item'
|
||||
import { Section } from '../../components/Player/types'
|
||||
import { useMutation, UseMutationResult } from '@tanstack/react-query'
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from '../../player/interfaces'
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces'
|
||||
import { storage } from '../../constants/storage'
|
||||
import { MMKVStorageKeys } from '../../enums/mmkv-storage-keys'
|
||||
import JellifyTrack from '../../types/JellifyTrack'
|
||||
@@ -14,7 +14,6 @@ import { useSettingsContext } from '../Settings'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import TrackPlayer, { Event, useTrackPlayerEvents } from 'react-native-track-player'
|
||||
import { findPlayQueueIndexStart } from './utils'
|
||||
import { play, seekTo } from 'react-native-track-player/lib/src/trackPlayer'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
|
||||
|
||||
@@ -27,6 +26,7 @@ import Toast from 'react-native-toast-message'
|
||||
import { useJellifyContext } from '..'
|
||||
import { networkStatusTypes } from '@/src/components/Network/internetConnectionWatcher'
|
||||
import move from './utils/move'
|
||||
import { ensureUpcomingTracksInQueue } from '../../player/helpers/gapless'
|
||||
|
||||
/**
|
||||
* @description The context for managing the queue
|
||||
@@ -76,7 +76,7 @@ interface QueueContext {
|
||||
/**
|
||||
* A hook that loads a new queue of tracks
|
||||
*/
|
||||
useLoadNewQueue: UseMutationResult<void, Error, QueueMutation, unknown>
|
||||
useLoadNewQueue: (mutation: QueueMutation) => void
|
||||
|
||||
/**
|
||||
* A hook that removes upcoming tracks from the queue
|
||||
@@ -173,9 +173,7 @@ const QueueContextInitailizer = () => {
|
||||
|
||||
//#endregion Context
|
||||
|
||||
useTrackPlayerEvents([Event.PlaybackActiveTrackChanged], async ({ index, track }) => {
|
||||
console.debug('Active track changed')
|
||||
|
||||
useTrackPlayerEvents([Event.PlaybackActiveTrackChanged], async ({ track }) => {
|
||||
let newIndex = -1
|
||||
|
||||
if (!isUndefined(track)) {
|
||||
@@ -191,26 +189,13 @@ const QueueContextInitailizer = () => {
|
||||
|
||||
if (!isUndefined(itemIndex) && itemIndex !== -1) {
|
||||
newIndex = itemIndex
|
||||
console.debug(`Active track changed to index ${itemIndex}`)
|
||||
console.debug(`Active track changed to item at index: ${itemIndex}`)
|
||||
|
||||
// Ensure upcoming tracks are in correct order (important for shuffle)
|
||||
// try {
|
||||
// const { ensureUpcomingTracksInQueue } = await import(
|
||||
// '../../player/helpers/gapless'
|
||||
// )
|
||||
// await ensureUpcomingTracksInQueue(playQueue, index)
|
||||
// } catch (error) {
|
||||
// console.debug('Failed to ensure upcoming tracks on track change:', error)
|
||||
// }
|
||||
} else if (!isUndefined(index) && index !== -1) {
|
||||
newIndex = index
|
||||
console.debug(`Active track changed to index ${index}`)
|
||||
await ensureUpcomingTracksInQueue(playQueue, itemIndex)
|
||||
} else {
|
||||
console.warn('No index found for active track')
|
||||
}
|
||||
} else if (!isUndefined(index) && index !== -1) {
|
||||
newIndex = index
|
||||
console.debug(`Active track changed to index ${index}`)
|
||||
} else {
|
||||
console.warn('No active track found')
|
||||
}
|
||||
@@ -358,7 +343,7 @@ const QueueContextInitailizer = () => {
|
||||
`Queued ${queue.length} tracks, starting at ${finalStartIndex}${shuffleQueue ? ' (shuffled)' : ''}`,
|
||||
)
|
||||
|
||||
await play()
|
||||
await TrackPlayer.play()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -452,7 +437,7 @@ const QueueContextInitailizer = () => {
|
||||
|
||||
if (currentIndex > 0 && Math.floor(position) < SKIP_TO_PREVIOUS_THRESHOLD) {
|
||||
TrackPlayer.skipToPrevious()
|
||||
} else await seekTo(0)
|
||||
} else await TrackPlayer.seekTo(0)
|
||||
}
|
||||
|
||||
const skip = async (index?: number | undefined) => {
|
||||
@@ -532,7 +517,7 @@ const QueueContextInitailizer = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const useLoadNewQueue = useMutation({
|
||||
const { mutate: useLoadNewQueue } = useMutation({
|
||||
mutationFn: async ({
|
||||
index,
|
||||
track,
|
||||
@@ -541,9 +526,11 @@ const QueueContextInitailizer = () => {
|
||||
queue,
|
||||
shuffled,
|
||||
}: QueueMutation) => loadQueue(tracklist, queue, index, shuffled),
|
||||
onSuccess: async (data, { queue }: QueueMutation) => {
|
||||
onSuccess: async (data, { queue, startPlayback }: QueueMutation) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
startPlayback && (await TrackPlayer.play())
|
||||
|
||||
if (typeof queue === 'object' && api && user) await markItemPlayed(api, user, queue)
|
||||
},
|
||||
})
|
||||
@@ -736,24 +723,7 @@ export const QueueContext = createContext<QueueContext>({
|
||||
failureReason: null,
|
||||
submittedAt: 0,
|
||||
},
|
||||
useLoadNewQueue: {
|
||||
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,
|
||||
},
|
||||
useLoadNewQueue: () => {},
|
||||
useSkip: {
|
||||
mutate: () => {},
|
||||
mutateAsync: async () => {},
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { isEmpty } from 'lodash'
|
||||
import { isEmpty, isUndefined } from 'lodash'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import { getActiveTrackIndex } from 'react-native-track-player/lib/src/trackPlayer'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
|
||||
/**
|
||||
* Finds and returns the index of the player queue to insert additional tracks into
|
||||
* @param playQueue The current player queue
|
||||
* @returns The index to insert songs to play next at
|
||||
*/
|
||||
export const findPlayNextIndexStart = async (playQueue: JellifyTrack[]) => {
|
||||
export async function findPlayNextIndexStart(playQueue: JellifyTrack[]) {
|
||||
if (isEmpty(playQueue)) return 0
|
||||
|
||||
return (await getActiveTrackIndex())! + 1
|
||||
const activeTrack = (await TrackPlayer.getActiveTrack()) as JellifyTrack
|
||||
|
||||
const activeIndex = playQueue.findIndex((track) => track.item.Id === activeTrack?.item.Id)
|
||||
|
||||
if (isUndefined(activeTrack) || activeIndex === -1) return 0
|
||||
else return activeIndex + 1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,16 +24,20 @@ export const findPlayNextIndexStart = async (playQueue: JellifyTrack[]) => {
|
||||
* @param playQueue The current player queue
|
||||
* @returns The index to insert songs to add to the user queue
|
||||
*/
|
||||
export const findPlayQueueIndexStart = async (playQueue: JellifyTrack[]) => {
|
||||
export async function findPlayQueueIndexStart(playQueue: JellifyTrack[]) {
|
||||
if (isEmpty(playQueue)) return 0
|
||||
|
||||
const activeIndex = await getActiveTrackIndex()
|
||||
const activeTrack = (await TrackPlayer.getActiveTrack()) as JellifyTrack
|
||||
|
||||
if (playQueue.findIndex((track) => track.QueuingType === QueuingType.FromSelection) === -1)
|
||||
return activeIndex! + 1
|
||||
const activeIndex = playQueue.findIndex((track) => track.item.Id === activeTrack?.item.Id)
|
||||
|
||||
return playQueue.findIndex(
|
||||
(queuedTrack, index) =>
|
||||
queuedTrack.QueuingType === QueuingType.FromSelection && index > activeIndex!,
|
||||
if (isUndefined(activeTrack) || activeIndex === -1) return 0
|
||||
|
||||
const insertIndex = playQueue.findIndex(
|
||||
({ QueuingType: queuingType, index }) =>
|
||||
queuingType === QueuingType.FromSelection && index > activeIndex,
|
||||
)
|
||||
|
||||
if (insertIndex === -1) return playQueue.length
|
||||
else return insertIndex
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ export function Tabs({
|
||||
name='music-box-multiple'
|
||||
color={color}
|
||||
size={size}
|
||||
testID='library-tab-icon'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
@@ -106,7 +107,12 @@ export function Tabs({
|
||||
options={{
|
||||
headerShown: false,
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<MaterialCommunityIcons name='earth' color={color} size={size} />
|
||||
<MaterialCommunityIcons
|
||||
name='earth'
|
||||
color={color}
|
||||
size={size}
|
||||
testID='discover-tab-icon'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -4,16 +4,16 @@ import {
|
||||
PlaybackInfoResponse,
|
||||
} from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import JellifyTrack from '../types/JellifyTrack'
|
||||
import { RatingType, TrackType } from 'react-native-track-player'
|
||||
import { TrackType } from 'react-native-track-player'
|
||||
import { QueuingType } from '../enums/queuing-type'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { JellifyDownload } from '../types/JellifyDownload'
|
||||
import { queryClient } from '../constants/query-client'
|
||||
import { QueryKeys } from '../enums/query-keys'
|
||||
import { Api } from '@jellyfin/sdk/lib/api'
|
||||
import RNFS from 'react-native-fs'
|
||||
import { DownloadQuality, StreamingQuality } from '../providers/Settings'
|
||||
import { Platform } from 'react-native'
|
||||
|
||||
/**
|
||||
* The container that the Jellyfin server will attempt to transcode to
|
||||
@@ -25,6 +25,16 @@ import { DownloadQuality, StreamingQuality } from '../providers/Settings'
|
||||
*/
|
||||
const transcodingContainer = 'ts'
|
||||
|
||||
/**
|
||||
* The type of track to use for the player
|
||||
*
|
||||
* iOS can use HLS, Android can't - and therefore uses Default
|
||||
*
|
||||
* Why? I'm not sure - someone way smarter than me can probably explain it
|
||||
* - Violet Caulfield - 2025-07-20
|
||||
*/
|
||||
const type = Platform.OS === 'ios' ? TrackType.HLS : TrackType.Default
|
||||
|
||||
/**
|
||||
* Gets quality-specific parameters for transcoding
|
||||
*
|
||||
@@ -97,15 +107,16 @@ export function mapDtoToTrack(
|
||||
console.debug(
|
||||
`Mapping BaseItemDTO to Track object with streaming quality: ${qualityForStreaming}`,
|
||||
)
|
||||
const isFavorite = !isUndefined(item.UserData) && (item.UserData.IsFavorite ?? false)
|
||||
|
||||
const downloads = downloadedTracks.filter((download) => download.item.Id === item.Id)
|
||||
|
||||
let url: string
|
||||
let image: string | undefined
|
||||
|
||||
if (downloads.length > 0 && downloads[0].path)
|
||||
if (downloads.length > 0 && downloads[0].path) {
|
||||
url = `file://${RNFS.DocumentDirectoryPath}/${downloads[0].path.split('/').pop()}`
|
||||
else {
|
||||
image = `file://${RNFS.DocumentDirectoryPath}/${downloads[0].artwork?.split('/').pop()}`
|
||||
} else {
|
||||
const PlaybackInfoResponse = queryClient.getQueryData([
|
||||
QueryKeys.MediaSources,
|
||||
item.Id!,
|
||||
@@ -118,12 +129,15 @@ export function mapDtoToTrack(
|
||||
)
|
||||
url = PlaybackInfoResponse.MediaSources![0].TranscodingUrl
|
||||
else url = `${api.basePath}/Audio/${item.Id!}/universal?${new URLSearchParams(urlParams)}`
|
||||
|
||||
image = item.AlbumId
|
||||
? getImageApi(api).getItemImageUrlById(item.AlbumId, ImageType.Primary)
|
||||
: undefined
|
||||
}
|
||||
|
||||
console.debug(url.length)
|
||||
return {
|
||||
url,
|
||||
type: TrackType.Default,
|
||||
type,
|
||||
headers: {
|
||||
'X-Emby-Token': api.accessToken,
|
||||
},
|
||||
@@ -131,14 +145,7 @@ export function mapDtoToTrack(
|
||||
album: item.Album,
|
||||
artist: item.Artists?.join(', '),
|
||||
duration: item.RunTimeTicks,
|
||||
artwork: item.AlbumId
|
||||
? getImageApi(api).getItemImageUrlById(item.AlbumId, ImageType.Primary, {
|
||||
width: 300,
|
||||
height: 300,
|
||||
})
|
||||
: undefined,
|
||||
|
||||
rating: isFavorite ? RatingType.Heart : undefined,
|
||||
artwork: image,
|
||||
item,
|
||||
QueuingType: queuingType ?? QueuingType.DirectlyQueued,
|
||||
} as JellifyTrack
|
||||
|
||||
Reference in New Issue
Block a user