mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-13 21:07:10 +02:00
fix(ui): refine responsive lyrics presentation
Signed-off-by: ranokay <github@ranokay.com>
This commit is contained in:
parent
e06fcbbaeb
commit
70b4de1f78
4 changed files with 49 additions and 63 deletions
|
|
@ -37,7 +37,6 @@ import { finiteTime } from './lyricsTimeline'
|
|||
import useLyricsTimeline from './useLyricsTimeline'
|
||||
|
||||
const KARAOKE_LAYER_OPACITY_TRANSITION = `opacity ${KARAOKE_LINE_OPACITY_MS}ms ${KARAOKE_EASING}`
|
||||
const KARAOKE_AUX_LINE_TRANSITION = `color ${KARAOKE_LINE_OPACITY_MS}ms ${KARAOKE_EASING}, -webkit-text-fill-color ${KARAOKE_LINE_OPACITY_MS}ms ${KARAOKE_EASING}`
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
|
|
@ -123,18 +122,15 @@ const useStyles = makeStyles((theme) => ({
|
|||
backgroundColor: alpha(theme.palette.text.primary, 0.055),
|
||||
},
|
||||
},
|
||||
'&[data-raised="true"][data-line-motion="line"]': {
|
||||
'&[data-raised="true"][data-line-motion="line"] $line': {
|
||||
transform: `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
transition: `transform ${KARAOKE_LINE_ENTER_MS}ms ${KARAOKE_LINE_MOTION_EASING}, background-color 150ms ${KARAOKE_LINE_MOTION_EASING}`,
|
||||
transition: `${KARAOKE_LAYER_OPACITY_TRANSITION}, transform ${KARAOKE_LINE_ENTER_MS}ms ${KARAOKE_LINE_MOTION_EASING}`,
|
||||
},
|
||||
'&[data-raised="true"][data-line-motion="character"][data-character-wave="false"] $line':
|
||||
{
|
||||
transform: `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
transition: `${KARAOKE_LAYER_OPACITY_TRANSITION}, transform ${KARAOKE_LINE_ENTER_MS}ms ${KARAOKE_LINE_MOTION_EASING}`,
|
||||
},
|
||||
'&[data-raised="true"][data-line-motion="character"] $translationLine': {
|
||||
transform: `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
transition: `${KARAOKE_AUX_LINE_TRANSITION}, transform ${KARAOKE_LINE_ENTER_MS}ms ${KARAOKE_LINE_MOTION_EASING}`,
|
||||
},
|
||||
'&[data-active="true"]': {
|
||||
'--lyrics-main-current-color':
|
||||
'var(--lyrics-main-active-color, var(--lyrics-main-idle-color, currentColor))',
|
||||
|
|
@ -147,11 +143,10 @@ const useStyles = makeStyles((theme) => ({
|
|||
'@media (prefers-reduced-motion: reduce)': {
|
||||
transition: 'none',
|
||||
transform: 'none',
|
||||
'&[data-line-motion="character"] $line, &[data-line-motion="character"] $translationLine':
|
||||
{
|
||||
transition: 'none',
|
||||
transform: 'none',
|
||||
},
|
||||
'& $line': {
|
||||
transition: 'none',
|
||||
transform: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
waveCharacter: {
|
||||
|
|
@ -200,7 +195,7 @@ const useStyles = makeStyles((theme) => ({
|
|||
color: 'var(--lyrics-translation-current-color, currentColor)',
|
||||
WebkitTextFillColor:
|
||||
'var(--lyrics-translation-current-color, currentColor)',
|
||||
transition: KARAOKE_AUX_LINE_TRANSITION,
|
||||
transition: 'none',
|
||||
'@media (prefers-reduced-motion: reduce)': {
|
||||
transition: 'none',
|
||||
transform: 'none',
|
||||
|
|
@ -508,7 +503,7 @@ const LyricsPanel = ({
|
|||
return {
|
||||
opacity: 1,
|
||||
color: sourceColor,
|
||||
'--lyrics-active-color': colors.main,
|
||||
'--lyrics-active-color': sourceColor,
|
||||
}
|
||||
}
|
||||
if (!hasTimedMainLines) {
|
||||
|
|
|
|||
|
|
@ -44,13 +44,20 @@ const renderPanel = (props, selectedTheme = theme) => {
|
|||
}
|
||||
|
||||
const parseCssColor = (value) => {
|
||||
const hex = String(value).match(/^#([0-9a-f]{6})$/i)
|
||||
const hex = String(value).match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)
|
||||
if (hex) {
|
||||
const normalized =
|
||||
hex[1].length === 3
|
||||
? hex[1]
|
||||
.split('')
|
||||
.map((channel) => channel.repeat(2))
|
||||
.join('')
|
||||
: hex[1]
|
||||
return {
|
||||
channels: [
|
||||
parseInt(hex[1].slice(0, 2), 16),
|
||||
parseInt(hex[1].slice(2, 4), 16),
|
||||
parseInt(hex[1].slice(4, 6), 16),
|
||||
parseInt(normalized.slice(0, 2), 16),
|
||||
parseInt(normalized.slice(2, 4), 16),
|
||||
parseInt(normalized.slice(4, 6), 16),
|
||||
],
|
||||
alpha: 1,
|
||||
}
|
||||
|
|
@ -347,7 +354,7 @@ describe('<LyricsPanel />', () => {
|
|||
expect(pronunciation.style.color).toBe('')
|
||||
})
|
||||
|
||||
it('uses one shared state transition for every static line layer', () => {
|
||||
it('keeps static translations visually stable while main layers transition', () => {
|
||||
renderPanel({
|
||||
mainLyric,
|
||||
pronunciationLyric: {
|
||||
|
|
@ -379,16 +386,14 @@ describe('<LyricsPanel />', () => {
|
|||
expect(window.getComputedStyle(mainRow).transition).toContain(
|
||||
`opacity ${KARAOKE_LINE_OPACITY_MS}ms`,
|
||||
)
|
||||
expect(window.getComputedStyle(translationRow).transition).toContain(
|
||||
`color ${KARAOKE_LINE_OPACITY_MS}ms`,
|
||||
)
|
||||
expect(window.getComputedStyle(translationRow).transition).toBe('none')
|
||||
pronunciation.forEach((token) =>
|
||||
expect(token).toHaveAttribute('data-timed', 'false'),
|
||||
)
|
||||
expect(window.getComputedStyle(translationRow).opacity).toBe('1')
|
||||
})
|
||||
|
||||
it('raises a line once and keeps it elevated after release', () => {
|
||||
it('raises the main line without moving its translation', () => {
|
||||
const { rerenderPanel } = renderPanel({
|
||||
mainLyric,
|
||||
translationLyric: {
|
||||
|
|
@ -403,12 +408,16 @@ describe('<LyricsPanel />', () => {
|
|||
const translation = screen
|
||||
.getByText('translated line')
|
||||
.closest('[data-tokenized]')
|
||||
const mainRow = screen.getByText('Main line').closest('[data-tokenized]')
|
||||
const activeStyle = window.getComputedStyle(group)
|
||||
expect(group).toHaveAttribute('data-highlight-active', 'true')
|
||||
expect(group).toHaveAttribute('data-raised', 'true')
|
||||
expect(group).toHaveAttribute('data-line-motion', 'line')
|
||||
expect(group).toHaveAttribute('data-character-wave', 'false')
|
||||
expect(activeStyle.transform).toBe(`translateY(-${KARAOKE_LINE_LIFT_PX}px)`)
|
||||
expect(activeStyle.transform).toBe('translateY(0)')
|
||||
expect(window.getComputedStyle(mainRow).transform).toBe(
|
||||
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
)
|
||||
expect(window.getComputedStyle(translation).transform).toBe('')
|
||||
|
||||
rerenderPanel({
|
||||
|
|
@ -424,13 +433,14 @@ describe('<LyricsPanel />', () => {
|
|||
const releasedStyle = window.getComputedStyle(group)
|
||||
expect(group).toHaveAttribute('data-highlight-active', 'false')
|
||||
expect(group).toHaveAttribute('data-raised', 'true')
|
||||
expect(releasedStyle.transform).toBe(
|
||||
expect(releasedStyle.transform).toBe('translateY(0)')
|
||||
expect(window.getComputedStyle(mainRow).transform).toBe(
|
||||
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
)
|
||||
expect(window.getComputedStyle(translation).transform).toBe('')
|
||||
})
|
||||
|
||||
it('keeps every word-timed layer on the same rise and release lifecycle', () => {
|
||||
it('keeps word-timed translations still while main and pronunciation rise', () => {
|
||||
const settledFirstCharacterTime =
|
||||
(KARAOKE_CHARACTER_RISE_MS - KARAOKE_HIGHLIGHT_LEAD_MS) / 1000
|
||||
const propsAt = (currentTime) => ({
|
||||
|
|
@ -464,9 +474,7 @@ describe('<LyricsPanel />', () => {
|
|||
expect(group).toHaveAttribute('data-line-motion', 'character')
|
||||
expect(group).toHaveAttribute('data-character-wave', 'true')
|
||||
expect(window.getComputedStyle(group).transform).toBe('translateY(0)')
|
||||
expect(window.getComputedStyle(translation).transform).toBe(
|
||||
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
)
|
||||
expect(window.getComputedStyle(translation).transform).toBe('')
|
||||
expect(mainCharacters).toHaveLength(4)
|
||||
expect(pronunciationCharacters).toHaveLength(4)
|
||||
expect(mainCharacters[0].style.transform).toBe(
|
||||
|
|
@ -499,16 +507,14 @@ describe('<LyricsPanel />', () => {
|
|||
expect(window.getComputedStyle(releasedMainRow).transform).toBe(
|
||||
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
)
|
||||
expect(window.getComputedStyle(translation).transform).toBe(
|
||||
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
)
|
||||
expect(window.getComputedStyle(translation).transform).toBe('')
|
||||
const idleText = screen
|
||||
.getAllByTestId('lyrics-token')[0]
|
||||
.querySelector('[data-lyrics-wave-text="true"]')
|
||||
expect(idleText.style.backgroundImage).toContain('linear-gradient')
|
||||
})
|
||||
|
||||
it('keeps the translation raised when a word-timed line wraps or unwraps', () => {
|
||||
it('keeps the translation still when a word-timed line wraps or unwraps', () => {
|
||||
let wrapped = false
|
||||
const observers = []
|
||||
window.ResizeObserver = class {
|
||||
|
|
@ -565,28 +571,20 @@ describe('<LyricsPanel />', () => {
|
|||
.closest('[data-tokenized]')
|
||||
|
||||
expect(row).toHaveAttribute('data-wrapped', 'false')
|
||||
expect(window.getComputedStyle(translation).transform).toBe(
|
||||
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
)
|
||||
expect(window.getComputedStyle(translation).transform).toBe('')
|
||||
|
||||
wrapped = true
|
||||
act(notifyResizeObservers)
|
||||
expect(row).toHaveAttribute('data-wrapped', 'true')
|
||||
expect(window.getComputedStyle(translation).transform).toBe(
|
||||
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
)
|
||||
expect(window.getComputedStyle(translation).transform).toBe('')
|
||||
|
||||
rerenderPanel(propsAt(1.1))
|
||||
expect(window.getComputedStyle(translation).transform).toBe(
|
||||
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
)
|
||||
expect(window.getComputedStyle(translation).transform).toBe('')
|
||||
|
||||
wrapped = false
|
||||
act(notifyResizeObservers)
|
||||
expect(row).toHaveAttribute('data-wrapped', 'false')
|
||||
expect(window.getComputedStyle(translation).transform).toBe(
|
||||
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
|
||||
)
|
||||
expect(window.getComputedStyle(translation).transform).toBe('')
|
||||
})
|
||||
|
||||
it('keeps detailed grapheme markup on active lines without changing token layout', () => {
|
||||
|
|
@ -841,13 +839,18 @@ describe('<LyricsPanel />', () => {
|
|||
).toBe(accessibleTheme.palette.text.secondary)
|
||||
expect(
|
||||
group.style.getPropertyValue('--lyrics-translation-active-color'),
|
||||
).toBe(accessibleTheme.palette.text.primary)
|
||||
).toBe(accessibleTheme.palette.text.secondary)
|
||||
expect(window.getComputedStyle(translation).opacity).toBe('1')
|
||||
expect(parseCssColor(futureColor).alpha).toBe(TOKEN_FUTURE_ALPHA)
|
||||
expect(contrastRatio(futureColor, background)).toBeGreaterThanOrEqual(4.5)
|
||||
expect(
|
||||
contrastRatio(accessibleTheme.palette.text.secondary, background),
|
||||
).toBeGreaterThanOrEqual(4.5)
|
||||
expect(
|
||||
contrastRatio(accessibleTheme.palette.text.secondary, background),
|
||||
).toBeLessThan(
|
||||
contrastRatio(accessibleTheme.palette.text.primary, background),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,4 @@
|
|||
import { APP_SIDEBAR_OPEN_WIDTH } from '../layout/layoutWidths'
|
||||
import { LYRICS_SIDEBAR_MAX_WIDTH } from './lyricsSidebarWidth'
|
||||
|
||||
export const LYRICS_SIDEBAR_MIN_CONTENT_WIDTH = LYRICS_SIDEBAR_MAX_WIDTH
|
||||
export const PLAYER_DESKTOP_MIN_WIDTH =
|
||||
APP_SIDEBAR_OPEN_WIDTH +
|
||||
LYRICS_SIDEBAR_MAX_WIDTH +
|
||||
LYRICS_SIDEBAR_MIN_CONTENT_WIDTH
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { APP_SIDEBAR_OPEN_WIDTH } from '../layout/layoutWidths'
|
||||
import {
|
||||
LYRICS_SIDEBAR_DESKTOP_MEDIA_QUERY,
|
||||
LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH,
|
||||
LYRICS_SIDEBAR_MIN_CONTENT_WIDTH,
|
||||
PLAYER_DESKTOP_MEDIA_QUERY,
|
||||
PLAYER_DESKTOP_MIN_WIDTH,
|
||||
PLAYER_MOBILE_MATCH_MEDIA_QUERY,
|
||||
|
|
@ -11,7 +9,6 @@ import {
|
|||
PLAYER_MOBILE_MEDIA_QUERY,
|
||||
PLAYER_MEDIA_QUERY_STEP,
|
||||
} from './playerBreakpoints'
|
||||
import { LYRICS_SIDEBAR_MAX_WIDTH } from './lyricsSidebarWidth'
|
||||
|
||||
describe('player breakpoints', () => {
|
||||
it('keeps compact and desktop player ranges from overlapping', () => {
|
||||
|
|
@ -29,15 +26,13 @@ describe('player breakpoints', () => {
|
|||
)
|
||||
})
|
||||
|
||||
it('only opens the desktop lyrics sidebar when primary content still fits', () => {
|
||||
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 -
|
||||
APP_SIDEBAR_OPEN_WIDTH -
|
||||
LYRICS_SIDEBAR_MAX_WIDTH,
|
||||
).toBeGreaterThanOrEqual(LYRICS_SIDEBAR_MIN_CONTENT_WIDTH)
|
||||
expect(LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH).toBe(PLAYER_DESKTOP_MIN_WIDTH)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue