Add additional lyrics customizability options (#146)
This commit is contained in:
parent
72b4a60c7b
commit
fca135ce2b
7 changed files with 230 additions and 64 deletions
|
@ -2,14 +2,9 @@ import type { ChangeEvent } from 'react';
|
||||||
import { MultiSelect } from '/@/renderer/components/select';
|
import { MultiSelect } from '/@/renderer/components/select';
|
||||||
import { Slider } from '/@/renderer/components/slider';
|
import { Slider } from '/@/renderer/components/slider';
|
||||||
import { Switch } from '/@/renderer/components/switch';
|
import { Switch } from '/@/renderer/components/switch';
|
||||||
import {
|
import { useSettingsStoreActions, useSettingsStore } from '/@/renderer/store/settings.store';
|
||||||
useSettingsStoreActions,
|
|
||||||
useSettingsStore,
|
|
||||||
useLyricsSettings,
|
|
||||||
} from '/@/renderer/store/settings.store';
|
|
||||||
import { TableColumn, TableType } from '/@/renderer/types';
|
import { TableColumn, TableType } from '/@/renderer/types';
|
||||||
import { Option } from '/@/renderer/components/option';
|
import { Option } from '/@/renderer/components/option';
|
||||||
import { NumberInput } from '/@/renderer/components/input';
|
|
||||||
|
|
||||||
export const SONG_TABLE_COLUMNS = [
|
export const SONG_TABLE_COLUMNS = [
|
||||||
{ label: 'Row Index', value: TableColumn.ROW_INDEX },
|
{ label: 'Row Index', value: TableColumn.ROW_INDEX },
|
||||||
|
@ -97,7 +92,6 @@ interface TableConfigDropdownProps {
|
||||||
export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
const tableConfig = useSettingsStore((state) => state.tables);
|
const tableConfig = useSettingsStore((state) => state.tables);
|
||||||
const lyricConfig = useLyricsSettings();
|
|
||||||
|
|
||||||
const handleAddOrRemoveColumns = (values: TableColumn[]) => {
|
const handleAddOrRemoveColumns = (values: TableColumn[]) => {
|
||||||
const existingColumns = tableConfig[type].columns;
|
const existingColumns = tableConfig[type].columns;
|
||||||
|
@ -182,24 +176,6 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLyricFollow = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setSettings({
|
|
||||||
lyrics: {
|
|
||||||
...useSettingsStore.getState().lyrics,
|
|
||||||
follow: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLyricOffset = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setSettings({
|
|
||||||
lyrics: {
|
|
||||||
...useSettingsStore.getState().lyrics,
|
|
||||||
delayMs: Number(e.currentTarget.value),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Option>
|
<Option>
|
||||||
|
@ -220,25 +196,6 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
||||||
/>
|
/>
|
||||||
</Option.Control>
|
</Option.Control>
|
||||||
</Option>
|
</Option>
|
||||||
<Option>
|
|
||||||
<Option.Label>Follow current lyrics</Option.Label>
|
|
||||||
<Option.Control>
|
|
||||||
<Switch
|
|
||||||
defaultChecked={lyricConfig.follow}
|
|
||||||
onChange={handleLyricFollow}
|
|
||||||
/>
|
|
||||||
</Option.Control>
|
|
||||||
</Option>
|
|
||||||
<Option>
|
|
||||||
<Option.Label>Lyric offset (ms)</Option.Label>
|
|
||||||
<Option.Control>
|
|
||||||
<NumberInput
|
|
||||||
defaultValue={lyricConfig.delayMs}
|
|
||||||
step={10}
|
|
||||||
onBlur={handleLyricOffset}
|
|
||||||
/>
|
|
||||||
</Option.Control>
|
|
||||||
</Option>
|
|
||||||
<Option>
|
<Option>
|
||||||
<Option.Control>
|
<Option.Control>
|
||||||
<Slider
|
<Slider
|
||||||
|
|
26
src/renderer/features/lyrics/lyric-line.module.scss
Normal file
26
src/renderer/features/lyrics/lyric-line.module.scss
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
.lyric-line {
|
||||||
|
color: var(--main-fg);
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
font-size: 2.5vmax;
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
.active {
|
||||||
|
font-weight: 800 !important;
|
||||||
|
transform: scale(1) !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active.unsynchronized {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyric-line.active {
|
||||||
|
font-weight: 800;
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
|
@ -1,31 +1,41 @@
|
||||||
import { ComponentPropsWithoutRef } from 'react';
|
import { ComponentPropsWithoutRef } from 'react';
|
||||||
import { TextTitle } from '/@/renderer/components/text-title';
|
import { TextTitle } from '/@/renderer/components/text-title';
|
||||||
|
import { TitleProps } from '@mantine/core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
|
interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
|
||||||
|
alignment: 'left' | 'center' | 'right';
|
||||||
|
fontSize: number;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledText = styled(TextTitle)`
|
const StyledText = styled(TextTitle)<TitleProps & { $alignment: string; $fontSize: number }>`
|
||||||
color: var(--main-fg);
|
color: var(--main-fg);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 2.5vmax;
|
text-align: ${(props) => props.$alignment};
|
||||||
transform: scale(0.95);
|
font-size: ${(props) => props.$fontSize}px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active.unsynchronized {
|
&.unsynchronized {
|
||||||
opacity: 0.8;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LyricLine = ({ text, ...props }: LyricLineProps) => {
|
export const LyricLine = ({ text, alignment, fontSize, ...props }: LyricLineProps) => {
|
||||||
return <StyledText {...props}>{text}</StyledText>;
|
return (
|
||||||
|
<StyledText
|
||||||
|
$alignment={alignment}
|
||||||
|
$fontSize={fontSize}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</StyledText>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,10 +15,10 @@ import styled from 'styled-components';
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
|
||||||
const SynchronizedLyricsContainer = styled.div`
|
const SynchronizedLyricsContainer = styled.div<{ $gap: number }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: ${(props) => props.$gap || 5}px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 10vh 0 6vh;
|
padding: 10vh 0 6vh;
|
||||||
|
@ -146,7 +146,8 @@ export const SynchronizedLyrics = ({
|
||||||
'sychronized-lyrics-scroll-container',
|
'sychronized-lyrics-scroll-container',
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
const currentLyric = document.querySelector(`#lyric-${index}`) as HTMLElement;
|
const currentLyric = document.querySelector(`#lyric-${index}`) as HTMLElement;
|
||||||
const offsetTop = currentLyric.offsetTop - doc.clientHeight / 2 ?? 0;
|
// eslint-disable-next-line no-unsafe-optional-chaining
|
||||||
|
const offsetTop = currentLyric?.offsetTop - doc?.clientHeight / 2 ?? 0;
|
||||||
|
|
||||||
if (currentLyric === null) {
|
if (currentLyric === null) {
|
||||||
lyricRef.current = undefined;
|
lyricRef.current = undefined;
|
||||||
|
@ -295,27 +296,34 @@ export const SynchronizedLyrics = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SynchronizedLyricsContainer
|
<SynchronizedLyricsContainer
|
||||||
|
$gap={settings.gap}
|
||||||
className="synchronized-lyrics overlay-scrollbar"
|
className="synchronized-lyrics overlay-scrollbar"
|
||||||
id="sychronized-lyrics-scroll-container"
|
id="sychronized-lyrics-scroll-container"
|
||||||
onMouseEnter={showScrollbar}
|
onMouseEnter={showScrollbar}
|
||||||
onMouseLeave={hideScrollbar}
|
onMouseLeave={hideScrollbar}
|
||||||
>
|
>
|
||||||
{source && (
|
{settings.showProvider && source && (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
|
alignment={settings.alignment}
|
||||||
className="lyric-credit"
|
className="lyric-credit"
|
||||||
|
fontSize={settings.fontSize}
|
||||||
text={`Provided by ${source}`}
|
text={`Provided by ${source}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{remote && (
|
{settings.showMatch && remote && (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
|
alignment={settings.alignment}
|
||||||
className="lyric-credit"
|
className="lyric-credit"
|
||||||
|
fontSize={settings.fontSize}
|
||||||
text={`"${name} by ${artist}"`}
|
text={`"${name} by ${artist}"`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{lyrics.map(([, text], idx) => (
|
{lyrics.map(([, text], idx) => (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
key={idx}
|
key={idx}
|
||||||
|
alignment={settings.alignment}
|
||||||
className="lyric-line synchronized"
|
className="lyric-line synchronized"
|
||||||
|
fontSize={settings.fontSize}
|
||||||
id={`lyric-${idx}`}
|
id={`lyric-${idx}`}
|
||||||
text={text}
|
text={text}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,15 +2,16 @@ import { useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
||||||
import { FullLyricsMetadata } from '/@/renderer/api/types';
|
import { FullLyricsMetadata } from '/@/renderer/api/types';
|
||||||
|
import { useLyricsSettings } from '/@/renderer/store';
|
||||||
|
|
||||||
interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
||||||
lyrics: string;
|
lyrics: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UnsynchronizedLyricsContainer = styled.div`
|
const UnsynchronizedLyricsContainer = styled.div<{ $gap: number }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: ${(props) => props.$gap || 5}px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 10vh 0 6vh;
|
padding: 10vh 0 6vh;
|
||||||
|
@ -37,28 +38,38 @@ export const UnsynchronizedLyrics = ({
|
||||||
remote,
|
remote,
|
||||||
source,
|
source,
|
||||||
}: UnsynchronizedLyricsProps) => {
|
}: UnsynchronizedLyricsProps) => {
|
||||||
|
const settings = useLyricsSettings();
|
||||||
const lines = useMemo(() => {
|
const lines = useMemo(() => {
|
||||||
return lyrics.split('\n');
|
return lyrics.split('\n');
|
||||||
}, [lyrics]);
|
}, [lyrics]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnsynchronizedLyricsContainer className="unsynchronized-lyrics">
|
<UnsynchronizedLyricsContainer
|
||||||
|
$gap={settings.gapUnsync}
|
||||||
|
className="unsynchronized-lyrics"
|
||||||
|
>
|
||||||
{source && (
|
{source && (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
|
alignment={settings.alignment}
|
||||||
className="lyric-credit"
|
className="lyric-credit"
|
||||||
|
fontSize={settings.fontSizeUnsync}
|
||||||
text={`Provided by ${source}`}
|
text={`Provided by ${source}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{remote && (
|
{remote && (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
|
alignment={settings.alignment}
|
||||||
className="lyric-credit"
|
className="lyric-credit"
|
||||||
|
fontSize={settings.fontSizeUnsync}
|
||||||
text={`"${name} by ${artist}"`}
|
text={`"${name} by ${artist}"`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{lines.map((text, idx) => (
|
{lines.map((text, idx) => (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
key={idx}
|
key={idx}
|
||||||
className="lyric-line"
|
alignment={settings.alignment}
|
||||||
|
className="lyric-line unsynchronized"
|
||||||
|
fontSize={settings.fontSizeUnsync}
|
||||||
id={`lyric-${idx}`}
|
id={`lyric-${idx}`}
|
||||||
text={text}
|
text={text}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
import { useLayoutEffect, useRef } from 'react';
|
import { useLayoutEffect, useRef } from 'react';
|
||||||
import { Group } from '@mantine/core';
|
import { Divider, Group } from '@mantine/core';
|
||||||
import { useHotkeys } from '@mantine/hooks';
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
import { Variants, motion } from 'framer-motion';
|
import { Variants, motion } from 'framer-motion';
|
||||||
import { RiArrowDownSLine, RiSettings3Line } from 'react-icons/ri';
|
import { RiArrowDownSLine, RiSettings3Line } from 'react-icons/ri';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Button, Option, Popover, Switch } from '/@/renderer/components';
|
import {
|
||||||
|
Button,
|
||||||
|
NumberInput,
|
||||||
|
Option,
|
||||||
|
Popover,
|
||||||
|
Select,
|
||||||
|
Slider,
|
||||||
|
Switch,
|
||||||
|
} from '/@/renderer/components';
|
||||||
import {
|
import {
|
||||||
useCurrentSong,
|
useCurrentSong,
|
||||||
useFullScreenPlayerStore,
|
useFullScreenPlayerStore,
|
||||||
useFullScreenPlayerStoreActions,
|
useFullScreenPlayerStoreActions,
|
||||||
|
useLyricsSettings,
|
||||||
|
useSettingsStore,
|
||||||
|
useSettingsStoreActions,
|
||||||
useWindowSettings,
|
useWindowSettings,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { useFastAverageColor } from '../../../hooks/use-fast-average-color';
|
import { useFastAverageColor } from '../../../hooks/use-fast-average-color';
|
||||||
|
@ -61,11 +72,22 @@ const BackgroundImageOverlay = styled.div`
|
||||||
const Controls = () => {
|
const Controls = () => {
|
||||||
const { dynamicBackground, expanded, useImageAspectRatio } = useFullScreenPlayerStore();
|
const { dynamicBackground, expanded, useImageAspectRatio } = useFullScreenPlayerStore();
|
||||||
const { setStore } = useFullScreenPlayerStoreActions();
|
const { setStore } = useFullScreenPlayerStoreActions();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
const lyricConfig = useLyricsSettings();
|
||||||
|
|
||||||
const handleToggleFullScreenPlayer = () => {
|
const handleToggleFullScreenPlayer = () => {
|
||||||
setStore({ expanded: !expanded });
|
setStore({ expanded: !expanded });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLyricsSettings = (property: string, value: any) => {
|
||||||
|
setSettings({
|
||||||
|
lyrics: {
|
||||||
|
...useSettingsStore.getState().lyrics,
|
||||||
|
[property]: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useHotkeys([['Escape', handleToggleFullScreenPlayer]]);
|
useHotkeys([['Escape', handleToggleFullScreenPlayer]]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -116,7 +138,7 @@ const Controls = () => {
|
||||||
<Option.Label>Use image aspect ratio</Option.Label>
|
<Option.Label>Use image aspect ratio</Option.Label>
|
||||||
<Option.Control>
|
<Option.Control>
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={useImageAspectRatio}
|
checked={useImageAspectRatio}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setStore({
|
setStore({
|
||||||
useImageAspectRatio: e.target.checked,
|
useImageAspectRatio: e.target.checked,
|
||||||
|
@ -125,6 +147,124 @@ const Controls = () => {
|
||||||
/>
|
/>
|
||||||
</Option.Control>
|
</Option.Control>
|
||||||
</Option>
|
</Option>
|
||||||
|
<Divider my="sm" />
|
||||||
|
<Option>
|
||||||
|
<Option.Label>Follow current lyrics</Option.Label>
|
||||||
|
<Option.Control>
|
||||||
|
<Switch
|
||||||
|
checked={lyricConfig.follow}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleLyricsSettings('follow', e.currentTarget.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
<Option>
|
||||||
|
<Option.Label>Show lyrics provider</Option.Label>
|
||||||
|
<Option.Control>
|
||||||
|
<Switch
|
||||||
|
checked={lyricConfig.showProvider}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleLyricsSettings('showProvider', e.currentTarget.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
<Option>
|
||||||
|
<Option.Label>Show lyrics match</Option.Label>
|
||||||
|
<Option.Control>
|
||||||
|
<Switch
|
||||||
|
checked={lyricConfig.showMatch}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleLyricsSettings('showMatch', e.currentTarget.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
<Option>
|
||||||
|
<Option.Label>Lyrics size</Option.Label>
|
||||||
|
<Option.Control>
|
||||||
|
<Group
|
||||||
|
noWrap
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
<Slider
|
||||||
|
defaultValue={lyricConfig.fontSize}
|
||||||
|
label={(e) => `Synchronized: ${e}px`}
|
||||||
|
max={72}
|
||||||
|
min={8}
|
||||||
|
w="100%"
|
||||||
|
onChangeEnd={(e) => handleLyricsSettings('fontSize', Number(e))}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
defaultValue={lyricConfig.fontSize}
|
||||||
|
label={(e) => `Unsynchronized: ${e}px`}
|
||||||
|
max={72}
|
||||||
|
min={8}
|
||||||
|
w="100%"
|
||||||
|
onChangeEnd={(e) =>
|
||||||
|
handleLyricsSettings('fontSizeUnsync', Number(e))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
<Option>
|
||||||
|
<Option.Label>Lyrics gap</Option.Label>
|
||||||
|
<Option.Control>
|
||||||
|
<Group
|
||||||
|
noWrap
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
<Slider
|
||||||
|
defaultValue={lyricConfig.gap}
|
||||||
|
label={(e) => `Synchronized: ${e}px`}
|
||||||
|
max={50}
|
||||||
|
min={0}
|
||||||
|
w="100%"
|
||||||
|
onChangeEnd={(e) => handleLyricsSettings('gap', Number(e))}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
defaultValue={lyricConfig.gap}
|
||||||
|
label={(e) => `Unsynchronized: ${e}px`}
|
||||||
|
max={50}
|
||||||
|
min={0}
|
||||||
|
w="100%"
|
||||||
|
onChangeEnd={(e) =>
|
||||||
|
handleLyricsSettings('gapUnsync', Number(e))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
<Option>
|
||||||
|
<Option.Label>Lyrics alignment</Option.Label>
|
||||||
|
<Option.Control>
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{ label: 'Left', value: 'left' },
|
||||||
|
{ label: 'Center', value: 'center' },
|
||||||
|
{ label: 'Right', value: 'right' },
|
||||||
|
]}
|
||||||
|
value={lyricConfig.alignment}
|
||||||
|
onChange={(e) => handleLyricsSettings('alignment', e)}
|
||||||
|
/>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
<Option>
|
||||||
|
<Option.Label>Lyrics offset (ms)</Option.Label>
|
||||||
|
<Option.Control>
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={lyricConfig.delayMs}
|
||||||
|
hideControls={false}
|
||||||
|
step={10}
|
||||||
|
onBlur={(e) =>
|
||||||
|
handleLyricsSettings('delayMs', Number(e.currentTarget.value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
<Divider my="sm" />
|
||||||
<TableConfigDropdown type="fullScreen" />
|
<TableConfigDropdown type="fullScreen" />
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
@ -132,9 +132,16 @@ export interface SettingsState {
|
||||||
globalMediaHotkeys: boolean;
|
globalMediaHotkeys: boolean;
|
||||||
};
|
};
|
||||||
lyrics: {
|
lyrics: {
|
||||||
|
alignment: 'left' | 'center' | 'right';
|
||||||
delayMs: number;
|
delayMs: number;
|
||||||
fetch: boolean;
|
fetch: boolean;
|
||||||
follow: boolean;
|
follow: boolean;
|
||||||
|
fontSize: number;
|
||||||
|
fontSizeUnsync: number;
|
||||||
|
gap: number;
|
||||||
|
gapUnsync: number;
|
||||||
|
showMatch: boolean;
|
||||||
|
showProvider: boolean;
|
||||||
sources: LyricSource[];
|
sources: LyricSource[];
|
||||||
};
|
};
|
||||||
playback: {
|
playback: {
|
||||||
|
@ -236,9 +243,16 @@ const initialState: SettingsState = {
|
||||||
globalMediaHotkeys: true,
|
globalMediaHotkeys: true,
|
||||||
},
|
},
|
||||||
lyrics: {
|
lyrics: {
|
||||||
|
alignment: 'center',
|
||||||
delayMs: 0,
|
delayMs: 0,
|
||||||
fetch: false,
|
fetch: false,
|
||||||
follow: true,
|
follow: true,
|
||||||
|
fontSize: 46,
|
||||||
|
fontSizeUnsync: 20,
|
||||||
|
gap: 5,
|
||||||
|
gapUnsync: 0,
|
||||||
|
showMatch: true,
|
||||||
|
showProvider: true,
|
||||||
sources: [],
|
sources: [],
|
||||||
},
|
},
|
||||||
playback: {
|
playback: {
|
||||||
|
|
Reference in a new issue