mirror of
https://github.com/jeffvli/feishin.git
synced 2024-11-20 14:37:06 +01:00
Add additional lyrics customizability options (#146)
This commit is contained in:
parent
72b4a60c7b
commit
fca135ce2b
@ -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
|
||||
|
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 { 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>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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: {
|
||||
|
Loading…
Reference in New Issue
Block a user