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      1.0
// @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: 400px;
            z-index: 9998;
            max-height: 90vh;
            overflow-y: auto;
        }
        #ttv-chapters {
            width: 100%;
            margin-bottom: 15px;
            border: 1px solid #eee;
            border-radius: 8px;
            max-height: 300px;
            overflow-y: auto;
            background: #fafafa;
        }
        #ttv-content {
            width: 100%;
            height: 150px;
            margin-bottom: 15px;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 8px;
            font-size: 14px;
            font-family: monospace;
            transition: border-color 0.2s;
            resize: vertical;
        }
        #ttv-content:focus {
            border-color: #4CAF50;
            outline: none;
        }
        .chapter-item {
            padding: 15px;
            border-bottom: 1px solid #eee;
            background: white;
            transition: all 0.2s;
        }
        .chapter-item:hover {
            background: #f5f5f5;
        }
        .chapter-item:last-child {
            border-bottom: none;
        }
        .chapter-title {
            font-weight: 600;
            margin-bottom: 8px;
            color: #333;
            font-size: 14px;
        }
        .chapter-stats {
            font-size: 12px;
            color: #666;
            display: flex;
            gap: 10px;
            align-items: center;
        }
        .chapter-warning {
            color: #ff0000;
            font-weight: 500;
            padding: 2px 6px;
            background: rgba(255,0,0,0.1);
            border-radius: 4px;
        }
        .chapter-long {
            color: #ff9800;
            font-weight: 500;
            padding: 2px 6px;
            background: rgba(255,152,0,0.1);
            border-radius: 4px;
        }
        .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;
            font-size: 14px;
            transition: all 0.2s;
            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); }
        }
        .btn-auto {
            background: #4CAF50;
            color: white;
        }
        .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 id="ttv-chapters"></div>
                <textarea id="ttv-content" placeholder="Dán nội dung vào đây để tự động tách chương..."></textarea>
                <div class="btn-group">
                    <button class="btn-auto" id="ttv-auto">🔄 Đăng tự độ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);
        },

        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 content = document.getElementById('ttv-content');
            const autoBtn = document.getElementById('ttv-auto');
            const manualBtn = document.getElementById('ttv-manual');

            // Xử lý paste vào textarea
            content.addEventListener('paste', (e) => {
                e.preventDefault();
                const text = e.clipboardData.getData('text');
                content.value = text;
                this.processContent(text);
            });

            // Xử lý input trực tiếp
            content.addEventListener('input', () => {
                const text = content.value;
                if (text) {
                    this.processContent(text);
                }
            });

            // Nút đăng tự động
            autoBtn.addEventListener('click', () => {
                if (this.STATE.isProcessing) return;

                console.log('[TTV-DEBUG] Auto button clicked');
                this.STATE.isAuto = true;
                this.STATE.isProcessing = true;
                autoBtn.disabled = true;
                manualBtn.disabled = true;
                autoBtn.classList.add('processing');

                const text = content.value;
                if (!text) {
                    this.showNotification('Vui lòng nhập hoặc dán nội dung trước', 'error');
                    this.STATE.isProcessing = false;
                    autoBtn.disabled = false;
                    manualBtn.disabled = false;
                    autoBtn.classList.remove('processing');
                    return;
                }

                this.processContent(text);
            });

            // Nút đăng thủ công
            manualBtn.addEventListener('click', () => {
                if (this.STATE.isProcessing) return;

                console.log('[TTV-DEBUG] Manual button clicked');
                this.STATE.isAuto = false;
                this.STATE.isProcessing = true;
                manualBtn.disabled = true;
                autoBtn.disabled = true;
                manualBtn.classList.add('processing');

                const text = content.value;
                if (!text) {
                    this.showNotification('Vui lòng nhập hoặc dán nội dung trước', 'error');
                    this.STATE.isProcessing = false;
                    manualBtn.disabled = false;
                    autoBtn.disabled = false;
                    manualBtn.classList.remove('processing');
                    return;
                }

                this.processContent(text);
            });
        },

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

        updateChapterList: function(chapters) {
            console.log('[TTV-DEBUG] Updating chapter list');
            const chapterList = document.getElementById('ttv-chapters');
            let html = '';

            chapters.forEach((chapter, index) => {
                const lines = chapter.split('\n');
                const title = lines.shift().trim();
                const content = lines.join('\n');
                const charCount = content.length;

                html += `
                    <div class="chapter-item">
                        <div class="chapter-title">${title}</div>
                        <div class="chapter-stats">
                            <span>Số ký tự: ${charCount.toLocaleString()}</span>
                            ${charCount < 3000 ? '<span class="chapter-warning">⚠️ Thiếu</span>' : ''}
                            ${charCount > 40000 ? '<span class="chapter-long">⚠️ Dài</span>' : ''}
                        </div>
                    </div>
                `;
            });

            chapterList.innerHTML = html;
        },

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

            // Cập nhật danh sách chương
            this.updateChapterList(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;

            // Hỗ trợ nhiều định dạng tiêu đề chương phổ biến
            const chapterPatterns = [
                /^\s*Chương\s+\d+\s*:/i,     // Chương X:
                /^\t+Chương\s+\d+\s*:/i,     // [Tab]Chương X:
                /^\s{4,}Chương\s+\d+\s*:/i   // [Spaces]Chương X:
            ];

            // Map để nhóm các tiêu đề theo số chương 
            const chapterGroups = new Map();

            // Hàm kiểm tra xem một dòng có phải là tiêu đề chương
            function isChapterTitle(line) {
                return chapterPatterns.some(pattern => pattern.test(line));
            }

            // Hàm trích xuất số chương từ tiêu đề
            function extractChapterNumber(line) {
                const match = line.match(/Chương\s+(\d+)\s*:/i);
                return match ? parseInt(match[1]) : 0;
            }

            // Pass đầu tiên: thu thập tất cả các tiêu đề chương và nhóm theo số chương
            for (let i = 0; i < lines.length; i++) {
                const line = lines[i].trim();
                if (isChapterTitle(line)) {
                    const chapterNum = extractChapterNumber(line);
                    if (!chapterGroups.has(chapterNum)) {
                        chapterGroups.set(chapterNum, []);
                    }
                    chapterGroups.get(chapterNum).push({
                        lineIndex: i,
                        title: lines[i], // Giữ nguyên định dạng gốc
                        content: []
                    });
                }
            }

            // Pass thứ hai: xây dựng nội dung chương từ các tiêu đề đã được xác định
            const sortedChapters = Array.from(chapterGroups.entries());
            sortedChapters.sort((a, b) => a[0] - b[0]); // Sắp xếp theo số chương

            for (let i = 0; i < sortedChapters.length; i++) {
                const [_chapterNum, chapterInfos] = sortedChapters[i];
                // Chọn tiêu đề đầu tiên nếu có nhiều tiêu đề trùng
                const chapterInfo = chapterInfos[0];

                const nextChapterIndex = (i < sortedChapters.length - 1) 
                    ? sortedChapters[i+1][1][0].lineIndex 
                    : lines.length;

                // Thu thập nội dung từ sau tiêu đề đến trước tiêu đề tiếp theo
                const chapterContent = [chapterInfo.title];
                for (let j = chapterInfo.lineIndex + 1; j < nextChapterIndex; j++) {
                    const line = lines[j];
                    // Bỏ qua các tiêu đề trùng lặp
                    if (!isChapterTitle(line.trim())) {
                        chapterContent.push(line);
                    }
                }

                chapters.push(chapterContent.join('\n'));
            }

            console.log(`[TTV-DEBUG] Found ${chapters.length} chapters`);
            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}`);
        },

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

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

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