Add additional lyrics customizability options (#146)

This commit is contained in:
jeffvli 2023-08-04 19:32:41 -07:00
parent 72b4a60c7b
commit fca135ce2b
7 changed files with 230 additions and 64 deletions

View File

@ -2,14 +2,9 @@ import type { ChangeEvent } from 'react';
import { MultiSelect } from '/@/renderer/components/select';
import { Slider } from '/@/renderer/components/slider';
import { Switch } from '/@/renderer/components/switch';
import {
useSettingsStoreActions,
useSettingsStore,
useLyricsSettings,
} from '/@/renderer/store/settings.store';
import { useSettingsStoreActions, useSettingsStore } from '/@/renderer/store/settings.store';
import { TableColumn, TableType } from '/@/renderer/types';
import { Option } from '/@/renderer/components/option';
import { NumberInput } from '/@/renderer/components/input';
export const SONG_TABLE_COLUMNS = [
{ label: 'Row Index', value: TableColumn.ROW_INDEX },
@ -97,7 +92,6 @@ interface TableConfigDropdownProps {
export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
const { setSettings } = useSettingsStoreActions();
const tableConfig = useSettingsStore((state) => state.tables);
const lyricConfig = useLyricsSettings();
const handleAddOrRemoveColumns = (values: TableColumn[]) => {
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 (
<>
<Option>
@ -220,25 +196,6 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
/>
</Option.Control>
</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.Control>
<Slider

View 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;
}

View File

@ -1,31 +1,41 @@
import { ComponentPropsWithoutRef } from 'react';
import { TextTitle } from '/@/renderer/components/text-title';
import { TitleProps } from '@mantine/core';
import styled from 'styled-components';
interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
alignment: 'left' | 'center' | 'right';
fontSize: number;
text: string;
}
const StyledText = styled(TextTitle)`
const StyledText = styled(TextTitle)<TitleProps & { $alignment: string; $fontSize: number }>`
color: var(--main-fg);
font-weight: 400;
font-size: 2.5vmax;
transform: scale(0.95);
text-align: ${(props) => props.$alignment};
font-size: ${(props) => props.$fontSize}px;
opacity: 0.5;
&.active {
font-weight: 800;
transform: scale(1);
opacity: 1;
}
&.active.unsynchronized {
opacity: 0.8;
&.unsynchronized {
opacity: 1;
}
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
`;
export const LyricLine = ({ text, ...props }: LyricLineProps) => {
return <StyledText {...props}>{text}</StyledText>;
export const LyricLine = ({ text, alignment, fontSize, ...props }: LyricLineProps) => {
return (
<StyledText
$alignment={alignment}
$fontSize={fontSize}
{...props}
>
{text}
</StyledText>
);
};

View File

@ -15,10 +15,10 @@ import styled from 'styled-components';
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const SynchronizedLyricsContainer = styled.div`
const SynchronizedLyricsContainer = styled.div<{ $gap: number }>`
display: flex;
flex-direction: column;
gap: 2rem;
gap: ${(props) => props.$gap || 5}px;
width: 100%;
height: 100%;
padding: 10vh 0 6vh;
@ -146,7 +146,8 @@ export const SynchronizedLyrics = ({
'sychronized-lyrics-scroll-container',
) 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) {
lyricRef.current = undefined;
@ -295,27 +296,34 @@ export const SynchronizedLyrics = ({
return (
<SynchronizedLyricsContainer
$gap={settings.gap}
className="synchronized-lyrics overlay-scrollbar"
id="sychronized-lyrics-scroll-container"
onMouseEnter={showScrollbar}
onMouseLeave={hideScrollbar}
>
{source && (
{settings.showProvider && source && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSize}
text={`Provided by ${source}`}
/>
)}
{remote && (
{settings.showMatch && remote && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSize}
text={`"${name} by ${artist}"`}
/>
)}
{lyrics.map(([, text], idx) => (
<LyricLine
key={idx}
alignment={settings.alignment}
className="lyric-line synchronized"
fontSize={settings.fontSize}
id={`lyric-${idx}`}
text={text}
/>

View File

@ -2,15 +2,16 @@ import { useMemo } from 'react';
import styled from 'styled-components';
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
import { FullLyricsMetadata } from '/@/renderer/api/types';
import { useLyricsSettings } from '/@/renderer/store';
interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: string;
}
const UnsynchronizedLyricsContainer = styled.div`
const UnsynchronizedLyricsContainer = styled.div<{ $gap: number }>`
display: flex;
flex-direction: column;
gap: 2rem;
gap: ${(props) => props.$gap || 5}px;
width: 100%;
height: 100%;
padding: 10vh 0 6vh;
@ -37,28 +38,38 @@ export const UnsynchronizedLyrics = ({
remote,
source,
}: UnsynchronizedLyricsProps) => {
const settings = useLyricsSettings();
const lines = useMemo(() => {
return lyrics.split('\n');
}, [lyrics]);
return (
<UnsynchronizedLyricsContainer className="unsynchronized-lyrics">
<UnsynchronizedLyricsContainer
$gap={settings.gapUnsync}
className="unsynchronized-lyrics"
>
{source && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSizeUnsync}
text={`Provided by ${source}`}
/>
)}
{remote && (
<LyricLine
alignment={settings.alignment}
className="lyric-credit"
fontSize={settings.fontSizeUnsync}
text={`"${name} by ${artist}"`}
/>
)}
{lines.map((text, idx) => (
<LyricLine
key={idx}
className="lyric-line"
alignment={settings.alignment}
className="lyric-line unsynchronized"
fontSize={settings.fontSizeUnsync}
id={`lyric-${idx}`}
text={text}
/>

View File

@ -1,15 +1,26 @@
import { useLayoutEffect, useRef } from 'react';
import { Group } from '@mantine/core';
import { Divider, Group } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { Variants, motion } from 'framer-motion';
import { RiArrowDownSLine, RiSettings3Line } from 'react-icons/ri';
import { useLocation } from 'react-router';
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 {
useCurrentSong,
useFullScreenPlayerStore,
useFullScreenPlayerStoreActions,
useLyricsSettings,
useSettingsStore,
useSettingsStoreActions,
useWindowSettings,
} from '/@/renderer/store';
import { useFastAverageColor } from '../../../hooks/use-fast-average-color';
@ -61,11 +72,22 @@ const BackgroundImageOverlay = styled.div`
const Controls = () => {
const { dynamicBackground, expanded, useImageAspectRatio } = useFullScreenPlayerStore();
const { setStore } = useFullScreenPlayerStoreActions();
const { setSettings } = useSettingsStoreActions();
const lyricConfig = useLyricsSettings();
const handleToggleFullScreenPlayer = () => {
setStore({ expanded: !expanded });
};
const handleLyricsSettings = (property: string, value: any) => {
setSettings({
lyrics: {
...useSettingsStore.getState().lyrics,
[property]: value,
},
});
};
useHotkeys([['Escape', handleToggleFullScreenPlayer]]);
return (
@ -116,7 +138,7 @@ const Controls = () => {
<Option.Label>Use image aspect ratio</Option.Label>
<Option.Control>
<Switch
defaultChecked={useImageAspectRatio}
checked={useImageAspectRatio}
onChange={(e) =>
setStore({
useImageAspectRatio: e.target.checked,
@ -125,6 +147,124 @@ const Controls = () => {
/>
</Option.Control>
</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" />
</Popover.Dropdown>
</Popover>

View File

@ -132,9 +132,16 @@ export interface SettingsState {
globalMediaHotkeys: boolean;
};
lyrics: {
alignment: 'left' | 'center' | 'right';
delayMs: number;
fetch: boolean;
follow: boolean;
fontSize: number;
fontSizeUnsync: number;
gap: number;
gapUnsync: number;
showMatch: boolean;
showProvider: boolean;
sources: LyricSource[];
};
playback: {
@ -236,9 +243,16 @@ const initialState: SettingsState = {
globalMediaHotkeys: true,
},
lyrics: {
alignment: 'center',
delayMs: 0,
fetch: false,
follow: true,
fontSize: 46,
fontSizeUnsync: 20,
gap: 5,
gapUnsync: 0,
showMatch: true,
showProvider: true,
sources: [],
},
playback: {