mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
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:
parent
45fac62285
commit
7c6ecd0cf6
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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':
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user