diff --git a/src/equicordplugins/musicControls/settings.tsx b/src/equicordplugins/musicControls/settings.tsx index 8e9c4eac0..a3b94de87 100644 --- a/src/equicordplugins/musicControls/settings.tsx +++ b/src/equicordplugins/musicControls/settings.tsx @@ -10,7 +10,8 @@ import { makeRange, OptionType } from "@utils/types"; import { Button, Forms, MaskedLink, showToast, Toasts } from "@webpack/common"; import hoverOnlyStyle from "./hoverOnly.css?managed"; -import { clearLyricsCache } from "./spotify/lyrics/api"; +import { clearLyricsCache, removeTranslations } from "./spotify/lyrics/api"; +import languages from "./spotify/lyrics/providers/translator/languages"; import { Provider } from "./spotify/lyrics/providers/types"; const sliderOptions = { @@ -61,6 +62,15 @@ export const settings = definePluginSettings({ { value: Provider.Lrclib, label: "LRCLIB" }, ], }, + TranslateTo: { + description: "Translate lyrics to - Changing this will clear existing translations", + type: OptionType.SELECT, + options: languages, + onChange: async () => { + await removeTranslations(); + showToast("Translations cleared", Toasts.Type.SUCCESS); + } + }, LyricsConversion: { description: "Automatically translate or romanize lyrics", type: OptionType.SELECT, diff --git a/src/equicordplugins/musicControls/spotify/lyrics/api.tsx b/src/equicordplugins/musicControls/spotify/lyrics/api.tsx index a92381d77..d9a95d040 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/api.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/api.tsx @@ -29,8 +29,7 @@ export const lyricFetchers = { export const providers = Object.keys(lyricFetchers) as Provider[]; export async function getLyrics(track: Track | null): Promise { - if (!track) return null; - if (!track.id) return null; + if (!track || !track.id) return null; const cacheKey = track.id; const cached = await DataStore.get(LyricsCacheKey) as Record; @@ -76,7 +75,7 @@ export async function clearLyricsCache() { export async function getLyricsCount(): Promise { const cache = await DataStore.get(LyricsCacheKey) as Record; - return Object.keys(cache).length; + return Object.keys(cache ?? {}).length; } export async function updateLyrics(trackId: string, newLyrics: SyncedLyric[], provider: Provider) { diff --git a/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx index 7a3770093..9e89a1c84 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx @@ -14,51 +14,45 @@ import { LyricsContextMenu } from "./ctxMenu"; import { LyricsModal } from "./modal"; import { cl, NoteSvg, useLyrics } from "./util"; +const prevCl = cl("prev"); +const nextCl = cl("next"); +const currentCl = cl("current"); + function LyricsDisplay({ scroll = true }: { scroll?: boolean; }) { const { ShowMusicNoteOnNoLyrics } = settings.use(["ShowMusicNoteOnNoLyrics"]); const { lyricsInfo, lyricRefs, currLrcIndex } = useLyrics({ scroll }); const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null; - const NoteElement = NoteSvg(cl("music-note")); - const makeClassName = (index: number): string => { - if (currLrcIndex === null) return ""; + if (currLrcIndex == null) return prevCl; const diff = index - currLrcIndex; - if (diff === 0) return cl("current"); - return cl(diff > 0 ? "next" : "prev"); + if (diff === 0) return currentCl; + return diff > 0 ? nextCl : prevCl; }; - if (!lyricsInfo) { - return ShowMusicNoteOnNoLyrics ? ( -
ContextMenuApi.openContextMenu(e, () => )} - > - - {NoteElement} - -
- ) : null; - } - return (
openModal(props => )} + onClick={() => openModal(props => )} onContextMenu={e => ContextMenuApi.openContextMenu(e, () => )} > - {currentLyrics?.map((line, i) => ( + {currentLyrics ? currentLyrics.map((line, i) => (
- {line.text || NoteElement} + {line.text || NoteSvg()}
- ))} + )) : ShowMusicNoteOnNoLyrics ? ( + + + + ) : null}
); } diff --git a/src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx index 6a3423fec..59831cbc7 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx @@ -17,7 +17,14 @@ const formatTime = (time: number) => { return `${minutes}:${seconds.toString().padStart(2, "0")}`; }; -function ModalHeaderContent({ track }: { track: Track; }) { +function ModalHeaderContent({ track }: { track: Track | null; }) { + if (!track) { + return ( + + No track playing + + ); + } return (
@@ -43,27 +50,30 @@ function ModalHeaderContent({ track }: { track: Track; }) { ); } -export function LyricsModal({ rootProps }: { rootProps: ModalProps; }) { +const modalCurrentLine = cl("modal-line-current"); +const modalLine = cl("modal-line"); + +export function LyricsModal({ props }: { props: ModalProps; }) { const { track, lyricsInfo, currLrcIndex } = useLyrics({ scroll: false }); - const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null; + const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric]; return ( - - + + -
+
{currentLyrics ? ( currentLyrics.map((line, i) => ( SpotifyStore.seek(line.time * 1000)}> {formatTime(line.time)} - {line.text || NoteSvg(cl("modal-note"))} + {line.text || NoteSvg()} )) ) : ( diff --git a/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx index 176df03a7..5c63eb967 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx @@ -12,23 +12,51 @@ import { settings } from "../../../settings"; import { SpotifyStore } from "../../SpotifyStore"; import { SpotifyLrcStore } from "../providers/store"; import { SyncedLyric } from "../providers/types"; + export const scrollClasses = findByPropsLazy("auto", "customTheme"); export const cl = classNameFactory("vc-spotify-lyrics-"); -export function NoteSvg(className: string) { +export function NoteSvg() { return ( - + ); } -const calculateIndexes = (lyrics: SyncedLyric[], position: number, delay: number) => { - const posInSec = position / 1000; - const currentIndex = lyrics.findIndex(l => l.time - (delay / 1000) > posInSec && l.time < posInSec + 8) - 1; - const nextLyric = lyrics.findIndex(l => l.time >= posInSec); - return [currentIndex, nextLyric]; + +const getIndexes = (lyrics: SyncedLyric[], position: number, delay: number) => { + const posInSec = (position + delay) / 1000; + + let left = 0, right = lyrics.length - 1; + let currentIndex: number | null = null; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const curr = lyrics[mid]; + const next = lyrics[mid + 1]; + + if (curr.time <= posInSec && (!next || next.time > posInSec)) { + currentIndex = mid; + break; + } + + if (curr.time > posInSec) { + right = mid - 1; + } else { + left = mid + 1; + } + } + + const nextIdx = currentIndex !== null ? currentIndex + 1 : left; + const nextLyricIdx = nextIdx < lyrics.length ? nextIdx : null; + + if (currentIndex !== null && posInSec - lyrics[currentIndex].time > 8) { + return [null, nextLyricIdx]; + } + + return [currentIndex, nextLyricIdx]; }; export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) { @@ -47,7 +75,7 @@ export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) { const [position, setPosition] = useState(storePosition); const [lyricRefs, setLyricRefs] = useState[]>([]); - const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null; + const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric]; useEffect(() => { if (currentLyrics) { @@ -57,12 +85,15 @@ export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) { useEffect(() => { - if (currentLyrics && position) { - const [currentIndex, nextLyric] = calculateIndexes(currentLyrics, position, LyricDelay); + if (currentLyrics && position != null) { + const [currentIndex, nextLyricIndex] = getIndexes(currentLyrics, position, LyricDelay); setCurrLrcIndex(currentIndex); - setNextLyric(nextLyric); + setNextLyric(nextLyricIndex); + } else { + setCurrLrcIndex(null); + setNextLyric(null); } - }, [currentLyrics, position]); + }, [currentLyrics, position, LyricDelay]); useEffect(() => { if (scroll && currLrcIndex !== null) { @@ -73,17 +104,15 @@ export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) { lyricRefs[nextLyric]?.current?.scrollIntoView({ behavior: "smooth", block: "center" }); } } - }, [currLrcIndex, nextLyric, scroll]); + }, [currLrcIndex, nextLyric, scroll, lyricRefs]); useEffect(() => { - if (isPlaying) { - setPosition(SpotifyStore.position); - const interval = setInterval(() => { - setPosition(p => p + 1000); - }, 1000); + if (!isPlaying) return; - return () => clearInterval(interval); - } + setPosition(SpotifyStore.position); + const interval = setInterval(() => setPosition(p => p + 1000), 1000); + + return () => clearInterval(interval); }, [storePosition, isPlaying]); return { track, lyricsInfo, lyricRefs, currLrcIndex, nextLyric }; diff --git a/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts b/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts index 02d7308ec..898072d13 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts +++ b/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts @@ -5,7 +5,7 @@ */ import settings from "../../../.."; -import { LyricsData, Provider, SyncedLyric } from "../types"; +import { Provider, SyncedLyric } from "../types"; // stolen from src\plugins\translate\utils.ts @@ -45,7 +45,7 @@ async function googleTranslate(text: string, targetLang: string, romanize: boole } async function processLyrics( - lyrics: LyricsData["lyricsVersions"][Provider], + lyrics: SyncedLyric[], targetLang: string, romanize: boolean ): Promise { @@ -67,7 +67,7 @@ async function processLyrics( }) ); - if (processedLyricsResp[0][1] === null) return null; + if (processedLyricsResp.every(mapping => mapping[1] === null)) return null; return lyrics.map(lyric => ({ ...lyric, @@ -75,13 +75,12 @@ async function processLyrics( })); } -async function translateLyrics(lyrics: LyricsData["lyricsVersions"][Provider]): Promise { - const language = settings.store.TranslateTo; - return processLyrics(lyrics, language, false); +async function translateLyrics(lyrics: SyncedLyric[]) { + return await processLyrics(lyrics, settings.store.TranslateTo, false); } -async function romanizeLyrics(lyrics: LyricsData["lyricsVersions"][Provider]): Promise { - return processLyrics(lyrics, "", true); +async function romanizeLyrics(lyrics: SyncedLyric[]) { + return await processLyrics(lyrics, "", true); } export const lyricsAlternativeFetchers = { diff --git a/src/equicordplugins/musicControls/styles.css b/src/equicordplugins/musicControls/styles.css index 3252938e7..878b13208 100644 --- a/src/equicordplugins/musicControls/styles.css +++ b/src/equicordplugins/musicControls/styles.css @@ -243,4 +243,4 @@ .eq-tidal-lyrics-word-active { transform: none; } -} +} \ No newline at end of file