From 14a6766072582a6d600a2fb1a376dd7519c2e612 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 8 Jun 2023 03:40:58 -0700 Subject: [PATCH] Add initial lyrics search UI --- .../lyrics/components/lyrics-search-form.tsx | 135 ++++++++++++++++++ .../features/lyrics/lyrics-actions.tsx | 51 +++++++ src/renderer/features/lyrics/lyrics.tsx | 101 +++++++++---- 3 files changed, 257 insertions(+), 30 deletions(-) create mode 100644 src/renderer/features/lyrics/components/lyrics-search-form.tsx create mode 100644 src/renderer/features/lyrics/lyrics-actions.tsx diff --git a/src/renderer/features/lyrics/components/lyrics-search-form.tsx b/src/renderer/features/lyrics/components/lyrics-search-form.tsx new file mode 100644 index 00000000..5b4bd952 --- /dev/null +++ b/src/renderer/features/lyrics/components/lyrics-search-form.tsx @@ -0,0 +1,135 @@ +import { useMemo } from 'react'; +import { Divider, Group, Stack, UnstyledButton } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useDebouncedValue } from '@mantine/hooks'; +import { openModal } from '@mantine/modals'; +import styled from 'styled-components'; +import { InternetProviderLyricSearchResponse } from '../../../api/types'; +import { useLyricSearch } from '../queries/lyric-search-query'; +import { Badge, ScrollArea, Spinner, Text, TextInput } from '/@/renderer/components'; + +const SearchItem = styled(UnstyledButton)` + &:hover, + &:focus-visible { + color: var(--btn-default-fg-hover); + background: var(--btn-default-bg-hover); + } + + padding: 0.5rem; + border-radius: 5px; +`; + +interface SearchResultProps { + artist?: string; + name?: string; + source?: string; +} +const SearchResult = ({ name, artist, source }: SearchResultProps) => { + return ( + + + + {name} + {artist} + + {source} + + + ); +}; + +interface LyricSearchFormProps { + artist?: string; + name?: string; + onSelect?: (lyrics: InternetProviderLyricSearchResponse) => void; +} + +export const LyricsSearchForm = ({ artist, name }: LyricSearchFormProps) => { + const form = useForm({ + initialValues: { + artist: artist || '', + name: name || '', + }, + }); + + const [debouncedArtist] = useDebouncedValue(form.values.artist, 500); + const [debouncedName] = useDebouncedValue(form.values.name, 500); + + const { data, isLoading } = useLyricSearch({ + options: { enabled: Boolean(form.values.artist && form.values.name) }, + query: { artist: debouncedArtist, name: debouncedName }, + }); + + const searchResults = useMemo(() => { + if (!data) return []; + + console.log('data', data); + + const results: InternetProviderLyricSearchResponse[] = []; + Object.keys(data).forEach((key) => { + (data[key as keyof typeof data] || []).forEach((result) => results.push(result)); + }); + + return results; + }, [data]); + + return ( + +
+ + + + +
+ + {isLoading ? ( + + ) : ( + + + {searchResults.map((result) => ( + + ))} + + + )} +
+ ); +}; + +export const openLyricSearchModal = ({ artist, name }: LyricSearchFormProps) => { + openModal({ + children: ( + + ), + size: 'lg', + title: 'Search for lyrics', + }); +}; diff --git a/src/renderer/features/lyrics/lyrics-actions.tsx b/src/renderer/features/lyrics/lyrics-actions.tsx new file mode 100644 index 00000000..221bc271 --- /dev/null +++ b/src/renderer/features/lyrics/lyrics-actions.tsx @@ -0,0 +1,51 @@ +import { RiAddFill, RiSubtractFill } from 'react-icons/ri'; +import { Button, NumberInput } from '/@/renderer/components'; +import { openLyricSearchModal } from '/@/renderer/features/lyrics/components/lyrics-search-form'; +import { useCurrentSong } from '/@/renderer/store'; + +export const LyricsActions = () => { + const currentSong = useCurrentSong(); + + return ( + <> + + + + + + + ); +}; diff --git a/src/renderer/features/lyrics/lyrics.tsx b/src/renderer/features/lyrics/lyrics.tsx index 1aa625c5..cc339619 100644 --- a/src/renderer/features/lyrics/lyrics.tsx +++ b/src/renderer/features/lyrics/lyrics.tsx @@ -11,8 +11,40 @@ import { ErrorFallback } from '/@/renderer/features/action-required'; import { UnsynchronizedLyrics } from '/@/renderer/features/lyrics/unsynchronized-lyrics'; import { getServerById, useCurrentSong } from '/@/renderer/store'; import { FullLyricsMetadata, SynchronizedLyricMetadata } from '/@/renderer/api/types'; +import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions'; -const LyricsScrollContainer = styled(motion(ScrollArea))` +const ActionsContainer = styled.div` + position: absolute; + bottom: 4rem; + 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; + } +`; + +const LyricsContainer = styled.div` + position: relative; + width: 100%; + height: 100%; + + &:hover { + ${ActionsContainer} { + opacity: 0.5; + } + } +`; + +const ScrollContainer = styled(motion(ScrollArea))` + position: relative; z-index: 1; text-align: center; transform: translateY(-2rem); @@ -50,7 +82,7 @@ export const Lyrics = () => { const [clear, setClear] = useState(false); - const { data, isLoading } = useSongLyrics( + const { data, isInitialLoading } = useSongLyrics( { query: { songId: currentSong?.id || '' }, serverId: currentServer?.id, @@ -65,42 +97,51 @@ export const Lyrics = () => { return ( - {isLoading ? ( + {isInitialLoading ? ( - ) : !data?.lyrics || clear ? ( -
- - - - No lyrics found - - -
) : ( - - {isSynchronized(data) ? ( - setClear(true)} - /> + + {!data?.lyrics || clear ? ( +
+ + + + No lyrics found + + +
) : ( - setClear(true)} - /> + + {isSynchronized(data) ? ( + setClear(true)} + /> + ) : ( + setClear(true)} + /> + )} + )} -
+ + + + +
)}