TTV Auto Upload

Tự động điền form đăng chương trên tangthuvien.net

当前为 2025-03-08 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         TTV Auto Upload
// @namespace    http://tampermonkey.net/
// @version      4.5
// @description  Tự động điền form đăng chương trên tangthuvien.net
// @author       HA
// @match        https://tangthuvien.net/dang-chuong/story/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // CSS tối giản
    const style = document.createElement('style');
    style.textContent = `
        .ttv-notification {position:fixed;top:20px;right:20px;padding:10px 20px;background:#4CAF50;color:white;border-radius:4px;z-index:9999;display:none}
        .ttv-error {background:#f44336}
        .ttv-control-panel {position:fixed;top:50px;right:20px;background:white;padding:25px;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.15);z-index:9998;width:500px;transition:all 0.3s ease}
        .ttv-control-panel.minimized {width:auto;height:auto;padding:10px;opacity:0.8;transform:translateX(calc(100% - 40px))}
        .ttv-control-panel.minimized:hover {opacity:1;transform:translateX(0)}
        .ttv-control-panel.minimized .ttv-button-group,.ttv-control-panel.minimized .ttv-header {display:none}
        .ttv-button-group {display:flex;flex-direction:column;gap:15px}
        .ttv-content-editor {width:100%;height:100px;margin:6px 0;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;resize:vertical}
        .ttv-preview {display:none;width:100%;height:100px;margin:6px 0;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;background:#f9f9f9}
        .ttv-heading {font-size:15px;color:#555;margin:12px 0;display:flex;justify-content:space-between;align-items:center}
        .ttv-word-count {font-size:12px;color:#888;padding:3px 8px;background:#f5f5f5;border-radius:4px}
        .ttv-chapter-list {width:100%;margin:10px 0;max-height:200px;overflow-y:auto;border:1px solid #eee;border-radius:6px;padding:8px}
        .ttv-chapter-item {padding:8px;border-bottom:1px solid #eee;cursor:pointer;font-size:12px;color:#666}
        .ttv-chapter-item .chapter-title {font-weight:bold;margin-bottom:4px}
        .ttv-chapter-item .chapter-name {color:#888;padding-left:10px;border-left:2px solid #ddd;margin:4px 0}
        .ttv-chapter-item .chapter-stats {font-size:11px;color:#999}
        .ttv-chapter-item:last-child {border-bottom:none}
        .ttv-chapter-item.selected {background:#f0f8ff;border-left:2px solid #5bc0de}
        button.btn-warning {background:#f0ad4e;color:white;border:none;padding:12px;border-radius:6px;width:100%;font-size:14px}
        button.btn-warning:hover {background:#ec971f}
        button.ttv-minimize {padding:2px 8px;background:none;border:none;cursor:pointer;font-size:16px;color:#666}
    `;
    document.head.appendChild(style);

    // State management
    const dăngnhanhTTV = {
        STATE: {
            CHAP_NUMBER: 1,
            CHAP_STT: 1,
            CHAP_SERIAL: 1,
            CHAP_NUMBER_ORIGINAL: 1,
            CHAP_STT_ORIGINAL: 1,
            CHAP_SERIAL_ORIGINAL: 1
        },
        initializeChapterValues: function() {
            try {
                const chap_number = parseInt(jQuery('#chap_number').val());
                let chap_stt = parseInt(jQuery('.chap_stt1').val());
                let chap_serial = parseInt(jQuery('.chap_serial').val());

                if (parseInt(jQuery('#chap_stt').val()) > chap_stt) {
                    chap_stt = parseInt(jQuery('#chap_stt').val());
                }
                if (parseInt(jQuery('#chap_serial').val()) > chap_serial) {
                    chap_serial = parseInt(jQuery('#chap_serial').val());
                }

                this.STATE.CHAP_NUMBER = this.STATE.CHAP_NUMBER_ORIGINAL = chap_number || 1;
                this.STATE.CHAP_STT = this.STATE.CHAP_STT_ORIGINAL = chap_stt || 1;
                this.STATE.CHAP_SERIAL = this.STATE.CHAP_SERIAL_ORIGINAL = chap_serial || 1;
            } catch (e) {
                console.error("Error initializing chapter values:", e);
            }
        }
    };

    // UI Elements
    const notification = document.createElement('div');
    notification.className = 'ttv-notification';
    document.body.appendChild(notification);

    // Hiển thị thông báo
    function showNotification(message, isError = false) {
        notification.textContent = message;
        notification.className = 'ttv-notification' + (isError ? ' ttv-error' : '');
        notification.style.display = 'block';
        setTimeout(() => notification.style.display = 'none', 3000);
    }

    // Phân tích chương
    function parseChapters(content) {
        const lines = content.split('\n');
        const chapters = [];
        let currentChapter = {title: '', name: '', content: []};
        const chapterPattern = /^\s*Chương\s+\d+:/;

        // Biến để kiểm soát tiêu đề trùng lặp
        let chapterTitles = new Set();
        let chapterNumbers = new Map(); // Lưu số chương đã gặp
        let duplicateCount = 0;

        // Mảng lưu nội dung của các dòng trống gần nhất
        let emptyLineBuffer = [];
        // Biến để theo dõi xem ta có đang ở trong một khối tiêu đề lặp không
        let inDuplicatedTitleBlock = false;
        // Theo dõi tiêu đề chương trước đó
        let previousChapterLine = '';
        // Khoảng cách dòng tối thiểu giữa các tiêu đề hợp lệ
        const MIN_LINES_BETWEEN_CHAPTERS = 5;
        // Số dòng đã qua kể từ tiêu đề cuối
        let linesSinceLastTitle = 0;

        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            const trimmedLine = line.trim();
            linesSinceLastTitle++;

            // Xử lý dòng trống
            if (trimmedLine === '') {
                emptyLineBuffer.push(line);
                continue;
            }

            if (chapterPattern.test(trimmedLine)) {
                // Trích xuất số chương từ tiêu đề
                const chapterMatch = trimmedLine.match(/Chương\s+(\d+):/);
                const chapterNum = chapterMatch ? parseInt(chapterMatch[1]) : 0;
                const chapterName = trimmedLine.split(':')[1]?.trim() || '';

                // Kiểm tra các điều kiện để xác định tiêu đề chương hợp lệ
                const isPotentialChapter = linesSinceLastTitle > MIN_LINES_BETWEEN_CHAPTERS || chapters.length === 0;
                const isFullTitle = chapterName.length > 2; // Tiêu đề đầy đủ phải có ít nhất 3 ký tự sau dấu :
                const isDuplicateOfPrevious = trimmedLine === previousChapterLine;

                if (isPotentialChapter && isFullTitle && !isDuplicateOfPrevious) {
                    // Lưu chương hiện tại nếu có
                    if (currentChapter.title && currentChapter.content.length > 0) {
                        chapters.push({...currentChapter});
                        currentChapter = {title: '', name: '', content: []};
                    }

                    // Khởi tạo chương mới
                    currentChapter.title = trimmedLine;
                    let name = chapterName;
                    name = name.replace(/^[.,;'"]+|[.,;'"]+$/g, '').trim();
                    currentChapter.name = name;

                    // Đánh dấu đã gặp tiêu đề và số chương này
                    chapterTitles.add(trimmedLine);
                    chapterNumbers.set(chapterNum, true);

                    // Đặt lại các biến theo dõi
                    inDuplicatedTitleBlock = false;
                    linesSinceLastTitle = 0;
                    previousChapterLine = trimmedLine;
                    emptyLineBuffer = [];
                } else {
                    // Đây là một tiêu đề bị lặp, xử lý như nội dung chương thông thường
                    if (currentChapter.title) {
                        // Thêm các dòng trống đã lưu trữ
                        currentChapter.content.push(...emptyLineBuffer);
                        emptyLineBuffer = [];
                        // Thêm dòng hiện tại
                        currentChapter.content.push(line);
                    }
                    inDuplicatedTitleBlock = true;
                    duplicateCount++;
                }
            } else if (currentChapter.title) {
                // Giữ nguyên định dạng gốc của dòng (không trim)
                currentChapter.content.push(line);
            }
        }

        if (currentChapter.title && currentChapter.content.length > 0) {
            chapters.push({...currentChapter});
        }

        if (chapters.length > 0) {
            displayChapters(chapters);
            if (duplicateCount > 0) {
                showNotification(`Đã tìm thấy ${chapters.length} chương và bỏ qua ${duplicateCount} tiêu đề lặp lại.`);
            } else {
                showNotification(`Đã tìm thấy ${chapters.length} chương và tự động điền vào form.`);
            }

            setTimeout(() => {
                try {
                    transferContent();
                } catch (error) {
                    console.error('Lỗi khi tự động điền form:', error);
                    showNotification('Có lỗi xảy ra khi tự động điền form!', true);
                }
            }, 1000);
        }

        return chapters;
    }

    // Tạo HTML cho form chương
    function createChapterHTML(chapNum) {
        const chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
        const chap_vol_name = jQuery('.chap_vol_name').val() || '';
        return `
        <div data-gen="MK_GEN" id="COUNT_CHAP_${chapNum}_MK">
            <div class="col-xs-12 form-group"></div>
            <div class="form-group">
                <label class="col-sm-2" for="chap_stt">STT</label>
                <div class="col-sm-8">
                    <input class="form-control" required name="chap_stt[${chapNum}]" value="${dăngnhanhTTV.STATE.CHAP_STT}" placeholder="Số thứ tự của chương" type="text"/>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="chap_number">Chương thứ..</label>
                <div class="col-sm-8">
                    <input value="${dăngnhanhTTV.STATE.CHAP_SERIAL}" required class="form-control" name="chap_number[${chapNum}]" placeholder="Chương thứ.. (1,2,3..)" type="text"/>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="chap_name">Quyển số</label>
                <div class="col-sm-8">
                    <input class="form-control" name="vol[${chapNum}]" placeholder="Quyển số" type="number" value="${chap_vol}" required/>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="chap_name">Tên quyển</label>
                <div class="col-sm-8">
                    <input class="form-control chap_vol_name" name="vol_name[${chapNum}]" placeholder="Tên quyển" type="text" value="${chap_vol_name}" />
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="chap_name">Tên chương</label>
                <div class="col-sm-8">
                    <input required class="form-control" name="chap_name[${chapNum}]" placeholder="Tên chương" type="text"/>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="introduce">Nội dung</label>
                <div class="col-sm-8">
                    <textarea maxlength="75000" style="color:#000;font-weight: 400;" required class="form-control" name="introduce[${chapNum}]" rows="20" placeholder="Nội dung" type="text"></textarea>
                    <div class="chapter-character-count"></div>
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2" for="adv">Quảng cáo</label>
                <div class="col-sm-8">
                    <textarea maxlength="1000" class="form-control" name="adv[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea>
                </div>
            </div>
        </div>`;
    }

    // Chuyển nội dung vào form
    async function transferContent() {
        try {
            const chapterItems = document.querySelectorAll('.ttv-chapter-item');
            if (!chapterItems.length) {
                throw new Error('Không tìm thấy chương nào để điền vào form');
            }

            const form = document.querySelector('form[name="postChapForm"]');
            if (!form) {
                throw new Error('Không tìm thấy form đăng chương');
            }

            let chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
            let chap_vol_name = jQuery('.chap_vol_name').val() || '';

            let maxChapStt = 0;
            let maxChapSerial = 0;

            const existingForms = form.querySelectorAll('[id^="COUNT_CHAP_"]');

            existingForms.forEach(formElem => {
                const formIdMatch = formElem.id.match(/COUNT_CHAP_(\d+)_MK/);
                if (formIdMatch && formIdMatch[1]) {
                    const formIndex = parseInt(formIdMatch[1]);

                    const sttInput = formElem.querySelector(`input[name="chap_stt[${formIndex}]"]`);
                    if (sttInput && sttInput.value && !isNaN(parseInt(sttInput.value))) {
                        const sttVal = parseInt(sttInput.value);
                        if (sttVal > maxChapStt) {
                            maxChapStt = sttVal;
                        }
                    }

                    const serialInput = formElem.querySelector(`input[name="chap_number[${formIndex}]"]`);
                    if (serialInput && serialInput.value && !isNaN(parseInt(serialInput.value))) {
                        const serialVal = parseInt(serialInput.value);
                        if (serialVal > maxChapSerial) {
                            maxChapSerial = serialVal;
                        }
                    }
                }
            });

            if (maxChapStt > 0) {
                dăngnhanhTTV.STATE.CHAP_STT = maxChapStt;
            }

            if (maxChapSerial > 0) {
                dăngnhanhTTV.STATE.CHAP_SERIAL = maxChapSerial;
            }

            const existingFormCount = existingForms.length;

            for (let i = 0; i < chapterItems.length; i++) {
                const formIndex = existingFormCount + i + 1;
                dăngnhanhTTV.STATE.CHAP_STT++;
                dăngnhanhTTV.STATE.CHAP_SERIAL++;

                const chapterHTML = createChapterHTML(formIndex);
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = chapterHTML;
                const newFormElement = tempDiv.firstElementChild;

                if (!newFormElement) {
                    throw new Error(`Không thể tạo element form cho chương ${formIndex}`);
                }

                form.appendChild(newFormElement);

                const chapterItem = chapterItems[i];
                const titleElement = chapterItem.querySelector('.chapter-title');
                const nameElement = chapterItem.querySelector('.chapter-name');

                if (!titleElement || !nameElement) {
                    throw new Error(`Thiếu thông tin tiêu đề hoặc tên cho chương ${formIndex}`);
                }

                const chapterTitle = titleElement.textContent;
                const chapterName = nameElement.textContent.replace('Tên chương: ', '');

                const formFields = {
                    chapterName: form.querySelector(`input[name="chap_name[${formIndex}]"]`),
                    content: form.querySelector(`textarea[name="introduce[${formIndex}]"]`),
                    chapterNumber: form.querySelector(`input[name="chap_number[${formIndex}]"]`),
                    chapterOrder: form.querySelector(`input[name="chap_stt[${formIndex}]"]`),
                    volume: form.querySelector(`input[name="vol[${formIndex}]"]`),
                    volumeName: form.querySelector(`input[name="vol_name[${formIndex}]"]`),
                    advertisement: form.querySelector(`textarea[name="adv[${formIndex}]"]`)
                };

                formFields.chapterName.value = chapterName;
                formFields.chapterNumber.value = dăngnhanhTTV.STATE.CHAP_SERIAL.toString();
                formFields.chapterOrder.value = dăngnhanhTTV.STATE.CHAP_STT.toString();
                formFields.volume.value = chap_vol;
                formFields.volumeName.value = chap_vol_name;

                if (chapterItem._content) {
                    formFields.content.value = chapterItem._content;
                } else {
                    formFields.content.value = '';
                }

                formFields.advertisement.value = '';

                const inputEvent = new Event('input', { bubbles: true });
                formFields.content.dispatchEvent(inputEvent);
            }

            showNotification(`Đã tự động điền ${chapterItems.length} chương vào form!`);

        } catch (error) {
            console.error('Lỗi khi điền form:', error);
            showNotification('Có lỗi xảy ra khi điền form: ' + error.message, true);
        }
    }

    // Hiển thị danh sách chương
    function displayChapters(chapters) {
        const chapterList = document.createElement('div');
        chapterList.className = 'ttv-chapter-list';

        chapters.forEach((chapter, index) => {
            const chapterItem = document.createElement('div');
            chapterItem.className = 'ttv-chapter-item';
            chapterItem._content = chapter.title + '\n' + chapter.content.join('\n');

            // Trích xuất số chương để hiển thị
            const chapterMatch = chapter.title.match(/Chương\s+(\d+):/);
            const chapterNum = chapterMatch ? chapterMatch[1] : '?';

            const titleDiv = document.createElement('div');
            titleDiv.className = 'chapter-title';
            titleDiv.textContent = chapter.title;

            const nameDiv = document.createElement('div');
            nameDiv.className = 'chapter-name';
            nameDiv.textContent = `Tên chương: ${chapter.name}`;

            const statsDiv = document.createElement('div');
            statsDiv.className = 'chapter-stats';
            statsDiv.textContent = `Chương ${chapterNum} | ${chapter.content.length} dòng`;

            chapterItem.appendChild(titleDiv);
            chapterItem.appendChild(nameDiv);
            chapterItem.appendChild(statsDiv);
            chapterItem.onclick = () => selectChapter(chapter, index);
            chapterList.appendChild(chapterItem);

            if (index === 0) {
                chapterItem.classList.add('selected');
            }
        });

        const existingList = document.querySelector('.ttv-chapter-list');
        if (existingList) {
            existingList.remove();
        }

        const contentEditor = document.querySelector('.ttv-content-editor');
        contentEditor.parentNode.insertBefore(chapterList, contentEditor);

        if (chapters.length > 0) {
            selectChapter(chapters[0], 0);
        }
    }

    // Chọn chương
    function selectChapter(chapter, index) {
        const contentEditor = document.querySelector('.ttv-content-editor');
        // Giữ nguyên định dạng gốc của nội dung
        contentEditor.value = chapter.title + '\n' + chapter.content.join('\n');

        const wordCountSpan = document.querySelector('.ttv-word-count');
        if (wordCountSpan) {
            wordCountSpan.textContent = updateWordCount(contentEditor.value);
        }

        const chapterItems = document.querySelectorAll('.ttv-chapter-item');
        chapterItems.forEach(item => item.classList.remove('selected'));
        chapterItems[index]?.classList.add('selected');

        try {
            transferContent();
        } catch (error) {
            console.error('Lỗi khi tự động điền form:', error);
            showNotification('Có lỗi xảy ra khi tự động điền form!', true);
        }
    }

    // Tính số từ
    function updateWordCount(content) {
        const wordCount = content.trim().split(/\s+/).length;
        const charCount = content.length;
        return `${wordCount} từ | ${charCount} ký tự`;
    }

    // Xem trước
    function togglePreview() {
        const contentEditor = document.querySelector('.ttv-content-editor');
        const preview = document.querySelector('.ttv-preview');
        const previewBtn = document.querySelector('.ttv-preview-btn');

        if (contentEditor.style.display !== 'none') {
            contentEditor.style.display = 'none';
            preview.style.display = 'block';
            preview.innerHTML = contentEditor.value.replace(/\n/g, '<br>');
            previewBtn.textContent = 'Soạn thảo';
        } else {
            contentEditor.style.display = 'block';
            preview.style.display = 'none';
            previewBtn.textContent = 'Xem trước';
        }
    }

    // Toàn màn hình
    function toggleFullscreen() {
        const panel = document.querySelector('.ttv-control-panel');
        const fullscreenBtn = document.querySelector('.ttv-fullscreen-btn');
        panel.classList.toggle('fullscreen');
        fullscreenBtn.textContent = panel.classList.contains('fullscreen') ? 'Thu nhỏ' : 'Toàn màn hình';
    }

    // Tạo panel điều khiển
    function addControlPanel() {
        const panel = document.createElement('div');
        panel.className = 'ttv-control-panel';

        const header = document.createElement('div');
        header.className = 'ttv-header';
        header.innerHTML = `
            <div>Soạn Thảo Nội Dung</div>
            <div class="ttv-toolbar">
                <button class="ttv-preview-btn" onclick="togglePreview()">Xem trước</button>
                <button class="ttv-fullscreen-btn" onclick="toggleFullscreen()">Toàn màn hình</button>
                <button class="ttv-minimize">−</button>
            </div>
        `;

        const buttonGroup = document.createElement('div');
        buttonGroup.className = 'ttv-button-group';

        const contentEditorLabel = document.createElement('div');
        contentEditorLabel.className = 'ttv-heading';
        contentEditorLabel.innerHTML = `
            Nội dung chương:
            <span class="ttv-word-count">0 từ | 0 ký tự</span>
        `;

        const contentEditor = document.createElement('textarea');
        contentEditor.className = 'ttv-content-editor';
        contentEditor.placeholder = 'Nhập hoặc dán nội dung chương vào đây...';

        const preview = document.createElement('div');
        preview.className = 'ttv-preview';

        contentEditor.oninput = () => {
            const wordCountSpan = document.querySelector('.ttv-word-count');
            if (wordCountSpan) {
                wordCountSpan.textContent = updateWordCount(contentEditor.value);
            }
        };

        contentEditor.onpaste = (e) => {
            setTimeout(() => {
                const content = contentEditor.value;
                const chapters = parseChapters(content);
                if (chapters.length > 0) {
                    displayChapters(chapters);
                    setTimeout(() => {
                        try {
                            transferContent();
                        } catch (error) {
                            console.error('Lỗi khi tự động điền form:', error);
                            showNotification('Có lỗi xảy ra khi tự động điền form!', true);
                        }
                    }, 1000);
                }
            }, 0);
        };

        const transferBtn = document.createElement('button');
        transferBtn.type = 'button';
        transferBtn.className = 'btn btn-warning';
        transferBtn.innerHTML = '<span>Chuyển nội dung sang form</span>';
        transferBtn.onclick = transferContent;

        panel.appendChild(header);
        buttonGroup.appendChild(contentEditorLabel);
        buttonGroup.appendChild(contentEditor);
        buttonGroup.appendChild(preview);
        buttonGroup.appendChild(transferBtn);
        panel.appendChild(buttonGroup);

        document.body.appendChild(panel);

        const minimizeBtn = panel.querySelector('.ttv-minimize');
        minimizeBtn.onclick = () => {
            panel.classList.toggle('minimized');
            minimizeBtn.innerHTML = panel.classList.contains('minimized') ? '+' : '−';
        };

        window.togglePreview = togglePreview;
        window.toggleFullscreen = toggleFullscreen;
    }

    window.addEventListener('load', function() {
        dăngnhanhTTV.initializeChapterValues();
        addControlPanel();
    });
})();