Upgrade all dependencies
(1) react-native-track-player to v2 (2) react-navigation to v6 (3) react-native to v0.66.4
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import TrackPlayer, { Capability } from 'react-native-track-player';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import Routes from '../screens';
|
||||
import store, { persistedStore } from 'store';
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { ColorSchemeContext, themes } from './Colors';
|
||||
// import ErrorReportingAlert from 'utility/ErrorReportingAlert';
|
||||
import PlayerStateUpdater from './PlayerStateUpdater';
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
const colorScheme = useColorScheme();
|
||||
@@ -24,12 +23,12 @@ export default function App(): JSX.Element {
|
||||
await TrackPlayer.setupPlayer();
|
||||
await TrackPlayer.updateOptions({
|
||||
capabilities: [
|
||||
TrackPlayer.CAPABILITY_PLAY,
|
||||
TrackPlayer.CAPABILITY_PAUSE,
|
||||
TrackPlayer.CAPABILITY_SKIP_TO_NEXT,
|
||||
TrackPlayer.CAPABILITY_SKIP_TO_PREVIOUS,
|
||||
TrackPlayer.CAPABILITY_STOP,
|
||||
TrackPlayer.CAPABILITY_SEEK_TO,
|
||||
Capability.Play,
|
||||
Capability.Pause,
|
||||
Capability.SkipToNext,
|
||||
Capability.SkipToPrevious,
|
||||
Capability.Stop,
|
||||
Capability.SeekTo,
|
||||
]
|
||||
});
|
||||
}
|
||||
@@ -42,7 +41,6 @@ export default function App(): JSX.Element {
|
||||
<ColorSchemeContext.Provider value={theme}>
|
||||
<NavigationContainer theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Routes />
|
||||
<PlayerStateUpdater />
|
||||
</NavigationContainer>
|
||||
</ColorSchemeContext.Provider>
|
||||
</PersistGate>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import TrackPlayer, { TrackPlayerEvents } from 'react-native-track-player';
|
||||
import { shallowEqual, useDispatch } from 'react-redux';
|
||||
import { useTypedSelector } from 'store';
|
||||
import player from 'store/player';
|
||||
|
||||
function PlayerStateUpdater() {
|
||||
const dispatch = useDispatch();
|
||||
const trackId = useTypedSelector(state => state.player.currentTrack?.id, shallowEqual);
|
||||
|
||||
const handleUpdate = useCallback(async () => {
|
||||
const currentTrackId = await TrackPlayer.getCurrentTrack();
|
||||
|
||||
// GUARD: Only retrieve new track if it is different from the one we
|
||||
// have currently in state.
|
||||
if (currentTrackId === trackId){
|
||||
return;
|
||||
}
|
||||
|
||||
// GUARD: Only fetch current track if there is a current track
|
||||
if (!currentTrackId) {
|
||||
dispatch(player.actions.setCurrentTrack(undefined));
|
||||
}
|
||||
|
||||
// If it is different, retrieve the track and save it
|
||||
try {
|
||||
const currentTrack = await TrackPlayer.getTrack(currentTrackId);
|
||||
dispatch(player.actions.setCurrentTrack(currentTrack));
|
||||
} catch {
|
||||
// Due to the async nature, a track might be removed at the
|
||||
// point when we try to retrieve it. If this happens, we'll just
|
||||
// smother the error and wait for a new track update to
|
||||
// finish.
|
||||
}
|
||||
}, [trackId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
function handler() {
|
||||
handleUpdate();
|
||||
}
|
||||
|
||||
handler();
|
||||
|
||||
const subscription = TrackPlayer.addEventListener(TrackPlayerEvents.PLAYBACK_TRACK_CHANGED, handler);
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default PlayerStateUpdater;
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { PropsWithChildren, useCallback } from 'react';
|
||||
import { Pressable, ViewStyle } from 'react-native';
|
||||
|
||||
interface TouchableHandlerProps {
|
||||
id: string;
|
||||
onPress: (id: string) => void;
|
||||
onLongPress?: (id: string) => void;
|
||||
interface TouchableHandlerProps<T = number> {
|
||||
id: T;
|
||||
onPress: (id: T) => void;
|
||||
onLongPress?: (id: T) => void;
|
||||
}
|
||||
|
||||
function TouchableStyles({ pressed }: { pressed: boolean }): ViewStyle {
|
||||
@@ -19,7 +19,12 @@ function TouchableStyles({ pressed }: { pressed: boolean }): ViewStyle {
|
||||
* This is a generic handler that accepts id as a prop, and return it when it is
|
||||
* pressed. This comes in handy with lists in which albums / tracks need to be selected.
|
||||
*/
|
||||
const TouchableHandler: React.FC<TouchableHandlerProps> = ({ id, onPress, onLongPress, children }) => {
|
||||
function TouchableHandler<T>({
|
||||
id,
|
||||
onPress,
|
||||
onLongPress,
|
||||
children
|
||||
}: PropsWithChildren<TouchableHandlerProps<T>>): JSX.Element {
|
||||
const handlePress = useCallback(() => {
|
||||
return onPress(id);
|
||||
}, [id, onPress]);
|
||||
@@ -37,6 +42,6 @@ const TouchableHandler: React.FC<TouchableHandlerProps> = ({ id, onPress, onLon
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default TouchableHandler;
|
||||
@@ -72,27 +72,20 @@ const Album: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const getImage = useGetImage();
|
||||
const playAlbum = usePlayAlbum();
|
||||
const currentTrack = useCurrentTrack();
|
||||
const { track: currentTrack } = useCurrentTrack();
|
||||
const navigation = useNavigation();
|
||||
|
||||
// Setup callbacks
|
||||
const selectAlbum = useCallback(() => { playAlbum(id); }, [playAlbum, id]);
|
||||
const refresh = useCallback(() => { dispatch(fetchTracksByAlbum(id)); }, [id, dispatch]);
|
||||
const selectTrack = useCallback(async (trackId) => {
|
||||
const tracks = await playAlbum(id, false);
|
||||
|
||||
if (tracks) {
|
||||
const track = tracks.find((t) => t.id.startsWith(trackId));
|
||||
|
||||
if (track) {
|
||||
await TrackPlayer.skip(track.id);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
}
|
||||
const selectTrack = useCallback(async (index: number) => {
|
||||
await playAlbum(id, false);
|
||||
await TrackPlayer.skip(index);
|
||||
await TrackPlayer.play();
|
||||
}, [playAlbum, id]);
|
||||
const longPressTrack = useCallback((trackId: string) => {
|
||||
navigation.navigate('TrackPopupMenu', { trackId });
|
||||
}, [navigation]);
|
||||
const longPressTrack = useCallback((index: number) => {
|
||||
navigation.navigate('TrackPopupMenu', { trackId: album?.Tracks?.[index] });
|
||||
}, [navigation, album]);
|
||||
|
||||
// Retrieve album tracks on load
|
||||
useEffect(() => {
|
||||
@@ -118,14 +111,14 @@ const Album: React.FC = () => {
|
||||
<Text style={[ defaultStyles.text, styles.artist ]}>{album?.AlbumArtist}</Text>
|
||||
<Button title={t('play-album')} icon={Play} onPress={selectAlbum} />
|
||||
<View style={{ marginTop: 15 }}>
|
||||
{album?.Tracks?.length ? album.Tracks.map((trackId) =>
|
||||
{album?.Tracks?.length ? album.Tracks.map((trackId, i) =>
|
||||
<TouchableHandler
|
||||
key={trackId}
|
||||
id={trackId}
|
||||
id={i}
|
||||
onPress={selectTrack}
|
||||
onLongPress={longPressTrack}
|
||||
>
|
||||
<TrackContainer isPlaying={currentTrack?.id.startsWith(trackId) || false} style={defaultStyles.border}>
|
||||
<TrackContainer isPlaying={currentTrack?.backendId === trackId || false} style={defaultStyles.border}>
|
||||
<Text style={[ defaultStyles.text, styles.index ]}>
|
||||
{tracks[trackId]?.IndexNumber}
|
||||
</Text>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useGetImage } from 'utility/JellyfinApi';
|
||||
import { Album, NavigationProp } from '../types';
|
||||
import { NavigationProp } from '../types';
|
||||
import { Text, SafeAreaView, FlatList, StyleSheet } from 'react-native';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
@@ -14,6 +14,7 @@ import { Header } from 'components/Typography';
|
||||
import ListButton from 'components/ListButton';
|
||||
import { t } from '@localisation';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import { Album } from 'store/music/types';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
columnWrapper: {
|
||||
|
||||
@@ -226,7 +226,7 @@ export default function Search() {
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHandler id={album.Id} onPress={selectAlbum}>
|
||||
<TouchableHandler<string> id={album.Id} onPress={selectAlbum}>
|
||||
<SearchResult style={defaultStyles.border}>
|
||||
<AlbumImage source={{ uri: getImage(album.Id) }} />
|
||||
<View>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { Album } from 'store/music/types';
|
||||
|
||||
export type StackParams = {
|
||||
[key: string]: Record<string, unknown> | undefined;
|
||||
Albums: undefined;
|
||||
Album: { id: string, album: Album };
|
||||
RecentAlbums: undefined;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import TrackPlayer, { usePlaybackState, STATE_PLAYING, STATE_PAUSED } from 'react-native-track-player';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import TrackPlayer, { Event, State, usePlaybackState, useTrackPlayerEvents } from 'react-native-track-player';
|
||||
import { TouchableOpacity, useColorScheme } from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
import { useHasQueue } from 'utility/useQueue';
|
||||
@@ -84,16 +84,12 @@ export function NextButton({ fill }: { fill: string }) {
|
||||
export function RepeatButton({ fill }: { fill: string}) {
|
||||
const [isRepeating, setRepeating] = useState(false);
|
||||
const handlePress = useCallback(() => setRepeating(!isRepeating), [isRepeating, setRepeating]);
|
||||
const listener = useRef<TrackPlayer.EmitterSubscription | null>(null);
|
||||
|
||||
// The callback that should determine whether we need to repeeat or not
|
||||
const handleEndEvent = useCallback(async () => {
|
||||
useTrackPlayerEvents([Event.PlaybackQueueEnded], async () => {
|
||||
if (isRepeating) {
|
||||
// Retrieve all current tracks
|
||||
const tracks = await TrackPlayer.getQueue();
|
||||
|
||||
// Then skip to the first track
|
||||
await TrackPlayer.skip(tracks[0].id);
|
||||
// Skip to the first track
|
||||
await TrackPlayer.skip(0);
|
||||
|
||||
// Cautiously reset the seek time, as there might only be a single
|
||||
// item in queue.
|
||||
@@ -102,19 +98,7 @@ export function RepeatButton({ fill }: { fill: string}) {
|
||||
// Then play the item
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
}, [isRepeating]);
|
||||
|
||||
// Subscribe to ended event handler so that we can restart the queue from
|
||||
// the start if looping is enabled
|
||||
useEffect(() => {
|
||||
// Set the event listener
|
||||
listener.current = TrackPlayer.addEventListener('playback-queue-ended', handleEndEvent);
|
||||
|
||||
// Then clean up after
|
||||
return function cleanup() {
|
||||
listener?.current?.remove();
|
||||
};
|
||||
}, [handleEndEvent]);
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={handlePress} style={{ opacity: isRepeating ? 1 : 0.5 }}>
|
||||
@@ -146,13 +130,13 @@ export function MainButton({ fill }: { fill: string }) {
|
||||
const state = usePlaybackState();
|
||||
|
||||
switch (state) {
|
||||
case STATE_PLAYING:
|
||||
case State.Playing:
|
||||
return (
|
||||
<TouchableOpacity onPress={pause}>
|
||||
<PauseIcon width={BUTTON_SIZE} height={BUTTON_SIZE} fill={fill} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
case STATE_PAUSED:
|
||||
case State.Paused:
|
||||
return (
|
||||
<TouchableOpacity onPress={play}>
|
||||
<PlayIcon width={BUTTON_SIZE} height={BUTTON_SIZE} fill={fill} />
|
||||
|
||||
@@ -32,7 +32,7 @@ const styles = StyleSheet.create({
|
||||
|
||||
|
||||
export default function NowPlaying() {
|
||||
const track = useCurrentTrack();
|
||||
const { track } = useCurrentTrack();
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
return (
|
||||
@@ -40,7 +40,7 @@ export default function NowPlaying() {
|
||||
<Artwork
|
||||
style={defaultStyles.imageBackground}
|
||||
source={{
|
||||
uri: track?.artwork,
|
||||
uri: track?.artwork as string | undefined,
|
||||
priority: FastImage.priority.high,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -38,10 +38,9 @@ const styles = StyleSheet.create({
|
||||
export default function Queue() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const queue = useQueue();
|
||||
const currentTrack = useCurrentTrack();
|
||||
const currentIndex = queue.findIndex(d => d.id === currentTrack?.id);
|
||||
const playTrack = useCallback(async (trackId: string) => {
|
||||
await TrackPlayer.skip(trackId);
|
||||
const { index: currentIndex } = useCurrentTrack();
|
||||
const playTrack = useCallback(async (index: number) => {
|
||||
await TrackPlayer.skip(index);
|
||||
await TrackPlayer.play();
|
||||
}, []);
|
||||
const clearQueue = useCallback(async () => {
|
||||
@@ -52,14 +51,14 @@ export default function Queue() {
|
||||
<View>
|
||||
<Text style={{ marginTop: 20, marginBottom: 20 }}>{t('queue')}</Text>
|
||||
{queue.map((track, i) => (
|
||||
<TouchableHandler id={track.id} onPress={playTrack} key={i}>
|
||||
<TouchableHandler id={i} onPress={playTrack} key={i}>
|
||||
<QueueItem
|
||||
active={currentTrack?.id === track.id}
|
||||
active={currentIndex === i}
|
||||
key={i}
|
||||
alreadyPlayed={i < currentIndex}
|
||||
alreadyPlayed={currentIndex ? i < currentIndex : false}
|
||||
style={[
|
||||
defaultStyles.border,
|
||||
currentTrack?.id === track.id ? defaultStyles.activeBackground : {},
|
||||
currentIndex === i ? defaultStyles.activeBackground : {},
|
||||
]}
|
||||
>
|
||||
<Text style={styles.trackTitle}>{track.title}</Text>
|
||||
|
||||
@@ -62,12 +62,11 @@ function Screens() {
|
||||
}
|
||||
|
||||
return <Icon fill={color} width={size} height={size} />;
|
||||
}
|
||||
},
|
||||
tabBarActiveTintColor: THEME_COLOR,
|
||||
tabBarInactiveTintColor: 'gray',
|
||||
headerShown: false,
|
||||
})}
|
||||
tabBarOptions={{
|
||||
activeTintColor: THEME_COLOR,
|
||||
inactiveTintColor: 'gray',
|
||||
}}
|
||||
>
|
||||
<Tab.Screen name="NowPlaying" component={Player} options={{ tabBarLabel: t('now-playing') }} />
|
||||
<Tab.Screen name="Music" component={Music} options={{ tabBarLabel: t('music') }} />
|
||||
@@ -85,10 +84,12 @@ type Routes = {
|
||||
|
||||
export default function Routes() {
|
||||
return (
|
||||
<Stack.Navigator mode="modal" headerMode="none" screenOptions={{
|
||||
<Stack.Navigator screenOptions={{
|
||||
cardStyle: {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
},
|
||||
presentation: 'modal',
|
||||
headerShown: false,
|
||||
}}>
|
||||
<Stack.Screen name="Screens" component={Screens} />
|
||||
<Stack.Screen name="SetJellyfinServer" component={SetJellyfinServer} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface ModalStackParams {
|
||||
[key: string]: Record<string, unknown> | undefined;
|
||||
SetJellyfinServer: undefined;
|
||||
TrackPopupMenu: { trackId: string };
|
||||
}
|
||||
@@ -13,11 +13,9 @@ const persistConfig: PersistConfig<AppState> = {
|
||||
|
||||
import settings from './settings';
|
||||
import music from './music';
|
||||
import player from './player';
|
||||
|
||||
const reducers = combineReducers({
|
||||
settings,
|
||||
player: player.reducer,
|
||||
music: music.reducer,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { Track } from 'react-native-track-player';
|
||||
|
||||
interface State {
|
||||
addedTrackCount: number,
|
||||
currentTrack: Track | undefined,
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
addedTrackCount: 0,
|
||||
currentTrack: undefined,
|
||||
};
|
||||
|
||||
const player = createSlice({
|
||||
name: 'player',
|
||||
initialState,
|
||||
reducers: {
|
||||
addNewTrackToPlayer: (state) => {
|
||||
state.addedTrackCount += 1;
|
||||
},
|
||||
setCurrentTrack: (state, action: PayloadAction<Track | undefined>) => {
|
||||
state.currentTrack = action.payload;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export default player;
|
||||
43
src/utility/AddedTrackEvents.ts
Normal file
43
src/utility/AddedTrackEvents.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import { useEffect } from 'react';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
const eventName = 'track-added';
|
||||
const addedTrackEmitter = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Emit the event that a track has been added
|
||||
*/
|
||||
export function emitTrackAdded() {
|
||||
addedTrackEmitter.emit(eventName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the callback whenever a track has been added to the queue
|
||||
*/
|
||||
export function onTrackAdded(callback: () => void) {
|
||||
addedTrackEmitter.addListener(eventName, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to manage the listeners for the added track function
|
||||
*/
|
||||
export function useOnTrackAdded(callback: () => void) {
|
||||
useEffect(() => {
|
||||
addedTrackEmitter.addListener(eventName, callback);
|
||||
return () => {
|
||||
addedTrackEmitter.removeListener(eventName, callback);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Monkey-patch the track-player to also emit track added events
|
||||
*/
|
||||
export function patchTrackPlayer() {
|
||||
const oldAddFunction = TrackPlayer.add;
|
||||
TrackPlayer.add = (...args: Parameters<typeof oldAddFunction>) => {
|
||||
emitTrackAdded();
|
||||
return oldAddFunction(...args);
|
||||
};
|
||||
}
|
||||
@@ -44,8 +44,8 @@ export function generateTrack(track: AlbumTrack, credentials: Credentials): Trac
|
||||
const url = encodeURI(`${credentials?.uri}/Audio/${track.Id}/universal?${trackParams}`);
|
||||
|
||||
return {
|
||||
id: track.Id,
|
||||
url,
|
||||
backendId: track.Id,
|
||||
title: track.Name,
|
||||
artist: track.Artists.join(', '),
|
||||
album: track.Album,
|
||||
|
||||
@@ -7,30 +7,30 @@
|
||||
* such as processing media buttons or analytics
|
||||
*/
|
||||
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import TrackPlayer, { Event } from 'react-native-track-player';
|
||||
|
||||
export default async function() {
|
||||
TrackPlayer.addEventListener('remote-play', () => {
|
||||
TrackPlayer.addEventListener(Event.RemotePlay, () => {
|
||||
TrackPlayer.play();
|
||||
});
|
||||
|
||||
TrackPlayer.addEventListener('remote-pause', () => {
|
||||
TrackPlayer.addEventListener(Event.RemotePause, () => {
|
||||
TrackPlayer.pause();
|
||||
});
|
||||
|
||||
TrackPlayer.addEventListener('remote-next', () => {
|
||||
TrackPlayer.addEventListener(Event.RemoteNext, () => {
|
||||
TrackPlayer.skipToNext();
|
||||
});
|
||||
|
||||
TrackPlayer.addEventListener('remote-previous', () => {
|
||||
TrackPlayer.addEventListener(Event.RemotePrevious, () => {
|
||||
TrackPlayer.skipToPrevious();
|
||||
});
|
||||
|
||||
TrackPlayer.addEventListener('remote-stop', () => {
|
||||
TrackPlayer.addEventListener(Event.RemoteStop, () => {
|
||||
TrackPlayer.destroy();
|
||||
});
|
||||
|
||||
TrackPlayer.addEventListener('remote-seek', (event) => {
|
||||
TrackPlayer.addEventListener(Event.RemoteSeek, (event) => {
|
||||
TrackPlayer.seekTo(event.position);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import { Track } from 'react-native-track-player';
|
||||
import { useTypedSelector } from 'store';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import TrackPlayer, { Event, Track, useTrackPlayerEvents } from 'react-native-track-player';
|
||||
|
||||
const idEqual = (left: Track | undefined, right: Track | undefined) => {
|
||||
return left?.id === right?.id;
|
||||
};
|
||||
interface CurrentTrackResponse {
|
||||
track: Track | undefined;
|
||||
index: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook retrieves the current playing track from TrackPlayer
|
||||
*/
|
||||
export default function useCurrentTrack(): Track | undefined {
|
||||
const track = useTypedSelector(state => state.player.currentTrack, idEqual);
|
||||
export default function useCurrentTrack(): CurrentTrackResponse {
|
||||
const [track, setTrack] = useState<Track | undefined>();
|
||||
const [index, setIndex] = useState<number | undefined>();
|
||||
|
||||
// Retrieve the current track from the queue using the index
|
||||
const retrieveCurrentTrack = useCallback(async () => {
|
||||
const queue = await TrackPlayer.getQueue();
|
||||
const currentTrackIndex = await TrackPlayer.getCurrentTrack();
|
||||
setTrack(queue[currentTrackIndex]);
|
||||
setIndex(currentTrackIndex);
|
||||
}, [setTrack, setIndex]);
|
||||
|
||||
// Then execute the function on component mount and track changes
|
||||
useEffect(() => { retrieveCurrentTrack(); }, [retrieveCurrentTrack]);
|
||||
useTrackPlayerEvents([ Event.PlaybackTrackChanged ], retrieveCurrentTrack);
|
||||
|
||||
return track;
|
||||
return { track, index };
|
||||
}
|
||||
@@ -2,55 +2,27 @@ import { useTypedSelector } from 'store';
|
||||
import { useCallback } from 'react';
|
||||
import TrackPlayer, { Track } from 'react-native-track-player';
|
||||
import { generateTrack } from './JellyfinApi';
|
||||
import useQueue from './useQueue';
|
||||
import player from 'store/player';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
/**
|
||||
* Generate a callback function that starts playing a full album given its
|
||||
* supplied id.
|
||||
*/
|
||||
export default function usePlayAlbum() {
|
||||
const dispatch = useDispatch();
|
||||
const credentials = useTypedSelector(state => state.settings.jellyfin);
|
||||
const albums = useTypedSelector(state => state.music.albums.entities);
|
||||
const tracks = useTypedSelector(state => state.music.tracks.entities);
|
||||
const queue = useQueue();
|
||||
|
||||
return useCallback(async function playAlbum(albumId: string, play = true): Promise<TrackPlayer.Track[] | undefined> {
|
||||
return useCallback(async function playAlbum(albumId: string, play: boolean = true): Promise<Track[] | undefined> {
|
||||
const album = albums[albumId];
|
||||
const trackIds = album?.Tracks;
|
||||
const backendTrackIds = album?.Tracks;
|
||||
|
||||
// GUARD: Check that the album actually has tracks
|
||||
if (!album || !trackIds?.length) {
|
||||
// GUARD: Check if the album has songs
|
||||
if (!backendTrackIds?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the queue already contains the consecutive track listing
|
||||
// that is described as part of the album
|
||||
const queuedAlbum = queue.reduce<TrackPlayer.Track[]>((sum, track) => {
|
||||
if (track.id.startsWith(trackIds[sum.length])) {
|
||||
sum.push(track);
|
||||
} else {
|
||||
sum = [];
|
||||
}
|
||||
|
||||
return sum;
|
||||
}, []);
|
||||
|
||||
// If the entire album is already in the queue, we can just return those
|
||||
// tracks, rather than adding it to the queue again.
|
||||
if (queuedAlbum.length === trackIds.length) {
|
||||
if (play) {
|
||||
await TrackPlayer.skip(trackIds[0]);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
|
||||
return queuedAlbum;
|
||||
}
|
||||
|
||||
// Convert all trackIds to the relevant format for react-native-track-player
|
||||
const newTracks = trackIds.map((trackId) => {
|
||||
// Convert all backendTrackIds to the relevant format for react-native-track-player
|
||||
const newTracks = backendTrackIds.map((trackId) => {
|
||||
const track = tracks[trackId];
|
||||
if (!trackId || !track) {
|
||||
return;
|
||||
@@ -60,17 +32,14 @@ export default function usePlayAlbum() {
|
||||
}).filter((t): t is Track => typeof t !== 'undefined');
|
||||
|
||||
// Clear the queue and add all tracks
|
||||
await TrackPlayer.removeUpcomingTracks();
|
||||
await TrackPlayer.reset();
|
||||
await TrackPlayer.add(newTracks);
|
||||
|
||||
// Then, we'll dispatch the added track event
|
||||
dispatch(player.actions.addNewTrackToPlayer());
|
||||
|
||||
// Play the queue
|
||||
if (play) {
|
||||
await TrackPlayer.skip(trackIds[0]);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
|
||||
return newTracks;
|
||||
}, [credentials, albums, tracks, queue, dispatch]);
|
||||
}, [credentials, albums, tracks]);
|
||||
}
|
||||
@@ -3,20 +3,17 @@ import TrackPlayer from 'react-native-track-player';
|
||||
import { useTypedSelector } from 'store';
|
||||
import { generateTrack } from './JellyfinApi';
|
||||
import useQueue from './useQueue';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import player from 'store/player';
|
||||
|
||||
/**
|
||||
* A hook that generates a callback that can setup and start playing a
|
||||
* particular trackId in the player.
|
||||
*/
|
||||
export default function usePlayTrack() {
|
||||
const dispatch = useDispatch();
|
||||
const credentials = useTypedSelector(state => state.settings.jellyfin);
|
||||
const tracks = useTypedSelector(state => state.music.tracks.entities);
|
||||
const queue = useQueue();
|
||||
|
||||
return useCallback(async function playTrack(trackId: string, play = true, addToEnd = true) {
|
||||
return useCallback(async function playTrack(trackId: string, play: boolean = true, addToEnd: boolean = true) {
|
||||
// Get the relevant track
|
||||
const track = tracks[trackId];
|
||||
|
||||
@@ -25,22 +22,21 @@ export default function usePlayTrack() {
|
||||
return;
|
||||
}
|
||||
|
||||
// GUARD: Check if the track is already in the queue
|
||||
const trackInstances = queue.filter((t) => t.id.startsWith(trackId));
|
||||
|
||||
// Generate the new track for the queue
|
||||
const newTrack = {
|
||||
...(trackInstances.length ? trackInstances[0] : generateTrack(track, credentials)),
|
||||
id: `${trackId}_${trackInstances.length}`
|
||||
};
|
||||
const newTrack = generateTrack(track, credentials);
|
||||
|
||||
// Then, we'll need to check where to add the track
|
||||
if (addToEnd) {
|
||||
await TrackPlayer.add([ newTrack ]);
|
||||
|
||||
// Then we'll skip to it and play it
|
||||
if (play) {
|
||||
await TrackPlayer.skip(await (await TrackPlayer.getQueue()).length);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
} else {
|
||||
// Try and locate the current track
|
||||
const currentTrackId = await TrackPlayer.getCurrentTrack();
|
||||
const currentTrackIndex = queue.findIndex(track => track.id === currentTrackId);
|
||||
const currentTrackIndex = await TrackPlayer.getCurrentTrack();
|
||||
|
||||
// Since the argument is the id to insert the track BEFORE, we need
|
||||
// to get the current track + 1
|
||||
@@ -51,17 +47,13 @@ export default function usePlayTrack() {
|
||||
// Depending on whether this track exists, we either add it there,
|
||||
// or at the end of the queue.
|
||||
await TrackPlayer.add([ newTrack ], targetTrack);
|
||||
}
|
||||
|
||||
// Then, we'll dispatch the added track event
|
||||
dispatch(player.actions.addNewTrackToPlayer());
|
||||
|
||||
// Then we'll skip to it and play it
|
||||
if (play) {
|
||||
await TrackPlayer.skip(newTrack.id);
|
||||
await TrackPlayer.play();
|
||||
if (play) {
|
||||
await TrackPlayer.skip(currentTrackIndex + 1);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
return newTrack;
|
||||
}, [credentials, tracks, queue, dispatch]);
|
||||
}, [credentials, tracks, queue]);
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import TrackPlayer, { usePlaybackState, Track } from 'react-native-track-player';
|
||||
import { useTypedSelector } from 'store';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import TrackPlayer, { Event, Track, useTrackPlayerEvents } from 'react-native-track-player';
|
||||
import { useOnTrackAdded } from './AddedTrackEvents';
|
||||
|
||||
/**
|
||||
* This hook retrieves the current playing track from TrackPlayer
|
||||
*/
|
||||
export default function useQueue(): Track[] {
|
||||
const state = usePlaybackState();
|
||||
const [queue, setQueue] = useState<Track[]>([]);
|
||||
const addedTrackCount = useTypedSelector(state => state.player.addedTrackCount);
|
||||
|
||||
useEffect(() => {
|
||||
TrackPlayer.getQueue().then(setQueue);
|
||||
}, [state, addedTrackCount]);
|
||||
// Define function that fetches the current queue
|
||||
const updateQueue = useCallback(() => TrackPlayer.getQueue().then(setQueue), [setQueue]);
|
||||
|
||||
// Then define the triggers for updating it
|
||||
useEffect(() => { updateQueue(); }, [updateQueue]);
|
||||
useTrackPlayerEvents([
|
||||
Event.PlaybackState,
|
||||
], updateQueue);
|
||||
useOnTrackAdded(updateQueue);
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user