refine karaoke lyrics overlay UI

- Remove glow effect, keep gradient text highlight with soft wipe edge
- Theme-adaptive: use theme.palette.background.default (85% opacity) for overlay bg
- Light theme support: dark text, borders, badges, settings panel colors
- Auto-switch main lyrics color (white↔black) on theme change
- Add black color preset, default for light themes
- Merge TR/PR toggle buttons into language badges (clickable, with tooltips)
- Fade edges via CSS mask-image (theme-independent, no pseudo-elements)
- Rising effect (font-size scaling) for TR/PR lines matching main
- Smooth scroll: custom rAF ease-out cubic animation (400ms)
- Mobile: full-width panel, backdrop blur, transparent background
- Timing: KARAOKE_RENDER_LEAD_MS=80, KARAOKE_SWITCH_EPSILON_MS=50
- Hide TR/PR badges when no data available
- Badge/pill vertical centering with lineHeight:1
- Remove unused Button import, layerControls/layerToggle styles
This commit is contained in:
ranokay 2026-04-14 23:17:31 +03:00
parent 45fac62285
commit 7c6ecd0cf6
No known key found for this signature in database
3 changed files with 207 additions and 143 deletions

View File

@ -1,8 +1,7 @@
import Button from '@material-ui/core/Button'
import IconButton from '@material-ui/core/IconButton'
import Popover from '@material-ui/core/Popover'
import Slider from '@material-ui/core/Slider'
import { makeStyles } from '@material-ui/core/styles'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import Tooltip from '@material-ui/core/Tooltip'
import Typography from '@material-ui/core/Typography'
import CloseIcon from '@material-ui/icons/Close'
@ -29,7 +28,7 @@ import {
utf8ByteRangeToCodeUnitRange,
} from './lyrics'
const KARAOKE_RENDER_LEAD_MS = 24
const KARAOKE_RENDER_LEAD_MS = 80
const KARAOKE_CLOCK_DRIFT_RESET_MS = 140
const KARAOKE_CLOCK_RESET_THRESHOLD_MS = 320
const KARAOKE_MONOTONIC_JITTER_MS = 60
@ -49,15 +48,17 @@ const KARAOKE_LINE_HEIGHT_STEP = 0.02
const KARAOKE_GROUP_SPACING_BASE_PX = 14
const KARAOKE_AUX_LINE_HEIGHT = 1.2
const KARAOKE_MAIN_INACTIVE_FONT_FACTOR = 0.8
const KARAOKE_AUX_INACTIVE_FONT_FACTOR = 0.88
const TOKEN_DONE_ALPHA = 1
const TOKEN_FUTURE_ALPHA = 0.34
const TOKEN_ACTIVE_ALPHA = 1
const TOKEN_WIPE_SOFT_SPREAD_PCT = 12
const TOKEN_WIPE_EDGE_PCT = 8
const TOKEN_WIPE_GLOW_PCT = 16
const COLOR_PRESETS = [
{ key: 'white', label: 'White', value: 'rgba(255, 255, 255, 0.92)' },
{ key: 'black', label: 'Black', value: 'rgba(0, 0, 0, 0.87)' },
{ key: 'blue', label: 'Blue', value: 'rgba(120, 160, 220, 0.75)' },
{ key: 'green', label: 'Green', value: 'rgba(100, 200, 130, 0.7)' },
{ key: 'pink', label: 'Pink', value: 'rgba(240, 140, 170, 0.75)' },
@ -77,11 +78,11 @@ const DEFAULT_LYRICS_SETTINGS = {
const SETTINGS_STORAGE_KEY = 'karaoke-lyrics-settings'
const createDefaultLyricsSettings = () => ({
const createDefaultLyricsSettings = (isDark = true) => ({
lineHeight: KARAOKE_DEFAULT_LINE_HEIGHT,
overlayHeight: KARAOKE_DEFAULT_HEIGHT_PX,
tr: { ...DEFAULT_LYRICS_SETTINGS.tr },
main: { ...DEFAULT_LYRICS_SETTINGS.main },
main: { ...DEFAULT_LYRICS_SETTINGS.main, colorKey: isDark ? 'white' : 'black' },
pr: { ...DEFAULT_LYRICS_SETTINGS.pr },
})
@ -135,7 +136,30 @@ const saveLyricsSettings = (settings) => {
const getColorValue = (colorKey) =>
COLOR_PRESETS.find((c) => c.key === colorKey)?.value || COLOR_PRESETS[0].value
const useStyles = makeStyles((theme) => ({
const hexToRgba = (hex, alpha) => {
const m = (hex || '').match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i)
if (m) return `rgba(${parseInt(m[1], 16)}, ${parseInt(m[2], 16)}, ${parseInt(m[3], 16)}, ${alpha})`
const rm = (hex || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
if (rm) return `rgba(${rm[1]}, ${rm[2]}, ${rm[3]}, ${alpha})`
return `rgba(48, 48, 48, ${alpha})`
}
const useStyles = makeStyles((theme) => {
const isDark = theme.palette.type === 'dark'
const overlayBg = hexToRgba(theme.palette.background.default, 0.85)
const primaryMain = theme.palette.primary.main
const primaryRgb = (() => {
const m = (primaryMain || '').match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i)
if (m) return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)]
const rm = (primaryMain || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
if (rm) return [parseInt(rm[1]), parseInt(rm[2]), parseInt(rm[3])]
return [144, 202, 249]
})()
const textPrimary = isDark ? 'rgba(255, 255, 255, 0.92)' : 'rgba(0, 0, 0, 0.87)'
const textSecondary = isDark ? 'rgba(255, 255, 255, 0.55)' : 'rgba(0, 0, 0, 0.54)'
const borderSubtle = isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.12)'
return ({
overlay: {
position: 'fixed',
left: '50%',
@ -144,19 +168,19 @@ const useStyles = makeStyles((theme) => ({
zIndex: 1400,
width: 'min(1000px, calc(100vw - 32px))',
minHeight: KARAOKE_MIN_HEIGHT_PX,
background: 'rgba(6, 8, 12, 0.9)',
background: overlayBg,
borderRadius: 12,
border: '1px solid rgba(255, 255, 255, 0.12)',
border: `1px solid ${borderSubtle}`,
boxShadow: '0 18px 48px rgba(0, 0, 0, 0.42)',
backdropFilter: 'blur(10px)',
color: theme.palette.common.white,
backdropFilter: 'blur(20px)',
color: textPrimary,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
'@media (max-width:810px)': {
bottom: 78,
width: 'calc(100vw - 12px)',
borderRadius: 8,
borderRadius: 12,
minHeight: 180,
maxHeight: '65vh',
},
@ -172,8 +196,9 @@ const useStyles = makeStyles((theme) => ({
borderRadius: 'inherit',
border: 'none',
boxShadow: 'none',
background: 'rgba(6, 8, 12, 0.92)',
backdropFilter: 'blur(12px)',
background: 'transparent',
backdropFilter: 'blur(16px)',
WebkitBackdropFilter: 'blur(16px)',
zIndex: 1,
},
resizeHandle: {
@ -190,7 +215,7 @@ const useStyles = makeStyles((theme) => ({
width: 56,
height: 3,
borderRadius: 999,
background: 'rgba(255, 255, 255, 0.22)',
background: `rgba(${primaryRgb.join(', ')}, 0.22)`,
},
'@media (max-width:810px)': {
display: 'none',
@ -223,20 +248,31 @@ const useStyles = makeStyles((theme) => ({
languageBadge: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: theme.spacing(0.35),
padding: theme.spacing(0.2, 0.7),
borderRadius: 999,
border: '1px solid rgba(148, 163, 184, 0.28)',
background: 'rgba(15, 23, 42, 0.42)',
color: 'rgba(226, 232, 240, 0.8)',
border: `1px solid ${borderSubtle}`,
background: isDark ? 'rgba(15, 23, 42, 0.42)' : 'rgba(0, 0, 0, 0.06)',
color: isDark ? 'rgba(226, 232, 240, 0.8)' : 'rgba(0, 0, 0, 0.6)',
fontSize: 10,
lineHeight: 1,
letterSpacing: '0.04em',
whiteSpace: 'nowrap',
transition: `all ${KARAOKE_ANIMATION_MS}ms ease-in-out`,
userSelect: 'none',
},
languageBadgeToggle: {
cursor: 'pointer',
'&:hover': {
borderColor: `rgba(${primaryRgb.join(', ')}, 0.35)`,
background: isDark ? 'rgba(15, 23, 42, 0.56)' : 'rgba(0, 0, 0, 0.1)',
},
},
languageBadgeActive: {
borderColor: 'rgba(148, 163, 184, 0.46)',
background: 'rgba(30, 41, 59, 0.56)',
color: 'rgba(248, 250, 252, 0.94)',
borderColor: `rgba(${primaryRgb.join(', ')}, 0.46)`,
background: `rgba(${primaryRgb.join(', ')}, 0.18)`,
color: isDark ? 'rgba(248, 250, 252, 0.94)' : 'rgba(0, 0, 0, 0.87)',
},
languageBadgeLabel: {
fontWeight: 700,
@ -246,35 +282,8 @@ const useStyles = makeStyles((theme) => ({
languageBadgeValue: {
opacity: 0.9,
},
layerControls: {
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
},
layerToggle: {
minWidth: 34,
minHeight: 24,
padding: theme.spacing(0, 0.8),
fontSize: 10,
letterSpacing: '0.08em',
borderRadius: 999,
color: 'rgba(203, 213, 225, 0.95)',
background: 'rgba(100, 116, 139, 0.26)',
border: '1px solid rgba(148, 163, 184, 0.45)',
transition: `all ${KARAOKE_ANIMATION_MS}ms ease-in-out`,
'&.Mui-disabled': {
color: 'rgba(148, 163, 184, 0.45)',
borderColor: 'rgba(100, 116, 139, 0.3)',
background: 'rgba(71, 85, 105, 0.2)',
},
},
layerToggleActive: {
color: 'rgba(220, 252, 231, 0.98)',
borderColor: 'rgba(34, 197, 94, 0.96)',
background: 'rgba(34, 197, 94, 0.28)',
},
closeButton: {
color: 'rgba(255, 255, 255, 0.72)',
color: textSecondary,
},
lineGroup: {
display: 'flex',
@ -290,7 +299,7 @@ const useStyles = makeStyles((theme) => ({
fontWeight: 400,
lineHeight: KARAOKE_AUX_LINE_HEIGHT,
letterSpacing: '0.01em',
transition: `opacity ${KARAOKE_ANIMATION_MS}ms ease-in-out, font-size ${KARAOKE_ANIMATION_MS}ms ease-in-out`,
transition: `opacity ${KARAOKE_ANIMATION_MS}ms ease-in-out, color ${KARAOKE_ANIMATION_MS}ms ease-in-out, font-size 280ms ease-in-out, max-width 280ms ease-in-out`,
},
inlinePr: {
margin: 0,
@ -304,23 +313,29 @@ const useStyles = makeStyles((theme) => ({
boxSizing: 'border-box',
textAlign: 'center',
fontWeight: 400,
lineHeight: KARAOKE_AUX_LINE_HEIGHT,
lineHeight: 1,
letterSpacing: '0.01em',
transition: `opacity ${KARAOKE_ANIMATION_MS}ms ease-in-out, font-size ${KARAOKE_ANIMATION_MS}ms ease-in-out`,
transition: `opacity ${KARAOKE_ANIMATION_MS}ms ease-in-out, color ${KARAOKE_ANIMATION_MS}ms ease-in-out, font-size 280ms ease-in-out, max-width 280ms ease-in-out`,
padding: theme.spacing(0.15, 0.9),
borderRadius: 999,
background: 'rgba(255, 255, 255, 0.08)',
border: '1px solid rgba(255, 255, 255, 0.12)',
background: isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.05)',
border: `1px solid ${borderSubtle}`,
},
bodyWrapper: {
position: 'relative',
flex: 1,
overflow: 'hidden',
},
body: {
padding: theme.spacing(0.5, 2, 1.4, 2),
overflowY: 'auto',
overflowX: 'hidden',
scrollBehavior: 'smooth',
flex: 1,
height: '100%',
overscrollBehavior: 'contain',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
maskImage: 'linear-gradient(to bottom, transparent 0%, black 8%, black 92%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 8%, black 92%, transparent 100%)',
'&::-webkit-scrollbar': {
display: 'none',
width: 0,
@ -347,7 +362,7 @@ const useStyles = makeStyles((theme) => ({
lineHeight: 1.24,
letterSpacing: '0.01em',
textAlign: 'center',
color: 'rgba(255, 255, 255, 0.62)',
color: isDark ? 'rgba(255, 255, 255, 0.62)' : 'rgba(0, 0, 0, 0.52)',
transition: `opacity ${KARAOKE_ANIMATION_MS}ms ease-in-out, color ${KARAOKE_ANIMATION_MS}ms ease-in-out, font-size 280ms ease-in-out, max-width 280ms ease-in-out`,
},
token: {
@ -356,15 +371,15 @@ const useStyles = makeStyles((theme) => ({
transition: `color ${KARAOKE_ANIMATION_MS}ms ease-in-out, text-shadow ${KARAOKE_ANIMATION_MS}ms ease-in-out`,
},
settingsButton: {
color: 'rgba(255, 255, 255, 0.55)',
color: textSecondary,
padding: 4,
'&:hover': {
color: 'rgba(255, 255, 255, 0.85)',
color: textPrimary,
},
},
settingsPanel: {
background: 'rgba(12, 14, 20, 0.96)',
border: '1px solid rgba(255, 255, 255, 0.12)',
background: isDark ? 'rgba(12, 14, 20, 0.96)' : 'rgba(255, 255, 255, 0.96)',
border: `1px solid ${borderSubtle}`,
borderRadius: 10,
padding: theme.spacing(1.5, 2),
width: 278,
@ -388,14 +403,14 @@ const useStyles = makeStyles((theme) => ({
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: 'rgba(255, 255, 255, 0.78)',
color: isDark ? 'rgba(255, 255, 255, 0.78)' : 'rgba(0, 0, 0, 0.72)',
},
settingsLabel: {
fontSize: 10,
fontWeight: 600,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: 'rgba(255, 255, 255, 0.55)',
color: isDark ? 'rgba(255, 255, 255, 0.55)' : 'rgba(0, 0, 0, 0.5)',
marginBottom: 4,
},
settingsRow: {
@ -405,7 +420,7 @@ const useStyles = makeStyles((theme) => ({
},
settingsSlider: {
flex: 1,
color: 'rgba(255, 255, 255, 0.6)',
color: `rgba(${primaryRgb.join(', ')}, 0.6)`,
'& .MuiSlider-thumb': {
width: 12,
height: 12,
@ -416,7 +431,7 @@ const useStyles = makeStyles((theme) => ({
},
settingsSliderValue: {
fontSize: 11,
color: 'rgba(255, 255, 255, 0.5)',
color: isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.45)',
minWidth: 22,
textAlign: 'right',
},
@ -424,15 +439,15 @@ const useStyles = makeStyles((theme) => ({
fontSize: 10,
letterSpacing: '0.06em',
textTransform: 'uppercase',
color: 'rgba(255, 255, 255, 0.45)',
color: isDark ? 'rgba(255, 255, 255, 0.45)' : 'rgba(0, 0, 0, 0.42)',
minWidth: 72,
whiteSpace: 'nowrap',
},
resetButton: {
color: 'rgba(255, 255, 255, 0.58)',
color: textSecondary,
padding: 4,
'&:hover': {
color: 'rgba(255, 255, 255, 0.9)',
color: textPrimary,
},
},
colorDots: {
@ -452,9 +467,9 @@ const useStyles = makeStyles((theme) => ({
},
},
colorDotActive: {
borderColor: 'rgba(255, 255, 255, 0.85)',
borderColor: isDark ? 'rgba(255, 255, 255, 0.85)' : 'rgba(0, 0, 0, 0.7)',
},
}))
})})
const clamp = (v, min, max) => Math.max(min, Math.min(max, v))
const lerp = (from, to, t) => from + (to - from) * t
@ -479,6 +494,8 @@ const buildLanguageBadges = ({
pronunciationLyric,
showTranslation,
showPronunciation,
translationEnabled,
pronunciationEnabled,
}) =>
[
{
@ -486,20 +503,25 @@ const buildLanguageBadges = ({
label: 'Main',
lang: mainLyric?.lang,
active: true,
toggleable: false,
},
{
pronunciationEnabled && {
key: 'pr',
label: 'PR',
lang: pronunciationLyric?.lang,
active: showPronunciation,
toggleable: true,
tooltip: showPronunciation ? 'Hide pronunciation' : 'Show pronunciation',
},
{
translationEnabled && {
key: 'tr',
label: 'TR',
lang: translationLyric?.lang,
active: showTranslation,
toggleable: true,
tooltip: showTranslation ? 'Hide translation' : 'Show translation',
},
].filter((badge) => badge.lang)
].filter((badge) => badge && badge.lang)
const SettingsSection = ({ label, layer, settings, onChange, classes }) => {
const s = settings[layer]
@ -913,22 +935,20 @@ const buildTokenWipeStyle = ({
const fillPct = clamp(fillProgress, 0, 1) * 100
const doneColor = `rgba(${r}, ${g}, ${b}, ${clamp(highlightAlpha, TOKEN_DONE_ALPHA, TOKEN_ACTIVE_ALPHA)})`
const futureColor = `rgba(${r}, ${g}, ${b}, ${futureAlpha})`
const activeShadow = `0 0 8px rgba(${r}, ${g}, ${b}, 0.34)`
if (fillPct <= 0) {
return { color: futureColor, textShadow: 'none' }
}
const edgeStart = clamp(fillPct - TOKEN_WIPE_EDGE_PCT, 0, 100)
const glowStop = clamp(fillPct + TOKEN_WIPE_GLOW_PCT, 0, 100)
const glowColor = `rgba(${r}, ${g}, ${b}, ${clamp(highlightAlpha + 0.18, TOKEN_DONE_ALPHA, TOKEN_ACTIVE_ALPHA)})`
const softEnd = clamp(fillPct + TOKEN_WIPE_SOFT_SPREAD_PCT, 0, 100)
return {
color: 'transparent',
WebkitTextFillColor: 'transparent',
backgroundImage: `linear-gradient(90deg, ${doneColor} 0%, ${doneColor} ${edgeStart}%, ${glowColor} ${fillPct}%, ${futureColor} ${glowStop}%, ${futureColor} 100%)`,
backgroundImage: `linear-gradient(90deg, ${doneColor} 0%, ${doneColor} ${edgeStart}%, ${doneColor} ${fillPct}%, ${futureColor} ${softEnd}%, ${futureColor} 100%)`,
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
textShadow: activeShadow,
textShadow: 'none',
}
}
@ -1077,6 +1097,8 @@ const KaraokeLyricsOverlay = ({
inline = false,
}) => {
const classes = useStyles()
const theme = useTheme()
const isDark = theme.palette.type === 'dark'
const [playbackMs, setPlaybackMs] = useState(0)
const [maxHeightPx, setMaxHeightPx] = useState(getMaxHeightPx())
const [bodyViewportHeight, setBodyViewportHeight] = useState(0)
@ -1092,10 +1114,10 @@ const KaraokeLyricsOverlay = ({
}, [])
const handleResetAppearance = useCallback(() => {
const defaults = createDefaultLyricsSettings()
const defaults = createDefaultLyricsSettings(isDark)
setLyricsSettings(defaults)
saveLyricsSettings(defaults)
}, [])
}, [isDark])
const bodyRef = useRef(null)
const activeLineRef = useRef(null)
@ -1127,6 +1149,23 @@ const KaraokeLyricsOverlay = ({
return () => window.removeEventListener('resize', onResize)
}, [])
useEffect(() => {
setLyricsSettings((prev) => {
const currentColor = prev.main.colorKey
const shouldSwap =
(isDark && currentColor === 'black') ||
(!isDark && currentColor === 'white')
if (!shouldSwap) return prev
const newColorKey = isDark ? 'white' : 'black'
const updated = {
...prev,
main: { ...prev.main, colorKey: newColorKey },
}
saveLyricsSettings(updated)
return updated
})
}, [isDark])
useEffect(() => {
const body = bodyRef.current
if (!body) {
@ -1308,6 +1347,8 @@ const KaraokeLyricsOverlay = ({
pronunciationLyric,
showTranslation,
showPronunciation,
translationEnabled,
pronunciationEnabled,
})
const trByMainIndex = useMemo(() => {
@ -1354,7 +1395,10 @@ const KaraokeLyricsOverlay = ({
return
}
const rafId = window.requestAnimationFrame(() => {
let animFrameId = null
let scrollAnimId = null
animFrameId = window.requestAnimationFrame(() => {
const body = bodyRef.current
const activeNode = activeLineRef.current
if (!body || !activeNode) {
@ -1368,23 +1412,36 @@ const KaraokeLyricsOverlay = ({
bodyRect.top -
(body.clientHeight - activeRect.height) / 2
const maxTop = Math.max(0, body.scrollHeight - body.clientHeight)
const centeredTop = clamp(body.scrollTop + deltaWithinBody, 0, maxTop)
const targetTop = clamp(body.scrollTop + deltaWithinBody, 0, maxTop)
const distance = targetTop - body.scrollTop
if (Math.abs(body.scrollTop - centeredTop) < 2) {
if (Math.abs(distance) < 2) {
return
}
if (typeof body.scrollTo === 'function') {
body.scrollTo({
top: centeredTop,
behavior: 'smooth',
})
} else {
body.scrollTop = centeredTop
const startTop = body.scrollTop
const duration = 400
const startTime = performance.now()
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3)
const step = (now) => {
const elapsed = now - startTime
const progress = Math.min(elapsed / duration, 1)
const eased = easeOutCubic(progress)
body.scrollTop = startTop + distance * eased
if (progress < 1) {
scrollAnimId = window.requestAnimationFrame(step)
}
}
scrollAnimId = window.requestAnimationFrame(step)
})
return () => window.cancelAnimationFrame(rafId)
return () => {
if (animFrameId) window.cancelAnimationFrame(animFrameId)
if (scrollAnimId) window.cancelAnimationFrame(scrollAnimId)
}
}, [
centerSpacerPx,
hasTimedMainLines,
@ -1453,10 +1510,11 @@ const KaraokeLyricsOverlay = ({
const [r, g, b] = parseColorRGB(
getColorValue(lyricsSettings[layerKey].colorKey),
)
const baseFontSize = lyricsSettings[layerKey].fontSize
if (!hasTimedMainLines) {
return {
opacity: 0.94,
fontSize: lyricsSettings[layerKey].fontSize,
fontSize: baseFontSize,
color: `rgba(${r}, ${g}, ${b}, 0.94)`,
lineHeight: KARAOKE_AUX_LINE_HEIGHT,
}
@ -1482,11 +1540,18 @@ const KaraokeLyricsOverlay = ({
opacity = Math.max(0.22, 0.5 - level * 0.08)
}
const fontSize = isActive
? baseFontSize
: Math.round(baseFontSize * KARAOKE_AUX_INACTIVE_FONT_FACTOR)
return {
opacity,
fontSize: lyricsSettings[layerKey].fontSize,
fontSize,
color,
lineHeight: KARAOKE_AUX_LINE_HEIGHT,
maxWidth: isActive
? '100%'
: `${Math.round(KARAOKE_AUX_INACTIVE_FONT_FACTOR * 100)}%`,
}
}
@ -1524,52 +1589,49 @@ const KaraokeLyricsOverlay = ({
>
<div className={classes.headerLeft}>
<div className={classes.languageBadges}>
{languageBadges.map((badge) => (
<div
key={badge.key}
className={clsx(classes.languageBadge, {
[classes.languageBadgeActive]: badge.active,
})}
data-testid={`lyrics-language-badge-${badge.key}`}
>
<span className={classes.languageBadgeLabel}>
{badge.label}
</span>
<span className={classes.languageBadgeValue}>{badge.lang}</span>
</div>
))}
</div>
<div className={classes.layerControls}>
<Tooltip title="Toggle translations">
<span>
<Button
size="small"
onClick={onToggleTranslation}
disabled={!translationEnabled}
className={clsx(classes.layerToggle, {
[classes.layerToggleActive]: showTranslation,
{languageBadges.map((badge) => {
const badgeEl = (
<div
key={badge.key}
className={clsx(classes.languageBadge, {
[classes.languageBadgeActive]: badge.active,
[classes.languageBadgeToggle]: badge.toggleable,
})}
data-testid="lyrics-toggle-translation"
data-testid={`lyrics-language-badge-${badge.key}`}
role={badge.toggleable ? 'button' : undefined}
tabIndex={badge.toggleable ? 0 : undefined}
onClick={
badge.toggleable
? badge.key === 'tr'
? onToggleTranslation
: onTogglePronunciation
: undefined
}
onKeyDown={
badge.toggleable
? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
;(badge.key === 'tr'
? onToggleTranslation
: onTogglePronunciation)()
}
}
: undefined
}
>
TR
</Button>
</span>
</Tooltip>
<Tooltip title="Toggle pronunciations">
<span>
<Button
size="small"
onClick={onTogglePronunciation}
disabled={!pronunciationEnabled}
className={clsx(classes.layerToggle, {
[classes.layerToggleActive]: showPronunciation,
})}
data-testid="lyrics-toggle-pronunciation"
>
PR
</Button>
</span>
</Tooltip>
<span className={classes.languageBadgeLabel}>
{badge.label}
</span>
<span className={classes.languageBadgeValue}>{badge.lang}</span>
</div>
)
return badge.toggleable ? (
<Tooltip key={badge.key} title={badge.tooltip}>
{badgeEl}
</Tooltip>
) : badgeEl
})}
</div>
</div>
@ -1590,6 +1652,7 @@ const KaraokeLyricsOverlay = ({
</div>
</div>
<div className={classes.bodyWrapper}>
<div
className={clsx(classes.body, {
[classes.bodyInline]: inline,
@ -1674,6 +1737,7 @@ const KaraokeLyricsOverlay = ({
<div aria-hidden style={{ height: centerSpacerPx }} />
</div>
</div>
</div>
</div>
)
}

View File

@ -2,7 +2,7 @@ const normalizeLanguageTag = (language) =>
(language || '').toLowerCase().replace('_', '-')
// Roughly one 60fps frame; keeps line/token switching stable near tight boundaries.
const KARAOKE_SWITCH_EPSILON_MS = 18
const KARAOKE_SWITCH_EPSILON_MS = 50
const LYRIC_KIND_MAIN = 'main'
const LYRIC_KIND_TRANSLATION = 'translation'
const LYRIC_KIND_PRONUNCIATION = 'pronunciation'

View File

@ -66,14 +66,14 @@ const useStyle = makeStyles(
},
'& .react-jinke-music-player-mobile .react-jinke-music-player-mobile-cover.nd-mobile-lyrics-active':
{
width: 'calc(100% - 40px)',
width: '100%',
maxWidth: 'none',
height: 'clamp(280px, 42vh, 460px)',
aspectRatio: 'auto',
borderRadius: 24,
border: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: '0 18px 40px rgba(0, 0, 0, 0.32)',
background: 'rgba(6, 8, 12, 0.82)',
borderRadius: 12,
border: 'none',
boxShadow: 'none',
background: 'transparent',
cursor: 'default',
},
'& .react-jinke-music-player-mobile .react-jinke-music-player-mobile-cover img.cover':