redesign lyrics search form

This commit is contained in:
jeffvli
2025-11-30 04:04:50 -08:00
parent 78fcc5c1c7
commit e8db28e112
4 changed files with 181 additions and 47 deletions
@@ -17,3 +17,19 @@
}
}
}
.selected {
background-color: alpha(var(--theme-colors-primary), 0.3);
&:hover,
&:active,
&:focus-visible {
@mixin dark {
background-color: alpha(var(--theme-colors-primary), 0.4);
}
@mixin light {
background-color: alpha(var(--theme-colors-primary), 0.4);
}
}
}
@@ -1,13 +1,25 @@
import { openModal } from '@mantine/modals';
import { closeAllModals, openModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import clsx from 'clsx';
import orderBy from 'lodash/orderBy';
import { useMemo } from 'react';
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 {
SynchronizedLyrics,
SynchronizedLyricsProps,
} from '/@/renderer/features/lyrics/synchronized-lyrics';
import {
UnsynchronizedLyrics,
UnsynchronizedLyricsProps,
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
import { usePlayerSong } from '/@/renderer/store';
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';
@@ -25,9 +37,10 @@ import {
interface SearchResultProps {
data: InternetProviderLyricSearchResponse;
isSelected?: boolean;
onClick?: () => void;
}
const SearchResult = ({ data, onClick }: SearchResultProps) => {
const SearchResult = ({ data, isSelected, onClick }: SearchResultProps) => {
const { artist, id, name, score, source } = data;
const percentageScore = useMemo(() => {
@@ -39,7 +52,12 @@ const SearchResult = ({ data, onClick }: SearchResultProps) => {
source === LyricSource.GENIUS ? id.replace(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)/g, '') : id;
return (
<button className={styles.searchItem} onClick={onClick}>
<button
className={clsx(styles.searchItem, {
[styles.selected]: isSelected,
})}
onClick={onClick}
>
<Group justify="space-between" wrap="nowrap">
<Stack gap={0} maw="65%">
<Text fw={600} size="md">
@@ -66,6 +84,10 @@ interface LyricSearchFormProps {
export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => {
const { t } = useTranslation();
const currentSong = usePlayerSong();
const [selectedResult, setSelectedResult] =
useState<InternetProviderLyricSearchResponse | null>(null);
const form = useForm({
initialValues: {
artist: artist || '',
@@ -82,6 +104,20 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
}),
);
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 [];
@@ -95,8 +131,21 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
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();
}
};
return (
<Stack w="100%">
<Stack h="100%" w="100%">
<form>
<Group grow>
<TextInput
@@ -117,34 +166,81 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
</Group>
</form>
<Divider />
{isInitialLoading ? (
<Spinner container />
) : (
<ScrollArea
style={{
height: '400px',
paddingRight: '1rem',
}}
>
<Stack gap="md">
{searchResults.map((result) => (
<SearchResult
data={result}
key={`${result.source}-${result.id}`}
onClick={() => {
onSearchOverride?.({
artist: result.artist,
id: result.id,
name: result.name,
<Group align="flex-start" grow style={{ flex: 1, minHeight: 0 }}>
<Stack style={{ flex: 1, height: '100%', minHeight: 0 }}>
<ScrollArea
style={{
height: '100%',
paddingRight: '1rem',
}}
>
{isInitialLoading ? (
<Spinner container />
) : (
<Stack gap="md">
{searchResults.map((result) => (
<SearchResult
data={result}
isSelected={
selectedResult?.id === result.id &&
selectedResult?.source === result.source
}
key={`${result.source}-${result.id}`}
onClick={() => setSelectedResult(result)}
/>
))}
</Stack>
)}
</ScrollArea>
</Stack>
{selectedResult && (
<Stack style={{ flex: 1, height: '100%', minHeight: 0 }}>
{isPreviewLoading ? (
<Spinner container />
) : previewData ? (
Array.isArray(previewData) ? (
<SynchronizedLyrics
style={{ padding: 0 }}
{...({
artist: selectedResult.artist,
lyrics: previewData,
name: selectedResult.name,
remote: true,
source: result.source as LyricSource,
});
}}
/>
))}
source: selectedResult.source,
} as SynchronizedLyricsProps)}
/>
) : (
<UnsynchronizedLyrics
{...({
artist: selectedResult.artist,
lyrics: previewData,
name: selectedResult.name,
remote: true,
source: selectedResult.source,
} as UnsynchronizedLyricsProps)}
/>
)
) : (
<Center>
<Text isMuted>
{t('page.fullscreenPlayer.noLyrics', {
postProcess: 'sentenceCase',
})}
</Text>
</Center>
)}
</Stack>
</ScrollArea>
)}
)}
</Group>
<Divider />
<Group justify="flex-end">
<Button onClick={() => closeAllModals()} variant="default">
{t('common.cancel', { postProcess: 'titleCase' })}
</Button>
<Button disabled={!selectedResult} onClick={handleApply} variant="filled">
{t('common.confirm', { postProcess: 'titleCase' })}
</Button>
</Group>
</Stack>
);
};
@@ -154,7 +250,12 @@ export const openLyricSearchModal = ({ artist, name, onSearchOverride }: LyricSe
children: (
<LyricsSearchForm artist={artist} name={name} onSearchOverride={onSearchOverride} />
),
size: 'lg',
size: 'xl',
styles: {
body: {
height: '600px',
},
},
title: i18n.t('form.lyricSearch.title', { postProcess: 'titleCase' }) as string,
});
};
+29 -14
View File
@@ -54,7 +54,34 @@ export const Lyrics = () => {
const [override, setOverride] = useState<LyricsOverride | undefined>(undefined);
const { data: overrideData, isInitialLoading: isOverrideLoading } = useQuery(
lyricsQueries.songLyricsByRemoteId({
options: {
enabled: !!override,
},
query: {
remoteSongId: override?.id,
remoteSource: override?.source as LyricSource | undefined,
song: currentSong,
},
serverId: currentSong?._serverId || '',
}),
);
const [lyrics, synced] = useMemo(() => {
// If override data is available, use it
if (override && overrideData) {
const overrideLyrics: FullLyricsMetadata = {
artist: override.artist,
lyrics: overrideData,
name: override.name,
remote: override.remote ?? true,
source: override.source,
};
return [overrideLyrics, Array.isArray(overrideData)];
}
// Otherwise, use the regular data
if (Array.isArray(data)) {
if (data.length > 0) {
const selectedLyric = data[Math.min(index, data.length - 1)];
@@ -65,13 +92,14 @@ export const Lyrics = () => {
}
return [undefined, false];
}, [data, index]);
}, [data, index, override, overrideData]);
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
setOverride(params);
}, []);
const handleOnResetLyric = useCallback(() => {
setOverride(undefined);
queryClient.invalidateQueries({
exact: true,
queryKey: queryKeys.songs.lyrics(currentSong?._serverId, { songId: currentSong?.id }),
@@ -117,19 +145,6 @@ export const Lyrics = () => {
await fetchTranslation();
}, [translatedLyrics, showTranslation, fetchTranslation]);
const { isInitialLoading: isOverrideLoading } = useQuery(
lyricsQueries.songLyricsByRemoteId({
options: {
enabled: !!override,
},
query: {
remoteSongId: override?.id,
remoteSource: override?.source as LyricSource | undefined,
song: currentSong,
},
serverId: currentSong?._serverId || '',
}),
);
usePlayerEvents(
{
@@ -21,6 +21,7 @@ const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null;
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: SynchronizedLyricsArray;
style?: React.CSSProperties;
translatedLyrics?: null | string;
}
@@ -30,6 +31,7 @@ export const SynchronizedLyrics = ({
name,
remote,
source,
style,
translatedLyrics,
}: SynchronizedLyricsProps) => {
const playbackType = usePlaybackType();
@@ -245,7 +247,7 @@ export const SynchronizedLyrics = ({
id="sychronized-lyrics-scroll-container"
onMouseEnter={showScrollbar}
onMouseLeave={hideScrollbar}
style={{ gap: `${settings.gap}px` }}
style={{ gap: `${settings.gap}px`, ...style }}
>
{settings.showProvider && source && (
<LyricLine