TTV Auto Upload

Công cụ đăng chương đơn giản cho Tàng Thư Viện

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

您需要先安裝使用者腳本管理器擴展,如 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      0.6
// @description  Công cụ đăng chương đơn giản cho Tàng Thư Viện
// @author       HA
// @match        https://tangthuvien.net/dang-chuong/story/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

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

    const style = document.createElement('style');
    style.textContent = `
        #ttv-panel {
            position: fixed;
            top: 50px;
            right: 20px;
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.15);
            width: 300px;
            z-index: 9998;
        }
        #ttv-panel .btn-group {
            display: flex;
            gap: 10px;
            margin-top: 15px;
        }
        #ttv-panel button {
            flex: 1;
            padding: 12px 15px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 600;
            transition: all 0.2s;
            font-size: 14px;
            position: relative;
        }
        #ttv-panel button:hover {
            opacity: 0.9;
        }
        #ttv-panel button:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }
        #ttv-panel button.processing:after {
            content: "";
            position: absolute;
            width: 20px;
            height: 20px;
            top: calc(50% - 10px);
            right: 10px;
            border: 2px solid rgba(255,255,255,0.3);
            border-top: 2px solid white;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        #ttv-panel .btn-auto {
            background: #4CAF50;
            color: white;
        }
        #ttv-panel .btn-manual {
            background: #2196F3;
            color: white;
        }
        .loading-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9999;
        }
        .loading-content {
            background: white;
            padding: 20px;
            border-radius: 10px;
            text-align: center;
        }
        .loading-spinner {
            width: 40px;
            height: 40px;
            margin: 0 auto 10px;
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3498db;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        .chapter-character-count {
            text-align: right;
            font-size: 12px;
            margin-top: 5px;
            color: #666;
        }
        textarea[name^="introduce"].short-chapter {
            border: 2px solid #ff0000 !important;
            background-color: rgba(255,0,0,0.1) !important;
            animation: shortChapterBlink 1s infinite;
        }
        @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); }
        }
    `;
    document.head.appendChild(style);

    const TTVManager = {
        STATE: {
            chapterNumber: 1,
            chapterSTT: 1,
            chapterSerial: 1,
            isAuto: false,
            isProcessing: false
        },

        init: function() {
            console.log('[TTV-DEBUG] Initializing script...');
            this.initializeChapterValues();
            this.createInterface();
            this.setupEventListeners();
            this.setupCharacterCounter();
            console.log('[TTV-DEBUG] Script initialized successfully');
            this.showNotification('Công cụ đã sẵn sàng', 'success');
        },

        createInterface: function() {
            console.log('[TTV-DEBUG] Creating interface');
            const panel = document.createElement('div');
            panel.id = 'ttv-panel';
            panel.innerHTML = `
                <h3 style="margin: 0 0 15px; color: #333; text-align: center;">📝 ĐĂNG CHƯƠNG</h3>
                <div class="btn-group">
                    <button class="btn-auto" id="ttv-auto">🔄 Đăng tự động 10 chương</button>
                    <button class="btn-manual" id="ttv-manual">📝 Đăng thủ công</button>
                </div>
                <div id="ttv-notification" style="margin-top: 10px;"></div>
            `;
            document.body.appendChild(panel);
        },

        showLoading: function(message = 'Đang xử lý...') {
            const overlay = document.createElement('div');
            overlay.className = 'loading-overlay';
            overlay.innerHTML = `
                <div class="loading-content">
                    <div class="loading-spinner"></div>
                    <div>${message}</div>
                </div>
            `;
            document.body.appendChild(overlay);
        },

        hideLoading: function() {
            const overlay = document.querySelector('.loading-overlay');
            if (overlay) overlay.remove();
        },

        initializeChapterValues: function() {
            try {
                const chap_number = parseInt(jQuery('#chap_number').val()) || 1;
                let chap_stt = parseInt(jQuery('.chap_stt1').val()) || 1;
                let chap_serial = parseInt(jQuery('.chap_serial').val()) || 1;

                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.chapterNumber = chap_number;
                this.STATE.chapterSTT = chap_stt;
                this.STATE.chapterSerial = chap_serial;

                console.log('[TTV-DEBUG] Chapter values initialized:', this.STATE);
            } catch (e) {
                console.error('[TTV-ERROR] Error initializing chapter values:', e);
            }
        },

        setupEventListeners: function() {
            const autoBtn = document.getElementById('ttv-auto');
            const manualBtn = document.getElementById('ttv-manual');

            const processClipboard = (button, isAuto = false) => {
                if (this.STATE.isProcessing) return;

                this.STATE.isAuto = isAuto;
                this.STATE.isProcessing = true;
                autoBtn.disabled = true;
                manualBtn.disabled = true;
                button.classList.add('processing');
                this.showLoading(isAuto ? 'Đang tự động xử lý chương...' : 'Đang đọc nội dung...');

                navigator.clipboard.readText()
                    .then(text => {
                        if (!text) {
                            this.showNotification('Không có nội dung trong clipboard', 'error');
                            return;
                        }
                        this.processContent(text);
                    })
                    .catch(err => {
                        console.error('[TTV-ERROR] Clipboard read error:', err);
                        this.showNotification('Không thể đọc clipboard. Vui lòng cấp quyền truy cập clipboard và thử lại.', 'error');
                    })
                    .finally(() => {
                        this.STATE.isProcessing = false;
                        autoBtn.disabled = false;
                        manualBtn.disabled = false;
                        button.classList.remove('processing');
                        this.hideLoading();
                    });
            };

            // Nút đăng tự động
            autoBtn.addEventListener('click', () => {
                console.log('[TTV-DEBUG] Auto button clicked');
                processClipboard(autoBtn, true);
            });

            // Nút đăng thủ công
            manualBtn.addEventListener('click', () => {
                console.log('[TTV-DEBUG] Manual button clicked');
                processClipboard(manualBtn, false);
            });
        },

        setupCharacterCounter: function() {
            document.addEventListener('input', (e) => {
                if (e.target.matches('textarea[name^="introduce"]')) {
                    const text = e.target.value;
                    const charCount = text.length;
                    let counter = e.target.nextElementSibling;

                    if (!counter || !counter.classList.contains('chapter-character-count')) {
                        counter = document.createElement('div');
                        counter.className = 'chapter-character-count';
                        e.target.parentNode.insertBefore(counter, e.target.nextSibling);
                    }

                    if (charCount < 3000) {
                        e.target.classList.add('short-chapter');
                        counter.innerHTML = `<span style="color: #ff0000;">${charCount.toLocaleString()}/40.000 ký tự</span>`;
                    } else {
                        e.target.classList.remove('short-chapter');
                        counter.innerHTML = `<span style="color: ${charCount > 40000 ? '#ff9800' : '#4caf50'}">${charCount.toLocaleString()}/40.000 ký tự</span>`;
                    }
                }
            });
        },

        processContent: function(text) {
            console.log('[TTV-DEBUG] Processing content, auto mode:', this.STATE.isAuto);
            if (!text) {
                this.showNotification('Không có nội dung để xử lý', 'error');
                return;
            }

            const chapters = this.splitChapters(text);
            if (chapters.length === 0) {
                this.showNotification('Không tìm thấy chương nào', 'error');
                return;
            }

            console.log(`[TTV-DEBUG] Found ${chapters.length} chapters`);

            // Lấy 10 chương đầu
            const chaptersToFill = chapters.slice(0, MAX_CHAPTER_POST);
            const remainingChapters = chapters.slice(MAX_CHAPTER_POST);

            // Điền form
            this.fillChaptersToForm(chaptersToFill);

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

            // Nếu đang ở chế độ tự động và có đủ 10 chương, tự động đăng
            if (this.STATE.isAuto && chaptersToFill.length === 10) {
                this.showNotification('Sẽ tự động đăng sau 2 giây...', 'info');
                setTimeout(() => {
                    this.submitChapters();
                }, 2000);
            } else if (this.STATE.isAuto) {
                this.showNotification(`Cần đủ 10 chương để tự động đăng (hiện có ${chaptersToFill.length} chương)`, 'warning');
            }
        },

        splitChapters: function(text) {
            console.log('[TTV-DEBUG] Splitting chapters');
            const chapters = [];
            const lines = text.split('\n');
            let currentChapter = [];
            let lastTitle = null;

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

                if (isChapterTitle) {
                    const currentChapterCode = getChapterCode(line);
                    const lastTitleCode = lastTitle ? getChapterCode(lastTitle) : null;

                    if (currentChapter.length > 0) {
                        // Kiểm tra nếu chương hiện tại khác chương trước đó
                        if (currentChapterCode !== lastTitleCode) {
                            chapters.push(currentChapter.join('\n'));
                            currentChapter = [line];
                            lastTitle = line;
                            console.log(`[TTV-DEBUG] Found new chapter: ${currentChapterCode}`);
                        } else {
                            console.log(`[TTV-DEBUG] Skipped duplicate chapter: ${currentChapterCode}`);
                        }
                    } else {
                        currentChapter = [line];
                        lastTitle = line;
                        console.log(`[TTV-DEBUG] Started first chapter: ${currentChapterCode}`);
                    }
                } else if (currentChapter.length > 0) {
                    currentChapter.push(line);
                }
            }

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

            return chapters;
        },

        fillChaptersToForm: function(chapters) {
            console.log('[TTV-DEBUG] Filling form with chapters');
            this.showLoading('Đang điền nội dung vào form...');

            try {
                // Thêm form cho đủ số chương
                while (document.querySelectorAll('input[name^="chap_name"]').length < chapters.length) {
                    this.addNewChapterForm();
                }

                const titles = document.querySelectorAll('input[name^="chap_name"]');
                const contents = document.querySelectorAll('textarea[name^="introduce"]');
                const advs = document.querySelectorAll('textarea[name^="adv"]');

                chapters.forEach((chapter, index) => {
                    if (index >= titles.length) return;

                    const lines = chapter.split('\n');
                    const title = lines.shift().trim();
                    let chapterName = title.includes(':') ? title.split(':')[1].trim() : title;
                    chapterName = chapterName || 'Vô đề';

                    titles[index].value = chapterName;
                    contents[index].value = HEADER_SIGN + "\n" + lines.join('\n') + "\n" + FOOTER_SIGN;
                    if (advs[index]) advs[index].value = '';

                    // Trigger character counter
                    const event = new Event('input', { bubbles: true });
                    contents[index].dispatchEvent(event);
                });

                console.log(`[TTV-DEBUG] Filled ${chapters.length} chapters into form`);
                this.showNotification(`Đã điền ${chapters.length} chương vào form`, 'success');
            } catch (error) {
                console.error('[TTV-ERROR] Form filling error:', error);
                this.showNotification('Có lỗi khi điền nội dung vào form', 'error');
            } finally {
                this.hideLoading();
            }
        },

        copyRemainingChapters: function(chapters) {
            console.log('[TTV-DEBUG] Copying remaining chapters to clipboard');
            try {
                const content = chapters.map(chapter => {
                    const lines = chapter.trim().split('\n');
                    if (lines[0] && !lines[0].startsWith('\t')) {
                        lines[0] = '\t' + lines[0];
                    }
                    return lines.join('\n');
                }).join('\n\n');

                navigator.clipboard.writeText(content)
                    .then(() => {
                        console.log(`[TTV-DEBUG] Copied ${chapters.length} chapters to clipboard`);
                        this.showNotification(`Đã copy ${chapters.length} chương còn lại vào clipboard`, 'info');
                    })
                    .catch(err => {
                        console.error('[TTV-ERROR] Clipboard write error:', err);
                        this.showNotification('Không thể copy vào clipboard', 'error');
                    });
            } catch (error) {
                console.error('[TTV-ERROR] Copy process error:', error);
                this.showNotification('Có lỗi khi copy các chương còn lại', 'error');
            }
        },

        submitChapters: function() {
            console.log('[TTV-DEBUG] Submitting chapters');
            const submitBtn = document.querySelector('button[type="submit"]');
            if (!submitBtn) {
                this.showNotification('Không tìm thấy nút đăng chương!', 'error');
                return;
            }

            // Kiểm tra độ dài chương
            const shortChapters = Array.from(document.querySelectorAll('textarea[name^="introduce"]'))
                .filter(textarea => textarea.value.length < 3000);

            if (shortChapters.length > 0) {
                this.showNotification(`Có ${shortChapters.length} chương chưa đủ 3000 ký tự`, 'error');
                return;
            }

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

            // Kiểm tra sau khi đăng
            setTimeout(() => {
                const remainingChapters = document.querySelectorAll('textarea[name^="introduce"]').length;
                console.log('[TTV-DEBUG] Chapters remaining after submit:', remainingChapters);

                if (remainingChapters < 10 && this.STATE.isAuto) {
                    console.log('[TTV-DEBUG] Less than 10 chapters remaining, stopping auto mode');
                    this.STATE.isAuto = false;
                    this.STATE.isProcessing = false;
                    this.showNotification(`Còn ${remainingChapters} chương, dưới 10 chương nên đã dừng tự động`, 'warning');
                } else if (this.STATE.isAuto) {
                    console.log('[TTV-DEBUG] Reloading page for next batch');
                    setTimeout(() => window.location.reload(), 2000);
                }
            }, 3000);
        },

        addNewChapterForm: function() {
            this.STATE.chapterNumber++;
            this.STATE.chapterSTT++;
            this.STATE.chapterSerial++;

            const formHtml = `
                <div data-gen="MK_GEN" id="COUNT_CHAP_${this.STATE.chapterNumber}_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[${this.STATE.chapterNumber}]" value="${this.STATE.chapterSTT}" 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="${this.STATE.chapterSerial}" required class="form-control" name="chap_number[${this.STATE.chapterNumber}]" 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[${this.STATE.chapterNumber}]" value="1" placeholder="Quyển số" type="number" 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[${this.STATE.chapterNumber}]" placeholder="Tên quyển" type="text" />
                        </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[${this.STATE.chapterNumber}]" 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[${this.STATE.chapterNumber}]" 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[${this.STATE.chapterNumber}]" placeholder="Quảng cáo" type="text"></textarea>
                        </div>
                    </div>
                </div>`;

            document.querySelector('#div_chapt_upload').insertAdjacentHTML('beforeend', formHtml);
            console.log(`[TTV-DEBUG] Added new chapter form #${this.STATE.chapterNumber}`);
        },

        showNotification: function(message, type = 'info') {
            const notification = document.getElementById('ttv-notification');
            notification.innerHTML = `
                <div style="
                    padding: 10px 15px;
                    border-radius: 6px;
                    background-color: ${type === 'error' ? '#ffebee' : type === 'success' ? '#e8f5e9' : type === 'warning' ? '#fff3e0' : '#e3f2fd'};
                    color: ${type === 'error' ? '#c62828' : type === 'success' ? '#1b5e20' : type === 'warning' ? '#e65100' : '#0d47a1'};
                    border: 1px solid ${type === 'error' ? '#ef9a9a' : type === 'success' ? '#a5d6a7' : type === 'warning' ? '#ffcc80' : '#90caf9'};
                ">
                    ${message}
                </div>
            `;
            console.log(`[TTV-DEBUG] ${type.toUpperCase()}: ${message}`);
        }
    };

    // Lấy mã chương dựa vào tiêu đề
    function getChapterCode(title) {
        const match = title.match(/[Cc]hương\s*(\d+)\s*:/);
        if (!match) return title.trim();
        const chapterNum = match[1];
        return `chap_${chapterNum}`;
    }

    // Initialize script
    TTVManager.init();
})();