Files
TidaLuna-Plugins/plugins/copy-lyrics-luna/src/index.ts
T
2025-06-02 11:26:43 +10:00

128 lines
4.3 KiB
TypeScript

import { LunaUnload, Tracer } from "@luna/core";
// Import CSS directly using Luna's file:// syntax
import unlockSelection from "file://styles.css?minify";
export const { trace } = Tracer("[Copy Lyrics]");
// clean up resources
export const unloads = new Set<LunaUnload>();
function ApplyCSS(style: string): HTMLStyleElement {
const styleElement = document.createElement("style");
styleElement.type = "text/css";
styleElement.textContent = style;
document.head.appendChild(styleElement);
return styleElement;
}
function SetClipboard(text: string): void {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed"; // Avoid scrolling to bottom
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand("copy");
if (!success) throw new Error("Failed to copy text.");
} catch (err) {
trace.msg.err(err instanceof Error ? err.message : String(err));
} finally {
document.body.removeChild(textarea);
}
}
const styleElement = ApplyCSS(unlockSelection);
let isSelecting = false;
const onMouseDown = function (): void {
isSelecting = true;
};
const onMouseUp = function (event: MouseEvent): void {
if (isSelecting) {
const selection = window.getSelection();
if (selection && selection.toString().length > 0) {
const selectedSpans: HTMLSpanElement[] = [];
const range = selection.getRangeAt(0);
let container = range.commonAncestorContainer;
// If the container is NOT an element and a document, adjust it.
if (
container.nodeType !== Node.ELEMENT_NODE &&
container.nodeType !== Node.DOCUMENT_NODE
) {
// Get the parent element if it's a text node
const parentElement = container.parentElement;
if (parentElement && parentElement.hasAttribute("data-current")) {
let text_ = selection.toString().trim();
SetClipboard(text_);
trace.msg.log("Copied to clipboard!");
return;
}
}
// Get all the spans inside the container.
const spans = (container as Element).getElementsByTagName("span");
for (let span of spans) {
if (selection.containsNode(span, true)) {
selectedSpans.push(span as HTMLSpanElement);
}
}
// Concat the text of the selected spans.
let hasCorrectAttribute = false;
let text = "";
selectedSpans.forEach((span) => {
if (span.hasAttribute("data-current")) {
hasCorrectAttribute = true;
text += span.textContent + "\n";
if ([...span.classList].some((className) => className.startsWith("endOfStanza--"))) {
text += "\n";
}
}
});
text = text.trim();
if (hasCorrectAttribute) {
SetClipboard(text);
trace.msg.log("Copied to clipboard!");
selection.removeAllRanges();
}
}
isSelecting = false;
}
};
const onClickHooked = function (event: MouseEvent): boolean | void {
if (!isSelecting) return;
const target = event.target as HTMLElement;
if (target.tagName.toLowerCase() === "span" && target.hasAttribute("data-current")) {
// Prevent default behavior and stop event propagation
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
};
// Add event listener with capture phase to intercept events before they reach other handlers
document.addEventListener("click", onClickHooked, true);
document.addEventListener("mousedown", onMouseDown);
document.addEventListener("mouseup", onMouseUp);
// Add cleanup to unloads
unloads.add(() => {
if (styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
// Remove event listeners
document.removeEventListener("click", onClickHooked, true);
document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mouseup", onMouseUp);
});