Turnitin Auto Submit (Sequential + Limit 3 + Reset Data)

Chọn file -> Chọn Student lần lượt (max 3) -> Có nút Reset về 0

当前为 2025-12-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Turnitin Auto Submit (Sequential + Limit 3 + Reset Data)
// @namespace    http://tampermonkey.net/
// @version      4.4
// @description  Chọn file -> Chọn Student lần lượt (max 3) -> Có nút Reset về 0
// @author       You
// @match        https://www.turnitin.com/t_submit.asp*
// @match        https://*.turnitin.com/*submit*
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // ===================== CSS =====================
    const css = `
        #turnitin-auto-panel { position: fixed; bottom: 20px; right: 20px; z-index: 9999999; font-family: sans-serif; }
        #turnitin-toggle-btn { width: 50px; height: 50px; border-radius: 50%; background: #2980b9; color: white; border: none; font-size: 24px; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.3); display:none; }

        #turnitin-main-box { display: none; position: absolute; bottom: 0; right: 0; width: 380px; background: white; border-radius: 8px; box-shadow: 0 5px 30px rgba(0,0,0,0.4); overflow: hidden; border: 1px solid #ddd; }
        #turnitin-main-box.active { display: block; }

        .t-header { background: #2980b9; color: white; padding: 12px 15px; font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
        .t-body { padding: 15px; max-height: 500px; overflow-y: auto; }

        .t-btn { width: 100%; padding: 12px; border: none; border-radius: 5px; cursor: pointer; margin-bottom: 8px; font-weight: bold; font-size: 14px; transition: background 0.2s; text-transform: uppercase; color: white; }
        .btn-select { background: #e67e22; box-shadow: 0 3px 0 #d35400; }
        .btn-select:active { box-shadow: none; transform: translateY(3px); }

        .btn-clear { background: #95a5a6; font-size: 12px; padding: 10px; }
        .btn-reset { background: #c0392b; font-size: 12px; padding: 10px; }

        .t-file-list { border: 1px solid #eee; padding: 5px; margin-bottom: 10px; border-radius: 4px; max-height: 150px; overflow-y: auto; background: #f9f9f9; }
        .t-file-item { font-size: 12px; padding: 8px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
        .t-current { background-color: #d5f5e3; font-weight:bold; color: #27ae60; border-left: 4px solid #27ae60; }

        .t-log-box { font-size: 11px; background: #34495e; color: #ecf0f1; padding: 8px; border-radius: 4px; height: 120px; overflow-y: auto; font-family: monospace; margin-top:5px; border: 1px solid #2c3e50; }
        .t-status-indicator { text-align:center; font-size: 13px; font-weight: bold; color: #7f8c8d; margin-bottom: 10px; }
        .t-running { color: #e74c3c; animation: blink 1s infinite; }

        .t-row-btn { display: flex; gap: 5px; }

        @keyframes blink { 50% { opacity: 0.5; } }
    `;
    GM_addStyle(css);

    // ===================== DATA =====================
    const firstNames = ['Nguyen', 'Tran', 'Le', 'Pham', 'Hoang', 'Huynh', 'Phan', 'Vu', 'Vo', 'Dang'];
    const lastNames = ['Van A', 'Thi B', 'Minh C', 'Duc D', 'Anh E', 'Tuan F', 'Hung G', 'Linh H'];
    let selectedFiles = [];
    let isProcessing = false;

    // ===================== HELPERS =====================
    function log(msg) {
        const box = document.getElementById('t-log-box');
        if (box) {
            const time = new Date().toLocaleTimeString();
            box.innerHTML += `<div>[${time}] ${msg}</div>`;
            box.scrollTop = box.scrollHeight;
        }
        console.log(`[AutoTurnitin] ${msg}`);
    }

    function setNativeValue(element, value) {
        if (!element) return;
        const prototype = Object.getPrototypeOf(element);
        const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
        if (prototypeValueSetter) prototypeValueSetter.call(element, value);
        else element.value = value;
        element.dispatchEvent(new Event('input', { bubbles: true }));
        element.dispatchEvent(new Event('change', { bubbles: true }));
        element.dispatchEvent(new Event('blur', { bubbles: true }));
    }

    function waitForElement(selector, timeout = 60000) {
        return new Promise((resolve, reject) => {
            const intervalTime = 500;
            let elapsed = 0;
            const check = setInterval(() => {
                const el = document.querySelector(selector);
                if (el && el.offsetParent !== null && !el.disabled) {
                    clearInterval(check);
                    resolve(el);
                }
                elapsed += intervalTime;
                if (elapsed >= timeout) {
                    clearInterval(check);
                    reject(new Error(`Timeout: ${selector}`));
                }
            }, intervalTime);
        });
    }

    function updateStatus(msg, isRunning = false) {
        const el = document.getElementById('t-status-text');
        if (el) {
            el.innerText = msg;
            el.className = `t-status-indicator ${isRunning ? 't-running' : ''}`;
        }
    }

    // ===================== LOGIC USER SELECT (SEQUENTIAL) =====================
    async function selectStudentLogic() {
        const select = document.getElementById('userID');
        if (!select) {
            log('⚠️ Không tìm thấy select box UserID. Bỏ qua chọn student.');
            return false;
        }

        const options = Array.from(select.options).filter(opt => opt.value !== "");

        if (options.length === 0) {
            log('⚠️ Danh sách Student rỗng.');
            return false;
        }

        // --- LOAD DATA ---
        let usageData = JSON.parse(localStorage.getItem('turnitin_student_usage') || '{}');
        let lastIndex = parseInt(localStorage.getItem('turnitin_last_index') || '-1');

        const getEmail = (text) => {
            const match = text.match(/\(([^)]+)\)$/);
            return match ? match[1] : text;
        };

        let foundOption = null;
        let newIndex = -1;

        // --- THUẬT TOÁN TÌM TUẦN TỰ ---
        for (let i = 1; i <= options.length; i++) {
            let currentIndex = (lastIndex + i) % options.length;
            let opt = options[currentIndex];
            let email = getEmail(opt.text);
            let usedCount = usageData[email] || 0;

            if (usedCount < 3) {
                foundOption = opt;
                newIndex = currentIndex;
                break;
            }
        }

        // --- RESET NẾU FULL ---
        if (!foundOption) {
            log('♻️ Tất cả User đã dùng 3 lần. TỰ ĐỘNG RESET VỀ 0!');
            usageData = {};
            localStorage.setItem('turnitin_student_usage', JSON.stringify(usageData));

            localStorage.setItem('turnitin_last_index', '-1');
            newIndex = 0;
            foundOption = options[0];
        }

        // --- ÁP DỤNG ---
        const chosenEmail = getEmail(foundOption.text);
        usageData[chosenEmail] = (usageData[chosenEmail] || 0) + 1;

        localStorage.setItem('turnitin_student_usage', JSON.stringify(usageData));
        localStorage.setItem('turnitin_last_index', newIndex.toString());

        log(`👤 Chọn [${newIndex + 1}/${options.length}]: ${chosenEmail} (Lần ${usageData[chosenEmail]})`);

        setNativeValue(select, foundOption.value);
        await new Promise(r => setTimeout(r, 1500));
        return true;
    }

    // ===================== LOGIC AUTOMATION =====================
    async function step1_FillAndUpload(file) {
        log(`➡️ FILE: ${file.name}`);
        const studentSelected = await selectStudentLogic();

        const inputs = {
            first: document.querySelector('input[name="author_first"]') || document.getElementById('author_first'),
            last: document.querySelector('input[name="author_last"]') || document.getElementById('author_last'),
            title: document.querySelector('input[name="title"]') || document.getElementById('title'),
            file: document.querySelector('input[type="file"]') || document.getElementById('userfile')
        };

        if (!inputs.file) throw new Error("Không tìm thấy ô input file!");

        const shouldFillName = !studentSelected || (inputs.first && !inputs.first.disabled && inputs.first.value === "");

        if (shouldFillName) {
            log('✏️ Điền tên Random...');
            if (inputs.first) setNativeValue(inputs.first, firstNames[Math.floor(Math.random() * firstNames.length)]);
            if (inputs.last) setNativeValue(inputs.last, lastNames[Math.floor(Math.random() * lastNames.length)]);
        } else {
            log('ℹ️ Tên đã được tự động điền theo Student.');
        }

        if (inputs.title) {
            let fileName = file.name;
            if (fileName.lastIndexOf('.') > 0) fileName = fileName.substring(0, fileName.lastIndexOf('.'));
            setNativeValue(inputs.title, fileName);
        }

        const dt = new DataTransfer();
        dt.items.add(file);
        inputs.file.files = dt.files;
        inputs.file.dispatchEvent(new Event('change', { bubbles: true }));

        await new Promise(r => setTimeout(r, 1500));

        const uploadBtn = document.querySelector('#upload-btn') || document.querySelector('.button_upload');
        if (uploadBtn) {
            log('🖱️ Click Upload...');
            uploadBtn.click();
        } else {
            throw new Error("Không tìm thấy nút Upload");
        }
    }

    async function step2_ConfirmAndNext() {
        try {
            log('⏳ Chờ Confirm...');
            const confirmBtn = await waitForElement('#confirm-btn');
            await new Promise(r => setTimeout(r, 1000));
            confirmBtn.click();

            log('⏳ Chờ Submit Next...');
            const nextBtn = await waitForElement('#submit-another-btn');
            await new Promise(r => setTimeout(r, 1000));
            nextBtn.click();

            log('⏳ Chờ reset form...');
            await waitForElement('#userID, #author_first');
            return true;
        } catch (e) { throw e; }
    }

    async function runAutoLoop() {
        if (selectedFiles.length === 0) {
            log('🏁 ĐÃ XONG HẾT!');
            updateStatus('SẴN SÀNG', false);
            isProcessing = false;
            return;
        }

        isProcessing = true;
        updateStatus('⛔ ĐANG CHẠY TỰ ĐỘNG...', true);

        try {
            const currentFile = selectedFiles[0];
            renderFiles();
            await step1_FillAndUpload(currentFile);
            await step2_ConfirmAndNext();
            log('✅ Xong 1 file.');
            selectedFiles.shift();
            setTimeout(runAutoLoop, 2000);
        } catch (err) {
            log(`❌ LỖI: ${err.message}`);
            updateStatus('BỊ DỪNG DO LỖI', false);
            isProcessing = false;
        }
    }

    // ===================== GUI & EVENTS =====================
    function renderFiles() {
        const container = document.getElementById('t-files-container');
        container.innerHTML = '';

        if (selectedFiles.length === 0) {
            container.innerHTML = '<div style="text-align:center; color:#999; padding:10px;">Chưa có file</div>';
            return;
        }

        selectedFiles.forEach((f, i) => {
            const div = document.createElement('div');
            div.className = `t-file-item ${i === 0 && isProcessing ? 't-current' : ''}`;
            div.innerHTML = `
                <span style="overflow:hidden; white-space:nowrap; max-width: 280px;">
                    ${i === 0 && isProcessing ? '🔄 ' : '📄 '}${f.name}
                </span>
                ${!isProcessing ? `<span class="t-remove" data-idx="${i}" style="color:red;cursor:pointer">✕</span>` : ''}
            `;
            container.appendChild(div);
        });

        if (!isProcessing) {
            container.querySelectorAll('.t-remove').forEach(btn => {
                btn.onclick = (e) => {
                    selectedFiles.splice(parseInt(e.target.getAttribute('data-idx')), 1);
                    renderFiles();
                };
            });
        }
    }

    function createGUI() {
        const wrapper = document.createElement('div');
        wrapper.id = 'turnitin-auto-panel';
        wrapper.innerHTML = `
            <div id="turnitin-main-box" class="active">
                <div class="t-header">
                    <span>🤖 Auto v4.4 (Reset Data)</span>
                    <span id="t-minimize" style="cursor:pointer; font-size:20px;">_</span>
                </div>
                <div class="t-body">
                    <div id="t-status-text" class="t-status-indicator">SẴN SÀNG</div>

                    <input type="file" id="hidden-file-picker" multiple style="display:none !important;">

                    <button class="t-btn btn-select" id="btn-trigger-select">
                        📂 CHỌN FILE & TỰ CHẠY
                    </button>

                    <div class="t-file-list" id="t-files-container">
                        <div style="text-align:center; color:#999; padding:10px;">Danh sách trống</div>
                    </div>

                    <div class="t-log-box" id="t-log-box"></div>

                    <div class="t-row-btn">
                        <button class="t-btn btn-clear" id="btn-clear-log" style="width:50%">
                            🧹 XÓA LOG VIEW
                        </button>
                        <button class="t-btn btn-reset" id="btn-reset-data" style="width:50%">
                            ♻️ RESET DATA (Về 0)
                        </button>
                    </div>
                </div>
            </div>
            <button id="turnitin-toggle-btn" title="Mở lại">🤖</button>
        `;

        document.body.appendChild(wrapper);

        // UI Logic
        const box = document.getElementById('turnitin-main-box');
        const toggleBtn = document.getElementById('turnitin-toggle-btn');
        document.getElementById('t-minimize').onclick = () => { box.classList.remove('active'); toggleBtn.style.display = 'block'; };
        toggleBtn.onclick = () => { box.classList.add('active'); toggleBtn.style.display = 'none'; };

        // File Selection
        const hiddenInput = document.getElementById('hidden-file-picker');
        const triggerBtn = document.getElementById('btn-trigger-select');
        triggerBtn.onclick = () => hiddenInput.click();

        hiddenInput.onchange = (e) => {
            const newFiles = Array.from(e.target.files);
            if (newFiles.length > 0) {
                selectedFiles = selectedFiles.concat(newFiles);
                renderFiles();
                hiddenInput.value = '';
                if (!isProcessing) {
                    log('🚀 Kích hoạt chạy tự động...');
                    runAutoLoop();
                }
            }
        };

        // === NÚT XÓA LOG VIEW ===
        document.getElementById('btn-clear-log').onclick = () => {
            document.getElementById('t-log-box').innerHTML = '<div>[LOG] Đã xóa màn hình log.</div>';
            console.clear();
        };

        // === NÚT RESET DATA (QUAN TRỌNG) ===
        document.getElementById('btn-reset-data').onclick = () => {
            if(confirm("Bạn có chắc muốn Reset bộ đếm về 0?\n(Lần chạy tới sẽ bắt đầu lại từ User đầu tiên)")) {
                localStorage.removeItem('turnitin_student_usage');
                localStorage.removeItem('turnitin_last_index');

                // Clear luôn màn hình cho sạch
                document.getElementById('t-log-box').innerHTML = '<div>[RESET] Đã xóa dữ liệu lưu trữ.</div>';
                document.getElementById('t-log-box').innerHTML += '<div>[INFO] Lần chạy tới sẽ bắt đầu từ User đầu tiên.</div>';
                console.log('[AutoTurnitin] DATA RESET SUCCESSFUL.');
            }
        };
    }

    if (document.readyState === 'complete') {
        createGUI();
    } else {
        window.addEventListener('load', createGUI);
    }

})();