CodePTIT Copier

Script CodePTIT Copier. Xóa dòng trống thừa và copy nhanh Testcase trên CodePTIT (bản cũ lẫn mới).

Installer ce script?
Script suggéré par l'auteur

Vous aimerez aussi YouTube-YTMusic-Nonstop.

Installer ce script
// ==UserScript==
// @name         CodePTIT Copier
// @namespace    https://github.com/nvbangg/CodePTIT_Copier
// @version      1.2
// @description  Script CodePTIT Copier. Xóa dòng trống thừa và copy nhanh Testcase trên CodePTIT (bản cũ lẫn mới).
// @author       nvbangg (https://github.com/nvbangg)
// @copyright    Copyright (c) 2025 Nguyễn Văn Bằng (nvbangg, github.com/nvbangg)
// @homepage     https://github.com/nvbangg/CodePTIT_Copier
// @match        https://code.ptit.edu.vn/student/question*
// @match        https://code.ptit.edu.vn/beta*
// @icon         https://code.ptit.edu.vn/favicon.ico
// @grant        GM_setClipboard
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT
// ==/UserScript==

//! HÃY XEM HƯỚNG DẪN TẠI: https://github.com/nvbangg/CodePTIT_Copier

(() => {
  "use strict";

  // Settings mặc định
  const DEFAULT_SETTINGS = {
    fileExtension: ".cpp",
    removeAccents: true,
    textCase: "titleCase",
    separator: "noSpaces",
  };
  const settings = { ...DEFAULT_SETTINGS, ...GM_getValue("settings", {}) };
  const ICONS = {
    copy: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1-2 2v1"/></svg>',
    check:
      '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',
    rowCopy:
      '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1-2 2v1"/><path d="M2 2h5v2"/></svg>',
  };

  const FORMATTERS = {
    case: {
      titleCase: (str) =>
        str.replace(
          /\S+/g,
          (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
        ),
      uppercase: (str) => str.toUpperCase(),
      lowercase: (str) => str.toLowerCase(),
      keepOriginal: (str) => str,
    },
    separator: { keepOriginal: " ", underscore: "_", dash: "-", noSpaces: "" },
  };
  const OPTIONS = {
    textCase: {
      titleCase: "In Hoa Đầu Từ",
      uppercase: "IN HOA",
      lowercase: "in thường",
      keepOriginal: "Giữ nguyên",
    },
    separator: {
      noSpaces: "Xóa khoảng cách",
      underscore: "Gạch dưới (_)",
      dash: "Gạch ngang (-)",
      keepOriginal: "Giữ nguyên",
    },
  };

  // CSS tối ưu - gộp các class chung
  GM_addStyle(`
    .copy-btn,.title-copy-btn,.row-copy-btn{background:rgba(30,144,255,.5);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:3px;padding:2px;position:relative;outline:none!important;user-select:none}
    .copy-btn{position:absolute;top:0;right:0}
    .title-copy-btn{margin-right:5px;vertical-align:middle}
    .row-copy-btn{position:absolute;left:-23px;top:0;background:rgba(255,165,0,.7);z-index:100}
    .row-copy-tooltip{position:absolute;bottom:100%;left:0;margin-bottom:8px;background:rgba(0,0,0,.8);color:white;padding:4px 6px;font-size:11px;white-space:nowrap;display:none;z-index:1000}
    .row-copy-btn.show-tooltip .row-copy-tooltip{display:block}
    .copied{background:rgba(50,205,50,1)!important}
    .settings-overlay{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center}
    .settings-modal{background:#fff;border-radius:8px;width:300px;padding:15px;max-width:90vw;position:relative}
    .settings-modal h3{margin:0 0 10px;text-align:center}
    .settings-group{margin-bottom:10px}.settings-group label{display:block;margin-bottom:4px}
    .settings-input,.settings-select{width:100%;padding:5px;border:1px solid #ddd;border-radius:4px}
    .settings-buttons{display:flex;justify-content:center;margin-top:15px;gap:10px}
    .settings-btn-save,.settings-btn-reset{padding:6px 15px;border:none;border-radius:4px;cursor:pointer}
    .settings-btn-save{background:#1890ff;color:#fff}.settings-btn-reset{background:#d8d8d8;color:#333}
    .settings-footer{border-top:1px solid #eee;margin-top:15px;padding-top:10px;text-align:center}
    .settings-footer a{color:#1890ff!important}
    .settings-close{position:absolute;top:15px;right:15px;width:25px;height:25px;cursor:pointer;text-align:center;line-height:25px;font-size:25px;font-weight:bold;color:#333}
  `);

  const $ = (s) => document.querySelector(s),
    $$ = (s) => [...document.querySelectorAll(s)];
  const isBeta = () => location.pathname.includes("/beta");
  const isValidTestCase = (el) =>
    el?.textContent?.trim() &&
    !el.querySelector(".copy-btn") &&
    !el.closest("table");
  const debounce = (fn, delay = 300) => {
    let timer;
    return (...args) => (
      clearTimeout(timer), (timer = setTimeout(() => fn(...args), delay))
    );
  };

  const showCopyEffect = (button, duration = 800) => {
    const originalContent = button.innerHTML;
    button.innerHTML = ICONS.check;
    button.classList.add("copied");
    setTimeout(
      () => (
        (button.innerHTML = originalContent), button.classList.remove("copied")
      ),
      duration
    );
  };

  const addTooltipEvents = (btn) => {
    let timer,
      isHovering = false;
    btn.addEventListener(
      "mouseenter",
      () => (
        (isHovering = true),
        btn.classList.remove("show-tooltip"),
        (timer = setTimeout(
          () => isHovering && btn.classList.add("show-tooltip"),
          1000
        ))
      )
    );
    btn.addEventListener(
      "mouseleave",
      () => (
        (isHovering = false),
        clearTimeout(timer),
        btn.classList.remove("show-tooltip")
      )
    );
  };

  const copyToClipboard = (text, button) => {
    try {
      GM_setClipboard(text, "text");
      showCopyEffect(button);
      return true;
    } catch (e) {
      console.error("Copy failed:", e);
      return false;
    }
  };

  const copyRowContent = (row, button) => {
    const cells = row.querySelectorAll("td");
    if (cells.length < 2) return false;
    const secondCellContent = getTestcaseContent(cells[1]),
      firstCellContent = getTestcaseContent(cells[0]);
    if (!secondCellContent.trim() && !firstCellContent.trim()) return false;
    secondCellContent.trim() && GM_setClipboard(secondCellContent, "text");
    setTimeout(
      () =>
        firstCellContent.trim() && GM_setClipboard(firstCellContent, "text"),
      300
    );
    showCopyEffect(button, 1000);
    return true;
  };
  const formatTitle = (title) => {
    if (!title) return "";
    const normalized = settings.removeAccents
      ? title
          .normalize("NFD")
          .replace(/[\u0300-\u036f]|[đĐ]/g, (m) =>
            m === "đ" ? "d" : m === "Đ" ? "D" : ""
          )
      : title.normalize("NFC");
    return (FORMATTERS.case[settings.textCase] || FORMATTERS.case.keepOriginal)(
      normalized
    ).replace(/\s+/g, FORMATTERS.separator[settings.separator] || "");
  };
  const getTestcaseContent = (cell) =>
    (cell.querySelector("code, pre")?.innerText || cell.innerText || "")
      .replace(
        /[\u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000\uFEFF]/g,
        " "
      )
      .trimEnd();

  // Gộp các hàm tạo button thành 1 hàm duy nhất
  const createButton = (type, onClick, extraContent = "") => {
    const btn = document.createElement("button");
    btn.className =
      type === "title"
        ? "title-copy-btn"
        : type === "row"
        ? "row-copy-btn"
        : "copy-btn";
    btn.innerHTML = type === "row" ? ICONS.rowCopy + extraContent : ICONS.copy;
    if (type === "row") addTooltipEvents(btn);
    btn.addEventListener("click", onClick);
    return btn;
  };

  const addCopyButton = (cell) => {
    if (!cell?.textContent?.trim() || cell.dataset.copyAdded) return;
    window.getComputedStyle(cell).position === "static" &&
      (cell.style.position = "relative");
    cell.dataset.copyAdded = "true";
    cell.appendChild(
      createButton("copy", (e) => {
        e.preventDefault();
        e.stopPropagation();
        const content = getTestcaseContent(cell);
        content.trim() && copyToClipboard(content, e.currentTarget);
      })
    );
  };
  const addTitleCopyButton = (titleEl) => {
    if (!titleEl || titleEl.dataset.copyAdded) return;
    titleEl.dataset.copyAdded = "true";
    const btn = createButton("title", (e) => {
      e.preventDefault();
      e.stopPropagation();
      const { code, title } = getProblemInfo();
      (code || title) &&
        copyToClipboard(
          `${code.trim()}_${formatTitle(title)}${settings.fileExtension}`,
          e.currentTarget
        );
    });
    Object.assign(btn.style, {
      marginRight: "8px",
      verticalAlign: "middle",
      display: "inline-flex",
    });
    titleEl.insertBefore(btn, titleEl.firstChild);
  };

  const addRowCopyButton = (row) => {
    if (!row || row.dataset.rowCopyAdded) return;
    const cells = row.querySelectorAll("td");
    if (cells.length < 2) return;
    const firstCell = cells[0];
    if (!firstCell?.textContent?.trim()) return;
    window.getComputedStyle(firstCell).position === "static" &&
      (firstCell.style.position = "relative");
    row.dataset.rowCopyAdded = "true";
    firstCell.appendChild(
      createButton(
        "row",
        (e) => {
          e.preventDefault();
          e.stopPropagation();
          copyRowContent(row, e.currentTarget);
        },
        '<div class="row-copy-tooltip">Copy input và output để Paste nhanh bằng KeyClipboard</div>'
      )
    );
  };

  const getProblemInfo = () => {
    if (isBeta()) {
      const problemCode = location.pathname.includes("/beta/problems/")
        ? location.pathname.split("/").pop().toUpperCase()
        : "";
      const element = $("h1") || $("h2");
      return { code: problemCode, title: element?.textContent.trim() || "" };
    }
    const titleElement = $(".submit__nav p span a.link--red");
    return titleElement
      ? {
          code: titleElement.href.match(/\/([^\/]+)$/)?.[1] || "",
          title: titleElement.textContent.trim(),
        }
      : { code: "", title: "" };
  };

  // Chuyển đổi các thẻ p thành div trong bảng tbody
  const convertParagraphsToDiv = () => {
    const table = document.querySelector("tbody");
    if (!table) return;
    Array.from(table.getElementsByTagName("p")).forEach(
      (p) => (p.outerHTML = `<div>${p.innerHTML}</div>`)
    );
  };

  const showSettingsModal = () => {
    $(".settings-overlay")?.remove();
    const overlay = document.createElement("div");
    overlay.className = "settings-overlay";
    const createOptions = (optionsObj, selected) =>
      Object.entries(optionsObj)
        .map(
          ([value, text]) =>
            `<option value="${value}" ${
              selected === value ? "selected" : ""
            }>${text}</option>`
        )
        .join("");

    overlay.innerHTML = `<div class="settings-modal">
      <span class="settings-close">×</span>
      <h3>⚙️ Settings</h3>
      <div class="settings-group"><label>Đuôi file: <input type="text" id="fileExtension" class="settings-input" value="${
        settings.fileExtension
      }"></label></div>
      <div class="settings-group"><label><input type="checkbox" id="removeAccents" ${
        settings.removeAccents ? "checked" : ""
      }> Xóa dấu tiếng Việt</label></div>
      <div class="settings-group"><label>Kiểu chữ: <select id="textCase" class="settings-select">${createOptions(
        OPTIONS.textCase,
        settings.textCase
      )}</select></label></div>
      <div class="settings-group"><label>Khoảng cách: <select id="separator" class="settings-select">${createOptions(
        OPTIONS.separator,
        settings.separator
      )}</select></label></div>
      <div class="settings-buttons"><button class="settings-btn-reset">Reset</button><button class="settings-btn-save">Lưu</button></div>
      <div class="settings-footer">CodePTIT Copier v1.2 <a href="https://github.com/nvbangg/CodePTIT_Copier" target="_blank">github.com/nvbangg/CodePTIT_Copier</a></div>
    </div>`;

    document.body.appendChild(overlay);
    const closeModal = () => overlay.remove();
    $(".settings-btn-save", overlay).addEventListener("click", () => {
      Object.assign(settings, {
        fileExtension: $("#fileExtension", overlay).value,
        removeAccents: $("#removeAccents", overlay).checked,
        textCase: $("#textCase", overlay).value,
        separator: $("#separator", overlay).value,
      });
      GM_setValue("settings", settings);
      closeModal();
    });
    $(".settings-btn-reset", overlay).addEventListener("click", () => {
      $("#fileExtension", overlay).value = DEFAULT_SETTINGS.fileExtension;
      $("#removeAccents", overlay).checked = DEFAULT_SETTINGS.removeAccents;
      $("#textCase", overlay).value = DEFAULT_SETTINGS.textCase;
      $("#separator", overlay).value = DEFAULT_SETTINGS.separator;
    });
    $(".settings-close", overlay).addEventListener("click", closeModal);
    overlay.addEventListener(
      "click",
      (e) => e.target === overlay && closeModal()
    );
  };

  const processLegacyPage = () => {
    const titleElement = $(".submit__nav p span a.link--red");
    if (titleElement) addTitleCopyButton(titleElement);
    $$(".submit__des tr:not(:first-child)").forEach((row) => {
      row.querySelectorAll("td").forEach(addCopyButton);
      addRowCopyButton(row);
    });
    $$(".submit__des [class*='testcase']")
      .filter(isValidTestCase)
      .forEach(addCopyButton);
  };
  const processBetaPage = () => {
    if (!/\/beta\/problems\/[A-Za-z0-9_]+/.test(location.pathname)) return;
    $$("table:not(.ant-table-fixed)").forEach((table) => {
      if (table?.querySelectorAll("tr").length > 1) {
        table.querySelectorAll("tr:not(:first-child)").forEach((row) => {
          row
            .querySelectorAll("td")
            .forEach(
              (cell) =>
                cell?.textContent?.trim() &&
                !cell.querySelector(".copy-btn") &&
                addCopyButton(cell)
            );
          addRowCopyButton(row);
        });
      }
    });
    $$("[class*='testcase']").filter(isValidTestCase).forEach(addCopyButton);
    const titleElement = $$("h1, h2").find(
      (el) =>
        el?.textContent?.trim() &&
        !el.parentElement?.querySelector(".title-copy-btn")
    );
    titleElement && addTitleCopyButton(titleElement);
  };
  const cleanupButtons = () => (
    $$(".copy-btn, .title-copy-btn, .row-copy-btn").forEach((btn) =>
      btn.remove()
    ),
    $$("[data-copy-added], [data-row-copy-added]").forEach((el) =>
      el.removeAttribute(
        el.dataset.copyAdded ? "data-copy-added" : "data-row-copy-added"
      )
    )
  );
  const processPage = () => (
    cleanupButtons(),
    isBeta() ? processBetaPage() : processLegacyPage(),
    convertParagraphsToDiv()
  );

  // Khởi tạo
  (() => {
    GM_registerMenuCommand("Settings", showSettingsModal);
    const observer = new MutationObserver(
      debounce(() => {
        if (observer.lastUrl !== location.href)
          return (observer.lastUrl = location.href), processPage();
        (isBeta() ? processBetaPage : processLegacyPage)();
        convertParagraphsToDiv();
      }, 300)
    );
    observer.lastUrl = location.href;
    const startObserver = () => (
      observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
        attributes: false,
      }),
      processPage()
    );
    document.readyState !== "loading"
      ? startObserver()
      : document.addEventListener("DOMContentLoaded", startObserver);
  })();
})();