feishin/src/renderer/features/lyrics/lyrics.tsx

223 lines
6.8 KiB
TypeScript
Raw Normal View History

import { useCallback, useEffect, useState } from 'react';
2023-06-03 18:03:32 -07:00
import { Center, Group } from '@mantine/core';
import { AnimatePresence, motion } from 'framer-motion';
2023-05-22 17:38:31 -07:00
import { ErrorBoundary } from 'react-error-boundary';
2023-06-02 23:54:34 -07:00
import { RiInformationFill } from 'react-icons/ri';
import styled from 'styled-components';
import { useSongLyricsByRemoteId, useSongLyricsBySong } from './queries/lyric-query';
2023-06-03 18:03:32 -07:00
import { SynchronizedLyrics } from './synchronized-lyrics';
import { Spinner, TextTitle } from '/@/renderer/components';
import { ErrorFallback } from '/@/renderer/features/action-required';
import { UnsynchronizedLyrics } from '/@/renderer/features/lyrics/unsynchronized-lyrics';
import { useCurrentSong, usePlayerStore } from '/@/renderer/store';
import {
2023-07-01 19:10:05 -07:00
FullLyricsMetadata,
LyricsOverride,
SynchronizedLyricMetadata,
UnsynchronizedLyricMetadata,
} from '/@/renderer/api/types';
2023-06-08 03:40:58 -07:00
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
import { queryKeys } from '/@/renderer/api/query-keys';
import { queryClient } from '/@/renderer/lib/react-query';
2023-05-22 17:38:31 -07:00
2023-06-08 03:40:58 -07:00
const ActionsContainer = styled.div`
2023-07-01 19:10:05 -07:00
position: absolute;
bottom: 0;
left: 0;
z-index: 50;
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
width: 100%;
opacity: 0;
transition: opacity 0.2s ease-in-out;
&:hover {
opacity: 1 !important;
}
&:focus-within {
opacity: 1 !important;
}
2023-06-08 03:40:58 -07:00
`;
const LyricsContainer = styled.div`
2023-07-01 19:10:05 -07:00
position: relative;
display: flex;
width: 100%;
height: 100%;
&:hover {
${ActionsContainer} {
opacity: 0.6;
}
2023-06-08 03:40:58 -07:00
}
`;
const ScrollContainer = styled(motion.div)`
2023-07-01 19:10:05 -07:00
position: relative;
z-index: 1;
width: 100%;
height: 100%;
2023-07-01 19:10:05 -07:00
text-align: center;
mask-image: linear-gradient(
180deg,
transparent 5%,
rgba(0, 0, 0, 100%) 20%,
rgba(0, 0, 0, 100%) 85%,
transparent 95%
);
&.mantine-ScrollArea-root {
width: 100%;
height: 100%;
}
2023-07-01 19:10:05 -07:00
& .mantine-ScrollArea-viewport {
height: 100% !important;
}
2023-07-01 19:10:05 -07:00
& .mantine-ScrollArea-viewport > div {
height: 100%;
}
`;
function isSynchronized(
2023-07-01 19:10:05 -07:00
data: Partial<FullLyricsMetadata> | undefined,
): data is SynchronizedLyricMetadata {
2023-07-01 19:10:05 -07:00
// Type magic. The only difference between Synchronized and Unsynchhronized is
// the datatype of lyrics. This makes Typescript happier later...
if (!data) return false;
return Array.isArray(data.lyrics);
}
2023-05-22 17:38:31 -07:00
export const Lyrics = () => {
2023-07-01 19:10:05 -07:00
const currentSong = useCurrentSong();
const { data, isInitialLoading } = useSongLyricsBySong(
{
query: { songId: currentSong?.id || '' },
serverId: currentSong?.serverId || '',
2023-07-01 19:10:05 -07:00
},
currentSong,
2023-06-09 03:39:38 -07:00
);
2023-07-01 19:10:05 -07:00
const [override, setOverride] = useState<LyricsOverride | undefined>(undefined);
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
setOverride(params);
}, []);
const handleOnResetLyric = useCallback(() => {
queryClient.resetQueries({
exact: true,
queryKey: queryKeys.songs.lyrics(currentSong?.serverId, { songId: currentSong?.id }),
});
}, [currentSong?.id, currentSong?.serverId]);
const handleOnRemoveLyric = useCallback(() => {
queryClient.setQueryData(
queryKeys.songs.lyrics(currentSong?.serverId, { songId: currentSong?.id }),
(prev: FullLyricsMetadata | undefined) => {
if (!prev) {
return undefined;
}
return {
...prev,
lyrics: '',
};
},
);
}, [currentSong?.id, currentSong?.serverId]);
const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({
2023-07-01 19:10:05 -07:00
options: {
enabled: !!override,
},
query: {
remoteSongId: override?.id,
remoteSource: override?.source,
song: currentSong,
2023-07-01 19:10:05 -07:00
},
serverId: currentSong?.serverId,
2023-07-01 19:10:05 -07:00
});
useEffect(() => {
const unsubSongChange = usePlayerStore.subscribe(
(state) => state.current.song,
() => {
setOverride(undefined);
},
{ equalityFn: (a, b) => a?.id === b?.id },
);
return () => {
unsubSongChange();
};
}, []);
const isLoadingLyrics = isInitialLoading || isOverrideLoading;
2023-08-04 12:29:55 -07:00
const hasNoLyrics = !data?.lyrics;
2023-07-01 19:10:05 -07:00
const lyricsMetadata:
| Partial<SynchronizedLyricMetadata>
| Partial<UnsynchronizedLyricMetadata>
| undefined = data;
2023-07-01 19:10:05 -07:00
const isSynchronizedLyrics = isSynchronized(lyricsMetadata);
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<LyricsContainer>
{isLoadingLyrics ? (
<Spinner
container
size={25}
/>
2023-06-08 03:40:58 -07:00
) : (
2023-07-01 19:10:05 -07:00
<AnimatePresence mode="sync">
{hasNoLyrics ? (
<Center w="100%">
<Group>
<RiInformationFill size="2rem" />
<TextTitle
order={3}
weight={700}
>
No lyrics found
</TextTitle>
</Group>
</Center>
) : (
<ScrollContainer
animate={{ opacity: 1 }}
initial={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
{isSynchronizedLyrics ? (
<SynchronizedLyrics {...lyricsMetadata} />
) : (
<UnsynchronizedLyrics
{...(lyricsMetadata as UnsynchronizedLyricMetadata)}
/>
)}
</ScrollContainer>
)}
</AnimatePresence>
2023-06-08 03:40:58 -07:00
)}
2023-07-01 19:10:05 -07:00
<ActionsContainer>
<LyricsActions
onRemoveLyric={handleOnRemoveLyric}
onResetLyric={handleOnResetLyric}
2023-07-01 19:10:05 -07:00
onSearchOverride={handleOnSearchOverride}
/>
</ActionsContainer>
</LyricsContainer>
</ErrorBoundary>
);
2023-05-22 17:38:31 -07:00
};