mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
redesign lyrics search form
This commit is contained in:
@@ -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 { useQuery } from '@tanstack/react-query';
|
||||||
|
import clsx from 'clsx';
|
||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import styles from './lyrics-search-form.module.css';
|
import styles from './lyrics-search-form.module.css';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { lyricsQueries } from '/@/renderer/features/lyrics/api/lyrics-api';
|
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 { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||||
@@ -25,9 +37,10 @@ import {
|
|||||||
|
|
||||||
interface SearchResultProps {
|
interface SearchResultProps {
|
||||||
data: InternetProviderLyricSearchResponse;
|
data: InternetProviderLyricSearchResponse;
|
||||||
|
isSelected?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
const SearchResult = ({ data, onClick }: SearchResultProps) => {
|
const SearchResult = ({ data, isSelected, onClick }: SearchResultProps) => {
|
||||||
const { artist, id, name, score, source } = data;
|
const { artist, id, name, score, source } = data;
|
||||||
|
|
||||||
const percentageScore = useMemo(() => {
|
const percentageScore = useMemo(() => {
|
||||||
@@ -39,7 +52,12 @@ const SearchResult = ({ data, onClick }: SearchResultProps) => {
|
|||||||
source === LyricSource.GENIUS ? id.replace(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)/g, '') : id;
|
source === LyricSource.GENIUS ? id.replace(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)/g, '') : id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={styles.searchItem} onClick={onClick}>
|
<button
|
||||||
|
className={clsx(styles.searchItem, {
|
||||||
|
[styles.selected]: isSelected,
|
||||||
|
})}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Stack gap={0} maw="65%">
|
<Stack gap={0} maw="65%">
|
||||||
<Text fw={600} size="md">
|
<Text fw={600} size="md">
|
||||||
@@ -66,6 +84,10 @@ interface LyricSearchFormProps {
|
|||||||
|
|
||||||
export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => {
|
export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const currentSong = usePlayerSong();
|
||||||
|
const [selectedResult, setSelectedResult] =
|
||||||
|
useState<InternetProviderLyricSearchResponse | null>(null);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
artist: artist || '',
|
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(() => {
|
const searchResults = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
|
|
||||||
@@ -95,8 +131,21 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
return scoredResults;
|
return scoredResults;
|
||||||
}, [data]);
|
}, [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 (
|
return (
|
||||||
<Stack w="100%">
|
<Stack h="100%" w="100%">
|
||||||
<form>
|
<form>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -117,34 +166,81 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
<Divider />
|
<Divider />
|
||||||
{isInitialLoading ? (
|
<Group align="flex-start" grow style={{ flex: 1, minHeight: 0 }}>
|
||||||
<Spinner container />
|
<Stack style={{ flex: 1, height: '100%', minHeight: 0 }}>
|
||||||
) : (
|
<ScrollArea
|
||||||
<ScrollArea
|
style={{
|
||||||
style={{
|
height: '100%',
|
||||||
height: '400px',
|
paddingRight: '1rem',
|
||||||
paddingRight: '1rem',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{isInitialLoading ? (
|
||||||
<Stack gap="md">
|
<Spinner container />
|
||||||
{searchResults.map((result) => (
|
) : (
|
||||||
<SearchResult
|
<Stack gap="md">
|
||||||
data={result}
|
{searchResults.map((result) => (
|
||||||
key={`${result.source}-${result.id}`}
|
<SearchResult
|
||||||
onClick={() => {
|
data={result}
|
||||||
onSearchOverride?.({
|
isSelected={
|
||||||
artist: result.artist,
|
selectedResult?.id === result.id &&
|
||||||
id: result.id,
|
selectedResult?.source === result.source
|
||||||
name: result.name,
|
}
|
||||||
|
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,
|
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>
|
</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>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -154,7 +250,12 @@ export const openLyricSearchModal = ({ artist, name, onSearchOverride }: LyricSe
|
|||||||
children: (
|
children: (
|
||||||
<LyricsSearchForm artist={artist} name={name} onSearchOverride={onSearchOverride} />
|
<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,
|
title: i18n.t('form.lyricSearch.title', { postProcess: 'titleCase' }) as string,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,7 +54,34 @@ export const Lyrics = () => {
|
|||||||
|
|
||||||
const [override, setOverride] = useState<LyricsOverride | undefined>(undefined);
|
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(() => {
|
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 (Array.isArray(data)) {
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
const selectedLyric = data[Math.min(index, data.length - 1)];
|
const selectedLyric = data[Math.min(index, data.length - 1)];
|
||||||
@@ -65,13 +92,14 @@ export const Lyrics = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [undefined, false];
|
return [undefined, false];
|
||||||
}, [data, index]);
|
}, [data, index, override, overrideData]);
|
||||||
|
|
||||||
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
|
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
|
||||||
setOverride(params);
|
setOverride(params);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleOnResetLyric = useCallback(() => {
|
const handleOnResetLyric = useCallback(() => {
|
||||||
|
setOverride(undefined);
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
exact: true,
|
exact: true,
|
||||||
queryKey: queryKeys.songs.lyrics(currentSong?._serverId, { songId: currentSong?.id }),
|
queryKey: queryKeys.songs.lyrics(currentSong?._serverId, { songId: currentSong?.id }),
|
||||||
@@ -117,19 +145,6 @@ export const Lyrics = () => {
|
|||||||
await fetchTranslation();
|
await fetchTranslation();
|
||||||
}, [translatedLyrics, showTranslation, 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(
|
usePlayerEvents(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null;
|
|||||||
|
|
||||||
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
||||||
lyrics: SynchronizedLyricsArray;
|
lyrics: SynchronizedLyricsArray;
|
||||||
|
style?: React.CSSProperties;
|
||||||
translatedLyrics?: null | string;
|
translatedLyrics?: null | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export const SynchronizedLyrics = ({
|
|||||||
name,
|
name,
|
||||||
remote,
|
remote,
|
||||||
source,
|
source,
|
||||||
|
style,
|
||||||
translatedLyrics,
|
translatedLyrics,
|
||||||
}: SynchronizedLyricsProps) => {
|
}: SynchronizedLyricsProps) => {
|
||||||
const playbackType = usePlaybackType();
|
const playbackType = usePlaybackType();
|
||||||
@@ -245,7 +247,7 @@ export const SynchronizedLyrics = ({
|
|||||||
id="sychronized-lyrics-scroll-container"
|
id="sychronized-lyrics-scroll-container"
|
||||||
onMouseEnter={showScrollbar}
|
onMouseEnter={showScrollbar}
|
||||||
onMouseLeave={hideScrollbar}
|
onMouseLeave={hideScrollbar}
|
||||||
style={{ gap: `${settings.gap}px` }}
|
style={{ gap: `${settings.gap}px`, ...style }}
|
||||||
>
|
>
|
||||||
{settings.showProvider && source && (
|
{settings.showProvider && source && (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
|
|||||||
Reference in New Issue
Block a user