/ plugins / copy-lyrics-luna / src / index.ts
index.ts
  1  import { LunaUnload, Tracer } from "@luna/core";
  2  import { StyleTag } from "@luna/lib";
  3  
  4  // Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3
  5  import unlockSelection from "file://styles.css?minify";
  6  
  7  export const { trace } = Tracer("[Copy Lyrics]");
  8  
  9  // clean up resources
 10  export const unloads = new Set<LunaUnload>();
 11  
 12  // StyleTag for lyrics selection styling
 13  const lyricsStyleTag = new StyleTag("Copy-Lyrics", unloads, unlockSelection);
 14  
 15  function SetClipboard(text: string): void {
 16      const textarea = document.createElement("textarea");
 17      textarea.value = text;
 18      textarea.style.position = "fixed"; // Avoid scrolling to bottom
 19      document.body.appendChild(textarea);
 20      textarea.select();
 21  
 22      try {
 23          const success = document.execCommand("copy");
 24          if (!success) throw new Error("Failed to copy text.");
 25      } catch (err) {
 26          trace.msg.err(err instanceof Error ? err.message : String(err));
 27      } finally {
 28          document.body.removeChild(textarea);
 29      }
 30  }
 31  
 32  let isSelecting = false;
 33  
 34  const onMouseDown = function (): void {
 35      isSelecting = true;
 36  };
 37  
 38  const onMouseUp = function (event: MouseEvent): void {
 39      if (isSelecting) {
 40          const selection = window.getSelection();
 41          if (selection && selection.toString().length > 0) {
 42              const selectedSpans: HTMLSpanElement[] = [];
 43              const range = selection.getRangeAt(0);
 44              let container = range.commonAncestorContainer;
 45              
 46              // If the container is NOT an element and a document, adjust it.
 47              if (
 48                  container.nodeType !== Node.ELEMENT_NODE &&
 49                  container.nodeType !== Node.DOCUMENT_NODE
 50              ) {
 51                  // Get the parent element if it's a text node
 52                  const parentElement = container.parentElement;
 53                  if (parentElement && parentElement.hasAttribute("data-current")) {
 54                      let text_ = selection.toString().trim();
 55                      SetClipboard(text_);
 56                      trace.msg.log("Copied to clipboard!");
 57                      return;
 58                  }
 59              }
 60  
 61              // Get all the spans inside the container.
 62              const spans = (container as Element).getElementsByTagName("span");
 63              for (let span of spans) {
 64                  if (selection.containsNode(span, true)) {
 65                      selectedSpans.push(span as HTMLSpanElement);
 66                  }
 67              }
 68  
 69              // Concat the text of the selected spans.
 70              let hasCorrectAttribute = false;
 71              let text = "";
 72              selectedSpans.forEach((span) => {
 73                  if (span.hasAttribute("data-current")) {
 74                      hasCorrectAttribute = true;
 75                      text += span.textContent + "\n";
 76                      if ([...span.classList].some((className) => className.startsWith("endOfStanza--"))) {
 77                          text += "\n";
 78                      }
 79                  }
 80              });
 81  
 82              text = text.trim();
 83  
 84              if (hasCorrectAttribute) {
 85                  SetClipboard(text);
 86                  trace.msg.log("Copied to clipboard!");
 87                  selection.removeAllRanges();
 88              }
 89          }
 90          isSelecting = false;
 91      }
 92  };
 93  
 94  const onClickHooked = function (event: MouseEvent): boolean | void {
 95      if (!isSelecting) return;
 96  
 97      const target = event.target as HTMLElement;
 98      if (target.tagName.toLowerCase() === "span" && target.hasAttribute("data-current")) {
 99          // Prevent default behavior and stop event propagation
100          event.preventDefault();
101          event.stopPropagation();
102          event.stopImmediatePropagation();
103          return false;
104      }
105  };
106  
107  // Add event listener with capture phase to intercept events before they reach other handlers
108  document.addEventListener("click", onClickHooked, true);
109  document.addEventListener("mousedown", onMouseDown);
110  document.addEventListener("mouseup", onMouseUp);
111  
112  // Add cleanup to unloads
113  unloads.add(() => {
114      // Remove event listeners
115      document.removeEventListener("click", onClickHooked, true);
116      document.removeEventListener("mousedown", onMouseDown);
117      document.removeEventListener("mouseup", onMouseUp);
118  });