mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
simplify lyrics implementation
- removes complex lyrics fetch and override logic, and instead uses a single query as a source of truth for the lyrics - properly handles loading state, invalidation, and refetch
This commit is contained in:
@@ -410,15 +410,7 @@ export const queryKeys: Record<
|
|||||||
if (query) return [serverId, 'songs', 'randomSongList', query] as const;
|
if (query) return [serverId, 'songs', 'randomSongList', query] as const;
|
||||||
return [serverId, 'songs', 'randomSongList'] as const;
|
return [serverId, 'songs', 'randomSongList'] as const;
|
||||||
},
|
},
|
||||||
remoteLyrics: (serverId: string, query?: LyricsQuery) => {
|
|
||||||
if (query) return [serverId, 'song', 'lyrics', 'remote', query] as const;
|
|
||||||
return [serverId, 'song', 'lyrics', 'remote'] as const;
|
|
||||||
},
|
|
||||||
root: (serverId: string) => [serverId, 'songs'] as const,
|
root: (serverId: string) => [serverId, 'songs'] as const,
|
||||||
serverLyrics: (serverId: string, query?: LyricsQuery) => {
|
|
||||||
if (query) return [serverId, 'song', 'lyrics', 'server', query] as const;
|
|
||||||
return [serverId, 'song', 'lyrics', 'server'] as const;
|
|
||||||
},
|
|
||||||
similar: (serverId: string, query?: SimilarSongsQuery) => {
|
similar: (serverId: string, query?: SimilarSongsQuery) => {
|
||||||
if (query) return [serverId, 'song', 'similar', query] as const;
|
if (query) return [serverId, 'song', 'similar', query] as const;
|
||||||
return [serverId, 'song', 'similar'] as const;
|
return [serverId, 'song', 'similar'] as const;
|
||||||
|
|||||||
@@ -12,16 +12,31 @@ import {
|
|||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricGetQuery,
|
LyricGetQuery,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
|
LyricsOverride,
|
||||||
LyricsQuery,
|
LyricsQuery,
|
||||||
QueueSong,
|
QueueSong,
|
||||||
|
Song,
|
||||||
StructuredLyric,
|
StructuredLyric,
|
||||||
SynchronizedLyricsArray,
|
SynchronizedLyricsArray,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { LyricSource } from '/@/shared/types/domain-types';
|
import { LyricSource } from '/@/shared/types/domain-types';
|
||||||
|
import { LyricsResponse } from '/@/shared/types/domain-types';
|
||||||
import { ServerFeature } from '/@/shared/types/features-types';
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
|
|
||||||
const lyricsIpc = isElectron() ? window.api.lyrics : null;
|
const lyricsIpc = isElectron() ? window.api.lyrics : null;
|
||||||
|
|
||||||
|
export type LyricsQueryResult = {
|
||||||
|
local: FullLyricsMetadata | null | StructuredLyric[];
|
||||||
|
overrideData: LyricsResponse | null;
|
||||||
|
overrideSelection: LyricsOverride | null;
|
||||||
|
remoteAuto: FullLyricsMetadata | null;
|
||||||
|
selected: FullLyricsMetadata | null | StructuredLyric;
|
||||||
|
selectedOffsetMs: number;
|
||||||
|
selectedStructuredIndex: number;
|
||||||
|
selectedSynced: boolean;
|
||||||
|
suppressRemoteAuto: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
// Match LRC lyrics format by https://github.com/ustbhuangyi/lyric-parser
|
// Match LRC lyrics format by https://github.com/ustbhuangyi/lyric-parser
|
||||||
// [mm:ss.SSS] text
|
// [mm:ss.SSS] text
|
||||||
const timeExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]([^\n]+)(\n|$)/g;
|
const timeExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]([^\n]+)(\n|$)/g;
|
||||||
@@ -63,35 +78,177 @@ const formatLyrics = (lyrics: string) => {
|
|||||||
return lyrics;
|
return lyrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatLyricsForDisplay = formatLyrics;
|
||||||
|
|
||||||
|
export function computeSelectedFromResult(
|
||||||
|
result: Pick<
|
||||||
|
LyricsQueryResult,
|
||||||
|
'local' | 'overrideData' | 'overrideSelection' | 'remoteAuto' | 'selectedOffsetMs'
|
||||||
|
>,
|
||||||
|
preferLocalLyrics: boolean,
|
||||||
|
selectedStructuredIndex: number,
|
||||||
|
): {
|
||||||
|
selected: FullLyricsMetadata | null | StructuredLyric;
|
||||||
|
selectedSynced: boolean;
|
||||||
|
} {
|
||||||
|
const { local, overrideData, overrideSelection, remoteAuto, selectedOffsetMs } = result;
|
||||||
|
|
||||||
|
// Override takes precedence over local and remote lyrics in all scenarios if available
|
||||||
|
if (overrideSelection && overrideData) {
|
||||||
|
const overrideLyrics: FullLyricsMetadata = {
|
||||||
|
artist: overrideSelection.artist,
|
||||||
|
lyrics: overrideData,
|
||||||
|
name: overrideSelection.name,
|
||||||
|
offsetMs: selectedOffsetMs,
|
||||||
|
remote: overrideSelection.remote ?? true,
|
||||||
|
source: overrideSelection.source,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
selected: overrideLyrics,
|
||||||
|
selectedSynced: Array.isArray(overrideData),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasLocalLocal =
|
||||||
|
(Array.isArray(local) && local.length > 0) ||
|
||||||
|
(local != null && !Array.isArray(local) && 'lyrics' in local && Boolean(local.lyrics));
|
||||||
|
|
||||||
|
// If setting is set to prefer local lyrics, return the local lyrics if available
|
||||||
|
if (preferLocalLyrics && hasLocalLocal) {
|
||||||
|
if (Array.isArray(local) && local.length > 0) {
|
||||||
|
const item = local[Math.min(selectedStructuredIndex, local.length - 1)];
|
||||||
|
return { selected: item, selectedSynced: item.synced };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (local != null && !Array.isArray(local) && 'lyrics' in local && local.lyrics) {
|
||||||
|
return { selected: local, selectedSynced: Array.isArray(local.lyrics) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If remote lyrics are automatically fetched and available, return the remote auto lyrics
|
||||||
|
if (remoteAuto) {
|
||||||
|
return {
|
||||||
|
selected: remoteAuto,
|
||||||
|
selectedSynced: Array.isArray(remoteAuto.lyrics),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we just return the local lyrics if available, using structured lyrics if available
|
||||||
|
if (Array.isArray(local) && local.length > 0) {
|
||||||
|
const item = local[Math.min(selectedStructuredIndex, local.length - 1)];
|
||||||
|
return { selected: item, selectedSynced: item.synced };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (local != null && !Array.isArray(local) && 'lyrics' in local && local.lyrics) {
|
||||||
|
return { selected: local, selectedSynced: Array.isArray(local.lyrics) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no lyrics are available, return null
|
||||||
|
return { selected: null, selectedSynced: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchLocalLyrics(params: {
|
||||||
|
serverId: string;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
song: QueueSong;
|
||||||
|
}): Promise<FullLyricsMetadata | null | StructuredLyric[]> {
|
||||||
|
const { serverId, signal, song } = params;
|
||||||
|
const server = getServerById(serverId);
|
||||||
|
if (!server) throw new Error('Server not found');
|
||||||
|
|
||||||
|
if (hasFeature(server, ServerFeature.LYRICS_MULTIPLE_STRUCTURED)) {
|
||||||
|
const subsonicLyrics = await api.controller
|
||||||
|
.getStructuredLyrics({
|
||||||
|
apiClientProps: { serverId, signal },
|
||||||
|
query: { songId: song.id },
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
if (subsonicLyrics?.length) return subsonicLyrics;
|
||||||
|
} else if (hasFeature(server, ServerFeature.LYRICS_SINGLE_STRUCTURED)) {
|
||||||
|
const jfLyrics = await api.controller
|
||||||
|
.getLyrics({
|
||||||
|
apiClientProps: { serverId, signal },
|
||||||
|
query: { songId: song.id },
|
||||||
|
})
|
||||||
|
.catch((err) => console.error(err));
|
||||||
|
if (jfLyrics) {
|
||||||
|
return {
|
||||||
|
artist: song.artists?.[0]?.name,
|
||||||
|
lyrics: jfLyrics,
|
||||||
|
name: song.name,
|
||||||
|
remote: false,
|
||||||
|
source: server?.name ?? 'music server',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (song.lyrics) {
|
||||||
|
return {
|
||||||
|
artist: song.artists?.[0]?.name,
|
||||||
|
lyrics: formatLyrics(song.lyrics),
|
||||||
|
name: song.name,
|
||||||
|
remote: false,
|
||||||
|
source: server?.name ?? 'music server',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRemoteLyricsAuto(song: QueueSong): Promise<FullLyricsMetadata | null> {
|
||||||
|
const { fetch } = useSettingsStore.getState().lyrics;
|
||||||
|
if (!fetch) return null;
|
||||||
|
const remoteLyricsResult: InternetProviderLyricResponse | null =
|
||||||
|
await lyricsIpc?.getRemoteLyricsBySong(song);
|
||||||
|
|
||||||
|
if (remoteLyricsResult) {
|
||||||
|
return {
|
||||||
|
...remoteLyricsResult,
|
||||||
|
lyrics: formatLyrics(remoteLyricsResult.lyrics),
|
||||||
|
remote: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRemoteLyricsById(params: {
|
||||||
|
remoteSongId: string;
|
||||||
|
remoteSource: LyricSource;
|
||||||
|
song?: QueueSong | Song;
|
||||||
|
}): Promise<LyricsResponse | null> {
|
||||||
|
const result = await lyricsIpc?.getRemoteLyricsByRemoteId(params as LyricGetQuery);
|
||||||
|
if (result) return formatLyrics(result);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDisplayOffset(
|
||||||
|
selected: FullLyricsMetadata | null | StructuredLyric,
|
||||||
|
storedOffsetMs: number,
|
||||||
|
selectedStructuredIndex: number,
|
||||||
|
local: FullLyricsMetadata | null | StructuredLyric[],
|
||||||
|
): number {
|
||||||
|
if (selected && 'offsetMs' in selected && selected.offsetMs !== undefined) {
|
||||||
|
return selected.offsetMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(local) && local.length > 0) {
|
||||||
|
const item = local[Math.min(selectedStructuredIndex, local.length - 1)];
|
||||||
|
return item.offsetMs ?? storedOffsetMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return storedOffsetMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyResult = (): LyricsQueryResult => ({
|
||||||
|
local: null,
|
||||||
|
overrideData: null,
|
||||||
|
overrideSelection: null,
|
||||||
|
remoteAuto: null,
|
||||||
|
selected: null,
|
||||||
|
selectedOffsetMs: 0,
|
||||||
|
selectedStructuredIndex: 0,
|
||||||
|
selectedSynced: false,
|
||||||
|
suppressRemoteAuto: false,
|
||||||
|
});
|
||||||
|
|
||||||
export const lyricsQueries = {
|
export const lyricsQueries = {
|
||||||
remoteLyrics: (args: QueryHookArgs<LyricsQuery>, song: QueueSong | undefined) => {
|
|
||||||
return queryOptions({
|
|
||||||
gcTime: Infinity,
|
|
||||||
queryFn: async (): Promise<FullLyricsMetadata | null> => {
|
|
||||||
if (!song) return null;
|
|
||||||
|
|
||||||
const { fetch } = useSettingsStore.getState().lyrics;
|
|
||||||
|
|
||||||
if (!fetch) return null;
|
|
||||||
|
|
||||||
const remoteLyricsResult: InternetProviderLyricResponse | null =
|
|
||||||
await lyricsIpc?.getRemoteLyricsBySong(song);
|
|
||||||
|
|
||||||
if (remoteLyricsResult) {
|
|
||||||
return {
|
|
||||||
...remoteLyricsResult,
|
|
||||||
lyrics: formatLyrics(remoteLyricsResult.lyrics),
|
|
||||||
remote: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
queryKey: queryKeys.songs.remoteLyrics(args.serverId, args.query),
|
|
||||||
staleTime: Infinity,
|
|
||||||
...args.options,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
search: (args: Omit<QueryHookArgs<LyricSearchQuery>, 'serverId'>) => {
|
search: (args: Omit<QueryHookArgs<LyricSearchQuery>, 'serverId'>) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
gcTime: 1000 * 60 * 1,
|
gcTime: 1000 * 60 * 1,
|
||||||
@@ -106,83 +263,82 @@ export const lyricsQueries = {
|
|||||||
...args.options,
|
...args.options,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
serverLyrics: (args: QueryHookArgs<LyricsQuery>, song: QueueSong | undefined) => {
|
|
||||||
return queryOptions({
|
|
||||||
gcTime: Infinity,
|
|
||||||
queryFn: async ({ signal }): Promise<FullLyricsMetadata | null | StructuredLyric[]> => {
|
|
||||||
const server = getServerById(args.serverId);
|
|
||||||
if (!server) throw new Error('Server not found');
|
|
||||||
if (!song) return null;
|
|
||||||
|
|
||||||
let localLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
|
|
||||||
|
|
||||||
if (hasFeature(server, ServerFeature.LYRICS_MULTIPLE_STRUCTURED)) {
|
|
||||||
const subsonicLyrics = await api.controller
|
|
||||||
.getStructuredLyrics({
|
|
||||||
apiClientProps: { serverId: args.serverId, signal },
|
|
||||||
query: { songId: song.id },
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
|
|
||||||
if (subsonicLyrics?.length) {
|
|
||||||
localLyrics = subsonicLyrics;
|
|
||||||
}
|
|
||||||
} else if (hasFeature(server, ServerFeature.LYRICS_SINGLE_STRUCTURED)) {
|
|
||||||
const jfLyrics = await api.controller
|
|
||||||
.getLyrics({
|
|
||||||
apiClientProps: { serverId: args.serverId, signal },
|
|
||||||
query: { songId: song.id },
|
|
||||||
})
|
|
||||||
.catch((err) => console.error(err));
|
|
||||||
|
|
||||||
if (jfLyrics) {
|
|
||||||
localLyrics = {
|
|
||||||
artist: song.artists?.[0]?.name,
|
|
||||||
lyrics: jfLyrics,
|
|
||||||
name: song.name,
|
|
||||||
remote: false,
|
|
||||||
source: server?.name ?? 'music server',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (song.lyrics) {
|
|
||||||
localLyrics = {
|
|
||||||
artist: song.artists?.[0]?.name,
|
|
||||||
lyrics: formatLyrics(song.lyrics),
|
|
||||||
name: song.name,
|
|
||||||
remote: false,
|
|
||||||
source: server?.name ?? 'music server',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return localLyrics;
|
|
||||||
},
|
|
||||||
queryKey: queryKeys.songs.serverLyrics(args.serverId, args.query),
|
|
||||||
staleTime: Infinity,
|
|
||||||
...args.options,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
songLyrics: (args: QueryHookArgs<LyricsQuery>, song: QueueSong | undefined) => {
|
songLyrics: (args: QueryHookArgs<LyricsQuery>, song: QueueSong | undefined) => {
|
||||||
|
const lyricsKey = queryKeys.songs.lyrics(args.serverId, args.query);
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
gcTime: Infinity,
|
gcTime: Infinity,
|
||||||
queryFn: async ({ signal }): Promise<FullLyricsMetadata | null | StructuredLyric[]> => {
|
queryFn: async ({ signal }): Promise<LyricsQueryResult> => {
|
||||||
if (!song) return null;
|
if (!song) return emptyResult();
|
||||||
|
|
||||||
const serverLyricsQuery = lyricsQueries.serverLyrics(args, song);
|
const prev = queryClient.getQueryData<LyricsQueryResult>(lyricsKey);
|
||||||
const serverLyrics = await queryClient.fetchQuery({
|
const overrideSelection = prev?.overrideSelection ?? null;
|
||||||
...serverLyricsQuery,
|
const suppressRemoteAuto = prev?.suppressRemoteAuto ?? false;
|
||||||
queryFn: async (context) => {
|
const selectedStructuredIndex = prev?.selectedStructuredIndex ?? 0;
|
||||||
return (
|
const selectedOffsetMs = prev?.selectedOffsetMs ?? 0;
|
||||||
serverLyricsQuery.queryFn?.({
|
const preferLocalLyrics = useSettingsStore.getState().lyrics.preferLocalLyrics;
|
||||||
...context,
|
|
||||||
signal,
|
|
||||||
}) ?? null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return serverLyrics;
|
// Fetch local lyrics
|
||||||
|
const localPromise = fetchLocalLyrics({ serverId: args.serverId, signal, song });
|
||||||
|
|
||||||
|
// Fetch remote auto lyrics
|
||||||
|
const remoteAutoPromise =
|
||||||
|
suppressRemoteAuto || !useSettingsStore.getState().lyrics.fetch
|
||||||
|
? null
|
||||||
|
: fetchRemoteLyricsAuto(song);
|
||||||
|
|
||||||
|
// Fetch override data
|
||||||
|
const overrideDataPromise = overrideSelection
|
||||||
|
? fetchRemoteLyricsById({
|
||||||
|
remoteSongId: overrideSelection.id,
|
||||||
|
remoteSource: overrideSelection.source as LyricSource,
|
||||||
|
song,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const [local, remoteAuto, overrideData] = await Promise.all([
|
||||||
|
localPromise,
|
||||||
|
remoteAutoPromise,
|
||||||
|
overrideDataPromise,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const partial: Pick<
|
||||||
|
LyricsQueryResult,
|
||||||
|
| 'local'
|
||||||
|
| 'overrideData'
|
||||||
|
| 'overrideSelection'
|
||||||
|
| 'remoteAuto'
|
||||||
|
| 'selectedOffsetMs'
|
||||||
|
> = {
|
||||||
|
local,
|
||||||
|
overrideData,
|
||||||
|
overrideSelection,
|
||||||
|
remoteAuto,
|
||||||
|
selectedOffsetMs,
|
||||||
|
};
|
||||||
|
const { selected, selectedSynced } = computeSelectedFromResult(
|
||||||
|
partial,
|
||||||
|
preferLocalLyrics,
|
||||||
|
selectedStructuredIndex,
|
||||||
|
);
|
||||||
|
const displayOffset = getDisplayOffset(
|
||||||
|
selected,
|
||||||
|
selectedOffsetMs,
|
||||||
|
selectedStructuredIndex,
|
||||||
|
local,
|
||||||
|
);
|
||||||
|
const resultSelectedOffsetMs = displayOffset;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...emptyResult(),
|
||||||
|
...partial,
|
||||||
|
selected,
|
||||||
|
selectedOffsetMs: resultSelectedOffsetMs,
|
||||||
|
selectedStructuredIndex,
|
||||||
|
selectedSynced,
|
||||||
|
suppressRemoteAuto,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.songs.lyrics(args.serverId, args.query),
|
queryKey: lyricsKey,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
...args.options,
|
...args.options,
|
||||||
});
|
});
|
||||||
@@ -191,15 +347,13 @@ export const lyricsQueries = {
|
|||||||
return queryOptions({
|
return queryOptions({
|
||||||
gcTime: Infinity,
|
gcTime: Infinity,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(
|
const q = args.query;
|
||||||
args.query as any,
|
if (!q?.remoteSongId || !q?.remoteSource) return null;
|
||||||
);
|
return fetchRemoteLyricsById({
|
||||||
|
remoteSongId: q.remoteSongId,
|
||||||
if (remoteLyricsResult) {
|
remoteSource: q.remoteSource as LyricSource,
|
||||||
return formatLyrics(remoteLyricsResult);
|
song: q.song as QueueSong | Song | undefined,
|
||||||
}
|
});
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.songs.lyricsByRemoteId(args.query),
|
queryKey: queryKeys.songs.lyricsByRemoteId(args.query),
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
|
|||||||
@@ -117,13 +117,13 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
const [debouncedArtist] = useDebouncedValue(form.values.artist, 500);
|
const [debouncedArtist] = useDebouncedValue(form.values.artist, 500);
|
||||||
const [debouncedName] = useDebouncedValue(form.values.name, 500);
|
const [debouncedName] = useDebouncedValue(form.values.name, 500);
|
||||||
|
|
||||||
const { data, isInitialLoading } = useQuery(
|
const { data, isLoading } = useQuery(
|
||||||
lyricsQueries.search({
|
lyricsQueries.search({
|
||||||
query: { artist: debouncedArtist, name: debouncedName },
|
query: { artist: debouncedArtist, name: debouncedName },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: previewData, isInitialLoading: isPreviewLoading } = useQuery(
|
const { data: previewData, isLoading: isPreviewLoading } = useQuery(
|
||||||
lyricsQueries.songLyricsByRemoteId({
|
lyricsQueries.songLyricsByRemoteId({
|
||||||
options: {
|
options: {
|
||||||
enabled: !!selectedResult,
|
enabled: !!selectedResult,
|
||||||
@@ -228,7 +228,7 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
|||||||
paddingRight: '1rem',
|
paddingRight: '1rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isInitialLoading ? (
|
{isLoading ? (
|
||||||
<Spinner container />
|
<Spinner container />
|
||||||
) : (
|
) : (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import styles from './lyrics.module.css';
|
|||||||
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { translateLyrics } from '/@/renderer/features/lyrics/api/lyric-translate';
|
import { translateLyrics } from '/@/renderer/features/lyrics/api/lyric-translate';
|
||||||
import { lyricsQueries } from '/@/renderer/features/lyrics/api/lyrics-api';
|
import {
|
||||||
|
computeSelectedFromResult,
|
||||||
|
getDisplayOffset,
|
||||||
|
lyricsQueries,
|
||||||
|
type LyricsQueryResult,
|
||||||
|
} from '/@/renderer/features/lyrics/api/lyrics-api';
|
||||||
import { openLyricsExportModal } from '/@/renderer/features/lyrics/components/lyrics-export-form';
|
import { openLyricsExportModal } from '/@/renderer/features/lyrics/components/lyrics-export-form';
|
||||||
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
||||||
import {
|
import {
|
||||||
@@ -22,18 +27,13 @@ import { openLyricsSettingsModal } from '/@/renderer/features/lyrics/utils/open-
|
|||||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||||
import { ComponentErrorBoundary } from '/@/renderer/features/shared/components/component-error-boundary';
|
import { ComponentErrorBoundary } from '/@/renderer/features/shared/components/component-error-boundary';
|
||||||
import { queryClient } from '/@/renderer/lib/react-query';
|
import { queryClient } from '/@/renderer/lib/react-query';
|
||||||
import { useLyricsSettings, usePlayerSong, useSettingsStore } from '/@/renderer/store';
|
import { useLyricsSettings, usePlayerSong } from '/@/renderer/store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Center } from '/@/shared/components/center/center';
|
import { Center } from '/@/shared/components/center/center';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import {
|
import { LyricsOverride } from '/@/shared/types/domain-types';
|
||||||
FullLyricsMetadata,
|
|
||||||
LyricSource,
|
|
||||||
LyricsOverride,
|
|
||||||
StructuredLyric,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
type LyricsProps = {
|
type LyricsProps = {
|
||||||
fadeOutNoLyricsMessage?: boolean;
|
fadeOutNoLyricsMessage?: boolean;
|
||||||
@@ -50,14 +50,13 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
translationTargetLanguage,
|
translationTargetLanguage,
|
||||||
} = useLyricsSettings();
|
} = useLyricsSettings();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndexState] = useState(0);
|
||||||
const [translatedLyrics, setTranslatedLyrics] = useState<null | string>(null);
|
const [translatedLyrics, setTranslatedLyrics] = useState<null | string>(null);
|
||||||
const [showTranslation, setShowTranslation] = useState(false);
|
const [showTranslation, setShowTranslation] = useState(false);
|
||||||
const [pendingSongId, setPendingSongId] = useState<string | undefined>(currentSong?.id);
|
const [pendingSongId, setPendingSongId] = useState<string | undefined>(currentSong?.id);
|
||||||
const lyricsFetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
const lyricsFetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
const previousSongIdRef = useRef<string | undefined>(currentSong?.id);
|
const previousSongIdRef = useRef<string | undefined>(currentSong?.id);
|
||||||
|
|
||||||
// Use a timeout to prevent fetching lyrics when switching songs quickly
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentSongId = currentSong?.id;
|
const currentSongId = currentSong?.id;
|
||||||
const previousSongId = previousSongIdRef.current;
|
const previousSongId = previousSongIdRef.current;
|
||||||
@@ -67,7 +66,6 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
}
|
}
|
||||||
|
|
||||||
previousSongIdRef.current = currentSongId;
|
previousSongIdRef.current = currentSongId;
|
||||||
|
|
||||||
setPendingSongId(undefined);
|
setPendingSongId(undefined);
|
||||||
|
|
||||||
if (!currentSongId) {
|
if (!currentSongId) {
|
||||||
@@ -75,7 +73,6 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearTimeout(lyricsFetchTimeoutRef.current);
|
clearTimeout(lyricsFetchTimeoutRef.current);
|
||||||
|
|
||||||
lyricsFetchTimeoutRef.current = setTimeout(() => {
|
lyricsFetchTimeoutRef.current = setTimeout(() => {
|
||||||
setPendingSongId(currentSongId);
|
setPendingSongId(currentSongId);
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -85,7 +82,12 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
};
|
};
|
||||||
}, [currentSong?.id]);
|
}, [currentSong?.id]);
|
||||||
|
|
||||||
const { data, isInitialLoading } = useQuery(
|
const lyricsKey = useMemo(() => {
|
||||||
|
if (!currentSong?._serverId || !currentSong?.id) return null;
|
||||||
|
return queryKeys.songs.lyrics(currentSong._serverId, { songId: currentSong.id });
|
||||||
|
}, [currentSong]);
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery(
|
||||||
lyricsQueries.songLyrics(
|
lyricsQueries.songLyrics(
|
||||||
{
|
{
|
||||||
options: {
|
options: {
|
||||||
@@ -98,250 +100,96 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const [override, setOverride] = useState<LyricsOverride | undefined>(undefined);
|
const indexToUse = data?.selectedStructuredIndex ?? index;
|
||||||
const [autoRemoteLyrics, setAutoRemoteLyrics] = useState<FullLyricsMetadata | null>(null);
|
|
||||||
const clearedOverrideRef = useRef<string | undefined>(undefined);
|
|
||||||
|
|
||||||
// Fetch remote lyrics automatically (but not if we've explicitly cleared the override)
|
|
||||||
const { data: remoteLyricsData, isInitialLoading: isRemoteLyricsLoading } = useQuery(
|
|
||||||
lyricsQueries.remoteLyrics(
|
|
||||||
{
|
|
||||||
options: {
|
|
||||||
enabled:
|
|
||||||
!!pendingSongId &&
|
|
||||||
pendingSongId === currentSong?.id &&
|
|
||||||
!override &&
|
|
||||||
clearedOverrideRef.current !== currentSong?.id &&
|
|
||||||
useSettingsStore.getState().lyrics.fetch,
|
|
||||||
},
|
|
||||||
query: { songId: currentSong?.id || '' },
|
|
||||||
serverId: currentSong?._serverId || '',
|
|
||||||
},
|
|
||||||
currentSong,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Automatically set remote lyrics as override when fetched
|
|
||||||
// Only auto-apply if preferLocalLyrics is disabled OR if local lyrics are not available
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Don't auto-set if we've explicitly cleared the override for this song
|
if (data != null) setIndexState(data.selectedStructuredIndex);
|
||||||
if (
|
}, [data]);
|
||||||
remoteLyricsData &&
|
|
||||||
!override &&
|
|
||||||
currentSong &&
|
|
||||||
clearedOverrideRef.current !== currentSong.id
|
|
||||||
) {
|
|
||||||
// If preferLocalLyrics is enabled, wait for local lyrics to finish loading
|
|
||||||
if (preferLocalLyrics && isInitialLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if local lyrics are available
|
const { selected: lyrics, selectedSynced: synced } = useMemo(() => {
|
||||||
const hasLocalLyrics =
|
if (!data) return { selected: null, selectedSynced: false };
|
||||||
(Array.isArray(data) && data.length > 0) ||
|
return computeSelectedFromResult(data, preferLocalLyrics, indexToUse);
|
||||||
(data && !Array.isArray(data) && data.lyrics);
|
}, [data, indexToUse, preferLocalLyrics]);
|
||||||
|
|
||||||
// Only auto-apply remote lyrics if:
|
|
||||||
// 1. preferLocalLyrics is disabled, OR
|
|
||||||
// 2. preferLocalLyrics is enabled but no local lyrics are available
|
|
||||||
if (!preferLocalLyrics || !hasLocalLyrics) {
|
|
||||||
// Store the remote lyrics data directly (it already contains the lyrics)
|
|
||||||
setAutoRemoteLyrics(remoteLyricsData);
|
|
||||||
} else {
|
|
||||||
// Clear auto remote lyrics if local lyrics are preferred and available
|
|
||||||
setAutoRemoteLyrics(null);
|
|
||||||
}
|
|
||||||
} else if (!remoteLyricsData || override) {
|
|
||||||
// Clear auto remote lyrics if override is set or remote lyrics are cleared
|
|
||||||
setAutoRemoteLyrics(null);
|
|
||||||
}
|
|
||||||
}, [remoteLyricsData, override, currentSong, preferLocalLyrics, data, isInitialLoading]);
|
|
||||||
|
|
||||||
const { data: overrideData, isInitialLoading: isOverrideLoading } = useQuery(
|
|
||||||
lyricsQueries.songLyricsByRemoteId({
|
|
||||||
options: {
|
|
||||||
enabled: !!override && !!override.id,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
remoteSongId: override?.id,
|
|
||||||
remoteSource: override?.source as LyricSource | undefined,
|
|
||||||
song: currentSong,
|
|
||||||
},
|
|
||||||
serverId: currentSong?._serverId || '',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the current song's offset from persisted lyrics, default to 0
|
|
||||||
const currentOffsetMs = useMemo(() => {
|
const currentOffsetMs = useMemo(() => {
|
||||||
if (Array.isArray(data)) {
|
if (!data) return 0;
|
||||||
if (data.length > 0) {
|
return getDisplayOffset(lyrics, data.selectedOffsetMs, indexToUse, data.local);
|
||||||
const selectedLyric = data[Math.min(index, data.length - 1)];
|
}, [data, indexToUse, lyrics]);
|
||||||
return selectedLyric.offsetMs ?? 0;
|
|
||||||
}
|
|
||||||
} else if (data?.offsetMs !== undefined) {
|
|
||||||
return data.offsetMs;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}, [data, index]);
|
|
||||||
|
|
||||||
const [lyrics, synced] = useMemo(() => {
|
const handleOnSearchOverride = useCallback(
|
||||||
// If override data is available, use it (manual override always takes priority)
|
(params: LyricsOverride) => {
|
||||||
if (override && overrideData && override.id) {
|
if (!lyricsKey) return;
|
||||||
const overrideLyrics: FullLyricsMetadata = {
|
queryClient.setQueryData<LyricsQueryResult>(lyricsKey, (prev) =>
|
||||||
artist: override.artist,
|
prev ? { ...prev, overrideSelection: params } : prev,
|
||||||
lyrics: overrideData,
|
|
||||||
name: override.name,
|
|
||||||
offsetMs: currentOffsetMs,
|
|
||||||
remote: override.remote ?? true,
|
|
||||||
source: override.source,
|
|
||||||
};
|
|
||||||
return [overrideLyrics, Array.isArray(overrideData), 'override'] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if local lyrics are available
|
|
||||||
const hasLocalLyrics =
|
|
||||||
(Array.isArray(data) && data.length > 0) ||
|
|
||||||
(data && !Array.isArray(data) && data.lyrics);
|
|
||||||
|
|
||||||
// If preferLocalLyrics is enabled and local lyrics are available, prioritize them
|
|
||||||
if (preferLocalLyrics && hasLocalLyrics) {
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
const selectedLyric = data[Math.min(index, data.length - 1)];
|
|
||||||
return [selectedLyric, selectedLyric.synced, 'server'] as const;
|
|
||||||
} else if (data?.lyrics) {
|
|
||||||
return [data, Array.isArray(data.lyrics), 'server'] as const;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If auto-fetched remote lyrics are available, use them
|
|
||||||
// (This will only be set if preferLocalLyrics is disabled OR no local lyrics exist)
|
|
||||||
if (autoRemoteLyrics) {
|
|
||||||
return [autoRemoteLyrics, Array.isArray(autoRemoteLyrics.lyrics), 'override'] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, use the server-side lyrics data
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
if (data.length > 0) {
|
|
||||||
const selectedLyric = data[Math.min(index, data.length - 1)];
|
|
||||||
return [selectedLyric, selectedLyric.synced, 'server'] as const;
|
|
||||||
}
|
|
||||||
} else if (data?.lyrics) {
|
|
||||||
return [data, Array.isArray(data.lyrics), 'server'] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [undefined, false, 'server'] as const;
|
|
||||||
}, [data, index, override, overrideData, autoRemoteLyrics, currentOffsetMs, preferLocalLyrics]);
|
|
||||||
|
|
||||||
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
|
|
||||||
setOverride(params);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Persist override lyrics to cache with current offset
|
|
||||||
useEffect(() => {
|
|
||||||
if (override && overrideData && currentSong && override.id) {
|
|
||||||
const persistedLyrics: FullLyricsMetadata = {
|
|
||||||
artist: override.artist,
|
|
||||||
lyrics: overrideData,
|
|
||||||
name: override.name,
|
|
||||||
offsetMs: currentOffsetMs,
|
|
||||||
remote: override.remote ?? true,
|
|
||||||
source: override.source,
|
|
||||||
};
|
|
||||||
|
|
||||||
queryClient.setQueryData(
|
|
||||||
queryKeys.songs.lyrics(currentSong._serverId, { songId: currentSong.id }),
|
|
||||||
persistedLyrics,
|
|
||||||
);
|
);
|
||||||
}
|
queryClient.invalidateQueries({ queryKey: lyricsKey });
|
||||||
}, [override, overrideData, currentSong, currentOffsetMs]);
|
},
|
||||||
|
[lyricsKey],
|
||||||
|
);
|
||||||
|
|
||||||
// Callback to update the song's persisted offset
|
|
||||||
const handleUpdateOffset = useCallback(
|
const handleUpdateOffset = useCallback(
|
||||||
(offsetMs: number) => {
|
(offsetMs: number) => {
|
||||||
if (!currentSong) return;
|
if (!currentSong || !lyricsKey) return;
|
||||||
|
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData<LyricsQueryResult>(lyricsKey, (prev) => {
|
||||||
queryKeys.songs.lyrics(currentSong._serverId, { songId: currentSong.id }),
|
if (!prev) return prev;
|
||||||
(prev: FullLyricsMetadata | null | StructuredLyric[] | undefined) => {
|
const updated = { ...prev, selectedOffsetMs: offsetMs };
|
||||||
if (!prev) return prev;
|
if (Array.isArray(prev.local) && prev.local.length > 0) {
|
||||||
|
const idx = Math.min(indexToUse, prev.local.length - 1);
|
||||||
// Handle array of structured lyrics
|
updated.local = [...prev.local];
|
||||||
if (Array.isArray(prev)) {
|
updated.local[idx] = {
|
||||||
if (prev.length > 0) {
|
...updated.local[idx],
|
||||||
const selectedIndex = Math.min(index, prev.length - 1);
|
|
||||||
const updated = [...prev];
|
|
||||||
updated[selectedIndex] = {
|
|
||||||
...updated[selectedIndex],
|
|
||||||
offsetMs,
|
|
||||||
};
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle single lyrics object
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
offsetMs,
|
offsetMs,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[currentSong, indexToUse, lyricsKey],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setIndex = useCallback(
|
||||||
|
(newIndex: number) => {
|
||||||
|
setIndexState(newIndex);
|
||||||
|
if (!lyricsKey || !data) return;
|
||||||
|
const { selected: nextSelected, selectedSynced: nextSynced } =
|
||||||
|
computeSelectedFromResult(data, preferLocalLyrics, newIndex);
|
||||||
|
const nextOffset = getDisplayOffset(
|
||||||
|
nextSelected,
|
||||||
|
data.selectedOffsetMs,
|
||||||
|
newIndex,
|
||||||
|
data.local,
|
||||||
|
);
|
||||||
|
queryClient.setQueryData<LyricsQueryResult>(lyricsKey, (prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
selected: nextSelected,
|
||||||
|
selectedOffsetMs: nextOffset,
|
||||||
|
selectedStructuredIndex: newIndex,
|
||||||
|
selectedSynced: nextSynced,
|
||||||
|
}
|
||||||
|
: prev,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[currentSong, index],
|
[data, lyricsKey, preferLocalLyrics],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnRemoveLyric = useCallback(async () => {
|
const handleOnRemoveLyric = useCallback(async () => {
|
||||||
if (!currentSong) return;
|
if (!currentSong || !lyricsKey) return;
|
||||||
|
|
||||||
const currentOverride = override;
|
queryClient.setQueryData<LyricsQueryResult>(lyricsKey, (prev) =>
|
||||||
clearedOverrideRef.current = currentSong.id;
|
prev
|
||||||
|
? {
|
||||||
// Clear the override state and auto remote lyrics
|
...prev,
|
||||||
setOverride(undefined);
|
overrideData: null,
|
||||||
setAutoRemoteLyrics(null);
|
overrideSelection: null,
|
||||||
|
remoteAuto: null,
|
||||||
// Clear the override query cache if it exists
|
suppressRemoteAuto: true,
|
||||||
if (currentOverride?.id) {
|
}
|
||||||
queryClient.removeQueries({
|
: prev,
|
||||||
queryKey: queryKeys.songs.lyricsByRemoteId({
|
);
|
||||||
remoteSongId: currentOverride.id,
|
await queryClient.invalidateQueries({ queryKey: lyricsKey });
|
||||||
remoteSource: currentOverride.source,
|
}, [currentSong, lyricsKey]);
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the remote lyrics cache
|
|
||||||
queryClient.removeQueries({
|
|
||||||
queryKey: queryKeys.songs.remoteLyrics(currentSong._serverId, {
|
|
||||||
songId: currentSong.id,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear the server lyrics cache so it refetches from server
|
|
||||||
const serverLyricsQueryKey = queryKeys.songs.serverLyrics(currentSong._serverId, {
|
|
||||||
songId: currentSong.id,
|
|
||||||
});
|
|
||||||
queryClient.removeQueries({
|
|
||||||
queryKey: serverLyricsQueryKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove the main lyrics query cache
|
|
||||||
const lyricsQueryKey = queryKeys.songs.lyrics(currentSong._serverId, {
|
|
||||||
songId: currentSong.id,
|
|
||||||
});
|
|
||||||
queryClient.removeQueries({
|
|
||||||
exact: true,
|
|
||||||
queryKey: lyricsQueryKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Refetch server lyrics first, then song lyrics to ensure fresh data from server
|
|
||||||
await queryClient.refetchQueries({
|
|
||||||
queryKey: serverLyricsQueryKey,
|
|
||||||
});
|
|
||||||
await queryClient.refetchQueries({
|
|
||||||
queryKey: lyricsQueryKey,
|
|
||||||
});
|
|
||||||
}, [currentSong, override]);
|
|
||||||
|
|
||||||
const fetchTranslation = useCallback(async () => {
|
const fetchTranslation = useCallback(async () => {
|
||||||
if (!lyrics) return;
|
if (!lyrics) return;
|
||||||
@@ -369,10 +217,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
usePlayerEvents(
|
usePlayerEvents(
|
||||||
{
|
{
|
||||||
onCurrentSongChange: () => {
|
onCurrentSongChange: () => {
|
||||||
setOverride(undefined);
|
setIndexState(0);
|
||||||
setAutoRemoteLyrics(null);
|
|
||||||
clearedOverrideRef.current = undefined;
|
|
||||||
setIndex(0);
|
|
||||||
setShowTranslation(false);
|
setShowTranslation(false);
|
||||||
setTranslatedLyrics(null);
|
setTranslatedLyrics(null);
|
||||||
},
|
},
|
||||||
@@ -387,46 +232,20 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
}, [lyrics, translatedLyrics, enableAutoTranslation, fetchTranslation]);
|
}, [lyrics, translatedLyrics, enableAutoTranslation, fetchTranslation]);
|
||||||
|
|
||||||
const languages = useMemo(() => {
|
const languages = useMemo(() => {
|
||||||
if (Array.isArray(data)) {
|
const local = data?.local;
|
||||||
return data.map((lyric, idx) => ({ label: lyric.lang, value: idx.toString() }));
|
if (Array.isArray(local)) {
|
||||||
} else if (data?.lyrics) {
|
return local.map((lyric, idx) => ({ label: lyric.lang, value: idx.toString() }));
|
||||||
// xxx denotes undefined lyrics language. If it's a single lyric (from a remote source)
|
}
|
||||||
// the language is most likely not available, so leave it undefined
|
if (local && !Array.isArray(local) && 'lyrics' in local) {
|
||||||
return [{ label: 'xxx', value: '0' }];
|
return [{ label: 'xxx', value: '0' }];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [data]);
|
}, [data?.local]);
|
||||||
|
|
||||||
const shouldShowRemoteLoading = useMemo(() => {
|
|
||||||
if (!isRemoteLyricsLoading) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If preferLocalLyrics is disabled, always show remote loading
|
|
||||||
if (!preferLocalLyrics) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If preferLocalLyrics is enabled, only show remote loading if:
|
|
||||||
// - Local lyrics have finished loading (!isInitialLoading)
|
|
||||||
// - No local lyrics are available
|
|
||||||
if (isInitialLoading) return false;
|
|
||||||
|
|
||||||
// Check if local lyrics are available
|
|
||||||
const hasLocalLyrics =
|
|
||||||
(Array.isArray(data) && data.length > 0) ||
|
|
||||||
(data && !Array.isArray(data) && data.lyrics);
|
|
||||||
|
|
||||||
// Show remote loading only if no local lyrics are available
|
|
||||||
return !hasLocalLyrics;
|
|
||||||
}, [isRemoteLyricsLoading, preferLocalLyrics, data, isInitialLoading]);
|
|
||||||
|
|
||||||
const isLoadingLyrics = isInitialLoading || isOverrideLoading || shouldShowRemoteLoading;
|
|
||||||
|
|
||||||
|
const isLoadingLyrics = isLoading;
|
||||||
const hasNoLyrics = !lyrics;
|
const hasNoLyrics = !lyrics;
|
||||||
const [shouldFadeOut, setShouldFadeOut] = useState(false);
|
const [shouldFadeOut, setShouldFadeOut] = useState(false);
|
||||||
|
|
||||||
// Trigger fade out after a few seconds when no lyrics are found
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fadeOutNoLyricsMessage) {
|
if (!fadeOutNoLyricsMessage) {
|
||||||
setShouldFadeOut(false);
|
setShouldFadeOut(false);
|
||||||
@@ -434,11 +253,9 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoadingLyrics && hasNoLyrics) {
|
if (!isLoadingLyrics && hasNoLyrics) {
|
||||||
// Start fade out after 3 seconds (message visible for 3s, then 0.5s fade)
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setShouldFadeOut(true);
|
setShouldFadeOut(true);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +337,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
<div className={styles.actionsContainer}>
|
<div className={styles.actionsContainer}>
|
||||||
<LyricsActions
|
<LyricsActions
|
||||||
hasLyrics={!!lyrics}
|
hasLyrics={!!lyrics}
|
||||||
index={index}
|
index={indexToUse}
|
||||||
languages={languages}
|
languages={languages}
|
||||||
offsetMs={currentOffsetMs}
|
offsetMs={currentOffsetMs}
|
||||||
onExportLyrics={handleExportLyrics}
|
onExportLyrics={handleExportLyrics}
|
||||||
|
|||||||
Reference in New Issue
Block a user