mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-15 22:07:08 +02:00
refactor(ui): simplify lyrics sidebar internals
Signed-off-by: ranokay <github@ranokay.com>
This commit is contained in:
parent
3f87ea4ac3
commit
57b0d418d7
9 changed files with 23 additions and 81 deletions
|
|
@ -120,7 +120,6 @@ const LyricsLayerControls = ({
|
|||
[classes.sidebarPlacement]: placement !== 'mobile',
|
||||
})}
|
||||
data-testid={testId}
|
||||
data-placement={placement}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<LayerButton
|
||||
|
|
|
|||
|
|
@ -240,9 +240,6 @@ export const KaraokeLineRow = memo(
|
|||
component="div"
|
||||
data-testid={testId}
|
||||
data-tokenized={hasTimedTokens ? 'true' : 'false'}
|
||||
data-layer-animation={
|
||||
hasTimedTokens ? 'token-gradient' : 'shared-opacity'
|
||||
}
|
||||
style={lineStyle}
|
||||
>
|
||||
{segments.map((segment, idx) => {
|
||||
|
|
@ -485,9 +482,6 @@ export const KaraokeStackedLineRow = memo(
|
|||
data-wrapped={isWrapped ? 'true' : 'false'}
|
||||
data-testid={testId}
|
||||
data-tokenized={hasTimedTokens ? 'true' : 'false'}
|
||||
data-layer-animation={
|
||||
hasTimedTokens ? 'token-gradient' : 'shared-opacity'
|
||||
}
|
||||
ref={rowRef}
|
||||
style={lineStyle}
|
||||
>
|
||||
|
|
@ -537,11 +531,7 @@ export const KaraokeStackedLineRow = memo(
|
|||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
key={`main-${idx}`}
|
||||
className={classes.stackedSpacer}
|
||||
style={buildStaticEmphasisStyle(segment.token)}
|
||||
>
|
||||
<span key={`main-${idx}`} className={classes.stackedSpacer}>
|
||||
{segment.text}
|
||||
</span>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ const useStyles = makeStyles((theme) => ({
|
|||
alignItems: 'stretch',
|
||||
gap: theme.spacing(3),
|
||||
},
|
||||
inlineLines: {},
|
||||
lineGroup: {
|
||||
width: '100%',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
|
|
@ -692,7 +691,6 @@ const LyricsPanel = ({
|
|||
<div
|
||||
className={clsx(classes.root, { [classes.inlineRoot]: inline })}
|
||||
data-testid="karaoke-lyrics-panel"
|
||||
data-inline={inline ? 'true' : 'false'}
|
||||
onClick={inline ? (event) => event.stopPropagation() : undefined}
|
||||
>
|
||||
<div
|
||||
|
|
@ -749,7 +747,6 @@ const LyricsPanel = ({
|
|||
<div
|
||||
className={clsx(classes.root, { [classes.inlineRoot]: inline })}
|
||||
data-testid="karaoke-lyrics-panel"
|
||||
data-inline={inline ? 'true' : 'false'}
|
||||
onClick={inline ? (event) => event.stopPropagation() : undefined}
|
||||
>
|
||||
<div
|
||||
|
|
@ -768,9 +765,7 @@ const LyricsPanel = ({
|
|||
onTouchMove={markManualScrollIntent}
|
||||
>
|
||||
<div
|
||||
className={clsx(classes.lines, {
|
||||
[classes.inlineLines]: inline,
|
||||
})}
|
||||
className={classes.lines}
|
||||
data-scroll-end-padding={scrollEndPadding}
|
||||
style={
|
||||
scrollEndPadding > 0 || scrollStartPadding > 0
|
||||
|
|
|
|||
|
|
@ -380,7 +380,6 @@ describe('<LyricsPanel />', () => {
|
|||
const pronunciation = screen.getAllByTestId('lyrics-pronunciation-token')
|
||||
|
||||
;[mainRow, translationRow].forEach((row) => {
|
||||
expect(row).toHaveAttribute('data-layer-animation', 'shared-opacity')
|
||||
expect(row).toHaveAttribute('data-tokenized', 'false')
|
||||
expect(row.style.opacity).toBe('')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -77,22 +77,6 @@ describe('<PlayerToolbar />', () => {
|
|||
expect(listItems[0].className).toContain('toolbar')
|
||||
})
|
||||
|
||||
it('disables save queue button when isRadio is true', () => {
|
||||
render(<PlayerToolbar id="song-1" isRadio={true} />)
|
||||
|
||||
const saveQueueButton = screen.getByTestId('save-queue-button')
|
||||
expect(saveQueueButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('disables love button when conditions are met', () => {
|
||||
useGetOne.mockReturnValue({ data: mockSongData, loading: true })
|
||||
|
||||
render(<PlayerToolbar id="song-1" />)
|
||||
|
||||
const loveButton = screen.getByTestId('love-button')
|
||||
expect(loveButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('opens save queue dialog when save button is clicked', () => {
|
||||
render(<PlayerToolbar id="song-1" />)
|
||||
|
||||
|
|
@ -154,22 +138,6 @@ describe('<PlayerToolbar />', () => {
|
|||
expect(listItems[2].className).toContain('mobileListItem')
|
||||
})
|
||||
|
||||
it('disables save queue button when isRadio is true', () => {
|
||||
render(<PlayerToolbar id="song-1" isRadio={true} />)
|
||||
|
||||
const saveQueueButton = screen.getByTestId('save-queue-button')
|
||||
expect(saveQueueButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('disables love button when conditions are met', () => {
|
||||
useGetOne.mockReturnValue({ data: mockSongData, loading: true })
|
||||
|
||||
render(<PlayerToolbar id="song-1" />)
|
||||
|
||||
const loveButton = screen.getByTestId('love-button')
|
||||
expect(loveButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('disables lyrics toggle when lyrics are unavailable', () => {
|
||||
render(
|
||||
<PlayerToolbar id="song-1" onToggleLyrics={vi.fn()} lyricsDisabled />,
|
||||
|
|
@ -192,6 +160,22 @@ describe('<PlayerToolbar />', () => {
|
|||
})
|
||||
|
||||
describe('Common behavior', () => {
|
||||
it('disables save queue button when isRadio is true', () => {
|
||||
useMediaQuery.mockReturnValue(true)
|
||||
render(<PlayerToolbar id="song-1" isRadio={true} />)
|
||||
|
||||
expect(screen.getByTestId('save-queue-button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('disables love button when conditions are met', () => {
|
||||
useMediaQuery.mockReturnValue(true)
|
||||
useGetOne.mockReturnValue({ data: mockSongData, loading: true })
|
||||
|
||||
render(<PlayerToolbar id="song-1" />)
|
||||
|
||||
expect(screen.getByTestId('love-button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('renders global hotkeys in both layouts', () => {
|
||||
// Test desktop layout
|
||||
useMediaQuery.mockReturnValue(true)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ export const PLAYER_DESKTOP_MIN_WIDTH = 810
|
|||
export const PLAYER_MEDIA_QUERY_STEP = 0.05
|
||||
export const PLAYER_MOBILE_MAX_WIDTH =
|
||||
PLAYER_DESKTOP_MIN_WIDTH - PLAYER_MEDIA_QUERY_STEP
|
||||
export const LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH = PLAYER_DESKTOP_MIN_WIDTH
|
||||
|
||||
export const PLAYER_DESKTOP_MEDIA_QUERY = `(min-width:${PLAYER_DESKTOP_MIN_WIDTH}px)`
|
||||
export const PLAYER_MOBILE_MATCH_MEDIA_QUERY = `(max-width:${PLAYER_MOBILE_MAX_WIDTH}px)`
|
||||
export const PLAYER_MOBILE_MEDIA_QUERY = `@media screen and ${PLAYER_MOBILE_MATCH_MEDIA_QUERY}`
|
||||
export const LYRICS_SIDEBAR_DESKTOP_MEDIA_QUERY = PLAYER_DESKTOP_MEDIA_QUERY
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
LYRICS_SIDEBAR_DESKTOP_MEDIA_QUERY,
|
||||
LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH,
|
||||
PLAYER_DESKTOP_MEDIA_QUERY,
|
||||
PLAYER_DESKTOP_MIN_WIDTH,
|
||||
PLAYER_MOBILE_MATCH_MEDIA_QUERY,
|
||||
|
|
@ -12,6 +10,8 @@ import {
|
|||
|
||||
describe('player breakpoints', () => {
|
||||
it('keeps compact and desktop player ranges from overlapping', () => {
|
||||
expect(PLAYER_DESKTOP_MIN_WIDTH).toBe(810)
|
||||
expect(PLAYER_MOBILE_MAX_WIDTH).toBe(809.95)
|
||||
expect(PLAYER_DESKTOP_MIN_WIDTH - PLAYER_MOBILE_MAX_WIDTH).toBeCloseTo(
|
||||
PLAYER_MEDIA_QUERY_STEP,
|
||||
)
|
||||
|
|
@ -25,14 +25,4 @@ describe('player breakpoints', () => {
|
|||
`(max-width:${PLAYER_MOBILE_MAX_WIDTH}px)`,
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps tablet layouts on the desktop player and lyrics sidebar', () => {
|
||||
expect(PLAYER_DESKTOP_MIN_WIDTH).toBe(810)
|
||||
expect(PLAYER_MOBILE_MAX_WIDTH).toBe(809.95)
|
||||
expect(LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH).toBe(810)
|
||||
expect(LYRICS_SIDEBAR_DESKTOP_MEDIA_QUERY).toBe(
|
||||
`(min-width:${LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH}px)`,
|
||||
)
|
||||
expect(LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH).toBe(PLAYER_DESKTOP_MIN_WIDTH)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export const emptyLyricLayers = Object.freeze({
|
|||
pronunciation: null,
|
||||
})
|
||||
|
||||
const LYRICS_CACHE_SCHEMA_VERSION = 3
|
||||
const MAX_LYRIC_CACHE_ENTRIES = 75
|
||||
const NEGATIVE_CACHE_TTL_MS = 30_000
|
||||
|
||||
|
|
@ -25,12 +24,7 @@ const readStructuredLyrics = (response) =>
|
|||
response?.json?.['subsonic-response']?.lyricsList?.structuredLyrics || []
|
||||
|
||||
const buildCacheKey = ({ trackId, preferredLanguage, updatedAt }) =>
|
||||
[
|
||||
trackId || '',
|
||||
updatedAt || '',
|
||||
preferredLanguage || '',
|
||||
LYRICS_CACHE_SCHEMA_VERSION,
|
||||
].join('\u0000')
|
||||
[trackId || '', updatedAt || '', preferredLanguage || ''].join('\u0000')
|
||||
|
||||
const rememberLyrics = (cacheKey, layers, expiresAt = null) => {
|
||||
cache.delete(cacheKey)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import LyricsLayerControls from './LyricsLayerControls'
|
||||
import LyricsPanel from './LyricsPanel'
|
||||
import MobileKaraokeLyricsPortal from './MobileKaraokeLyricsPortal'
|
||||
|
|
@ -19,15 +19,10 @@ const usePlayerLyrics = ({
|
|||
translate,
|
||||
}) => {
|
||||
const [lyricsVisiblePreference, setLyricsVisiblePreference] = useState(false)
|
||||
const [lyricsRequested, setLyricsRequested] = useState(false)
|
||||
const [translationPreference, setTranslationPreference] = useState(null)
|
||||
const [pronunciationPreference, setPronunciationPreference] = useState(null)
|
||||
const lyricsToggleRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLyricsRequested(lyricsVisiblePreference)
|
||||
}, [trackId]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const {
|
||||
layers: lyricLayers,
|
||||
loading: lyricsLoading,
|
||||
|
|
@ -37,7 +32,7 @@ const usePlayerLyrics = ({
|
|||
trackId,
|
||||
updatedAt: trackUpdatedAt,
|
||||
disabled: isRadio,
|
||||
requested: lyricsRequested || lyricsVisiblePreference,
|
||||
requested: lyricsVisiblePreference,
|
||||
})
|
||||
|
||||
const hasTranslationLyric = hasStructuredLyricContent(lyricLayers.translation)
|
||||
|
|
@ -74,7 +69,6 @@ const usePlayerLyrics = ({
|
|||
const toggleLyrics = useCallback(() => {
|
||||
const next = !lyricsVisiblePreference
|
||||
setLyricsVisiblePreference(next)
|
||||
setLyricsRequested(next)
|
||||
if (next) {
|
||||
if (lyricsError) retryLyrics()
|
||||
}
|
||||
|
|
@ -82,7 +76,6 @@ const usePlayerLyrics = ({
|
|||
|
||||
const closeLyrics = useCallback(() => {
|
||||
setLyricsVisiblePreference(false)
|
||||
setLyricsRequested(false)
|
||||
}, [])
|
||||
|
||||
const toggleTranslation = useCallback(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue