const CreditFooter = react.memo(({ provider, copyright }) => { if (provider === "local") return null; const credit = [Spicetify.Locale.get("web-player.lyrics.providedBy", provider)]; if (copyright) { credit.push(...copyright.split("\n")); } return ( provider && react.createElement( "p", { className: "lyrics-lyricsContainer-Provider main-type-mesto", dir: "auto" }, credit.join(" • ") ) ); }); const IdlingIndicator = ({ isActive, progress, delay }) => { return react.createElement( "div", { className: `lyrics-idling-indicator ${ !isActive ? "lyrics-idling-indicator-hidden" : "" } lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active`, style: { "--position-index": 0, "--animation-index": 1, "--indicator-delay": `${delay}ms` } }, react.createElement("div", { className: `lyrics-idling-indicator__circle ${progress >= 0.05 ? "active" : ""}` }), react.createElement("div", { className: `lyrics-idling-indicator__circle ${progress >= 0.33 ? "active" : ""}` }), react.createElement("div", { className: `lyrics-idling-indicator__circle ${progress >= 0.66 ? "active" : ""}` }) ); }; const emptyLine = { startTime: 0, endTime: 0, text: [] }; const useTrackPosition = callback => { const callbackRef = useRef(); callbackRef.current = callback; useEffect(() => { const interval = setInterval(callbackRef.current, 50); return () => { clearInterval(interval); }; }, [callbackRef]); }; const KaraokeLine = ({ text, isActive, position, startTime }) => { if (!isActive) { return text.map(({ word }) => word).join(""); } return text.map(({ word, time }) => { const isWordActive = position >= startTime; startTime += time; return react.createElement( "span", { className: `lyrics-lyricsContainer-Karaoke-Word${isWordActive ? " lyrics-lyricsContainer-Karaoke-WordActive" : ""}`, style: { "--word-duration": `${time}ms` } }, word ); }); }; const SyncedLyricsPage = react.memo(({ lyrics = [], provider, copyright, isKara }) => { const [position, setPosition] = useState(0); const activeLineEle = useRef(); const lyricContainerEle = useRef(); useTrackPosition(() => { const newPos = Spicetify.Player.getProgress(); const delay = CONFIG.visual["global-delay"] + CONFIG.visual.delay; if (newPos !== position) { setPosition(newPos + delay); } }); const lyricWithEmptyLines = useMemo( () => [emptyLine, emptyLine, ...lyrics].map((line, i) => ({ ...line, lineNumber: i })), [lyrics] ); const lyricsId = lyrics[0].text; let activeLineIndex = 0; for (let i = lyricWithEmptyLines.length - 1; i > 0; i--) { if (position >= lyricWithEmptyLines[i].startTime) { activeLineIndex = i; break; } } const activeLines = useMemo(() => { const startIndex = Math.max(activeLineIndex - 1 - CONFIG.visual["lines-before"], 0); // 3 lines = 1 padding top + 1 padding bottom + 1 active const linesCount = CONFIG.visual["lines-before"] + CONFIG.visual["lines-after"] + 3; return lyricWithEmptyLines.slice(startIndex, startIndex + linesCount); }, [activeLineIndex, lyricWithEmptyLines]); let offset = lyricContainerEle.current ? lyricContainerEle.current.clientHeight / 2 : 0; if (activeLineEle.current) { offset += -(activeLineEle.current.offsetTop + activeLineEle.current.clientHeight / 2); } const rawLyrics = Utils.convertParsedToLRC(lyrics); return react.createElement( "div", { className: "lyrics-lyricsContainer-SyncedLyricsPage", ref: lyricContainerEle }, react.createElement( "div", { className: "lyrics-lyricsContainer-SyncedLyrics", style: { "--offset": `${offset}px` }, key: lyricsId }, activeLines.map(({ text, lineNumber, startTime }, i) => { if (i === 1 && activeLineIndex === 1) { return react.createElement(IdlingIndicator, { progress: position / activeLines[2].startTime, delay: activeLines[2].startTime / 3 }); } let className = "lyrics-lyricsContainer-LyricsLine"; const activeElementIndex = Math.min(activeLineIndex, CONFIG.visual["lines-before"] + 1); let ref; const isActive = activeElementIndex === i; if (isActive) { className += " lyrics-lyricsContainer-LyricsLine-active"; ref = activeLineEle; } let animationIndex; if (activeLineIndex <= CONFIG.visual["lines-before"]) { animationIndex = i - activeLineIndex; } else { animationIndex = i - CONFIG.visual["lines-before"] - 1; } const paddingLine = (animationIndex < 0 && -animationIndex > CONFIG.visual["lines-before"]) || animationIndex > CONFIG.visual["lines-after"]; if (paddingLine) { className += " lyrics-lyricsContainer-LyricsLine-paddingLine"; } return react.createElement( "p", { className, style: { cursor: "pointer", "--position-index": animationIndex, "--animation-index": (animationIndex < 0 ? 0 : animationIndex) + 1, "--blur-index": Math.abs(animationIndex) }, key: lineNumber, dir: "auto", ref, onClick: event => { if (startTime) { Spicetify.Player.seek(startTime); } }, onContextMenu: event => { event.preventDefault(); Spicetify.Platform.ClipboardAPI.copy(rawLyrics) .then(() => Spicetify.showNotification("Lyrics copied to clipboard")) .catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard")); } }, !isKara ? text : react.createElement(KaraokeLine, { text, startTime, position, isActive }) ); }) ), react.createElement(CreditFooter, { provider, copyright }) ); }); class SearchBar extends react.Component { constructor() { super(); this.state = { hidden: true, atNode: 0, foundNodes: [] }; this.container = null; } componentDidMount() { this.viewPort = document.querySelector(".main-view-container .os-viewport"); this.mainViewOffsetTop = document.querySelector(".Root__main-view").offsetTop; this.toggleCallback = () => { if (!(Spicetify.Platform.History.location.pathname === "/lyrics-plus" && this.container)) return; if (this.state.hidden) { this.setState({ hidden: false }); this.container.focus(); } else { this.setState({ hidden: true }); this.container.blur(); } }; this.unFocusCallback = () => { this.container.blur(); this.setState({ hidden: true }); }; this.loopThroughCallback = event => { if (!this.state.foundNodes.length) { return; } if (event.key === "Enter") { const dir = event.shiftKey ? -1 : 1; let atNode = this.state.atNode + dir; if (atNode < 0) { atNode = this.state.foundNodes.length - 1; } atNode %= this.state.foundNodes.length; const rects = this.state.foundNodes[atNode].getBoundingClientRect(); this.viewPort.scrollBy(0, rects.y - 100); this.setState({ atNode }); } }; Spicetify.Mousetrap().bind("mod+shift+f", this.toggleCallback); Spicetify.Mousetrap(this.container).bind("mod+shift+f", this.toggleCallback); Spicetify.Mousetrap(this.container).bind("enter", this.loopThroughCallback); Spicetify.Mousetrap(this.container).bind("shift+enter", this.loopThroughCallback); Spicetify.Mousetrap(this.container).bind("esc", this.unFocusCallback); } componentWillUnmount() { Spicetify.Mousetrap().unbind("mod+shift+f", this.toggleCallback); Spicetify.Mousetrap(this.container).unbind("mod+shift+f", this.toggleCallback); Spicetify.Mousetrap(this.container).unbind("enter", this.loopThroughCallback); Spicetify.Mousetrap(this.container).unbind("shift+enter", this.loopThroughCallback); Spicetify.Mousetrap(this.container).unbind("esc", this.unFocusCallback); } getNodeFromInput(event) { const value = event.target.value.toLowerCase(); if (!value) { this.setState({ foundNodes: [] }); this.viewPort.scrollTo(0, 0); return; } const lyricsPage = document.querySelector(".lyrics-lyricsContainer-UnsyncedLyricsPage"); const walker = document.createTreeWalker( lyricsPage, NodeFilter.SHOW_TEXT, node => { if (node.textContent.toLowerCase().includes(value)) { return NodeFilter.FILTER_ACCEPT; } return NodeFilter.FILTER_REJECT; }, false ); const foundNodes = []; while (walker.nextNode()) { const range = document.createRange(); range.selectNodeContents(walker.currentNode); foundNodes.push(range); } if (!foundNodes.length) { this.viewPort.scrollBy(0, 0); } else { const rects = foundNodes[0].getBoundingClientRect(); this.viewPort.scrollBy(0, rects.y - 100); } this.setState({ foundNodes, atNode: 0 }); } render() { let y = 0; let height = 0; if (this.state.foundNodes.length) { const node = this.state.foundNodes[this.state.atNode]; const rects = node.getBoundingClientRect(); y = rects.y + this.viewPort.scrollTop - this.mainViewOffsetTop; height = rects.height; } return react.createElement( "div", { className: `lyrics-Searchbar${this.state.hidden ? " hidden" : ""}` }, react.createElement("input", { ref: c => { this.container = c; }, onChange: this.getNodeFromInput.bind(this) }), react.createElement("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "currentColor", dangerouslySetInnerHTML: { __html: Spicetify.SVGIcons.search } }), react.createElement( "span", { hidden: this.state.foundNodes.length === 0 }, `${this.state.atNode + 1}/${this.state.foundNodes.length}` ), react.createElement("div", { className: "lyrics-Searchbar-highlight", style: { "--search-highlight-top": `${y}px`, "--search-highlight-height": `${height}px` } }) ); } } function isInViewport(element) { const rect = element.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyright, isKara }) => { const [position, setPosition] = useState(0); const activeLineRef = useRef(null); const pageRef = useRef(null); useTrackPosition(() => { if (!Spicetify.Player.data.is_paused) { setPosition(Spicetify.Player.getProgress() + CONFIG.visual["global-delay"] + CONFIG.visual.delay); } }); const padded = useMemo(() => [emptyLine, ...lyrics], [lyrics]); const intialScroll = useMemo(() => [false], [lyrics]); const lyricsId = lyrics[0].text; let activeLineIndex = 0; for (let i = padded.length - 1; i >= 0; i--) { const line = padded[i]; if (position >= line.startTime) { activeLineIndex = i; break; } } const rawLyrics = Utils.convertParsedToLRC(lyrics); useEffect(() => { if (activeLineRef.current && (!intialScroll[0] || isInViewport(activeLineRef.current))) { activeLineRef.current.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" }); intialScroll[0] = true; } }, [activeLineRef.current]); return react.createElement( "div", { className: "lyrics-lyricsContainer-UnsyncedLyricsPage", key: lyricsId, ref: pageRef }, react.createElement("p", { className: "lyrics-lyricsContainer-LyricsUnsyncedPadding" }), padded.map(({ text, startTime }, i) => { if (i === 0) { return react.createElement(IdlingIndicator, { isActive: activeLineIndex === 0, progress: position / padded[1].startTime, delay: padded[1].startTime / 3 }); } const isActive = i === activeLineIndex; return react.createElement( "p", { className: `lyrics-lyricsContainer-LyricsLine${i <= activeLineIndex ? " lyrics-lyricsContainer-LyricsLine-active" : ""}`, style: { cursor: "pointer" }, dir: "auto", ref: isActive ? activeLineRef : null, onClick: event => { if (startTime) { Spicetify.Player.seek(startTime); } }, onContextMenu: event => { event.preventDefault(); Spicetify.Platform.ClipboardAPI.copy(rawLyrics) .then(() => Spicetify.showNotification("Lyrics copied to clipboard")) .catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard")); } }, !isKara ? text : react.createElement(KaraokeLine, { text, startTime, position, isActive }) ); }), react.createElement("p", { className: "lyrics-lyricsContainer-LyricsUnsyncedPadding" }), react.createElement(CreditFooter, { provider, copyright }), react.createElement(SearchBar, null) ); }); const UnsyncedLyricsPage = react.memo(({ lyrics, provider, copyright }) => { const rawLyrics = lyrics.map(lyrics => (typeof lyrics.text !== "object" ? lyrics.text : lyrics.text?.props?.children?.[0])).join("\n"); return react.createElement( "div", { className: "lyrics-lyricsContainer-UnsyncedLyricsPage" }, react.createElement("p", { className: "lyrics-lyricsContainer-LyricsUnsyncedPadding" }), lyrics.map(({ text }) => { return react.createElement( "p", { className: "lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active", dir: "auto", onContextMenu: event => { event.preventDefault(); Spicetify.Platform.ClipboardAPI.copy(rawLyrics) .then(() => Spicetify.showNotification("Lyrics copied to clipboard")) .catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard")); } }, text ); }), react.createElement("p", { className: "lyrics-lyricsContainer-LyricsUnsyncedPadding" }), react.createElement(CreditFooter, { provider, copyright }), react.createElement(SearchBar, null) ); }); const noteContainer = document.createElement("div"); noteContainer.classList.add("lyrics-Genius-noteContainer"); const noteDivider = document.createElement("div"); noteDivider.classList.add("lyrics-Genius-divider"); noteDivider.innerHTML = ``; noteDivider.style.setProperty("--link-left", 0); const noteTextContainer = document.createElement("div"); noteTextContainer.classList.add("lyrics-Genius-noteTextContainer"); noteTextContainer.onclick = event => { event.preventDefault(); event.stopPropagation(); }; noteContainer.append(noteDivider, noteTextContainer); function showNote(parent, note) { if (noteContainer.parentElement === parent) { noteContainer.remove(); return; } noteTextContainer.innerText = note; parent.append(noteContainer); const arrowPos = parent.offsetLeft - noteContainer.offsetLeft; noteDivider.style.setProperty("--link-left", `${arrowPos}px`); const box = noteTextContainer.getBoundingClientRect(); if (box.y + box.height > window.innerHeight) { // Wait for noteContainer is mounted setTimeout(() => { noteContainer.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" }); }, 50); } } const GeniusPage = react.memo( ({ lyrics, provider, copyright, versions, versionIndex, onVersionChange, isSplitted, lyrics2, versionIndex2, onVersionChange2 }) => { let notes = {}; let container = null; let container2 = null; // Fetch notes useEffect(() => { if (!container) return; notes = {}; let links = container.querySelectorAll("a"); if (isSplitted && container2) { links = [...links, ...container2.querySelectorAll("a")]; } for (const link of links) { let id = link.pathname.match(/\/(\d+)\//); if (!id) { id = link.dataset.id; } else { id = id[1]; } ProviderGenius.getNote(id).then(note => { notes[id] = note; link.classList.add("fetched"); }); link.onclick = event => { event.preventDefault(); if (!notes[id]) return; showNote(link, notes[id]); }; } }, [lyrics, lyrics2]); const lyricsEl1 = react.createElement( "div", null, react.createElement(VersionSelector, { items: versions, index: versionIndex, callback: onVersionChange }), react.createElement("div", { className: "lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active", ref: c => { container = c; }, dangerouslySetInnerHTML: { __html: lyrics }, onContextMenu: event => { event.preventDefault(); const copylyrics = lyrics.replace(/
/g, "\n").replace(/<[^>]*>/g, ""); Spicetify.Platform.ClipboardAPI.copy(copylyrics) .then(() => Spicetify.showNotification("Lyrics copied to clipboard")) .catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard")); } }) ); const mainContainer = [lyricsEl1]; const shouldSplit = versions.length > 1 && isSplitted; if (shouldSplit) { const lyricsEl2 = react.createElement( "div", null, react.createElement(VersionSelector, { items: versions, index: versionIndex2, callback: onVersionChange2 }), react.createElement("div", { className: "lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active", ref: c => { container2 = c; }, dangerouslySetInnerHTML: { __html: lyrics2 }, onContextMenu: event => { event.preventDefault(); const copylyrics = lyrics.replace(/
/g, "\n").replace(/<[^>]*>/g, ""); Spicetify.Platform.ClipboardAPI.copy(copylyrics) .then(() => Spicetify.showNotification("Lyrics copied to clipboard")) .catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard")); } }) ); mainContainer.push(lyricsEl2); } return react.createElement( "div", { className: "lyrics-lyricsContainer-UnsyncedLyricsPage" }, react.createElement("p", { className: "lyrics-lyricsContainer-LyricsUnsyncedPadding main-type-ballad" }), react.createElement("div", { className: shouldSplit ? "split" : "" }, mainContainer), react.createElement(CreditFooter, { provider, copyright }), react.createElement(SearchBar, null) ); } ); const LoadingIcon = react.createElement( "svg", { width: "200px", height: "200px", viewBox: "0 0 100 100", preserveAspectRatio: "xMidYMid" }, react.createElement( "circle", { cx: "50", cy: "50", r: "0", fill: "none", stroke: "currentColor", "stroke-width": "2" }, react.createElement("animate", { attributeName: "r", repeatCount: "indefinite", dur: "1s", values: "0;40", keyTimes: "0;1", keySplines: "0 0.2 0.8 1", calcMode: "spline", begin: "0s" }), react.createElement("animate", { attributeName: "opacity", repeatCount: "indefinite", dur: "1s", values: "1;0", keyTimes: "0;1", keySplines: "0.2 0 0.8 1", calcMode: "spline", begin: "0s" }) ), react.createElement( "circle", { cx: "50", cy: "50", r: "0", fill: "none", stroke: "currentColor", "stroke-width": "2" }, react.createElement("animate", { attributeName: "r", repeatCount: "indefinite", dur: "1s", values: "0;40", keyTimes: "0;1", keySplines: "0 0.2 0.8 1", calcMode: "spline", begin: "-0.5s" }), react.createElement("animate", { attributeName: "opacity", repeatCount: "indefinite", dur: "1s", values: "1;0", keyTimes: "0;1", keySplines: "0.2 0 0.8 1", calcMode: "spline", begin: "-0.5s" }) ) ); const VersionSelector = react.memo(({ items, index, callback }) => { if (items.length < 2) { return null; } return react.createElement( "div", { className: "lyrics-versionSelector" }, react.createElement( "select", { onChange: event => { callback(items, event.target.value); }, value: index }, items.map((a, i) => { return react.createElement("option", { value: i }, a.title); }) ), react.createElement( "svg", { height: "16", width: "16", fill: "currentColor", viewBox: "0 0 16 16" }, react.createElement("path", { d: "M3 6l5 5.794L13 6z" }) ) ); });