TTV Auto Upload

Công cụ đăng chương hiện đại cho Tàng Thư Viện với UI/UX được tối ưu

Ekde 2025/03/08. Vidu La ĝisdata versio.

// ==UserScript==
// @name         TTV Auto Upload
// @namespace    http://tampermonkey.net/
// @version      6.2
// @description  Công cụ đăng chương hiện đại cho Tàng Thư Viện với UI/UX được tối ưu
// @author       HA
// @match        https://tangthuvien.net/dang-chuong/story/*
// @match        https://tangthuvien.net/danh-sach-chuong/story/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @required     https://code.jquery.com/jquery-3.2.1.min.js
// ==/UserScript==

(function() {
    'use strict';
    if (window.location.href.includes('/danh-sach-chuong/story/')) {
        const storyId = window.location.pathname.split('/').pop();
        setTimeout(() => {
            window.location.href = `https://tangthuvien.net/dang-chuong/story/${storyId}`;
        }, 3000);
        return;
    }

    const HEADER_SIGN = "";
    const FOOTER_SIGN = "";
    const MAX_CHAPTER_POST = 10;

    GM_addStyle(`
        #modern-uploader {
            background-color: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
            position: fixed;
            right: 20px;
            top: 50%;
            transform: translateY(-50%);
            width: 400px;
            max-height: 90vh;
            overflow-y: auto;
            z-index: 1000;
        }
        @keyframes shortChapterBlink {
            0% { background-color: rgba(255, 0, 0, 0.1); }
            50% { background-color: rgba(255, 0, 0, 0.2); }
            100% { background-color: rgba(255, 0, 0, 0.1); }
        }
        textarea[name^="introduce"] {
            transition: all 0.3s ease;
        }
        textarea[name^="introduce"].short-chapter {
            animation: shortChapterBlink 1s infinite;
            border: 2px solid #ff0000 !important;
            background-color: rgba(255, 0, 0, 0.1) !important;
        }
        .chapter-character-count {
            text-align: right;
            font-size: 12px;
            margin-top: 5px;
            color: #666;
        }
        .short-chapters-warning {
            color: #ff0000;
            font-weight: bold;
            animation: shortChapterBlink 1s infinite;
        }
        .button-container {
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 15px;
            margin-top: 15px;
        }
        #modern-uploader .btn {
            padding: 10px 20px;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 600;
            font-size: 14px;
            transition: all 0.2s ease;
        }
        #modern-uploader .form-control {
            width: 100%;
            padding: 15px;
            border: 1px solid #ddd;
            border-radius: 8px;
            margin-bottom: 15px;
            font-size: 16px;
            transition: border-color 0.2s ease;
        }
        #modern-uploader .form-control:focus {
            border-color: #4285f4;
            outline: none;
        }
    `);

    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,
            AUTO_MODE: false
        },

        ELEMENTS: {
            qpContent: null,
            qpButtonPaste: null,
            qpOptionAuto: null
        },

        init: function() {
            try {
                console.log('[TTV-DEBUG] Script bắt đầu khởi tạo...');
                this.initializeChapterValues();
                this.createInterface();
                this.cacheElements();
                this.registerEvents();
                console.log('[TTV-DEBUG] Script đã khởi động thành công');
                showNotification('Công cụ đã chạy', 'success');

                // Khôi phục trạng thái tự động
                const isAutoMode = localStorage.getItem('TTV_AUTO_MODE') === 'true';
                if (isAutoMode) {
                    this.ELEMENTS.qpOptionAuto.prop('checked', true);
                    this.STATE.AUTO_MODE = true;
                    this.handlePasteButton(); // Tự động paste nếu đang ở chế độ tự động
                }
            } catch (e) {
                console.error('[TTV-ERROR] Lỗi khởi tạo:', e);
                showNotification('Có lỗi khi khởi tạo Script', 'error');
            }
        },

        createInterface: function() {
            const html = `
            <div id="modern-uploader">
                <div class="text-center mb-4">
                    <h3 style="color: #4285f4; margin-bottom: 15px; font-weight: 700; font-size: 18px;">📝 CÔNG CỤ ĐĂNG NHANH</h3>
                </div>
                <div class="form-group">
                    <textarea placeholder="Nội dung truyện (Dán vào đây để tự động tách chương)" id="qpContent" class="form-control" rows="5"></textarea>
                </div>
                <div class="text-center mb-3">
                    <label style="color: #bef385;">
                        <input type="checkbox" id="qpOptionAuto" class="form-control" style="height:10px;width: 10px;display: inline-block;">
                        Chế độ tự động
                    </label>
                </div>
                <div class="button-container" style="display: flex; justify-content: center; gap: 15px;">
                    <button class="btn btn-primary" id="qpButtonPaste">📋 Paste</button>
                </div>
                <div class="notification-container"></div>
            </div>`;

            jQuery(".list-in-user").before(html);
        },

        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);
            }
        },

        cacheElements: function() {
            this.ELEMENTS.qpContent = jQuery("#qpContent");
            this.ELEMENTS.qpButtonPaste = jQuery("#qpButtonPaste");
            this.ELEMENTS.qpOptionAuto = jQuery("#qpOptionAuto");
        },

        registerEvents: function() {
            this.ELEMENTS.qpContent.on("paste", this.handlePaste.bind(this));
            this.ELEMENTS.qpButtonPaste.on('click', this.handlePasteButton.bind(this));
            this.ELEMENTS.qpOptionAuto.on('change', this.toggleAutoMode.bind(this));
            setupCharacterCounter();
        },

        toggleAutoMode: function() {
            this.STATE.AUTO_MODE = this.ELEMENTS.qpOptionAuto.prop('checked');
            localStorage.setItem('TTV_AUTO_MODE', this.STATE.AUTO_MODE);

            if (this.STATE.AUTO_MODE) {
                showNotification('Đã bật chế độ tự động - sẽ tự động paste và đăng chương', 'info');
                // Tự động paste khi bật chế độ tự động
                setTimeout(() => {
                    this.handlePasteButton();
                }, 100);
            } else {
                showNotification('Đã tắt chế độ tự động', 'info');
            }
        },

        handlePasteButton: function() {
            this.showLoading();
            navigator.clipboard.readText()
                .then(text => {
                    this.ELEMENTS.qpContent.val(text);
                    setTimeout(() => {
                        this.performAction();
                        this.hideLoading();
                    }, 100);
                })
                .catch(err => {
                    console.error('Không thể đọc dữ liệu từ clipboard:', err);
                    this.hideLoading();
                    showNotification('Không thể truy cập clipboard. Vui lòng dán trực tiếp vào ô nội dung.', 'error');
                });
        },

        handlePaste: function(e) {
            e.preventDefault();
            this.ELEMENTS.qpContent.val("");
            this.showLoading();
            const pastedText = e.originalEvent.clipboardData.getData('text');
            this.ELEMENTS.qpContent.val(pastedText);
            setTimeout(() => {
                this.performAction();
                this.hideLoading();
            }, 100);
        },

        performAction: function() {
            try {
                console.log("[TTV-DEBUG] Bắt đầu performAction");
                var text = this.ELEMENTS.qpContent.val();

                if (!text) {
                    showNotification('Không có nội dung để tách chương', 'error');
                    return;
                }

                // Xử lý tách chương và điền form
                var chapters = this.splitChapters(text);
                if (chapters.length === 0) {
                    showNotification('Không tìm thấy chương nào', 'error');
                    return;
                }

                // Lấy 10 chương đầu để điền vào form
                const chaptersToFill = chapters.slice(0, MAX_CHAPTER_POST);
                const remainingChapters = chapters.slice(MAX_CHAPTER_POST);

                // Điền 10 chương đầu vào form
                this.fillChaptersToForm(chaptersToFill);
                console.log(`[TTV-DEBUG] Đã điền ${chaptersToFill.length} chương vào form`);

                // Copy các chương còn lại vào clipboard nếu có
                if (remainingChapters.length > 0) {
                    this.copyRemainingChapters(remainingChapters);
                    console.log(`[TTV-DEBUG] Đã copy ${remainingChapters.length} chương vào clipboard`);
                }

                // Nếu đang ở chế độ tự động, đợi 2 giây rồi đăng
                if (this.STATE.AUTO_MODE) {
                    showNotification('Sẽ tự động đăng sau 2 giây...', 'info');
                    setTimeout(() => {
                        this.submitChapters();
                    }, 2000);
                }

            } catch (error) {
                console.error('[TTV-ERROR] Lỗi xử lý chương:', error);
                showNotification('Có lỗi khi xử lý các chương. Vui lòng thử lại.', 'error');
            }
        },

        splitChapters: function(text) {
            var chapters = [];
            var lines = text.split('\n');
            var currentChapter = [];
            var lastTitle = null;

            for (let i = 0; i < lines.length; i++) {
                let line = lines[i];
                let isChapterTitle = /^\t[Cc]hương\s*\d+\s*:/.test(line) || /^\s{4,}[Cc]hương\s*\d+\s*:/.test(line);

                if (isChapterTitle) {
                    if (currentChapter.length > 0) {
                        if (line !== lastTitle) {
                            chapters.push(currentChapter.join('\n'));
                            currentChapter = [line];
                            lastTitle = line;
                        }
                    } else {
                        currentChapter = [line];
                        lastTitle = line;
                    }
                } else if (currentChapter.length > 0) {
                    currentChapter.push(line);
                }
            }

            if (currentChapter.length > 0) {
                chapters.push(currentChapter.join('\n'));
            }

            return chapters;
        },

        fillChaptersToForm: function(chapters) {
            var titles = jQuery("input[name^='chap_name']");
            var contents = jQuery("textarea[name^='introduce']");
            var advs = jQuery("textarea[name^='adv']");

            // Thêm form cho đủ số chương cần thiết
            const neededForms = chapters.length - titles.length;
            if (neededForms > 0 && titles.length < MAX_CHAPTER_POST) {
                for (let i = 0; i < neededForms && (titles.length + i) < MAX_CHAPTER_POST; i++) {
                    this.addNewChapter();
                }
                titles = jQuery("input[name^='chap_name']");
                contents = jQuery("textarea[name^='introduce']");
                advs = jQuery("textarea[name^='adv']");
            }

            // Điền nội dung vào form
            jQuery.each(titles, function(k, v) {
                if (k < chapters.length) {
                    var content = chapters[k].split('\n');
                    var title = content.shift().trim();
                    var chapterTitle = title;
                    if (title.includes(':')) {
                        chapterTitle = title.substring(title.indexOf(':') + 1).trim();
                    }
                    if (!chapterTitle || chapterTitle.trim() === '') {
                        chapterTitle = "Vô đề";
                    }
                    titles[k].value = chapterTitle;
                    contents[k].value = HEADER_SIGN + "\r\n" + content.join('\n') + "\r\n" + FOOTER_SIGN;
                    if (advs[k]) advs[k].value = "";
                    jQuery(contents[k]).trigger('input');
                }
            });

            showNotification(`Đã điền ${chapters.length} chương vào form`, 'success');
        },

        copyRemainingChapters: function(chapters) {
            try {
                const clipboardContent = chapters.map(chap => {
                    const lines = chap.trim().split('\n');
                    if (lines.length > 0 && !lines[0].startsWith('\t')) {
                        lines[0] = '\t' + lines[0];
                    }
                    return lines.join('\n');
                }).join('\n\n---CHAPTER_SEPARATOR---\n\n');

                navigator.clipboard.writeText(clipboardContent)
                    .then(() => {
                        showNotification(`Đã copy ${chapters.length} chương còn lại vào clipboard`, 'success');
                    })
                    .catch(() => {
                        showNotification('Không thể copy vào clipboard', 'error');
                    });
            } catch (error) {
                console.error('Lỗi copy clipboard:', error);
                showNotification('Có lỗi khi copy các chương còn lại', 'error');
            }
        },

        submitChapters: function() {
            // Kiểm tra nút submit
            const postButton = jQuery('button[type="submit"]');
            if (!postButton.length) {
                showNotification('Không tìm thấy nút đăng chương!', 'error');
                return;
            }

            // Kiểm tra độ dài chương
            if (!validateChapterLengths()) {
                showNotification('Có chương chưa đủ độ dài tối thiểu (3000 ký tự)!', 'error');
                return;
            }

            // Đăng chương
            postButton.click();
            showNotification('Đang đăng chương...', 'info');

            // Nếu đang ở chế độ tự động, reload trang sau 5 giây
            if (this.STATE.AUTO_MODE) {
                setTimeout(() => {
                    window.location.reload();
                }, 5000);
            }
        },

        addNewChapter: function() {
            if ((this.STATE.CHAP_NUMBER + 1) <= MAX_CHAPTER_POST) {
                this.STATE.CHAP_NUMBER++;
                this.STATE.CHAP_STT++;
                this.STATE.CHAP_SERIAL++;
                var html = createChapterHTML(this.STATE.CHAP_NUMBER);
                jQuery('#div_chapt_upload').append(html);
            }
        },

        showLoading: function() {
            jQuery(".loading-overlay").remove();
            var loading = jQuery("<div>", {
                class: "loading-overlay",
                css: {
                    position: "fixed",
                    top: "0",
                    left: "0",
                    width: "100%",
                    height: "100%",
                    backgroundColor: "rgba(0, 0, 0, 0.5)",
                    zIndex: "9999",
                    display: "flex",
                    justifyContent: "center",
                    alignItems: "center"
                }
            });
            loading.append(`
                <div style="
                    background-color: white;
                    padding: 20px;
                    border-radius: 10px;
                    text-align: center;
                ">
                    <div style="
                        border: 4px solid #f3f3f3;
                        border-top: 4px solid #3498db;
                        border-radius: 50%;
                        width: 40px;
                        height: 40px;
                        margin: 0 auto 10px;
                        animation: spin 1s linear infinite;
                    "></div>
                    <div>Đang xử lý...</div>
                </div>
            `);
            jQuery("body").append(loading);
            jQuery("head").append(`
                <style>
                    @keyframes spin {
                        0% { transform: rotate(0deg); }
                        100% { transform: rotate(360deg); }
                    }
                </style>
            `);
        },

        hideLoading: function() {
            jQuery(".loading-overlay").remove();
        }
    };

    function showNotification(message, type) {
        jQuery('#modern-uploader .notification-container').remove();
        const container = jQuery("<div>", {
            class: "notification-container",
            css: {
                width: "100%",
                padding: "10px 0",
                marginTop: "10px",
                textAlign: "left",
                borderTop: "1px solid rgba(0,0,0,0.1)"
            }
        });
        const notification = jQuery("<div>", {
            class: `notification-${type}`,
            css: {
                backgroundColor: type === 'success' ? "#e8f5e9" : (type === 'error' ? "#ffebee" : "#fff8e1"),
                color: type === 'success' ? "#000000" : (type === 'error' ? "#d32f2f" : "#ff9800"),
                padding: "10px 15px",
                borderRadius: "8px",
                fontSize: "14px",
                fontWeight: "500",
                boxShadow: "0 4px 10px rgba(0,0,0,0.15)",
                display: "inline-block",
                maxWidth: "90%",
                margin: "0",
                wordBreak: "break-word",
                border: type === 'success' ? "1px solid #81c784" : (type === 'error' ? "1px solid #d32f2f" : "1px solid #ff9800")

            }
        });
        const lines = message.split('\n');
        lines.forEach((line, index) => {
            notification.append(jQuery("<div>").html(line));
        });
        container.append(notification);
        jQuery("#modern-uploader .button-container").after(container);
        notification.fadeIn(300);
    }

    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>`;
    }

    function setupCharacterCounter() {
        jQuery(document).on("input", "[name^=introduce]", function() {
            const text = jQuery(this).val();
            const charCount = text.length;
            let charCountElement = jQuery(this).next('.chapter-character-count');
            if (charCountElement.length === 0) {
                charCountElement = jQuery('<div class="chapter-character-count"></div>');
                jQuery(this).after(charCountElement);
            }
            if(charCount < 3000) {
                jQuery(this).addClass('short-chapter');
                charCountElement.html(`<span class="short-chapters-warning">${charCount.toLocaleString()}/40.000 ký tự</span>`);
            } else {
                jQuery(this).removeClass('short-chapter');
                if(charCount > 40000) {
                    charCountElement.html(`<span style="color: #fbbc05;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
                } else {
                    charCountElement.html(`<span style="color: #34a853;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
                }
            }
        });
    }

    function validateChapterLengths() {
        let hasError = false;
        jQuery('form[name="postChapForm"] .chapter-detail').each(function() {
            const form = this;
            const contentTextarea = form.querySelector('textarea[name^="introduce"]');
            const content = contentTextarea.value;
            if (content.length < 3000) {
                jQuery(contentTextarea).addClass('short-chapter');
                let warningIcon = form.querySelector('.warning-icon');
                if (!warningIcon) {
                    warningIcon = document.createElement('div');
                    warningIcon.className = 'warning-icon';
                    warningIcon.innerHTML = '⚠️';
                    contentTextarea.parentNode.appendChild(warningIcon);
                }
                hasError = true;
            } else {
                jQuery(contentTextarea).removeClass('short-chapter');
                const warningIcon = form.querySelector('.warning-icon');
                if (warningIcon) {
                    warningIcon.remove();
                }
            }
        });
        return !hasError;
    }

    dăngnhanhTTV.init();
})();