dotfiles/.config/spicetify/CustomApps/lyrics-plus/Pages.js
2024-06-26 12:05:08 +02:00

739 lines
20 KiB
JavaScript
Executable file

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 = `<svg width="32" height="32" viewBox="0 0 13 4" fill="currentColor"><path d=\"M13 10L8 4.206 3 10z\"/></svg>`;
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(/<br>/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(/<br>/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"
})
)
);
});