diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index b5a25fd0c..58cb4359e 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -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",
diff --git a/src/renderer/features/lyrics/components/lyrics-scroll-content.module.css b/src/renderer/features/lyrics/components/lyrics-scroll-content.module.css
new file mode 100644
index 000000000..18fc85fdc
--- /dev/null
+++ b/src/renderer/features/lyrics/components/lyrics-scroll-content.module.css
@@ -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;
+ }
+}
diff --git a/src/renderer/features/lyrics/components/lyrics-scroll-content.tsx b/src/renderer/features/lyrics/components/lyrics-scroll-content.tsx
new file mode 100644
index 000000000..e6a014536
--- /dev/null
+++ b/src/renderer/features/lyrics/components/lyrics-scroll-content.tsx
@@ -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 (
+
+ {children}
+
+ );
+};
diff --git a/src/renderer/features/lyrics/components/lyrics-settings-form.tsx b/src/renderer/features/lyrics/components/lyrics-settings-form.tsx
index bce14c847..741d920e2 100644
--- a/src/renderer/features/lyrics/components/lyrics-settings-form.tsx
+++ b/src/renderer/features/lyrics/components/lyrics-settings-form.tsx
@@ -147,6 +147,40 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
`${t('page.fullscreenPlayer.config.lyricGap')} (${t('page.fullscreenPlayer.config.unsynchronized')})`,
),
},
+ {
+ control: (
+ `${value}%`}
+ max={50}
+ min={0}
+ onChangeEnd={(value) => {
+ updateDisplaySetting({ paddingLeft: value });
+ }}
+ step={1}
+ w={100}
+ />
+ ),
+ description: '',
+ title: t('page.fullscreenPlayer.config.lyricPaddingLeft'),
+ },
+ {
+ control: (
+ `${value}%`}
+ max={50}
+ min={0}
+ onChangeEnd={(value) => {
+ updateDisplaySetting({ paddingRight: value });
+ }}
+ step={1}
+ w={100}
+ />
+ ),
+ description: '',
+ title: t('page.fullscreenPlayer.config.lyricPaddingRight'),
+ },
{
control: (
{
diff --git a/src/renderer/features/lyrics/synchronized-karaoke-lyrics.module.css b/src/renderer/features/lyrics/synchronized-karaoke-lyrics.module.css
index 98b03e0cd..c6bc21817 100644
--- a/src/renderer/features/lyrics/synchronized-karaoke-lyrics.module.css
+++ b/src/renderer/features/lyrics/synchronized-karaoke-lyrics.module.css
@@ -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;
- }
}
diff --git a/src/renderer/features/lyrics/synchronized-karaoke-lyrics.tsx b/src/renderer/features/lyrics/synchronized-karaoke-lyrics.tsx
index c17483f32..76e9fa47a 100644
--- a/src/renderer/features/lyrics/synchronized-karaoke-lyrics.tsx
+++ b/src/renderer/features/lyrics/synchronized-karaoke-lyrics.tsx
@@ -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 && (
-
- )}
- {settings.showMatch && remote && (
-
- )}
- {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],
- );
+
+ {settings.showProvider && source && (
+
+ )}
+ {settings.showMatch && remote && (
+
+ )}
+ {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 (
+
+ );
+ }
- if (!rawLine.cueLines?.length) {
return (
-
);
- }
-
- return (
-
- );
- })}
+ })}
+
);
};
diff --git a/src/renderer/features/lyrics/synchronized-lyrics.module.css b/src/renderer/features/lyrics/synchronized-lyrics.module.css
index f7b5a8e37..7f722d293 100644
--- a/src/renderer/features/lyrics/synchronized-lyrics.module.css
+++ b/src/renderer/features/lyrics/synchronized-lyrics.module.css
@@ -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;
- }
}
diff --git a/src/renderer/features/lyrics/synchronized-lyrics.tsx b/src/renderer/features/lyrics/synchronized-lyrics.tsx
index 24165a2d6..28e24cdde 100644
--- a/src/renderer/features/lyrics/synchronized-lyrics.tsx
+++ b/src/renderer/features/lyrics/synchronized-lyrics.tsx
@@ -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 && (
-
- )}
- {settings.showMatch && remote && (
-
- )}
- {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 (
+
+ {settings.showProvider && source && (
- );
- })}
+ )}
+ {settings.showMatch && remote && (
+
+ )}
+ {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 (
+
+ );
+ })}
+
);
};
diff --git a/src/renderer/features/lyrics/unsynchronized-lyrics.module.css b/src/renderer/features/lyrics/unsynchronized-lyrics.module.css
index f207f1adf..4f2133c7a 100644
--- a/src/renderer/features/lyrics/unsynchronized-lyrics.module.css
+++ b/src/renderer/features/lyrics/unsynchronized-lyrics.module.css
@@ -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;
- }
}
diff --git a/src/renderer/features/lyrics/unsynchronized-lyrics.tsx b/src/renderer/features/lyrics/unsynchronized-lyrics.tsx
index 6ee2597c5..6b40b4f08 100644
--- a/src/renderer/features/lyrics/unsynchronized-lyrics.tsx
+++ b/src/renderer/features/lyrics/unsynchronized-lyrics.tsx
@@ -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 (
-
- {settings.showProvider && source && (
-
- )}
- {settings.showMatch && remote && (
-
- )}
- {lines.map((text, idx) => (
-
- ))}
+
+
+ {settings.showProvider && source && (
+
+ )}
+ {settings.showMatch && remote && (
+
+ )}
+ {lines.map((text, idx) => (
+
+ ))}
+
);
};
diff --git a/src/renderer/features/player/components/full-screen-player.tsx b/src/renderer/features/player/components/full-screen-player.tsx
index d4196aa03..b89444e87 100644
--- a/src/renderer/features/player/components/full-screen-player.tsx
+++ b/src/renderer/features/player/components/full-screen-player.tsx
@@ -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 = () => {
+
+ {t('page.fullscreenPlayer.config.lyricPaddingLeft')}
+
+
+ `${value}%`}
+ max={20}
+ min={0}
+ onChangeEnd={(value) => handleLyricsSettings('paddingLeft', value)}
+ step={1}
+ w="100%"
+ />
+
+
+
+ {t('page.fullscreenPlayer.config.lyricPaddingRight')}
+
+
+ `${value}%`}
+ max={20}
+ min={0}
+ onChangeEnd={(value) => handleLyricsSettings('paddingRight', value)}
+ step={1}
+ w="100%"
+ />
+
+
{t('page.fullscreenPlayer.config.lyricAlignment')}
diff --git a/src/renderer/features/player/components/mobile-fullscreen-player-header.tsx b/src/renderer/features/player/components/mobile-fullscreen-player-header.tsx
index fe7855dea..1dd08fa7e 100644
--- a/src/renderer/features/player/components/mobile-fullscreen-player-header.tsx
+++ b/src/renderer/features/player/components/mobile-fullscreen-player-header.tsx
@@ -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(
+
+ {t('page.fullscreenPlayer.config.lyricPaddingLeft')}
+
+
+ `${value}%`}
+ max={20}
+ min={0}
+ onChangeEnd={(value) =>
+ handleLyricsSettings('paddingLeft', value)
+ }
+ step={1}
+ w="100%"
+ />
+
+
+
+ {t('page.fullscreenPlayer.config.lyricPaddingRight')}
+
+
+ `${value}%`}
+ max={20}
+ min={0}
+ onChangeEnd={(value) =>
+ handleLyricsSettings('paddingRight', value)
+ }
+ step={1}
+ w="100%"
+ />
+
+
{t('page.fullscreenPlayer.config.lyricAlignment')}
diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts
index f8c212ea9..87a3d7992 100644
--- a/src/renderer/store/settings.store.ts
+++ b/src/renderer/store/settings.store.ts
@@ -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()(
);
}
+ 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,
},
),
);