mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
Merge 7ff6744af0 into 52135913d4
This commit is contained in:
commit
24361722e2
2 changed files with 122 additions and 2 deletions
|
|
@ -49,6 +49,7 @@ const Player = () => {
|
|||
const stoppedRef = useRef(false)
|
||||
const [audioInstance, setAudioInstance] = useState(null)
|
||||
const isDesktop = useMediaQuery('(min-width:810px)')
|
||||
const [playerMode, setPlayerMode] = useState('full')
|
||||
const isMobilePlayer =
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent,
|
||||
|
|
@ -56,6 +57,15 @@ const Player = () => {
|
|||
|
||||
const { authenticated } = useAuthState()
|
||||
|
||||
// On desktop the player is the footer panel and `toggleMode` is disabled, so
|
||||
// a player left collapsed while the window was narrow can never be reopened.
|
||||
// Driving `mode` back to full on the transition restores the footer layout.
|
||||
useEffect(() => {
|
||||
if (isDesktop) {
|
||||
setPlayerMode('full')
|
||||
}
|
||||
}, [isDesktop])
|
||||
|
||||
// Keep a ref to playerState so the mount effect can read the latest value
|
||||
// without re-triggering on every queue/position change
|
||||
const playerStateRef = useRef(playerState)
|
||||
|
|
@ -207,7 +217,8 @@ const Player = () => {
|
|||
theme: playerTheme,
|
||||
bounds: 'body',
|
||||
playMode: playerState.mode,
|
||||
mode: 'full',
|
||||
mode: playerMode,
|
||||
onModeChange: setPlayerMode,
|
||||
loadAudioErrorPlayNext: false,
|
||||
autoPlayInitLoadPlayList: true,
|
||||
clearPriorAudioLists: false,
|
||||
|
|
@ -236,7 +247,7 @@ const Player = () => {
|
|||
locale: locale(translate),
|
||||
sortableOptions: { delay: 200, delayOnTouchOnly: true },
|
||||
}),
|
||||
[gainInfo, isDesktop, playerTheme, translate, playerState.mode],
|
||||
[gainInfo, isDesktop, playerMode, playerTheme, translate, playerState.mode],
|
||||
)
|
||||
|
||||
const options = useMemo(() => {
|
||||
|
|
|
|||
109
ui/src/audioplayer/Player.test.jsx
Normal file
109
ui/src/audioplayer/Player.test.jsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import React from 'react'
|
||||
import { render, act, cleanup } from '@testing-library/react'
|
||||
import { useMediaQuery } from '@material-ui/core'
|
||||
import { useAuthState } from 'react-admin'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Player } from './Player'
|
||||
|
||||
// Captures the props the music player is rendered with, and exposes the
|
||||
// onModeChange callback so a test can simulate the user collapsing the player.
|
||||
const playerProps = { current: null }
|
||||
|
||||
vi.mock('navidrome-music-player', () => ({
|
||||
default: (props) => {
|
||||
playerProps.current = props
|
||||
return <div data-testid="music-player" />
|
||||
},
|
||||
}))
|
||||
vi.mock('navidrome-music-player/assets/index.css', () => ({}))
|
||||
|
||||
vi.mock('@material-ui/core', async () => {
|
||||
const actual = await import('@material-ui/core')
|
||||
return { ...actual, useMediaQuery: vi.fn() }
|
||||
})
|
||||
|
||||
vi.mock('react-admin', async () => {
|
||||
const actual = await import('react-admin')
|
||||
return {
|
||||
...actual,
|
||||
useAuthState: vi.fn(),
|
||||
useDataProvider: () => ({ getOne: vi.fn().mockResolvedValue({}) }),
|
||||
useTranslate: () => (key) => key,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('react-redux', () => ({
|
||||
useDispatch: () => vi.fn(),
|
||||
useSelector: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../transcode', () => ({
|
||||
detectBrowserProfile: () => ({}),
|
||||
decisionService: {
|
||||
setProfile: vi.fn(),
|
||||
resolveStreamUrl: vi.fn().mockResolvedValue(''),
|
||||
prefetchDecisions: vi.fn(),
|
||||
invalidateAll: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./PlayerToolbar', () => ({ default: () => <div /> }))
|
||||
vi.mock('../subsonic', () => ({ default: { reportPlayback: vi.fn() } }))
|
||||
|
||||
const state = {
|
||||
player: {
|
||||
queue: [{ trackId: 't1', uuid: 'u1', isRadio: false }],
|
||||
playIndex: 0,
|
||||
volume: 1,
|
||||
mode: 'order',
|
||||
current: { trackId: 't1' },
|
||||
clear: false,
|
||||
savedPlayIndex: 0,
|
||||
},
|
||||
settings: { notifications: false },
|
||||
replayGain: { gainMode: 'none' },
|
||||
theme: 'dark',
|
||||
}
|
||||
|
||||
describe('<Player />', () => {
|
||||
beforeEach(() => {
|
||||
playerProps.current = null
|
||||
useAuthState.mockReturnValue({ authenticated: true })
|
||||
useSelector.mockImplementation((selector) => selector(state))
|
||||
})
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
// Regression test for #5917: a player collapsed while the window was narrow
|
||||
// could never be reopened once the window grew past the desktop breakpoint,
|
||||
// because toggleMode is disabled on desktop.
|
||||
it('returns to full mode when the viewport grows into desktop', () => {
|
||||
useMediaQuery.mockReturnValue(false)
|
||||
const { rerender } = render(<Player />)
|
||||
expect(playerProps.current.mode).toBe('full')
|
||||
|
||||
// The user collapses the player into the mini circle.
|
||||
act(() => playerProps.current.onModeChange('mini'))
|
||||
expect(playerProps.current.mode).toBe('mini')
|
||||
|
||||
// The window is widened past the desktop breakpoint.
|
||||
useMediaQuery.mockReturnValue(true)
|
||||
act(() => {
|
||||
rerender(<Player />)
|
||||
})
|
||||
|
||||
expect(playerProps.current.mode).toBe('full')
|
||||
})
|
||||
|
||||
it('keeps the collapsed mode while the viewport stays narrow', () => {
|
||||
useMediaQuery.mockReturnValue(false)
|
||||
const { rerender } = render(<Player />)
|
||||
|
||||
act(() => playerProps.current.onModeChange('mini'))
|
||||
act(() => {
|
||||
rerender(<Player />)
|
||||
})
|
||||
|
||||
expect(playerProps.current.mode).toBe('mini')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue