add jellyfin, improvements
This commit is contained in:
parent
85d2576bdc
commit
58f38b2655
11 changed files with 168 additions and 17 deletions
|
@ -46,6 +46,8 @@ import type {
|
||||||
AuthenticationResponse,
|
AuthenticationResponse,
|
||||||
SearchArgs,
|
SearchArgs,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
|
LyricsArgs,
|
||||||
|
SynchronizedLyricsArray,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { ServerType } from '/@/renderer/types';
|
import { ServerType } from '/@/renderer/types';
|
||||||
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
|
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
|
||||||
|
@ -76,6 +78,7 @@ export type ControllerEndpoint = Partial<{
|
||||||
getFolderList: () => void;
|
getFolderList: () => void;
|
||||||
getFolderSongs: () => void;
|
getFolderSongs: () => void;
|
||||||
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
||||||
|
getLyrics: (args: LyricsArgs) => Promise<SynchronizedLyricsArray>;
|
||||||
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
|
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
|
||||||
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
|
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
|
||||||
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
|
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
|
||||||
|
@ -119,6 +122,7 @@ const endpoints: ApiController = {
|
||||||
getFolderList: undefined,
|
getFolderList: undefined,
|
||||||
getFolderSongs: undefined,
|
getFolderSongs: undefined,
|
||||||
getGenreList: jfController.getGenreList,
|
getGenreList: jfController.getGenreList,
|
||||||
|
getLyrics: jfController.getLyrics,
|
||||||
getMusicFolderList: jfController.getMusicFolderList,
|
getMusicFolderList: jfController.getMusicFolderList,
|
||||||
getPlaylistDetail: jfController.getPlaylistDetail,
|
getPlaylistDetail: jfController.getPlaylistDetail,
|
||||||
getPlaylistList: jfController.getPlaylistList,
|
getPlaylistList: jfController.getPlaylistList,
|
||||||
|
@ -154,6 +158,7 @@ const endpoints: ApiController = {
|
||||||
getFolderList: undefined,
|
getFolderList: undefined,
|
||||||
getFolderSongs: undefined,
|
getFolderSongs: undefined,
|
||||||
getGenreList: ndController.getGenreList,
|
getGenreList: ndController.getGenreList,
|
||||||
|
getLyrics: undefined,
|
||||||
getMusicFolderList: ssController.getMusicFolderList,
|
getMusicFolderList: ssController.getMusicFolderList,
|
||||||
getPlaylistDetail: ndController.getPlaylistDetail,
|
getPlaylistDetail: ndController.getPlaylistDetail,
|
||||||
getPlaylistList: ndController.getPlaylistList,
|
getPlaylistList: ndController.getPlaylistList,
|
||||||
|
@ -188,6 +193,7 @@ const endpoints: ApiController = {
|
||||||
getFolderList: undefined,
|
getFolderList: undefined,
|
||||||
getFolderSongs: undefined,
|
getFolderSongs: undefined,
|
||||||
getGenreList: undefined,
|
getGenreList: undefined,
|
||||||
|
getLyrics: undefined,
|
||||||
getMusicFolderList: ssController.getMusicFolderList,
|
getMusicFolderList: ssController.getMusicFolderList,
|
||||||
getPlaylistDetail: undefined,
|
getPlaylistDetail: undefined,
|
||||||
getPlaylistList: undefined,
|
getPlaylistList: undefined,
|
||||||
|
@ -448,6 +454,12 @@ const getRandomSongList = async (args: RandomSongListArgs) => {
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getLyrics = async (args: LyricsArgs) => {
|
||||||
|
return (
|
||||||
|
apiController('getLyrics', args.apiClientProps.server?.type) as ControllerEndpoint['getLyrics']
|
||||||
|
)?.(args);
|
||||||
|
};
|
||||||
|
|
||||||
export const controller = {
|
export const controller = {
|
||||||
addToPlaylist,
|
addToPlaylist,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
@ -461,6 +473,7 @@ export const controller = {
|
||||||
getAlbumList,
|
getAlbumList,
|
||||||
getArtistList,
|
getArtistList,
|
||||||
getGenreList,
|
getGenreList,
|
||||||
|
getLyrics,
|
||||||
getMusicFolderList,
|
getMusicFolderList,
|
||||||
getPlaylistDetail,
|
getPlaylistDetail,
|
||||||
getPlaylistList,
|
getPlaylistList,
|
||||||
|
|
|
@ -174,6 +174,14 @@ export const contract = c.router({
|
||||||
400: jfType._response.error,
|
400: jfType._response.error,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getSongLyrics: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'users/:userId/Items/:id/Lyrics',
|
||||||
|
responses: {
|
||||||
|
200: jfType._response.lyrics,
|
||||||
|
404: jfType._response.error,
|
||||||
|
},
|
||||||
|
},
|
||||||
getTopSongsList: {
|
getTopSongsList: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'users/:userId/items',
|
path: 'users/:userId/items',
|
||||||
|
|
|
@ -44,6 +44,8 @@ import {
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
RandomSongListResponse,
|
RandomSongListResponse,
|
||||||
RandomSongListArgs,
|
RandomSongListArgs,
|
||||||
|
LyricsArgs,
|
||||||
|
SynchronizedLyricsArray,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
||||||
import { jfNormalize } from './jellyfin-normalize';
|
import { jfNormalize } from './jellyfin-normalize';
|
||||||
|
@ -846,6 +848,28 @@ const getRandomSongList = async (args: RandomSongListArgs): Promise<RandomSongLi
|
||||||
totalRecordCount: res.body.Items.length || 0,
|
totalRecordCount: res.body.Items.length || 0,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getLyrics = async (args: LyricsArgs): Promise<SynchronizedLyricsArray> => {
|
||||||
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
|
if (!apiClientProps.server?.userId) {
|
||||||
|
throw new Error('No userId found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await jfApiClient(apiClientProps).getSongLyrics({
|
||||||
|
params: {
|
||||||
|
id: query.songId,
|
||||||
|
userId: apiClientProps.server?.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get lyrics');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body.Lyrics.map((lyric) => [lyric.Start / 1e4, lyric.Text]);
|
||||||
|
};
|
||||||
|
|
||||||
export const jfController = {
|
export const jfController = {
|
||||||
addToPlaylist,
|
addToPlaylist,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
@ -859,6 +883,7 @@ export const jfController = {
|
||||||
getAlbumList,
|
getAlbumList,
|
||||||
getArtistList,
|
getArtistList,
|
||||||
getGenreList,
|
getGenreList,
|
||||||
|
getLyrics,
|
||||||
getMusicFolderList,
|
getMusicFolderList,
|
||||||
getPlaylistDetail,
|
getPlaylistDetail,
|
||||||
getPlaylistList,
|
getPlaylistList,
|
||||||
|
|
|
@ -631,6 +631,15 @@ const searchParameters = paginationParameters.merge(baseParameters);
|
||||||
|
|
||||||
const search = z.any();
|
const search = z.any();
|
||||||
|
|
||||||
|
const lyricText = z.object({
|
||||||
|
Start: z.number(),
|
||||||
|
Text: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const lyrics = z.object({
|
||||||
|
Lyrics: z.array(lyricText),
|
||||||
|
});
|
||||||
|
|
||||||
export const jfType = {
|
export const jfType = {
|
||||||
_enum: {
|
_enum: {
|
||||||
collection: jfCollection,
|
collection: jfCollection,
|
||||||
|
@ -670,6 +679,7 @@ export const jfType = {
|
||||||
favorite,
|
favorite,
|
||||||
genre,
|
genre,
|
||||||
genreList,
|
genreList,
|
||||||
|
lyrics,
|
||||||
musicFolderList,
|
musicFolderList,
|
||||||
playlist,
|
playlist,
|
||||||
playlistList,
|
playlistList,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
||||||
SearchQuery,
|
SearchQuery,
|
||||||
SongDetailQuery,
|
SongDetailQuery,
|
||||||
RandomSongListQuery,
|
RandomSongListQuery,
|
||||||
|
LyricsQuery,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const queryKeys: Record<
|
export const queryKeys: Record<
|
||||||
|
@ -102,6 +103,10 @@ export const queryKeys: Record<
|
||||||
if (query) return [serverId, 'songs', 'list', query] as const;
|
if (query) return [serverId, 'songs', 'list', query] as const;
|
||||||
return [serverId, 'songs', 'list'] as const;
|
return [serverId, 'songs', 'list'] as const;
|
||||||
},
|
},
|
||||||
|
lyrics: (serverId: string, query?: LyricsQuery) => {
|
||||||
|
if (query) return [serverId, 'song', 'lyrics', query] as const;
|
||||||
|
return [serverId, 'song', 'lyrics'] as const;
|
||||||
|
},
|
||||||
randomSongList: (serverId: string, query?: RandomSongListQuery) => {
|
randomSongList: (serverId: string, query?: RandomSongListQuery) => {
|
||||||
if (query) return [serverId, 'songs', 'randomSongList', query] as const;
|
if (query) return [serverId, 'songs', 'randomSongList', query] as const;
|
||||||
return [serverId, 'songs', 'randomSongList'] as const;
|
return [serverId, 'songs', 'randomSongList'] as const;
|
||||||
|
|
|
@ -1017,6 +1017,16 @@ export type RandomSongListArgs = {
|
||||||
|
|
||||||
export type RandomSongListResponse = SongListResponse;
|
export type RandomSongListResponse = SongListResponse;
|
||||||
|
|
||||||
|
export type LyricsQuery = {
|
||||||
|
songId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LyricsArgs = {
|
||||||
|
query: LyricsQuery;
|
||||||
|
} & BaseEndpointArgs;
|
||||||
|
|
||||||
|
export type SynchronizedLyricsArray = Array<[number, string]>;
|
||||||
|
|
||||||
export const instanceOfCancellationError = (error: any) => {
|
export const instanceOfCancellationError = (error: any) => {
|
||||||
return 'revert' in error;
|
return 'revert' in error;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,9 +3,14 @@ import isElectron from 'is-electron';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { ErrorFallback } from '/@/renderer/features/action-required';
|
import { ErrorFallback } from '/@/renderer/features/action-required';
|
||||||
import { useCurrentServer, useCurrentSong } from '/@/renderer/store';
|
import { useCurrentServer, useCurrentSong } from '/@/renderer/store';
|
||||||
import { SynchronizedLyricsArray, SynchronizedLyrics } from './synchronized-lyrics';
|
import { SynchronizedLyrics } from './synchronized-lyrics';
|
||||||
import { UnsynchronizedLyrics } from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
import { UnsynchronizedLyrics } from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
||||||
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
||||||
|
import { Center, Group } from '@mantine/core';
|
||||||
|
import { RiInformationFill } from 'react-icons/ri';
|
||||||
|
import { TextTitle } from '/@/renderer/components';
|
||||||
|
import { SynchronizedLyricsArray } from '/@/renderer/api/types';
|
||||||
|
import { useSongLyrics } from '/@/renderer/features/lyrics/queries/lyric-query';
|
||||||
|
|
||||||
const lyrics = isElectron() ? window.electron.lyrics : null;
|
const lyrics = isElectron() ? window.electron.lyrics : null;
|
||||||
|
|
||||||
|
@ -22,6 +27,11 @@ export const Lyrics = () => {
|
||||||
const [source, setSource] = useState<string | null>(null);
|
const [source, setSource] = useState<string | null>(null);
|
||||||
const [songLyrics, setSongLyrics] = useState<SynchronizedLyricsArray | string | null>(null);
|
const [songLyrics, setSongLyrics] = useState<SynchronizedLyricsArray | string | null>(null);
|
||||||
|
|
||||||
|
const remoteLyrics = useSongLyrics({
|
||||||
|
query: { songId: currentSong?.id ?? '' },
|
||||||
|
serverId: currentServer?.id,
|
||||||
|
});
|
||||||
|
|
||||||
const songRef = useRef<string | null>(null);
|
const songRef = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -38,7 +48,7 @@ export const Lyrics = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentSong && !currentSong.lyrics) {
|
if (currentSong && !currentSong.lyrics && !remoteLyrics.isLoading && !remoteLyrics.isSuccess) {
|
||||||
lyrics?.fetchLyrics(currentSong);
|
lyrics?.fetchLyrics(currentSong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +56,7 @@ export const Lyrics = () => {
|
||||||
|
|
||||||
setOverride(null);
|
setOverride(null);
|
||||||
setSource(null);
|
setSource(null);
|
||||||
}, [currentSong]);
|
}, [currentSong, remoteLyrics.isLoading, remoteLyrics.isSuccess]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let lyrics: string | null = null;
|
let lyrics: string | null = null;
|
||||||
|
@ -57,6 +67,10 @@ export const Lyrics = () => {
|
||||||
setSource(currentServer?.name ?? 'music server');
|
setSource(currentServer?.name ?? 'music server');
|
||||||
} else if (override) {
|
} else if (override) {
|
||||||
lyrics = override;
|
lyrics = override;
|
||||||
|
} else if (remoteLyrics.isSuccess) {
|
||||||
|
setSource(currentServer?.name ?? 'music server');
|
||||||
|
setSongLyrics(remoteLyrics.data!);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lyrics) {
|
if (lyrics) {
|
||||||
|
@ -82,16 +96,23 @@ export const Lyrics = () => {
|
||||||
} else {
|
} else {
|
||||||
setSongLyrics(null);
|
setSongLyrics(null);
|
||||||
}
|
}
|
||||||
}, [currentServer?.name, currentSong, override]);
|
}, [currentServer?.name, currentSong, override, remoteLyrics.data, remoteLyrics.isSuccess]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
{songLyrics &&
|
{!songLyrics && (
|
||||||
(Array.isArray(songLyrics) ? (
|
<Center>
|
||||||
<SynchronizedLyrics lyrics={songLyrics} />
|
<Group>
|
||||||
) : (
|
<RiInformationFill size="2rem" />
|
||||||
<UnsynchronizedLyrics lyrics={songLyrics} />
|
<TextTitle
|
||||||
))}
|
order={3}
|
||||||
|
weight={700}
|
||||||
|
>
|
||||||
|
No lyrics found
|
||||||
|
</TextTitle>
|
||||||
|
</Group>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
{source && (
|
{source && (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
key="provided-by"
|
key="provided-by"
|
||||||
|
@ -99,6 +120,12 @@ export const Lyrics = () => {
|
||||||
text={`Provided by: ${source}`}
|
text={`Provided by: ${source}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{songLyrics &&
|
||||||
|
(Array.isArray(songLyrics) ? (
|
||||||
|
<SynchronizedLyrics lyrics={songLyrics} />
|
||||||
|
) : (
|
||||||
|
<UnsynchronizedLyrics lyrics={songLyrics} />
|
||||||
|
))}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
25
src/renderer/features/lyrics/queries/lyric-query.ts
Normal file
25
src/renderer/features/lyrics/queries/lyric-query.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { LyricsQuery } from '/@/renderer/api/types';
|
||||||
|
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import { getServerById } from '/@/renderer/store';
|
||||||
|
import { controller } from '/@/renderer/api/controller';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { ServerType } from '/@/renderer/types';
|
||||||
|
|
||||||
|
export const useSongLyrics = (args: QueryHookArgs<LyricsQuery>) => {
|
||||||
|
const { query, serverId } = args;
|
||||||
|
const server = getServerById(serverId);
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
// Note: This currently fetches for every song, even if it shouldn't have
|
||||||
|
// lyrics, because for some reason HasLyrics is not exposed. Thus, ignore the error
|
||||||
|
onError: () => {},
|
||||||
|
queryFn: ({ signal }) => {
|
||||||
|
if (!server) throw new Error('Server not found');
|
||||||
|
// This should only be called for Jellyfin. Return null to ignore errors
|
||||||
|
if (server.type !== ServerType.JELLYFIN) return null;
|
||||||
|
return controller.getLyrics({ apiClientProps: { server, signal }, query });
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.songs.lyrics(server?.id || '', query),
|
||||||
|
});
|
||||||
|
};
|
|
@ -10,11 +10,10 @@ import { PlaybackType, PlayerStatus } from '/@/renderer/types';
|
||||||
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||||
|
import { SynchronizedLyricsArray } from '/@/renderer/api/types';
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
|
||||||
export type SynchronizedLyricsArray = Array<[number, string]>;
|
|
||||||
|
|
||||||
interface SynchronizedLyricsProps {
|
interface SynchronizedLyricsProps {
|
||||||
lyrics: SynchronizedLyricsArray;
|
lyrics: SynchronizedLyricsArray;
|
||||||
}
|
}
|
||||||
|
@ -40,7 +39,12 @@ export const SynchronizedLyrics = ({ lyrics }: SynchronizedLyricsProps) => {
|
||||||
// whether to proceed or stop
|
// whether to proceed or stop
|
||||||
const timerEpoch = useRef(0);
|
const timerEpoch = useRef(0);
|
||||||
|
|
||||||
const followRef = useRef<boolean>(settings.follow);
|
const delayMsRef = useRef(settings.delayMs);
|
||||||
|
const followRef = useRef(settings.follow);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
delayMsRef.current = settings.delayMs;
|
||||||
|
}, [settings.delayMs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Copy the follow settings into a ref that can be accessed in the timeout
|
// Copy the follow settings into a ref that can be accessed in the timeout
|
||||||
|
@ -127,7 +131,7 @@ export const SynchronizedLyrics = ({ lyrics }: SynchronizedLyricsProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index !== lyricRef.current!.length - 1) {
|
if (index !== lyricRef.current!.length - 1) {
|
||||||
const [nextTime] = lyricRef.current![index + 1];
|
const nextTime = lyricRef.current![index + 1][0];
|
||||||
|
|
||||||
const elapsed = performance.now() - start;
|
const elapsed = performance.now() - start;
|
||||||
|
|
||||||
|
@ -149,7 +153,7 @@ export const SynchronizedLyrics = ({ lyrics }: SynchronizedLyricsProps) => {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentLyric(timeInSec * 1000);
|
setCurrentLyric(timeInSec * 1000 + delayMsRef.current);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
|
@ -185,7 +189,7 @@ export const SynchronizedLyrics = ({ lyrics }: SynchronizedLyricsProps) => {
|
||||||
clearTimeout(lyricTimer.current);
|
clearTimeout(lyricTimer.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentLyric(now * 1000);
|
setCurrentLyric(now * 1000 + delayMsRef.current);
|
||||||
}, [now, seeked, setCurrentLyric, status]);
|
}, [now, seeked, setCurrentLyric, status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Switch } from '@mantine/core';
|
import { NumberInput, Switch } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
SettingOption,
|
SettingOption,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
|
@ -82,6 +82,28 @@ export const LyricSettings = () => {
|
||||||
isHidden: !isElectron(),
|
isHidden: !isElectron(),
|
||||||
title: 'Providers to fetch music',
|
title: 'Providers to fetch music',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={settings.delayMs}
|
||||||
|
step={10}
|
||||||
|
width={100}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const value = Number(e.currentTarget.value);
|
||||||
|
setSettings({
|
||||||
|
lyrics: {
|
||||||
|
...settings,
|
||||||
|
delayMs: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'Lyric offset (in milliseconds). Positive values mean that lyrics are shown later, and negative mean that lyrics are shown earlier',
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: 'Lyric offset',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return <SettingsSection options={lyricOptions} />;
|
return <SettingsSection options={lyricOptions} />;
|
||||||
|
|
|
@ -123,6 +123,7 @@ export interface SettingsState {
|
||||||
globalMediaHotkeys: boolean;
|
globalMediaHotkeys: boolean;
|
||||||
};
|
};
|
||||||
lyrics: {
|
lyrics: {
|
||||||
|
delayMs: number;
|
||||||
fetch: boolean;
|
fetch: boolean;
|
||||||
follow: boolean;
|
follow: boolean;
|
||||||
sources: LyricSource[];
|
sources: LyricSource[];
|
||||||
|
@ -209,6 +210,7 @@ const initialState: SettingsState = {
|
||||||
globalMediaHotkeys: true,
|
globalMediaHotkeys: true,
|
||||||
},
|
},
|
||||||
lyrics: {
|
lyrics: {
|
||||||
|
delayMs: 0,
|
||||||
fetch: false,
|
fetch: false,
|
||||||
follow: true,
|
follow: true,
|
||||||
sources: [],
|
sources: [],
|
||||||
|
|
Reference in a new issue