add setting for lyrics container padding

This commit is contained in:
jeffvli
2026-07-09 01:20:11 -07:00
parent fefbf7e3e6
commit bdcc88cbfe
14 changed files with 343 additions and 140 deletions
+2
View File
@@ -552,6 +552,8 @@
"lyricOffset": "Lyrics offset (ms)",
"lyricGap": "Lyric gap",
"lyricLineLeadTime": "Line lead time (ms)",
"lyricPaddingLeft": "Lyrics left padding (%)",
"lyricPaddingRight": "Lyrics right padding (%)",
"lyricSize": "Lyric size",
"lyricOpacityNonActive": "Non-active lyric opacity",
"lyricScaleNonActive": "Non-active lyric scale",
@@ -0,0 +1,13 @@
.content {
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
@media screen and (orientation: portrait) {
padding-top: 5vh !important;
padding-right: var(--lyric-padding-right, 0%) !important;
padding-bottom: 5vh !important;
padding-left: var(--lyric-padding-left, 0%) !important;
}
}
@@ -0,0 +1,45 @@
import clsx from 'clsx';
import { useMemo } from 'react';
import styles from './lyrics-scroll-content.module.css';
export interface LyricsScrollContentProps {
bottomScrollPadding?: string;
children: React.ReactNode;
className?: string;
gap?: number;
paddingLeft?: number;
paddingRight?: number;
style?: React.CSSProperties;
}
export const LyricsScrollContent = ({
bottomScrollPadding = '50vh',
children,
className,
gap,
paddingLeft = 0,
paddingRight = 0,
style,
}: LyricsScrollContentProps) => {
const contentStyle = useMemo(
() =>
({
'--lyric-padding-left': `${paddingLeft}%`,
'--lyric-padding-right': `${paddingRight}%`,
gap: gap !== undefined ? `${gap}px` : undefined,
paddingBottom: bottomScrollPadding,
paddingLeft: `${paddingLeft}%`,
paddingRight: `${paddingRight}%`,
paddingTop: '10vh',
...style,
}) as React.CSSProperties,
[bottomScrollPadding, gap, paddingLeft, paddingRight, style],
);
return (
<div className={clsx(styles.content, className)} style={contentStyle}>
{children}
</div>
);
};
@@ -147,6 +147,40 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
`${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.unsynchronized')})`,
),
},
{
control: (
<Slider
defaultValue={displaySettings.paddingLeft ?? 0}
label={(value) => `${value}%`}
max={50}
min={0}
onChangeEnd={(value) => {
updateDisplaySetting({ paddingLeft: value });
}}
step={1}
w={100}
/>
),
description: '',
title: t('page.fullscreenPlayer.config.lyricPaddingLeft'),
},
{
control: (
<Slider
defaultValue={displaySettings.paddingRight ?? 0}
label={(value) => `${value}%`}
max={50}
min={0}
onChangeEnd={(value) => {
updateDisplaySetting({ paddingRight: value });
}}
step={1}
w={100}
/>
),
description: '',
title: t('page.fullscreenPlayer.config.lyricPaddingRight'),
},
{
control: (
<SegmentedControl
@@ -38,6 +38,8 @@ export const useSynchronizedLyricsBase = (settingsKey = 'default', offsetMs?: nu
: 24,
gap: displaySettings.gap && displaySettings.gap !== 0 ? displaySettings.gap : 24,
opacityNonActive: displaySettings.opacityNonActive,
paddingLeft: displaySettings.paddingLeft ?? 0,
paddingRight: displaySettings.paddingRight ?? 0,
scaleNonActive:
displaySettings.scaleNonActive && displaySettings.scaleNonActive !== 0
? displaySettings.scaleNonActive
@@ -135,9 +137,8 @@ export const useSynchronizedLyricsBase = (settingsKey = 'default', offsetMs?: nu
'--lyric-opacity': settings.opacityNonActive,
'--lyric-scale': settings.scaleNonActive,
'--lyric-scale-origin': settings.alignment,
gap: `${settings.gap}px`,
}) as React.CSSProperties,
[settings.alignment, settings.gap, settings.opacityNonActive, settings.scaleNonActive],
[settings.alignment, settings.opacityNonActive, settings.scaleNonActive],
);
const hideScrollbar = useCallback(() => {
@@ -4,11 +4,6 @@
flex-direction: column;
width: 100%;
min-height: 0;
padding: 10vh 0 50vh;
overflow: hidden auto;
word-break: break-all;
@media screen and (orientation: portrait) {
padding: 5vh 0;
}
}
@@ -9,6 +9,7 @@ import {
getLyricLineText,
normalizeLyrics,
} from '/@/renderer/features/lyrics/api/lyrics-utils';
import { LyricsScrollContent } from '/@/renderer/features/lyrics/components/lyrics-scroll-content';
import { SyncedRomajiLyrics } from '/@/renderer/features/lyrics/hooks/use-furigana-lyrics';
import { useLyricsAnimationEngine } from '/@/renderer/features/lyrics/hooks/use-lyrics-animation-engine';
import {
@@ -290,68 +291,74 @@ export const SynchronizedKaraokeLyrics = ({
ref={containerRef}
style={{ ...containerStyle, ...style }}
>
{settings.showProvider && source && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSize}
text={`Provided by ${source}`}
/>
)}
{settings.showMatch && remote && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSize}
text={`"${name} by ${artist}"`}
/>
)}
{normalizedLyrics.map((rawLine, idx) => {
const lineStartMs = getLyricLineStartMs(rawLine);
const pronunciationText = getOverlayText(
pronunciationLyrics,
lineStartMs,
romajiLyrics?.[idx] ? getLyricLineText(romajiLyrics[idx]) : undefined,
);
const translationText = getOverlayText(
translationLyrics,
lineStartMs,
translatedLyrics?.split('\n')[idx],
);
<LyricsScrollContent
gap={settings.gap}
paddingLeft={settings.paddingLeft}
paddingRight={settings.paddingRight}
>
{settings.showProvider && source && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSize}
text={`Provided by ${source}`}
/>
)}
{settings.showMatch && remote && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSize}
text={`"${name} by ${artist}"`}
/>
)}
{normalizedLyrics.map((rawLine, idx) => {
const lineStartMs = getLyricLineStartMs(rawLine);
const pronunciationText = getOverlayText(
pronunciationLyrics,
lineStartMs,
romajiLyrics?.[idx] ? getLyricLineText(romajiLyrics[idx]) : undefined,
);
const translationText = getOverlayText(
translationLyrics,
lineStartMs,
translatedLyrics?.split('\n')[idx],
);
if (!rawLine.cueLines?.length) {
return (
<LyricLine
alignment={settings.alignment}
className="lyric-line synchronized"
data-lyric-time={lineStartMs}
fontSize={settings.fontSize}
id={`karaoke-line-${idx}`}
key={idx}
romajiText={pronunciationText}
text={getLyricLineText(rawLine)}
translatedText={translationText}
/>
);
}
if (!rawLine.cueLines?.length) {
return (
<LyricLine
<KaraokeLyricLine
agents={agents}
alignment={settings.alignment}
className="lyric-line synchronized"
className="synchronized"
cueLines={rawLine.cueLines}
data-lyric-time={lineStartMs}
fontSize={settings.fontSize}
id={`karaoke-line-${idx}`}
key={idx}
lineIndex={idx}
romajiCueLines={syncedRomajiLyrics?.[idx] ?? null}
romajiText={pronunciationText}
text={getLyricLineText(rawLine)}
translatedText={translationText}
/>
);
}
return (
<KaraokeLyricLine
agents={agents}
alignment={settings.alignment}
className="synchronized"
cueLines={rawLine.cueLines}
data-lyric-time={lineStartMs}
fontSize={settings.fontSize}
id={`karaoke-line-${idx}`}
key={idx}
lineIndex={idx}
romajiCueLines={syncedRomajiLyrics?.[idx] ?? null}
romajiText={pronunciationText}
translatedText={translationText}
/>
);
})}
})}
</LyricsScrollContent>
</div>
);
};
@@ -4,12 +4,7 @@
flex-direction: column;
width: 100%;
min-height: 0;
padding: 10vh 0 50vh;
overflow: hidden auto;
word-break: break-all;
transform: translateY(-2rem);
@media screen and (orientation: portrait) {
padding: 5vh 0;
}
}
@@ -9,6 +9,7 @@ import {
getLyricLineText,
normalizeLyrics,
} from '/@/renderer/features/lyrics/api/lyrics-utils';
import { LyricsScrollContent } from '/@/renderer/features/lyrics/components/lyrics-scroll-content';
import { useLyricsAnimationEngine } from '/@/renderer/features/lyrics/hooks/use-lyrics-animation-engine';
import {
LYRICS_SCROLL_CONTAINER_ID,
@@ -235,50 +236,56 @@ export const SynchronizedLyrics = ({
ref={containerRef}
style={{ ...containerStyle, ...style }}
>
{settings.showProvider && source && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSize}
text={`Provided by ${source}`}
/>
)}
{settings.showMatch && remote && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSize}
text={`"${name} by ${artist}"`}
/>
)}
{normalizedLyrics.map((rawLine, idx) => {
const lineStartMs = getLyricLineStartMs(rawLine);
const lineText = getLyricLineText(rawLine);
const pronunciationText = getOverlayText(
pronunciationLyrics,
lineStartMs,
romajiLyrics?.[idx] ? getLyricLineText(romajiLyrics[idx]) : undefined,
);
const translationText = getOverlayText(
translationLyrics,
lineStartMs,
translatedLyrics?.split('\n')[idx],
);
return (
<LyricsScrollContent
gap={settings.gap}
paddingLeft={settings.paddingLeft}
paddingRight={settings.paddingRight}
>
{settings.showProvider && source && (
<LyricLine
alignment={settings.alignment}
className="lyric-line synchronized"
data-lyric-time={lineStartMs}
className="lyric-credit"
fontSize={settings.fontSize}
id={`lyric-${idx}`}
key={idx}
romajiText={pronunciationText}
text={lineText}
translatedText={translationText}
text={`Provided by ${source}`}
/>
);
})}
)}
{settings.showMatch && remote && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSize}
text={`"${name} by ${artist}"`}
/>
)}
{normalizedLyrics.map((rawLine, idx) => {
const lineStartMs = getLyricLineStartMs(rawLine);
const lineText = getLyricLineText(rawLine);
const pronunciationText = getOverlayText(
pronunciationLyrics,
lineStartMs,
romajiLyrics?.[idx] ? getLyricLineText(romajiLyrics[idx]) : undefined,
);
const translationText = getOverlayText(
translationLyrics,
lineStartMs,
translatedLyrics?.split('\n')[idx],
);
return (
<LyricLine
alignment={settings.alignment}
className="lyric-line synchronized"
data-lyric-time={lineStartMs}
fontSize={settings.fontSize}
id={`lyric-${idx}`}
key={idx}
romajiText={pronunciationText}
text={lineText}
translatedText={translationText}
/>
);
})}
</LyricsScrollContent>
</div>
);
};
@@ -4,11 +4,6 @@
flex-direction: column;
width: 100%;
min-height: 0;
padding: 10vh 0 6vh;
overflow: hidden auto;
transform: translateY(-2rem);
@media screen and (orientation: portrait) {
padding: 5vh 0;
}
}
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
import styles from './unsynchronized-lyrics.module.css';
import { LyricsScrollContent } from '/@/renderer/features/lyrics/components/lyrics-scroll-content';
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
import { useLyricsDisplaySettings, useLyricsSettings } from '/@/renderer/store';
import { FullLyricsMetadata } from '/@/shared/types/domain-types';
@@ -49,35 +50,42 @@ export const UnsynchronizedLyrics = ({
}, [romajiLyrics]);
return (
<div className={styles.container} style={{ gap: `${settings.gapUnsync}px` }}>
{settings.showProvider && source && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSizeUnsync}
text={`Provided by ${source}`}
/>
)}
{settings.showMatch && remote && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSizeUnsync}
text={`"${name} by ${artist}"`}
/>
)}
{lines.map((text, idx) => (
<LyricLine
alignment={settings.alignment}
className="lyric-line unsynchronized"
fontSize={settings.fontSizeUnsync}
id={`lyric-${idx}`}
key={idx}
romajiText={romajiLines[idx]}
text={text}
translatedText={translatedLines[idx]}
/>
))}
<div className={styles.container}>
<LyricsScrollContent
bottomScrollPadding="6vh"
gap={settings.gapUnsync}
paddingLeft={displaySettings.paddingLeft ?? 0}
paddingRight={displaySettings.paddingRight ?? 0}
>
{settings.showProvider && source && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSizeUnsync}
text={`Provided by ${source}`}
/>
)}
{settings.showMatch && remote && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSizeUnsync}
text={`"${name} by ${artist}"`}
/>
)}
{lines.map((text, idx) => (
<LyricLine
alignment={settings.alignment}
className="lyric-line unsynchronized"
fontSize={settings.fontSizeUnsync}
id={`lyric-${idx}`}
key={idx}
romajiText={romajiLines[idx]}
text={text}
translatedText={translatedLines[idx]}
/>
))}
</LyricsScrollContent>
</div>
);
};
@@ -248,7 +248,14 @@ const Controls = () => {
};
const handleLyricsSettings = (property: string, value: any) => {
const displayProperties = ['fontSize', 'fontSizeUnsync', 'gap', 'gapUnsync'];
const displayProperties = [
'fontSize',
'fontSizeUnsync',
'gap',
'gapUnsync',
'paddingLeft',
'paddingRight',
];
if (displayProperties.includes(property)) {
const currentDisplay = useSettingsStore.getState().lyricsDisplay;
setSettings({
@@ -475,6 +482,38 @@ const Controls = () => {
</Group>
</Option.Control>
</Option>
<Option>
<Option.Label>
{t('page.fullscreenPlayer.config.lyricPaddingLeft')}
</Option.Label>
<Option.Control>
<Slider
defaultValue={lyricConfig.paddingLeft ?? 0}
label={(value) => `${value}%`}
max={20}
min={0}
onChangeEnd={(value) => handleLyricsSettings('paddingLeft', value)}
step={1}
w="100%"
/>
</Option.Control>
</Option>
<Option>
<Option.Label>
{t('page.fullscreenPlayer.config.lyricPaddingRight')}
</Option.Label>
<Option.Control>
<Slider
defaultValue={lyricConfig.paddingRight ?? 0}
label={(value) => `${value}%`}
max={20}
min={0}
onChangeEnd={(value) => handleLyricsSettings('paddingRight', value)}
step={1}
w="100%"
/>
</Option.Control>
</Option>
<Option>
<Option.Label>
{t('page.fullscreenPlayer.config.lyricAlignment')}
@@ -51,7 +51,14 @@ export const MobileFullscreenPlayerHeader = memo(
const lyricConfig = { ...lyricsSettings, ...displaySettings };
const handleLyricsSettings = (property: string, value: any) => {
const displayProperties = ['fontSize', 'fontSizeUnsync', 'gap', 'gapUnsync'];
const displayProperties = [
'fontSize',
'fontSizeUnsync',
'gap',
'gapUnsync',
'paddingLeft',
'paddingRight',
];
if (displayProperties.includes(property)) {
const currentDisplay = useSettingsStore.getState().lyricsDisplay;
setSettings({
@@ -284,6 +291,42 @@ export const MobileFullscreenPlayerHeader = memo(
</Group>
</Option.Control>
</Option>
<Option>
<Option.Label>
{t('page.fullscreenPlayer.config.lyricPaddingLeft')}
</Option.Label>
<Option.Control>
<Slider
defaultValue={lyricConfig.paddingLeft ?? 0}
label={(value) => `${value}%`}
max={20}
min={0}
onChangeEnd={(value) =>
handleLyricsSettings('paddingLeft', value)
}
step={1}
w="100%"
/>
</Option.Control>
</Option>
<Option>
<Option.Label>
{t('page.fullscreenPlayer.config.lyricPaddingRight')}
</Option.Label>
<Option.Control>
<Slider
defaultValue={lyricConfig.paddingRight ?? 0}
label={(value) => `${value}%`}
max={20}
min={0}
onChangeEnd={(value) =>
handleLyricsSettings('paddingRight', value)
}
step={1}
w="100%"
/>
</Option.Control>
</Option>
<Option>
<Option.Label>
{t('page.fullscreenPlayer.config.lyricAlignment')}
+20 -1
View File
@@ -570,6 +570,8 @@ const LyricsDisplaySettingsSchema = z.object({
gap: z.number(),
gapUnsync: z.number(),
opacityNonActive: z.number(),
paddingLeft: z.number(),
paddingRight: z.number(),
scaleNonActive: z.number(),
});
@@ -1872,6 +1874,8 @@ const initialState: SettingsState = {
gap: 24,
gapUnsync: 24,
opacityNonActive: 0.2,
paddingLeft: 0,
paddingRight: 0,
scaleNonActive: 0.95,
},
},
@@ -2539,10 +2543,25 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
);
}
if (version < 30) {
for (const [key, displaySettings] of Object.entries(state.lyricsDisplay)) {
const legacySettings = displaySettings as typeof displaySettings & {
paddingX?: number;
};
const legacyPaddingX = legacySettings.paddingX ?? 0;
state.lyricsDisplay[key] = {
...displaySettings,
paddingLeft: displaySettings.paddingLeft ?? legacyPaddingX,
paddingRight: displaySettings.paddingRight ?? legacyPaddingX,
};
}
}
return persistedState;
},
name: 'store_settings',
version: 28,
version: 30,
},
),
);