mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 09:29:29 +01:00
rebuild queue with new urls on net state change
This commit is contained in:
parent
88d0c6089e
commit
44617740fd
@ -4,6 +4,7 @@ import android.app.Application;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import com.facebook.react.PackageList;
|
import com.facebook.react.PackageList;
|
||||||
import com.facebook.react.ReactApplication;
|
import com.facebook.react.ReactApplication;
|
||||||
|
import com.reactnativecommunity.netinfo.NetInfoPackage;
|
||||||
import com.oblador.vectoricons.VectorIconsPackage;
|
import com.oblador.vectoricons.VectorIconsPackage;
|
||||||
import com.rnfs.RNFSPackage;
|
import com.rnfs.RNFSPackage;
|
||||||
import com.facebook.react.ReactInstanceManager;
|
import com.facebook.react.ReactInstanceManager;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
rootProject.name = 'SubSonify'
|
rootProject.name = 'SubSonify'
|
||||||
|
include ':@react-native-community_netinfo'
|
||||||
|
project(':@react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
|
||||||
include ':react-native-vector-icons'
|
include ':react-native-vector-icons'
|
||||||
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
|
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
|
||||||
include ':react-native-fs'
|
include ':react-native-fs'
|
||||||
|
|||||||
@ -187,11 +187,12 @@ export const useSetQueue = () => {
|
|||||||
const setCurrentTrackIdx = useStore(selectTrackPlayer.setCurrentTrackIdx)
|
const setCurrentTrackIdx = useStore(selectTrackPlayer.setCurrentTrackIdx)
|
||||||
const setQueue = useStore(selectTrackPlayer.setQueue)
|
const setQueue = useStore(selectTrackPlayer.setQueue)
|
||||||
const setShuffleOrder = useStore(selectTrackPlayer.setShuffleOrder)
|
const setShuffleOrder = useStore(selectTrackPlayer.setShuffleOrder)
|
||||||
const setQueueName = useStore(selectTrackPlayer.setName)
|
const setQueueName = useStore(selectTrackPlayer.setQueueName)
|
||||||
const getQueueShuffled = useCallback(() => !!useStore.getState().shuffleOrder, [])
|
const getQueueShuffled = useCallback(() => !!useStore.getState().shuffleOrder, [])
|
||||||
const setQueueContextType = useStore(selectTrackPlayer.setQueueContextType)
|
const setQueueContextType = useStore(selectTrackPlayer.setQueueContextType)
|
||||||
const setQueueContextId = useStore(selectTrackPlayer.setQueueContextId)
|
const setQueueContextId = useStore(selectTrackPlayer.setQueueContextId)
|
||||||
const fetchCoverArtFilePath = useStore(selectCache.fetchCoverArtFilePath)
|
const fetchCoverArtFilePath = useStore(selectCache.fetchCoverArtFilePath)
|
||||||
|
const buildStreamUri = useStore(selectTrackPlayer.buildStreamUri)
|
||||||
|
|
||||||
return async (
|
return async (
|
||||||
songs: Song[],
|
songs: Song[],
|
||||||
@ -222,6 +223,14 @@ export const useSetQueue = () => {
|
|||||||
|
|
||||||
let queue = songs.map(s => mapSongToTrack(s, coverArtPaths))
|
let queue = songs.map(s => mapSongToTrack(s, coverArtPaths))
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const t of queue) {
|
||||||
|
t.url = buildStreamUri(t.id)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (shuffled) {
|
if (shuffled) {
|
||||||
const { tracks, shuffleOrder } = shuffleTracks(queue, playTrack)
|
const { tracks, shuffleOrder } = shuffleTracks(queue, playTrack)
|
||||||
setShuffleOrder(shuffleOrder)
|
setShuffleOrder(shuffleOrder)
|
||||||
|
|||||||
5
app/models/error.ts
Normal file
5
app/models/error.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class NoClientError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('no client in state')
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { getCurrentTrack, getPlayerState, TrackExt, trackPlayerCommands } from '
|
|||||||
import TrackPlayer, { Event, State } from 'react-native-track-player'
|
import TrackPlayer, { Event, State } from 'react-native-track-player'
|
||||||
import { useStore } from './state/store'
|
import { useStore } from './state/store'
|
||||||
import { unstable_batchedUpdates } from 'react-native'
|
import { unstable_batchedUpdates } from 'react-native'
|
||||||
|
import NetInfo, { NetInfoStateType } from '@react-native-community/netinfo'
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
unstable_batchedUpdates(() => {
|
unstable_batchedUpdates(() => {
|
||||||
@ -21,6 +22,12 @@ const setCurrentTrackIdx = (idx?: number) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setNetState = (netState: 'mobile' | 'wifi') => {
|
||||||
|
unstable_batchedUpdates(() => {
|
||||||
|
useStore.getState().setNetState(netState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let serviceCreated = false
|
let serviceCreated = false
|
||||||
const createService = async () => {
|
const createService = async () => {
|
||||||
useStore.subscribe(
|
useStore.subscribe(
|
||||||
@ -33,6 +40,14 @@ const createService = async () => {
|
|||||||
(prev, next) => prev?.id === next?.id,
|
(prev, next) => prev?.id === next?.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NetInfo.addEventListener(state => {
|
||||||
|
const currentType = useStore.getState().netState
|
||||||
|
const newType = state.type === NetInfoStateType.cellular ? 'mobile' : 'wifi'
|
||||||
|
if (currentType !== newType) {
|
||||||
|
setNetState(newType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
TrackPlayer.addEventListener(Event.RemoteStop, () => {
|
TrackPlayer.addEventListener(Event.RemoteStop, () => {
|
||||||
reset()
|
reset()
|
||||||
trackPlayerCommands.enqueue(TrackPlayer.destroy)
|
trackPlayerCommands.enqueue(TrackPlayer.destroy)
|
||||||
|
|||||||
@ -49,7 +49,7 @@ function getContextName(type?: QueueContextType) {
|
|||||||
const NowPlayingHeader = React.memo<{
|
const NowPlayingHeader = React.memo<{
|
||||||
track?: TrackExt
|
track?: TrackExt
|
||||||
}>(({ track }) => {
|
}>(({ track }) => {
|
||||||
const queueName = useStore(selectTrackPlayer.name)
|
const queueName = useStore(selectTrackPlayer.queueName)
|
||||||
const queueContextType = useStore(selectTrackPlayer.queueContextType)
|
const queueContextType = useStore(selectTrackPlayer.queueContextType)
|
||||||
|
|
||||||
if (!track) {
|
if (!track) {
|
||||||
|
|||||||
@ -35,19 +35,8 @@ export type MusicMapSlice = {
|
|||||||
mapPlaylistWithSongs: (playlist: PlaylistWithSongsElement) => Promise<PlaylistWithSongs>
|
mapPlaylistWithSongs: (playlist: PlaylistWithSongsElement) => Promise<PlaylistWithSongs>
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoClientError extends Error {
|
|
||||||
constructor() {
|
|
||||||
super('no client in state')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createMusicMapSlice = (set: SetState<Store>, get: GetState<Store>): MusicMapSlice => ({
|
export const createMusicMapSlice = (set: SetState<Store>, get: GetState<Store>): MusicMapSlice => ({
|
||||||
mapChildToSong: async child => {
|
mapChildToSong: async child => {
|
||||||
const client = get().client
|
|
||||||
if (!client) {
|
|
||||||
throw new NoClientError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
itemType: 'song',
|
itemType: 'song',
|
||||||
id: child.id,
|
id: child.id,
|
||||||
@ -60,7 +49,7 @@ export const createMusicMapSlice = (set: SetState<Store>, get: GetState<Store>):
|
|||||||
duration: child.duration,
|
duration: child.duration,
|
||||||
starred: child.starred,
|
starred: child.starred,
|
||||||
coverArt: await get().getAlbumCoverArt(child.albumId),
|
coverArt: await get().getAlbumCoverArt(child.albumId),
|
||||||
streamUri: client.streamUri({ id: child.id }),
|
streamUri: get().buildStreamUri(child.id),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { NoClientError } from '@app/models/error'
|
||||||
import PromiseQueue from '@app/util/PromiseQueue'
|
import PromiseQueue from '@app/util/PromiseQueue'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
|
import { ToastAndroid } from 'react-native'
|
||||||
import TrackPlayer, { RepeatMode, State, Track } from 'react-native-track-player'
|
import TrackPlayer, { RepeatMode, State, Track } from 'react-native-track-player'
|
||||||
import { GetState, SetState } from 'zustand'
|
import { GetState, SetState } from 'zustand'
|
||||||
import { Store } from './store'
|
import { Store } from './store'
|
||||||
@ -20,8 +22,8 @@ export type Progress = {
|
|||||||
export type QueueContextType = 'album' | 'playlist' | 'song' | 'artist'
|
export type QueueContextType = 'album' | 'playlist' | 'song' | 'artist'
|
||||||
|
|
||||||
export type TrackPlayerSlice = {
|
export type TrackPlayerSlice = {
|
||||||
name?: string
|
queueName?: string
|
||||||
setName: (name?: string) => void
|
setQueueName: (name?: string) => void
|
||||||
|
|
||||||
queueContextType?: QueueContextType
|
queueContextType?: QueueContextType
|
||||||
setQueueContextType: (queueContextType?: QueueContextType) => void
|
setQueueContextType: (queueContextType?: QueueContextType) => void
|
||||||
@ -50,12 +52,17 @@ export type TrackPlayerSlice = {
|
|||||||
|
|
||||||
scrobbleTrack: (id: string) => Promise<void>
|
scrobbleTrack: (id: string) => Promise<void>
|
||||||
|
|
||||||
|
netState: 'mobile' | 'wifi'
|
||||||
|
setNetState: (netState: 'mobile' | 'wifi') => Promise<void>
|
||||||
|
|
||||||
|
buildStreamUri: (id: string) => string
|
||||||
|
|
||||||
reset: () => void
|
reset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectTrackPlayer = {
|
export const selectTrackPlayer = {
|
||||||
name: (store: TrackPlayerSlice) => store.name,
|
queueName: (store: TrackPlayerSlice) => store.queueName,
|
||||||
setName: (store: TrackPlayerSlice) => store.setName,
|
setQueueName: (store: TrackPlayerSlice) => store.setQueueName,
|
||||||
|
|
||||||
queueContextType: (store: TrackPlayerSlice) => store.queueContextType,
|
queueContextType: (store: TrackPlayerSlice) => store.queueContextType,
|
||||||
setQueueContextType: (store: TrackPlayerSlice) => store.setQueueContextType,
|
setQueueContextType: (store: TrackPlayerSlice) => store.setQueueContextType,
|
||||||
@ -85,14 +92,17 @@ export const selectTrackPlayer = {
|
|||||||
|
|
||||||
scrobbleTrack: (store: TrackPlayerSlice) => store.scrobbleTrack,
|
scrobbleTrack: (store: TrackPlayerSlice) => store.scrobbleTrack,
|
||||||
|
|
||||||
|
setNetState: (store: TrackPlayerSlice) => store.setNetState,
|
||||||
|
buildStreamUri: (store: TrackPlayerSlice) => store.buildStreamUri,
|
||||||
|
|
||||||
reset: (store: TrackPlayerSlice) => store.reset,
|
reset: (store: TrackPlayerSlice) => store.reset,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const trackPlayerCommands = new PromiseQueue(1)
|
export const trackPlayerCommands = new PromiseQueue(1)
|
||||||
|
|
||||||
export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store>): TrackPlayerSlice => ({
|
export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store>): TrackPlayerSlice => ({
|
||||||
name: undefined,
|
queueName: undefined,
|
||||||
setName: name => set({ name }),
|
setQueueName: name => set({ queueName: name }),
|
||||||
|
|
||||||
queueContextType: undefined,
|
queueContextType: undefined,
|
||||||
setQueueContextType: queueContextType => set({ queueContextType }),
|
setQueueContextType: queueContextType => set({ queueContextType }),
|
||||||
@ -141,9 +151,73 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
|
|||||||
} catch {}
|
} catch {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
netState: 'mobile',
|
||||||
|
setNetState: async netState => {
|
||||||
|
if (netState === get().netState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set({ netState })
|
||||||
|
ToastAndroid.show('switched netState to ' + netState, ToastAndroid.SHORT)
|
||||||
|
|
||||||
|
await trackPlayerCommands.enqueue(async () => {
|
||||||
|
const queue = await getQueue()
|
||||||
|
if (!queue.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTrack = await getCurrentTrack()
|
||||||
|
const state = await getPlayerState()
|
||||||
|
const position = (await TrackPlayer.getPosition()) || 0
|
||||||
|
|
||||||
|
const queueName = get().queueName
|
||||||
|
const queueContextId = get().queueContextId
|
||||||
|
const queueContextType = get().queueContextType
|
||||||
|
|
||||||
|
await TrackPlayer.reset()
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const track of queue) {
|
||||||
|
track.url = get().buildStreamUri(track.id)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
queue,
|
||||||
|
queueName,
|
||||||
|
queueContextId,
|
||||||
|
queueContextType,
|
||||||
|
})
|
||||||
|
get().setCurrentTrackIdx(currentTrack)
|
||||||
|
|
||||||
|
await TrackPlayer.add(queue)
|
||||||
|
if (currentTrack) {
|
||||||
|
await TrackPlayer.skip(currentTrack)
|
||||||
|
}
|
||||||
|
await TrackPlayer.seekTo(position)
|
||||||
|
if (state === State.Playing) {
|
||||||
|
await TrackPlayer.play()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
buildStreamUri: id => {
|
||||||
|
const client = get().client
|
||||||
|
if (!client) {
|
||||||
|
throw new NoClientError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.streamUri({
|
||||||
|
id,
|
||||||
|
estimateContentLength: get().settings.estimateContentLength,
|
||||||
|
maxBitRate: get().netState === 'mobile' ? get().settings.maxBitrateMobile : get().settings.maxBitrateWifi,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
reset: () => {
|
reset: () => {
|
||||||
set({
|
set({
|
||||||
name: undefined,
|
queueName: undefined,
|
||||||
queueContextType: undefined,
|
queueContextType: undefined,
|
||||||
queueContextId: undefined,
|
queueContextId: undefined,
|
||||||
shuffleOrder: undefined,
|
shuffleOrder: undefined,
|
||||||
|
|||||||
@ -16,6 +16,8 @@ target 'SubSonify' do
|
|||||||
|
|
||||||
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
|
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
|
||||||
|
|
||||||
|
pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo'
|
||||||
|
|
||||||
target 'SubSonifyTests' do
|
target 'SubSonifyTests' do
|
||||||
inherit! :complete
|
inherit! :complete
|
||||||
# Pods for testing
|
# Pods for testing
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"@react-native-async-storage/async-storage": "^1.15.5",
|
"@react-native-async-storage/async-storage": "^1.15.5",
|
||||||
"@react-native-community/hooks": "^2.6.0",
|
"@react-native-community/hooks": "^2.6.0",
|
||||||
"@react-native-community/masked-view": "^0.1.11",
|
"@react-native-community/masked-view": "^0.1.11",
|
||||||
|
"@react-native-community/netinfo": "^6.0.0",
|
||||||
"@react-native-community/slider": "^3.0.3",
|
"@react-native-community/slider": "^3.0.3",
|
||||||
"@react-navigation/bottom-tabs": "^5.11.11",
|
"@react-navigation/bottom-tabs": "^5.11.11",
|
||||||
"@react-navigation/material-top-tabs": "^5.3.15",
|
"@react-navigation/material-top-tabs": "^5.3.15",
|
||||||
|
|||||||
@ -1118,6 +1118,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.11.tgz#2f4c6e10bee0786abff4604e39a37ded6f3980ce"
|
resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.11.tgz#2f4c6e10bee0786abff4604e39a37ded6f3980ce"
|
||||||
integrity sha512-rQfMIGSR/1r/SyN87+VD8xHHzDYeHaJq6elOSCAD+0iLagXkSI2pfA0LmSXP21uw5i3em7GkkRjfJ8wpqWXZNw==
|
integrity sha512-rQfMIGSR/1r/SyN87+VD8xHHzDYeHaJq6elOSCAD+0iLagXkSI2pfA0LmSXP21uw5i3em7GkkRjfJ8wpqWXZNw==
|
||||||
|
|
||||||
|
"@react-native-community/netinfo@^6.0.0":
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-6.0.0.tgz#2a4d7190b508dd0c2293656c9c1aa068f6f60a71"
|
||||||
|
integrity sha512-Z9M8VGcF2IZVOo2x+oUStvpCW/8HjIRi4+iQCu5n+PhC7OqCQX58KYAzdBr///alIfRXiu6oMb+lK+rXQH1FvQ==
|
||||||
|
|
||||||
"@react-native-community/slider@^3.0.3":
|
"@react-native-community/slider@^3.0.3":
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-3.0.3.tgz#830167fd757ba70ac638747ba3169b2dbae60330"
|
resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-3.0.3.tgz#830167fd757ba70ac638747ba3169b2dbae60330"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user