MusicControls: Update SpotifyLyrics
This commit is contained in:
parent
bfb765097c
commit
fc36d08465
7 changed files with 103 additions and 62 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ export const lyricFetchers = {
|
|||
export const providers = Object.keys(lyricFetchers) as Provider[];
|
||||
|
||||
export async function getLyrics(track: Track | null): Promise<LyricsData | null> {
|
||||
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<string, LyricsData | null>;
|
||||
|
|
@ -76,7 +75,7 @@ export async function clearLyricsCache() {
|
|||
|
||||
export async function getLyricsCount(): Promise<number> {
|
||||
const cache = await DataStore.get(LyricsCacheKey) as Record<string, LyricsData | null>;
|
||||
return Object.keys(cache).length;
|
||||
return Object.keys(cache ?? {}).length;
|
||||
}
|
||||
|
||||
export async function updateLyrics(trackId: string, newLyrics: SyncedLyric[], provider: Provider) {
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<div className="vc-spotify-lyrics"
|
||||
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <LyricsContextMenu />)}
|
||||
>
|
||||
<TooltipContainer text="No synced lyrics found">
|
||||
{NoteElement}
|
||||
</TooltipContainer>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="vc-spotify-lyrics"
|
||||
onClick={() => openModal(props => <LyricsModal rootProps={props} />)}
|
||||
onClick={() => openModal(props => <LyricsModal props={props} />)}
|
||||
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <LyricsContextMenu />)}
|
||||
>
|
||||
{currentLyrics?.map((line, i) => (
|
||||
{currentLyrics ? currentLyrics.map((line, i) => (
|
||||
<div ref={lyricRefs[i]} key={i}>
|
||||
<Text
|
||||
variant={currLrcIndex === i ? "text-sm/normal" : "text-xs/normal"}
|
||||
className={makeClassName(i)}
|
||||
>
|
||||
{line.text || NoteElement}
|
||||
{line.text || NoteSvg()}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
)) : ShowMusicNoteOnNoLyrics ? (
|
||||
<TooltipContainer text="No synced lyrics found">
|
||||
<NoteSvg />
|
||||
</TooltipContainer>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ModalHeader>
|
||||
<Text variant="text-sm/semibold">No track playing</Text>
|
||||
</ModalHeader>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ModalHeader>
|
||||
<div className={cl("header-content")}>
|
||||
|
|
@ -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 (
|
||||
<ModalRoot {...rootProps}>
|
||||
<ModalHeaderContent track={track!} />
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeaderContent track={track} />
|
||||
<ModalContent>
|
||||
<div className={cl("lyrics-modal-container") + ` ${scrollClasses.auto}`}>
|
||||
<div className={`${cl("lyrics-modal-container")} ${scrollClasses.auto}`}>
|
||||
{currentLyrics ? (
|
||||
currentLyrics.map((line, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
variant={currLrcIndex === i ? "text-md/semibold" : "text-sm/normal"}
|
||||
selectable
|
||||
className={currLrcIndex === i ? cl("modal-line-current") : cl("modal-line")}
|
||||
className={currLrcIndex === i ? modalCurrentLine : modalLine}
|
||||
>
|
||||
<span className={cl("modal-timestamp")} onClick={() => SpotifyStore.seek(line.time * 1000)}>
|
||||
{formatTime(line.time)}
|
||||
</span>
|
||||
{line.text || NoteSvg(cl("modal-note"))}
|
||||
{line.text || NoteSvg()}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 480 720" fill="currentColor" className={className} >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 480 720" fill="currentColor" className={cl("music-note")}>
|
||||
<path d="m160,-240 q -66,0 -113,-47 -47,-47 -47,-113 0,-66 47,-113 47,-47 113,-47 23,0 42.5,5.5 19.5,5.5 37.5,16.5 v -422 h 240 v 160 H 320 v 400 q 0,66 -47,113 -47,47 -113,47 z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
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<React.RefObject<HTMLDivElement | null>[]>([]);
|
||||
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -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<SyncedLyric[] | null> {
|
||||
|
|
@ -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<SyncedLyric[] | null> {
|
||||
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<SyncedLyric[] | null> {
|
||||
return processLyrics(lyrics, "", true);
|
||||
async function romanizeLyrics(lyrics: SyncedLyric[]) {
|
||||
return await processLyrics(lyrics, "", true);
|
||||
}
|
||||
|
||||
export const lyricsAlternativeFetchers = {
|
||||
|
|
|
|||
|
|
@ -243,4 +243,4 @@
|
|||
.eq-tidal-lyrics-word-active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue