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

目前為 2025-03-08 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         TTV Auto Upload
// @namespace    http://tampermonkey.net/
// @version      6.1
// @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');
                this.handlePasteButton(); // Tự động paste khi bật chế độ tự động
            } 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("Starting 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);

                // Copy các chương còn lại vào clipboard nếu có
                if (remainingChapters.length > 0) {
                    this.copyRemainingChapters(remainingChapters);
                }

                // 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('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();
})();