import { closeAllModals, openModal } from '@mantine/modals'; import { useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; import orderBy from 'lodash/orderBy'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styles from './lyrics-search-form.module.css'; import i18n from '/@/i18n/i18n'; import { lyricsQueries } from '/@/renderer/features/lyrics/api/lyrics-api'; import { openLyricsExportModal } from '/@/renderer/features/lyrics/components/lyrics-export-form'; import { SynchronizedLyrics, SynchronizedLyricsProps, } from '/@/renderer/features/lyrics/synchronized-lyrics'; import { UnsynchronizedLyrics, UnsynchronizedLyricsProps, } from '/@/renderer/features/lyrics/unsynchronized-lyrics'; import { usePlayerSong } from '/@/renderer/store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Button } from '/@/shared/components/button/button'; import { Center } from '/@/shared/components/center/center'; import { Divider } from '/@/shared/components/divider/divider'; import { Group } from '/@/shared/components/group/group'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { Spinner } from '/@/shared/components/spinner/spinner'; import { Stack } from '/@/shared/components/stack/stack'; import { TextInput } from '/@/shared/components/text-input/text-input'; import { Text } from '/@/shared/components/text/text'; import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value'; import { useForm } from '/@/shared/hooks/use-form'; import { FullLyricsMetadata, InternetProviderLyricSearchResponse, LyricSource, LyricsOverride, } from '/@/shared/types/domain-types'; interface SearchResultProps { data: InternetProviderLyricSearchResponse; isSelected?: boolean; onClick?: () => void; } const SearchResult = ({ data, isSelected, onClick }: SearchResultProps) => { const { t } = useTranslation(); const { artist, id, isSync, name, score, source } = data; const percentageScore = useMemo(() => { if (!score) return 0; return ((1 - score) * 100).toFixed(2); }, [score]); const cleanId = source === LyricSource.GENIUS ? id.replace(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)/g, '') : id; const syncStatus = useMemo(() => { if (isSync === true) { return t('page.fullscreenPlayer.config.synchronized', { postProcess: 'sentenceCase', }); } if (isSync === false) { return t('page.fullscreenPlayer.config.unsynchronized', { postProcess: 'sentenceCase', }); } return t('common.unknown', { postProcess: 'titleCase' }); }, [isSync, t]); return ( {name} {artist} {[source, cleanId, syncStatus].join(' — ')} {percentageScore}% ); }; interface LyricSearchFormProps { artist?: string; name?: string; onSearchOverride?: (params: LyricsOverride) => void; } export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => { const { t } = useTranslation(); const currentSong = usePlayerSong(); const [selectedResult, setSelectedResult] = useState(null); const form = useForm({ initialValues: { artist: artist || '', name: name || '', }, }); const [debouncedArtist] = useDebouncedValue(form.values.artist, 500); const [debouncedName] = useDebouncedValue(form.values.name, 500); const { data, isInitialLoading } = useQuery( lyricsQueries.search({ query: { artist: debouncedArtist, name: debouncedName }, }), ); const { data: previewData, isInitialLoading: isPreviewLoading } = useQuery( lyricsQueries.songLyricsByRemoteId({ options: { enabled: !!selectedResult, }, query: { remoteSongId: selectedResult?.id, remoteSource: selectedResult?.source as LyricSource | undefined, song: currentSong, }, serverId: currentSong?._serverId || '', }), ); const searchResults = useMemo(() => { if (!data) return []; const results: InternetProviderLyricSearchResponse[] = []; Object.keys(data).forEach((key) => { (data[key as keyof typeof data] || []).forEach((result) => results.push(result)); }); const scoredResults = orderBy(results, ['score'], ['asc']); return scoredResults; }, [data]); const handleApply = () => { if (selectedResult && onSearchOverride) { onSearchOverride({ artist: selectedResult.artist, id: selectedResult.id, name: selectedResult.name, remote: true, source: selectedResult.source as LyricSource, }); closeAllModals(); } }; const handleExport = () => { if (selectedResult && previewData) { const lyricsMetadata: FullLyricsMetadata = { artist: selectedResult.artist, lyrics: previewData, name: selectedResult.name, offsetMs: 0, remote: true, source: selectedResult.source, }; const synced = Array.isArray(previewData); openLyricsExportModal({ lyrics: lyricsMetadata, offsetMs: 0, synced }); } }; return ( form.setFieldValue('name', '')} size="sm" variant="transparent" /> ) : null } {...form.getInputProps('name')} /> form.setFieldValue('artist', '')} size="sm" variant="transparent" /> ) : null } {...form.getInputProps('artist')} /> {isInitialLoading ? ( ) : ( {searchResults.map((result) => ( setSelectedResult(result)} /> ))} )} {selectedResult && ( {isPreviewLoading ? ( ) : previewData ? ( {Array.isArray(previewData) ? ( ) : ( )} ) : ( {t('page.fullscreenPlayer.noLyrics', { postProcess: 'sentenceCase', })} )} )} closeAllModals()} variant="default"> {t('common.cancel', { postProcess: 'titleCase' })} {t('form.lyricsExport.export', { postProcess: 'titleCase' })} {t('common.confirm', { postProcess: 'titleCase' })} ); }; export const openLyricSearchModal = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => { openModal({ children: ( ), size: 'xl', styles: { body: { height: '600px', }, }, title: i18n.t('form.lyricSearch.title', { postProcess: 'titleCase' }) as string, }); };