EZLinkZ

Copy hrefs from selected links with toast at top; toast clickable to show copied links; user can set keyboard shortcut via menu command

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         EZLinkZ
// @namespace    https://github.com/codeMonkeyHopeful/EZLinkZ
// @homepage     https://github.com/codeMonkeyHopeful/EZLinkZ
// @version      1.4
// @description  Copy hrefs from selected links with toast at top; toast clickable to show copied links; user can set keyboard shortcut via menu command
// @author       CodeMonkeyHopeful
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @supportURL   https://github.com/codeMonkeyHopeful/EZLinkZ
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // Default shortcut keys (Ctrl+Shift+C)
  let shortcut = {
    ctrlKey: true,
    shiftKey: true,
    altKey: false,
    metaKey: false,
    key: "c",
  };

  // Load saved shortcut from storage
  async function loadShortcut() {
    const saved = await GM_getValue("shortcut");
    if (saved) {
      try {
        const parsed = JSON.parse(saved);
        if (parsed.key) shortcut = parsed;
      } catch {}
    }
  }

  // Save shortcut to storage
  async function saveShortcut(newShortcut) {
    await GM_setValue("shortcut", JSON.stringify(newShortcut));
    shortcut = newShortcut;
    showToast(`Shortcut updated to ${formatShortcut(newShortcut)}`);
  }

  function formatShortcut(sc) {
    let parts = [];
    if (sc.ctrlKey) parts.push("Ctrl");
    if (sc.shiftKey) parts.push("Shift");
    if (sc.altKey) parts.push("Alt");
    if (sc.metaKey) parts.push("Meta");
    parts.push(sc.key.toUpperCase());
    return parts.join("+");
  }

  // Prompt user to enter new shortcut (simple example, expects something like "Ctrl+Shift+X")
  async function promptShortcut() {
    const input = prompt(
      `Enter new shortcut keys separated by +\nExample: Ctrl+Shift+X\nCurrent: ${formatShortcut(
        shortcut
      )}`
    );
    if (!input) return;

    // Parse input
    const parts = input
      .toLowerCase()
      .split("+")
      .map((s) => s.trim());
    const newSc = {
      ctrlKey: parts.includes("ctrl"),
      shiftKey: parts.includes("shift"),
      altKey: parts.includes("alt"),
      metaKey: parts.includes("meta"),
      key:
        parts.find((k) => !["ctrl", "shift", "alt", "meta"].includes(k)) || "c",
    };

    await saveShortcut(newSc);
  }

  // Toast and popup elements
  const toast = document.createElement("div");
  const popup = document.createElement("div");
  const closeBtn = document.createElement("button");
  let popupVisible = false;

  // Style toast (top center)
  Object.assign(toast.style, {
    position: "fixed",
    top: "20px",
    left: "50%",
    transform: "translateX(-50%)",
    backgroundColor: "rgba(60,60,60,0.9)",
    color: "#fff",
    padding: "10px 20px",
    borderRadius: "5px",
    fontSize: "14px",
    fontFamily: "sans-serif",
    zIndex: 9999,
    opacity: "0",
    transition: "opacity 0.3s ease",
    pointerEvents: "auto",
    cursor: "pointer",
    userSelect: "none",
    maxWidth: "80vw",
    boxSizing: "border-box",
  });
  document.body.appendChild(toast);

  // Style popup (hidden by default)
  Object.assign(popup.style, {
    position: "fixed",
    top: "60px",
    left: "50%",
    transform: "translateX(-50%)",
    backgroundColor: "rgba(30,30,30,0.95)",
    color: "#fff",
    padding: "15px 20px 20px 20px",
    borderRadius: "8px",
    fontSize: "13px",
    fontFamily: "monospace",
    zIndex: 10000,
    maxHeight: "300px",
    maxWidth: "90vw",
    overflowY: "auto",
    boxSizing: "border-box",
    display: "none",
    whiteSpace: "pre-wrap",
    wordBreak: "break-word",
  });
  document.body.appendChild(popup);

  // Close button inside popup
  closeBtn.textContent = "×";
  Object.assign(closeBtn.style, {
    position: "absolute",
    top: "5px",
    right: "10px",
    background: "transparent",
    border: "none",
    color: "#fff",
    fontSize: "20px",
    cursor: "pointer",
    userSelect: "none",
  });
  popup.appendChild(closeBtn);

  // Show toast message for duration (ms)
  let toastTimeout;
  function showToast(message, duration = 2500) {
    if (popupVisible) return; // don't show toast if popup open
    toast.textContent = message;
    toast.style.opacity = "1";
    clearTimeout(toastTimeout);
    toastTimeout = setTimeout(() => {
      toast.style.opacity = "0";
    }, duration);
  }

  // Show popup with links text
  function showPopup(text) {
    popup.textContent = text;
    popup.appendChild(closeBtn);
    popup.style.display = "block";
    popupVisible = true;
    toast.style.opacity = "0";
  }

  // Hide popup
  function hidePopup() {
    popup.style.display = "none";
    popupVisible = false;
  }

  // Get links from selection
  function getSelectedLinks() {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
      return null;
    }
    const range = selection.getRangeAt(0);
    const container = document.createElement("div");
    container.appendChild(range.cloneContents());

    const links = container.querySelectorAll("a");
    const hrefs = Array.from(links)
      .map((a) => a.href)
      .filter((href) => href);

    return hrefs.length > 0 ? hrefs : null;
  }

  // Copy selected links and handle UI
  function copySelectedLinks() {
    const hrefs = getSelectedLinks();
    if (!hrefs) {
      showToast("No links found in selection");
      return;
    }

    const textToCopy = hrefs.join("\n");
    navigator.clipboard
      .writeText(textToCopy)
      .then(() => {
        showToast(`Copied ${hrefs.length} link(s) to clipboard`);
        // When toast clicked, show popup with links
        toast.onclick = () => showPopup(textToCopy);
      })
      .catch(() => showToast("Failed to copy to clipboard"));
  }

  // Hide popup when clicking outside it
  document.addEventListener("click", (e) => {
    if (!popupVisible) return;
    if (popup.contains(e.target) || toast.contains(e.target)) return;
    hidePopup();
  });

  // Close button click
  closeBtn.addEventListener("click", () => {
    hidePopup();
  });

  // Keyboard shortcut listener
  document.addEventListener("keydown", (e) => {
    const keyMatches =
      e.key.toLowerCase() === shortcut.key.toLowerCase() &&
      e.ctrlKey === !!shortcut.ctrlKey &&
      e.shiftKey === !!shortcut.shiftKey &&
      e.altKey === !!shortcut.altKey &&
      e.metaKey === !!shortcut.metaKey;

    if (keyMatches) {
      e.preventDefault();
      copySelectedLinks();
    }
  });

  // Register Tampermonkey menu command to set shortcut
  GM_registerMenuCommand("Set Keyboard Shortcut", promptShortcut);

  // Load shortcut from storage on start
  loadShortcut();
})();