From 7c6ecd0cf683d279201e79fd9e502c139bb78a17 Mon Sep 17 00:00:00 2001 From: ranokay Date: Tue, 14 Apr 2026 23:17:31 +0300 Subject: [PATCH] refine karaoke lyrics overlay UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- ui/src/audioplayer/KaraokeLyricsOverlay.jsx | 338 ++++++++++++-------- ui/src/audioplayer/lyrics.js | 2 +- ui/src/audioplayer/styles.js | 10 +- 3 files changed, 207 insertions(+), 143 deletions(-) diff --git a/ui/src/audioplayer/KaraokeLyricsOverlay.jsx b/ui/src/audioplayer/KaraokeLyricsOverlay.jsx index c016df9e5..aefb0127e 100644 --- a/ui/src/audioplayer/KaraokeLyricsOverlay.jsx +++ b/ui/src/audioplayer/KaraokeLyricsOverlay.jsx @@ -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 = ({ >
- {languageBadges.map((badge) => ( -
- - {badge.label} - - {badge.lang} -
- ))} -
-
- - - - - - - - - - + + {badge.label} + + {badge.lang} +
+ ) + return badge.toggleable ? ( + + {badgeEl} + + ) : badgeEl + })}
@@ -1590,6 +1652,7 @@ const KaraokeLyricsOverlay = ({ +
+ ) } diff --git a/ui/src/audioplayer/lyrics.js b/ui/src/audioplayer/lyrics.js index b44e4d9f0..98c638ab3 100644 --- a/ui/src/audioplayer/lyrics.js +++ b/ui/src/audioplayer/lyrics.js @@ -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' diff --git a/ui/src/audioplayer/styles.js b/ui/src/audioplayer/styles.js index 09ccb8fcf..30ccf7afb 100644 --- a/ui/src/audioplayer/styles.js +++ b/ui/src/audioplayer/styles.js @@ -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':