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)", "lyricOffset": "Lyrics offset (ms)",
"lyricGap": "Lyric gap", "lyricGap": "Lyric gap",
"lyricLineLeadTime": "Line lead time (ms)", "lyricLineLeadTime": "Line lead time (ms)",
"lyricPaddingLeft": "Lyrics left padding (%)",
"lyricPaddingRight": "Lyrics right padding (%)",
"lyricSize": "Lyric size", "lyricSize": "Lyric size",
"lyricOpacityNonActive": "Non-active lyric opacity", "lyricOpacityNonActive": "Non-active lyric opacity",
"lyricScaleNonActive": "Non-active lyric scale", "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')})`, `${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: ( control: (
<SegmentedControl <SegmentedControl
@@ -38,6 +38,8 @@ export const useSynchronizedLyricsBase = (settingsKey = 'default', offsetMs?: nu
: 24, : 24,
gap: displaySettings.gap && displaySettings.gap !== 0 ? displaySettings.gap : 24, gap: displaySettings.gap && displaySettings.gap !== 0 ? displaySettings.gap : 24,
opacityNonActive: displaySettings.opacityNonActive, opacityNonActive: displaySettings.opacityNonActive,
paddingLeft: displaySettings.paddingLeft ?? 0,
paddingRight: displaySettings.paddingRight ?? 0,
scaleNonActive: scaleNonActive:
displaySettings.scaleNonActive && displaySettings.scaleNonActive !== 0 displaySettings.scaleNonActive && displaySettings.scaleNonActive !== 0
? displaySettings.scaleNonActive ? displaySettings.scaleNonActive
@@ -135,9 +137,8 @@ export const useSynchronizedLyricsBase = (settingsKey = 'default', offsetMs?: nu
'--lyric-opacity': settings.opacityNonActive, '--lyric-opacity': settings.opacityNonActive,
'--lyric-scale': settings.scaleNonActive, '--lyric-scale': settings.scaleNonActive,
'--lyric-scale-origin': settings.alignment, '--lyric-scale-origin': settings.alignment,
gap: `${settings.gap}px`,
}) as React.CSSProperties, }) as React.CSSProperties,
[settings.alignment, settings.gap, settings.opacityNonActive, settings.scaleNonActive], [settings.alignment, settings.opacityNonActive, settings.scaleNonActive],
); );
const hideScrollbar = useCallback(() => { const hideScrollbar = useCallback(() => {
@@ -4,11 +4,6 @@
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
min-height: 0; min-height: 0;
padding: 10vh 0 50vh;
overflow: hidden auto; overflow: hidden auto;
word-break: break-all; word-break: break-all;
@media screen and (orientation: portrait) {
padding: 5vh 0;
}
} }
@@ -9,6 +9,7 @@ import {
getLyricLineText, getLyricLineText,
normalizeLyrics, normalizeLyrics,
} from '/@/renderer/features/lyrics/api/lyrics-utils'; } 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 { SyncedRomajiLyrics } from '/@/renderer/features/lyrics/hooks/use-furigana-lyrics';
import { useLyricsAnimationEngine } from '/@/renderer/features/lyrics/hooks/use-lyrics-animation-engine'; import { useLyricsAnimationEngine } from '/@/renderer/features/lyrics/hooks/use-lyrics-animation-engine';
import { import {
@@ -290,68 +291,74 @@ export const SynchronizedKaraokeLyrics = ({
ref={containerRef} ref={containerRef}
style={{ ...containerStyle, ...style }} style={{ ...containerStyle, ...style }}
> >
{settings.showProvider && source && ( <LyricsScrollContent
<LyricLine gap={settings.gap}
alignment={settings.alignment} paddingLeft={settings.paddingLeft}
className="lyric-credit" paddingRight={settings.paddingRight}
fontSize={settings.fontSize} >
text={`Provided by ${source}`} {settings.showProvider && source && (
/> <LyricLine
)} alignment={settings.alignment}
{settings.showMatch && remote && ( className="lyric-credit"
<LyricLine fontSize={settings.fontSize}
alignment={settings.alignment} text={`Provided by ${source}`}
className="lyric-credit" />
fontSize={settings.fontSize} )}
text={`"${name} by ${artist}"`} {settings.showMatch && remote && (
/> <LyricLine
)} alignment={settings.alignment}
{normalizedLyrics.map((rawLine, idx) => { className="lyric-credit"
const lineStartMs = getLyricLineStartMs(rawLine); fontSize={settings.fontSize}
const pronunciationText = getOverlayText( text={`"${name} by ${artist}"`}
pronunciationLyrics, />
lineStartMs, )}
romajiLyrics?.[idx] ? getLyricLineText(romajiLyrics[idx]) : undefined, {normalizedLyrics.map((rawLine, idx) => {
); const lineStartMs = getLyricLineStartMs(rawLine);
const translationText = getOverlayText( const pronunciationText = getOverlayText(
translationLyrics, pronunciationLyrics,
lineStartMs, lineStartMs,
translatedLyrics?.split('\n')[idx], 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 ( return (
<LyricLine <KaraokeLyricLine
agents={agents}
alignment={settings.alignment} alignment={settings.alignment}
className="lyric-line synchronized" className="synchronized"
cueLines={rawLine.cueLines}
data-lyric-time={lineStartMs} data-lyric-time={lineStartMs}
fontSize={settings.fontSize} fontSize={settings.fontSize}
id={`karaoke-line-${idx}`} id={`karaoke-line-${idx}`}
key={idx} key={idx}
lineIndex={idx}
romajiCueLines={syncedRomajiLyrics?.[idx] ?? null}
romajiText={pronunciationText} romajiText={pronunciationText}
text={getLyricLineText(rawLine)}
translatedText={translationText} translatedText={translationText}
/> />
); );
} })}
</LyricsScrollContent>
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}
/>
);
})}
</div> </div>
); );
}; };
@@ -4,12 +4,7 @@
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
min-height: 0; min-height: 0;
padding: 10vh 0 50vh;
overflow: hidden auto; overflow: hidden auto;
word-break: break-all; word-break: break-all;
transform: translateY(-2rem); transform: translateY(-2rem);
@media screen and (orientation: portrait) {
padding: 5vh 0;
}
} }
@@ -9,6 +9,7 @@ import {
getLyricLineText, getLyricLineText,
normalizeLyrics, normalizeLyrics,
} from '/@/renderer/features/lyrics/api/lyrics-utils'; } 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 { useLyricsAnimationEngine } from '/@/renderer/features/lyrics/hooks/use-lyrics-animation-engine';
import { import {
LYRICS_SCROLL_CONTAINER_ID, LYRICS_SCROLL_CONTAINER_ID,
@@ -235,50 +236,56 @@ export const SynchronizedLyrics = ({
ref={containerRef} ref={containerRef}
style={{ ...containerStyle, ...style }} style={{ ...containerStyle, ...style }}
> >
{settings.showProvider && source && ( <LyricsScrollContent
<LyricLine gap={settings.gap}
alignment={settings.alignment} paddingLeft={settings.paddingLeft}
className="lyric-credit" paddingRight={settings.paddingRight}
fontSize={settings.fontSize} >
text={`Provided by ${source}`} {settings.showProvider && 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 <LyricLine
alignment={settings.alignment} alignment={settings.alignment}
className="lyric-line synchronized" className="lyric-credit"
data-lyric-time={lineStartMs}
fontSize={settings.fontSize} fontSize={settings.fontSize}
id={`lyric-${idx}`} text={`Provided by ${source}`}
key={idx}
romajiText={pronunciationText}
text={lineText}
translatedText={translationText}
/> />
); )}
})} {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> </div>
); );
}; };
@@ -4,11 +4,6 @@
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
min-height: 0; min-height: 0;
padding: 10vh 0 6vh;
overflow: hidden auto; overflow: hidden auto;
transform: translateY(-2rem); 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 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 { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
import { useLyricsDisplaySettings, useLyricsSettings } from '/@/renderer/store'; import { useLyricsDisplaySettings, useLyricsSettings } from '/@/renderer/store';
import { FullLyricsMetadata } from '/@/shared/types/domain-types'; import { FullLyricsMetadata } from '/@/shared/types/domain-types';
@@ -49,35 +50,42 @@ export const UnsynchronizedLyrics = ({
}, [romajiLyrics]); }, [romajiLyrics]);
return ( return (
<div className={styles.container} style={{ gap: `${settings.gapUnsync}px` }}> <div className={styles.container}>
{settings.showProvider && source && ( <LyricsScrollContent
<LyricLine bottomScrollPadding="6vh"
alignment={settings.alignment} gap={settings.gapUnsync}
className="lyric-credit" paddingLeft={displaySettings.paddingLeft ?? 0}
fontSize={settings.fontSizeUnsync} paddingRight={displaySettings.paddingRight ?? 0}
text={`Provided by ${source}`} >
/> {settings.showProvider && source && (
)} <LyricLine
{settings.showMatch && remote && ( alignment={settings.alignment}
<LyricLine className="lyric-credit"
alignment={settings.alignment} fontSize={settings.fontSizeUnsync}
className="lyric-credit" text={`Provided by ${source}`}
fontSize={settings.fontSizeUnsync} />
text={`"${name} by ${artist}"`} )}
/> {settings.showMatch && remote && (
)} <LyricLine
{lines.map((text, idx) => ( alignment={settings.alignment}
<LyricLine className="lyric-credit"
alignment={settings.alignment} fontSize={settings.fontSizeUnsync}
className="lyric-line unsynchronized" text={`"${name} by ${artist}"`}
fontSize={settings.fontSizeUnsync} />
id={`lyric-${idx}`} )}
key={idx} {lines.map((text, idx) => (
romajiText={romajiLines[idx]} <LyricLine
text={text} alignment={settings.alignment}
translatedText={translatedLines[idx]} className="lyric-line unsynchronized"
/> fontSize={settings.fontSizeUnsync}
))} id={`lyric-${idx}`}
key={idx}
romajiText={romajiLines[idx]}
text={text}
translatedText={translatedLines[idx]}
/>
))}
</LyricsScrollContent>
</div> </div>
); );
}; };
@@ -248,7 +248,14 @@ const Controls = () => {
}; };
const handleLyricsSettings = (property: string, value: any) => { const handleLyricsSettings = (property: string, value: any) => {
const displayProperties = ['fontSize', 'fontSizeUnsync', 'gap', 'gapUnsync']; const displayProperties = [
'fontSize',
'fontSizeUnsync',
'gap',
'gapUnsync',
'paddingLeft',
'paddingRight',
];
if (displayProperties.includes(property)) { if (displayProperties.includes(property)) {
const currentDisplay = useSettingsStore.getState().lyricsDisplay; const currentDisplay = useSettingsStore.getState().lyricsDisplay;
setSettings({ setSettings({
@@ -475,6 +482,38 @@ const Controls = () => {
</Group> </Group>
</Option.Control> </Option.Control>
</Option> </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>
<Option.Label> <Option.Label>
{t('page.fullscreenPlayer.config.lyricAlignment')} {t('page.fullscreenPlayer.config.lyricAlignment')}
@@ -51,7 +51,14 @@ export const MobileFullscreenPlayerHeader = memo(
const lyricConfig = { ...lyricsSettings, ...displaySettings }; const lyricConfig = { ...lyricsSettings, ...displaySettings };
const handleLyricsSettings = (property: string, value: any) => { const handleLyricsSettings = (property: string, value: any) => {
const displayProperties = ['fontSize', 'fontSizeUnsync', 'gap', 'gapUnsync']; const displayProperties = [
'fontSize',
'fontSizeUnsync',
'gap',
'gapUnsync',
'paddingLeft',
'paddingRight',
];
if (displayProperties.includes(property)) { if (displayProperties.includes(property)) {
const currentDisplay = useSettingsStore.getState().lyricsDisplay; const currentDisplay = useSettingsStore.getState().lyricsDisplay;
setSettings({ setSettings({
@@ -284,6 +291,42 @@ export const MobileFullscreenPlayerHeader = memo(
</Group> </Group>
</Option.Control> </Option.Control>
</Option> </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>
<Option.Label> <Option.Label>
{t('page.fullscreenPlayer.config.lyricAlignment')} {t('page.fullscreenPlayer.config.lyricAlignment')}
+20 -1
View File
@@ -570,6 +570,8 @@ const LyricsDisplaySettingsSchema = z.object({
gap: z.number(), gap: z.number(),
gapUnsync: z.number(), gapUnsync: z.number(),
opacityNonActive: z.number(), opacityNonActive: z.number(),
paddingLeft: z.number(),
paddingRight: z.number(),
scaleNonActive: z.number(), scaleNonActive: z.number(),
}); });
@@ -1872,6 +1874,8 @@ const initialState: SettingsState = {
gap: 24, gap: 24,
gapUnsync: 24, gapUnsync: 24,
opacityNonActive: 0.2, opacityNonActive: 0.2,
paddingLeft: 0,
paddingRight: 0,
scaleNonActive: 0.95, 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; return persistedState;
}, },
name: 'store_settings', name: 'store_settings',
version: 28, version: 30,
}, },
), ),
); );