PTIT Code Helper Lite

Hỗ trợ thực hành PTIT Code

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         PTIT Code Helper Lite
// @version      1.2.0
// @description  Hỗ trợ thực hành PTIT Code
// @author       An Vũ
// @match        *://code.ptit.edu.vn/student/question/*
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @run-at       document-body
// @connect      localhost
// @namespace https://anvu.web.app
// ==/UserScript==

(function () {
    'use strict';
    console.log("Đã nạp PTIT Code Helper. Tải bản extension để trải nghiệm đầy đủ tính năng.");
    const TABLE_REGEX = /(?:\b(?:input|output|in|out|giải thích)\b|\.(?:inp?|out?))/i;
    const NBSP_REGEX = /[\u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000\uFEFF]/g;
    const ZERO_WIDTH_REGEX = /[\u200B-\u200D\uFEFF]/g;
    const SELECTOR_TITLE = '.submit__nav p span a.link--red';
    const SELECTOR_CONTENT = '.submit__des';
    const GITHUB_REPOS = { CPP: 'anvu17/PTIT-C-CPP', C: 'anvu17/PTIT-C-CPP', CTDL: 'anvu17/PTIT-DSA', DSA: 'anvu17/PTIT-DSA' };
    const PROBLEM_ID = location.pathname.split('/').pop();
    const I = {
        copy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
        download: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
        cph: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.15 2.587L18.21.22a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"/></svg>',
        github: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>',
        check: '<svg viewBox="0 0 24 24" fill="none" stroke="#28a745" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>',
        code: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
        paste: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/></svg>',
        upload: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>',
        trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
        loading: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>'
    };

    const CSS = ':root{--pch-red:#b71c1c;--pch-red-hover:#d32f2f;--pch-green:#28a745}' +
        '.pch-title-actions{display:inline-flex;align-items:center;gap:6px;margin-left:12px;vertical-align:middle}' +
        '.pch-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;color:#5f6368;cursor:pointer;border-radius:4px;border:1px solid #dadce0;background:#fff;transition:all .15s ease}' +
        '.pch-icon-btn:hover{color:var(--pch-red);border-color:var(--pch-red);background:#fff5f5}' +
        '.pch-icon-btn svg{width:16px;height:16px;stroke-width:2}' +
        '.pch-copy-btn{position:absolute;top:4px;right:4px;width:24px;height:24px;border-radius:4px;display:flex;align-items:center;justify-content:center;border:none;background:transparent;color:#adb5bd;cursor:pointer;transition:all .2s}' +
        '.pch-copy-btn:hover{color:var(--pch-red);background:rgba(183,28,28,.08)}' +
        '.pch-copy-btn svg{width:14px;height:14px}' +
        '.pch-helper-box{background:#fff;border:1px solid #dcdcdc;border-radius:4px;box-shadow:0 4px 10px rgba(0,0,0,.05);margin:20px 0 25px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;overflow:hidden}' +
        '.pch-box-header{padding:8px 15px;background:#f8f9fa;border-bottom:1px solid #dcdcdc;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}' +
        '.pch-box-title{font-weight:700;color:var(--pch-red);font-size:14px;display:flex;align-items:center;gap:8px;white-space:nowrap;text-decoration:none}' +
        '.pch-toolbar{display:flex;gap:6px;align-items:center;flex-wrap:wrap}' +
        '.pch-btn{display:inline-flex;align-items:center;justify-content:center;font-weight:500;padding:5px 12px;font-size:13px;line-height:1.5;border-radius:4px;transition:all .15s;cursor:pointer;background:#fff;border:1px solid #ced4da;color:#495057;gap:6px;white-space:nowrap;flex-shrink:0}' +
        '.pch-btn svg{width:14px;height:14px;flex-shrink:0}' +
        '.pch-btn:hover{background:#e9ecef;border-color:#adb5bd}' +
        '.pch-btn-red{color:#fff;background:var(--pch-red);border-color:var(--pch-red)}' +
        '.pch-btn-red:hover{background:var(--pch-red-hover);border-color:#a71d2a}' +
        '#pch-ace-editor{width:100%;height:500px;border-bottom:1px solid #dcdcdc;font-size:14px}' +
        '.pch-drag-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(183,28,28,.7);z-index:999999;display:none;align-items:center;justify-content:center;pointer-events:all}' +
        '.pch-drag-overlay.active{display:flex}' +
        '.pch-drag-content{display:flex;flex-direction:column;align-items:center;gap:20px;color:#fff;pointer-events:none}' +
        '.pch-drag-content svg{width:80px;height:80px;stroke-width:1.5;animation:pchBounce 1s ease-in-out infinite}' +
        '.pch-drag-text{font-size:24px;font-weight:600;text-shadow:0 2px 4px rgba(0,0,0,.3)}' +
        '@keyframes pchBounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}';

    const getTextFromElement = el => {
        if (!el) return '';
        return el.cloneNode(true).innerText.replace(NBSP_REGEX, ' ').split('\n').map(l => l.trim()).filter(l => l.length > 0).join('\n');
    };

    const getProblemDescription = () => {
        const c = document.querySelector(SELECTOR_CONTENT);
        if (!c) return '';
        const cl = c.cloneNode(true);
        cl.querySelectorAll('table').forEach(t => {
            if (t.rows && t.rows.length && [...t.rows[0].querySelectorAll('th,td')].some(c => TABLE_REGEX.test(c.innerText))) t.remove();
        });
        cl.querySelectorAll('p,b,strong').forEach(p => { if (p.innerText.trim() === 'Ví dụ:') p.remove(); });
        return cl.innerText.replace(NBSP_REGEX, ' ').replace(ZERO_WIDTH_REGEX, '').split('\n').map(l => l.trimEnd()).filter((l, i, a) => l !== '' || (i > 0 && a[i - 1] !== '')).join('\n').replace(/\n{2,}/g, '\n\n').trim();
    };

    const getProblemInfo = () => {
        const el = document.querySelector(SELECTOR_TITLE);
        const nameRaw = el ? el.textContent : PROBLEM_ID;
        const code = PROBLEM_ID;
        return { code, name: code, nameRaw, url: location.href };
    };

    const getTests = () => [...document.querySelectorAll('table')].reduce((acc, t) => {
        if (!t.rows || !t.rows.length) return acc;
        const cols = [...t.rows[0].querySelectorAll('th,td')].reduce((a, c) => { if (TABLE_REGEX.test(c.innerText)) a.push(c.cellIndex); return a; }, []);
        if (cols.length >= 2) {
            const max = Math.max(...cols.slice(0, 2));
            [...t.querySelectorAll('tr')].slice(1).forEach(r => {
                if (r.cells.length > max && !TABLE_REGEX.test(r.cells[cols[0]].innerText)) {
                    const inp = getTextFromElement(r.cells[cols[0]]), out = getTextFromElement(r.cells[cols[1]]);
                    const exp = cols[2] !== undefined ? getTextFromElement(r.cells[cols[2]]) : '';
                    if (inp || out) acc.push({ input: inp, output: out, explanation: exp });
                }
            });
        }
        return acc;
    }, []);

    const generateProblemText = () => {
        const info = getProblemInfo(), tests = getTests();
        const parts = ['PROBLEM: ' + PROBLEM_ID + ' - ' + info.nameRaw, 'URL: ' + info.url + '\n', 'DESCRIPTION:', getProblemDescription() + '\n'];
        tests.forEach((t, i) => {
            const tp = ['[TEST ' + (i + 1) + ']', '--- Input ---\n' + t.input];
            if (t.output) tp.push('--- Output ---\n' + t.output);
            if (t.explanation) tp.push('--- Explanation ---\n' + t.explanation);
            parts.push(tp.join('\n') + '\n');
        });
        parts.push('Extracted with PTIT Code Helper - https://anvu.web.app/pch');
        return parts.join('\n');
    };

    const submitCode = code => {
        if (!code || !code.trim()) return alert('Code trống!');
        const fi = document.querySelector("input[type='file']"), btn = document.querySelector('.submit__pad__btn');
        if (!fi || !btn) return alert('Không tìm thấy form nộp bài!');
        const dt = new DataTransfer();
        dt.items.add(new File([code], 'pch.cpp', { type: 'text/plain' }));
        fi.files = dt.files;
        fi.dispatchEvent(new Event('change', { bubbles: true }));
        setTimeout(() => btn.click(), 500);
    };

    const fb = btn => { const o = btn.innerHTML; btn.innerHTML = I.check; setTimeout(() => btn.innerHTML = o, 1500); };

    const injectStyles = () => {
        if (document.getElementById('pch-us-style')) return;
        const s = document.createElement('style');
        s.id = 'pch-us-style';
        s.textContent = CSS;
        document.head.appendChild(s);
    };

    const injectTitleButtons = () => {
        const titleEl = document.querySelector(SELECTOR_TITLE);
        if (!titleEl || titleEl.parentNode.querySelector('.pch-title-actions')) return;
        const wrap = document.createElement('span');
        wrap.className = 'pch-title-actions';
        const mk = (icon, title, fn) => { const b = document.createElement('span'); b.className = 'pch-icon-btn'; b.title = title; b.innerHTML = icon; b.onclick = fn; return b; };

        wrap.appendChild(mk(I.copy, 'Sao chép bài tập', function () { GM_setClipboard(generateProblemText()); fb(this); }));

        wrap.appendChild(mk(I.download, 'Tải bài tập (.txt)', function () {
            const blob = new Blob([generateProblemText()], { type: 'text/plain' }), a = document.createElement('a');
            a.href = URL.createObjectURL(blob); a.download = getProblemInfo().code + '.txt'; a.click(); URL.revokeObjectURL(a.href); fb(this);
        }));

        wrap.appendChild(mk(I.cph, 'Nhập vào CPH', function () {
            const self = this, orig = self.innerHTML;
            self.innerHTML = I.loading;
            const tests = getTests(), info = getProblemInfo();
            if (!tests.length) { self.innerHTML = orig; return alert('Không tìm thấy test case!'); }
            const cphName = PROBLEM_ID.length > 10 ? 'CONTEST_' + PROBLEM_ID.slice(0, 3).toUpperCase() : info.name;
            GM_xmlhttpRequest({
                method: 'POST', url: 'http://localhost:27121/',
                headers: { 'Content-Type': 'application/json' },
                data: JSON.stringify({
                    group: 'PTIT Code Helper', interactive: false, memoryLimit: 128, timeLimit: 5000,
                    testType: 'single', input: { type: 'stdin' }, output: { type: 'stdout' },
                    languages: { java: { mainClass: 'Main', taskClass: cphName } },
                    name: cphName, url: info.url, tests: tests
                }),
                onload: res => { if (res.status === 200) { self.innerHTML = I.check; setTimeout(() => self.innerHTML = orig, 1500); } else { self.innerHTML = orig; alert('Không thể kết nối với CPH!'); } },
                onerror: () => { self.innerHTML = orig; alert('Không thể kết nối với CPH!'); }
            });
        }));

        const prefixes = Object.keys(GITHUB_REPOS).sort((a, b) => b.length - a.length);
        const mp = prefixes.find(p => PROBLEM_ID.startsWith(p));
        if (mp) wrap.appendChild(mk(I.github, 'Tìm bài giải mẫu (GitHub)', () => window.open('https://github.com/' + GITHUB_REPOS[mp] + '/search?q=' + PROBLEM_ID, '_blank')));
        titleEl.parentNode.appendChild(wrap);
    };

    const injectCopyButtons = () => {
        document.querySelectorAll('table:not([data-pch])').forEach(t => {
            if (!t.rows || !t.rows.length) return;
            const cols = [];
            t.rows[0].querySelectorAll('th,td').forEach(c => { if (TABLE_REGEX.test(c.innerText)) cols.push(c.cellIndex); });
            if (!cols.length) return;
            t.querySelectorAll('tr').forEach(r => cols.forEach(ci => {
                if (r.cells.length <= ci) return;
                const c = r.cells[ci];
                if (TABLE_REGEX.test(c.innerText) || c.querySelector('.pch-copy-btn')) return;
                c.style.position = 'relative';
                const b = document.createElement('span');
                b.className = 'pch-copy-btn';
                b.title = 'Copy content';
                b.innerHTML = I.copy;
                b.onclick = e => { e.stopPropagation(); e.preventDefault(); GM_setClipboard(getTextFromElement(c)); fb(b); };
                c.appendChild(b);
            }));
            t.dataset.pch = '1';
        });
    };

    let aceEditor = null;
    const getAceMode = text => {
        if (/Java/i.test(text)) return 'java';
        if (/Python/i.test(text)) return 'python';
        if (/Golang/i.test(text)) return 'golang';
        if (/C#/i.test(text)) return 'csharp';
        return 'c_cpp';
    };

    const injectEditor = () => {
        const target = document.querySelector(SELECTOR_CONTENT);
        if (!target || document.querySelector('.pch-helper-box')) return;

        const box = document.createElement('div');
        box.className = 'pch-helper-box';
        const header = document.createElement('div');
        header.className = 'pch-box-header';
        header.innerHTML = '<a href="https://anvu.web.app/pch" target="_blank" class="pch-box-title" style="text-decoration:none;color:#d0011b;font-weight:bold;display:flex;align-items:center;gap:6px">' + I.code + ' PTIT Code Helper</a>';
        const toolbar = document.createElement('div');
        toolbar.className = 'pch-toolbar';

        const langSelect = document.createElement('select');
        langSelect.className = 'pch-btn';
        const siteCompiler = document.querySelector('#compiler');
        if (siteCompiler) {
            [...siteCompiler.options].forEach(o => { const opt = document.createElement('option'); opt.value = o.value; opt.text = o.text; langSelect.add(opt); });
            langSelect.value = siteCompiler.value;
            langSelect.onchange = () => {
                siteCompiler.value = langSelect.value;
                siteCompiler.dispatchEvent(new Event('change'));
                if (aceEditor) aceEditor.session.setMode('ace/mode/' + getAceMode(langSelect.options[langSelect.selectedIndex].text));
            };
        }
        toolbar.appendChild(langSelect);

        const mkTb = (icon, text, fn) => { const b = document.createElement('button'); b.className = 'pch-btn'; b.innerHTML = icon + ' ' + text; b.onclick = fn; return b; };
        toolbar.appendChild(mkTb(I.trash, 'Xóa tất cả', () => { if (aceEditor) aceEditor.setValue(''); }));
        toolbar.appendChild(mkTb(I.paste, 'Nộp code từ bộ nhớ tạm', () => navigator.clipboard.readText().then(submitCode).catch(() => alert('Không thể đọc clipboard!'))));
        const submitBtn = document.createElement('button');
        submitBtn.className = 'pch-btn pch-btn-red';
        submitBtn.textContent = 'Nộp code trong editor';
        submitBtn.onclick = () => { if (aceEditor) submitCode(aceEditor.getValue()); };
        toolbar.appendChild(submitBtn);
        header.appendChild(toolbar);
        box.appendChild(header);

        const editorDiv = document.createElement('div');
        editorDiv.id = 'pch-ace-editor';
        box.appendChild(editorDiv);

        let insertAfter = null;
        const parent = target.parentNode;
        if (parent) {
            const w = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
            let n;
            while (n = w.nextNode()) { if (n.textContent.includes('Giới hạn')) insertAfter = n.parentElement; }
        }
        if (insertAfter && insertAfter.nextSibling) insertAfter.parentNode.insertBefore(box, insertAfter.nextSibling);
        else target.parentNode.insertBefore(box, target.nextSibling);

        const tryAce = () => {
            if (!unsafeWindow.ace) return false;
            aceEditor = unsafeWindow.ace.edit('pch-ace-editor');
            aceEditor.setTheme('ace/theme/textmate');
            aceEditor.setFontSize(14);
            aceEditor.setShowPrintMargin(false);
            if (langSelect.options.length) aceEditor.session.setMode('ace/mode/' + getAceMode(langSelect.options[langSelect.selectedIndex].text));
            return true;
        };
        if (!tryAce()) {
            const obs = new MutationObserver((_, o) => { if (tryAce()) o.disconnect(); });
            obs.observe(document.documentElement, { childList: true, subtree: true });
            setTimeout(() => { obs.disconnect(); tryAce(); }, 10000);
        }

        const overlay = document.createElement('div');
        overlay.className = 'pch-drag-overlay';
        overlay.innerHTML = '<div class="pch-drag-content">' + I.upload + '<div class="pch-drag-text">Thả để nhập vào Code Editor</div></div>';
        overlay.addEventListener('dragover', e => { e.preventDefault(); e.stopPropagation(); });
        overlay.addEventListener('drop', e => {
            e.preventDefault(); e.stopPropagation(); overlay.classList.remove('active');
            if (e.dataTransfer.files.length && aceEditor) { const r = new FileReader(); r.onload = ev => aceEditor.setValue(ev.target.result, -1); r.readAsText(e.dataTransfer.files[0]); }
        });
        document.body.appendChild(overlay);
        let dc = 0;
        document.addEventListener('dragenter', e => { if (e.dataTransfer.types.includes('Files')) { dc++; overlay.classList.add('active'); } });
        document.addEventListener('dragleave', () => { if (--dc === 0) overlay.classList.remove('active'); });
        document.addEventListener('drop', () => { dc = 0; overlay.classList.remove('active'); });
        document.addEventListener('paste', e => {
            if (!aceEditor) return;
            const t = document.querySelector('.ace_text-input');
            if (t && document.activeElement === t) return;
            const items = e.clipboardData?.items;
            if (!items) return;
            for (const i of items) if (i.kind === 'file') { e.preventDefault(); const f = i.getAsFile(); if (f) { const r = new FileReader(); r.onload = ev => aceEditor.setValue(ev.target.result, -1); r.readAsText(f); } break; }
        });
    };

    const init = () => {
        if (document.querySelector('.pch-helper-box')) return;
        if (!document.querySelector(SELECTOR_CONTENT)) return;
        injectStyles();
        injectTitleButtons();
        injectCopyButtons();
        injectEditor();
    };

    const waitAndInit = () => {
        if (init(), document.querySelector('.pch-helper-box')) return;
        const obs = new MutationObserver(() => { if (document.querySelector(SELECTOR_CONTENT)) { obs.disconnect(); init(); } });
        obs.observe(document.body || document.documentElement, { childList: true, subtree: true });
    };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', waitAndInit);
    else waitAndInit();
})();