diff --git a/eslint.config.js b/eslint.config.js index c64a8025..3a729fd3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ const { defineConfig } = require('eslint/config') const tsParser = require('@typescript-eslint/parser') diff --git a/jest/setup/setup.ts b/jest/setup/setup.ts index 25ac3404..976e107f 100644 --- a/jest/setup/setup.ts +++ b/jest/setup/setup.ts @@ -1,3 +1,5 @@ +import mockRefreshControl from './refresh-control' + jest.mock('../../src/api/info', () => { return { JellyfinInfo: { @@ -31,9 +33,6 @@ jest.mock('react-native-haptic-feedback', () => { } }) -// eslint-disable-next-line @typescript-eslint/no-var-requires -const mockRefreshControl = require('./refresh-control').default - // Mock the network status types to avoid dependency issues jest.mock('../../src/components/Network/internetConnectionWatcher', () => ({ networkStatusTypes: { diff --git a/metro.config.js b/metro.config.js index 8b861417..75a17f2e 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ // Learn more https://docs.expo.io/guides/customizing-metro const { wrapWithReanimatedMetroConfig } = require('react-native-reanimated/metro-config') diff --git a/scripts/maestro-android.js b/scripts/maestro-android.js index 7657f2ac..33392b0a 100644 --- a/scripts/maestro-android.js +++ b/scripts/maestro-android.js @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ const { execSync, exec, spawn } = require('child_process') const path = require('path') const fs = require('fs') diff --git a/scripts/updateEnv.js b/scripts/updateEnv.js index 691149e5..489fc0f2 100644 --- a/scripts/updateEnv.js +++ b/scripts/updateEnv.js @@ -1,5 +1,3 @@ -/* eslint-disable no-prototype-builtins */ -/* eslint-disable @typescript-eslint/no-var-requires */ const fs = require('fs') const path = require('path') @@ -32,7 +30,7 @@ function updateEnvFile(filePath, updates) { const [key, ...rest] = line.split('=') const trimmedKey = key.trim() - if (updates.hasOwnProperty(trimmedKey)) { + if (Object.prototype.hasOwnProperty.call(updates, trimmedKey)) { seenKeys.add(trimmedKey) return `${trimmedKey}=${updates[trimmedKey]}` } diff --git a/src/api/mutations/download/offlineModeUtils.ts b/src/api/mutations/download/offlineModeUtils.ts index d47d09af..0c7f17ba 100644 --- a/src/api/mutations/download/offlineModeUtils.ts +++ b/src/api/mutations/download/offlineModeUtils.ts @@ -27,7 +27,9 @@ export async function downloadJellyfinFile( if (contentType && contentType.includes('/')) { const parts = contentType.split('/') const container = parts[1].split(';')[0] // handles "audio/m4a; charset=utf-8" - container !== 'mpeg' && (extension = container) // don't use mpeg as an extension, use the default extension + if (container !== 'mpeg') { + extension = container // don't use mpeg as an extension, use the default extension + } } // Step 3: Build path diff --git a/src/components/AddToPlaylist/index.tsx b/src/components/AddToPlaylist/index.tsx index 561e520b..4dc4d7d9 100644 --- a/src/components/AddToPlaylist/index.tsx +++ b/src/components/AddToPlaylist/index.tsx @@ -97,7 +97,7 @@ export default function AddToPlaylist({ trigger('notificationSuccess') - refetch() + if (refetch) void refetch() queryClient.invalidateQueries({ queryKey: [QueryKeys.ItemTracks, playlist.Id!], diff --git a/src/components/Playlist/index.tsx b/src/components/Playlist/index.tsx index c9038c04..33315617 100644 --- a/src/components/Playlist/index.tsx +++ b/src/components/Playlist/index.tsx @@ -85,12 +85,14 @@ export default function Playlist({ queue={playlist} showArtwork onLongPress={() => { - editing - ? drag() - : rootNavigation.navigate('Context', { - item: track, - navigation, - }) + if (editing) { + drag() + } else { + rootNavigation.navigate('Context', { + item: track, + navigation, + }) + } }} showRemove={editing} onRemove={() => diff --git a/src/providers/Player/functions/index.ts b/src/providers/Player/functions/index.ts index ca4a6639..8b13fad6 100644 --- a/src/providers/Player/functions/index.ts +++ b/src/providers/Player/functions/index.ts @@ -1,3 +1,4 @@ +import TrackPlayer from 'react-native-track-player' import JellifyTrack from '../../../types/JellifyTrack' import { queryClient } from '../../../constants/query-client' import { @@ -6,6 +7,7 @@ import { PLAY_QUEUE_QUERY_KEY, } from '../constants/query-keys' import { CURRENT_INDEX_QUERY, NOW_PLAYING_QUERY } from '../constants/queries' +import { usePlayerQueueStore } from '../../../stores/player/queue' export function getActiveIndex(): number | undefined { return queryClient.getQueryData(ACTIVE_INDEX_QUERY_KEY) as number | undefined @@ -27,7 +29,19 @@ export function setPlayQueue(tracks: JellifyTrack[]): void { queryClient.setQueryData(PLAY_QUEUE_QUERY_KEY, tracks) } -export function handleActiveTrackChanged(): void { - queryClient.refetchQueries(NOW_PLAYING_QUERY) - queryClient.refetchQueries(CURRENT_INDEX_QUERY) +export async function handleActiveTrackChanged(): Promise { + const [queue, activeTrack, activeIndex] = await Promise.all([ + TrackPlayer.getQueue(), + TrackPlayer.getActiveTrack(), + TrackPlayer.getActiveTrackIndex(), + ]) + + usePlayerQueueStore.getState().setQueue(queue as JellifyTrack[]) + usePlayerQueueStore.getState().setCurrentTrack((activeTrack as JellifyTrack) ?? null) + usePlayerQueueStore.getState().setCurrentIndex(activeIndex ?? null) + + await Promise.all([ + queryClient.refetchQueries(NOW_PLAYING_QUERY), + queryClient.refetchQueries(CURRENT_INDEX_QUERY), + ]) } diff --git a/src/providers/Player/functions/initialization.ts b/src/providers/Player/functions/initialization.ts index 4b5864d3..85a621b5 100644 --- a/src/providers/Player/functions/initialization.ts +++ b/src/providers/Player/functions/initialization.ts @@ -1,23 +1,39 @@ import { isUndefined } from 'lodash' import { getActiveIndex, getCurrentTrack, getPlayQueue } from '.' import TrackPlayer from 'react-native-track-player' +import { usePlayerQueueStore } from '../../../stores/player/queue' export default async function Initialize() { - const storedPlayQueue = getPlayQueue() - const storedIndex = getActiveIndex() - const storedTrack = getCurrentTrack() + const { + queue: persistedQueue, + currentIndex: persistedIndex, + currentTrack: persistedTrack, + } = usePlayerQueueStore.getState() + + const storedPlayQueue = persistedQueue.length > 0 ? persistedQueue : getPlayQueue() + const storedIndex = persistedIndex ?? getActiveIndex() + const storedTrack = persistedTrack ?? getCurrentTrack() console.debug( `StoredIndex: ${storedIndex}, storedPlayQueue: ${storedPlayQueue?.map((track, index) => index)}, track: ${storedTrack?.item.Id}`, ) - if (!isUndefined(storedPlayQueue) && !isUndefined(storedIndex)) { + if ( + Array.isArray(storedPlayQueue) && + storedPlayQueue.length > 0 && + !isUndefined(storedIndex) && + storedIndex !== null + ) { console.debug('Initializing play queue from storage') await TrackPlayer.reset() await TrackPlayer.add(storedPlayQueue) await TrackPlayer.skip(storedIndex) + usePlayerQueueStore.getState().setQueue(storedPlayQueue) + usePlayerQueueStore.getState().setCurrentIndex(storedIndex) + usePlayerQueueStore.getState().setCurrentTrack(storedPlayQueue[storedIndex] ?? null) + console.debug('Initialized play queue from storage') } } diff --git a/src/providers/Player/functions/queries.ts b/src/providers/Player/functions/queries.ts index b9db9749..6d534032 100644 --- a/src/providers/Player/functions/queries.ts +++ b/src/providers/Player/functions/queries.ts @@ -6,15 +6,28 @@ import { REPEAT_MODE_QUERY, } from '../constants/queries' import TrackPlayer from 'react-native-track-player' +import { usePlayerQueueStore } from '../../../stores/player/queue' +import JellifyTrack from '../../../types/JellifyTrack' -export function refetchActiveIndex(): void { - queryClient.refetchQueries(CURRENT_INDEX_QUERY) +export async function refetchActiveIndex(): Promise { + await queryClient.refetchQueries(CURRENT_INDEX_QUERY) + + const activeIndex = await TrackPlayer.getActiveTrackIndex() + usePlayerQueueStore.getState().setCurrentIndex(activeIndex ?? null) } -export function refetchNowPlaying(): void { - queryClient.refetchQueries(NOW_PLAYING_QUERY) +export async function refetchNowPlaying(): Promise { + await queryClient.refetchQueries(NOW_PLAYING_QUERY) - refetchActiveIndex() + const [activeTrack, queue] = await Promise.all([ + TrackPlayer.getActiveTrack(), + TrackPlayer.getQueue(), + ]) + + usePlayerQueueStore.getState().setCurrentTrack((activeTrack as JellifyTrack) ?? null) + usePlayerQueueStore.getState().setQueue(queue as JellifyTrack[]) + + await refetchActiveIndex() } /** @@ -25,10 +38,10 @@ export function refetchNowPlaying(): void { * Under the hood, this will refetch the active queue from the {@link TrackPlayer} * and the currently playing track */ -export function refetchPlayerQueue(): void { - queryClient.refetchQueries(QUEUE_QUERY) +export async function refetchPlayerQueue(): Promise { + await queryClient.refetchQueries(QUEUE_QUERY) - refetchNowPlaying() + await refetchNowPlaying() } export function invalidateRepeatMode(): void { diff --git a/src/providers/Player/functions/queue.ts b/src/providers/Player/functions/queue.ts index 2d2803cf..6469c941 100644 --- a/src/providers/Player/functions/queue.ts +++ b/src/providers/Player/functions/queue.ts @@ -81,6 +81,10 @@ export async function loadQueue({ await TrackPlayer.setQueue(queue) + usePlayerQueueStore.getState().setQueue(queue) + usePlayerQueueStore.getState().setCurrentIndex(finalStartIndex) + usePlayerQueueStore.getState().setCurrentTrack(queue[finalStartIndex] ?? null) + console.debug( `Queued ${queue.length} tracks, starting at ${finalStartIndex}${shuffled ? ' (shuffled)' : ''}`, ) @@ -117,6 +121,9 @@ export const playNextInQueue = async ({ // Then update RNTP await TrackPlayer.add(tracksToPlayNext, (currentIndex ?? 0) + 1) + const updatedQueue = (await TrackPlayer.getQueue()) as JellifyTrack[] + usePlayerQueueStore.getState().setQueue(updatedQueue) + // Add to the state unshuffled queue, using the currently playing track as the index usePlayerQueueStore .getState() @@ -161,6 +168,9 @@ export const playLaterInQueue = async ({ // Then update RNTP await TrackPlayer.add(newTracks) + const updatedQueue = (await TrackPlayer.getQueue()) as JellifyTrack[] + usePlayerQueueStore.getState().setQueue(updatedQueue) + // Update unshuffled queue with the same mapped tracks to avoid duplication usePlayerQueueStore .getState() diff --git a/src/providers/Player/hooks/mutations.ts b/src/providers/Player/hooks/mutations.ts index 67d533d7..97b94ab2 100644 --- a/src/providers/Player/hooks/mutations.ts +++ b/src/providers/Player/hooks/mutations.ts @@ -4,12 +4,7 @@ import { loadQueue, playLaterInQueue, playNextInQueue } from '../functions/queue import { isUndefined } from 'lodash' import { previous, skip } from '../functions/controls' import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from '../interfaces' -import { - refetchNowPlaying, - refetchPlayerQueue, - invalidateRepeatMode, - refetchActiveIndex, -} from '../functions/queries' +import { refetchNowPlaying, refetchPlayerQueue, invalidateRepeatMode } from '../functions/queries' import { QueuingType } from '../../../enums/queuing-type' import Toast from 'react-native-toast-message' import { handleDeshuffle, handleShuffle } from '../functions/shuffle' @@ -232,7 +227,7 @@ export const useLoadNewQueue = () => { queryClient.setQueryData(NOW_PLAYING_QUERY_KEY, tracks[finalStartIndex]) usePlayerQueueStore.getState().setQueueRef(variables.queue) - refetchPlayerQueue() + await refetchPlayerQueue() }, [isCasting, remoteClient, navigation, downloadedTracks, trigger], ) @@ -246,7 +241,7 @@ export const usePrevious = () => { await previous() console.debug('Skipped to previous track') - refetchNowPlaying() + await refetchNowPlaying() }, [trigger]) } @@ -262,7 +257,7 @@ export const useSkip = () => { ) skip(index) console.debug('Skipped to next track') - refetchNowPlaying() + await refetchNowPlaying() }, [trigger], ) @@ -330,6 +325,9 @@ export const useResetQueue = () => usePlayerQueueStore.getState().setUnshuffledQueue([]) usePlayerQueueStore.getState().setShuffled(false) usePlayerQueueStore.getState().setQueueRef('Recently Played') + usePlayerQueueStore.getState().setQueue([]) + usePlayerQueueStore.getState().setCurrentTrack(null) + usePlayerQueueStore.getState().setCurrentIndex(null) await TrackPlayer.reset() }, onSettled: refetchPlayerQueue, @@ -351,7 +349,7 @@ export const useToggleShuffle = () => { }, onSuccess: async (_, shuffled) => { usePlayerQueueStore.getState().setShuffled(!shuffled) - refetchPlayerQueue() + await refetchPlayerQueue() }, }) } diff --git a/src/providers/Player/hooks/queries.ts b/src/providers/Player/hooks/queries.ts index 2497fa89..6c3dca7a 100644 --- a/src/providers/Player/hooks/queries.ts +++ b/src/providers/Player/hooks/queries.ts @@ -96,7 +96,9 @@ export const usePlaybackState = (): State | undefined => { useMemo(() => { if (client && isCasting) { client.onMediaStatusUpdated((status) => { - status?.playerState && setPlaybackState(castToRNTPState(status.playerState)) + if (status?.playerState) { + setPlaybackState(castToRNTPState(status.playerState)) + } }) } else { setPlaybackState(state) diff --git a/src/stores/player/queue.ts b/src/stores/player/queue.ts index f0f5d159..dceec65e 100644 --- a/src/stores/player/queue.ts +++ b/src/stores/player/queue.ts @@ -1,7 +1,8 @@ import { Queue } from '@/src/player/types/queue-item' import JellifyTrack from '@/src/types/JellifyTrack' -import { devtools, persist } from 'zustand/middleware' -import { create } from 'zustand/react' +import { stateStorage } from '../../constants/storage' +import { create } from 'zustand' +import { createJSONStorage, devtools, persist } from 'zustand/middleware' type PlayerQueueStore = { shuffled: boolean @@ -12,6 +13,15 @@ type PlayerQueueStore = { unShuffledQueue: JellifyTrack[] setUnshuffledQueue: (unShuffledQueue: JellifyTrack[]) => void + + queue: JellifyTrack[] + setQueue: (queue: JellifyTrack[]) => void + + currentTrack: JellifyTrack | null + setCurrentTrack: (track: JellifyTrack | null) => void + + currentIndex: number | null + setCurrentIndex: (index: number | null) => void } export const usePlayerQueueStore = create()( @@ -32,9 +42,28 @@ export const usePlayerQueueStore = create()( set({ unShuffledQueue, }), + + queue: [], + setQueue: (queue: JellifyTrack[]) => + set({ + queue, + }), + + currentTrack: null, + setCurrentTrack: (currentTrack: JellifyTrack | null) => + set({ + currentTrack, + }), + + currentIndex: null, + setCurrentIndex: (currentIndex: number | null) => + set({ + currentIndex, + }), }), { name: 'player-queue-storage', + storage: createJSONStorage(() => stateStorage), }, ), ), @@ -43,3 +72,7 @@ export const usePlayerQueueStore = create()( export const useShuffle = () => usePlayerQueueStore((state) => state.shuffled) export const useQueueRef = () => usePlayerQueueStore((state) => state.queueRef) + +export const useCurrentTrack = () => usePlayerQueueStore((state) => state.currentTrack) + +export const useCurrentIndex = () => usePlayerQueueStore((state) => state.currentIndex)