mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
fix(ui): restore the player when the window grows into desktop
`toggleMode` is disabled on desktop, and `onOpenPanel` is gated behind it, so a player collapsed into the mini circle while the window was narrow could never be reopened once the window grew past the 810px breakpoint. The circle stayed on screen, labelled "Open", with its click handler inert. The player already supports `mode` as a controlled prop paired with `onModeChange`, but `mode` was hardcoded to `full`. Since `updateMode` only reacts when the value actually changes, that hardcoded prop never triggered anything. Track the mode in state, feed it back through `onModeChange`, and reset it to `full` when the viewport crosses into desktop. The footer player is restored on resize, `toggleMode` keeps its current behaviour, and the narrow-window experience is unchanged.
This commit is contained in:
parent
17db7d4077
commit
7ff6744af0
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