TTV Auto Upload

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         TTV Auto Upload
// @namespace    http://tampermonkey.net/
// @version      1.1
// @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;
            display: none;
        }
        #ttv-chapters.has-chapters {
            display: block;
        }
        #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;
        }
        #ttv-panel button:hover {
            opacity: 0.9;
        }
        #ttv-panel button:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }
        .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;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        .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.createFormContainer(); 
            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() {
            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);
        },

        createFormContainer: function() {
            let formContainer = document.querySelector('#div_chapt_upload');
            if (!formContainer) {
                formContainer = document.createElement('div');
                formContainer.id = 'div_chapt_upload';
                let parent = document.querySelector('.tab-content');
                if (!parent) {
                    parent = document.createElement('div');
                    parent.className = 'tab-content';
                    document.body.appendChild(parent);
                }
                parent.appendChild(formContainer);
                console.log('[TTV-DEBUG] Created form container');
            }
            return formContainer;
        },

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

            content.addEventListener('paste', (e) => {
                e.preventDefault();
                console.log('[TTV-DEBUG] Content pasted');
                const text = e.clipboardData.getData('text');
                content.value = text;
                this.processContent(text);
            });

            let inputTimer;
            content.addEventListener('input', () => {
                clearTimeout(inputTimer);
                inputTimer = setTimeout(() => {
                    const text = content.value;
                    if (text && text.length > 0) {
                        console.log('[TTV-DEBUG] Processing input content');
                        this.processContent(text);
                    }
                }, 500);
            });

            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;

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

                this.processContent(text);
            });

            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;

                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;
                    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 class="short-chapters-warning">${charCount.toLocaleString()}/40.000 ký tự</span>`;
                    } else {
                        e.target.classList.remove('short-chapter');
                        counter.innerHTML = `<span style="color: ${charCount > 40000 ? '#fbbc05' : '#34a853'}">${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;
            chapterList.classList.toggle('has-chapters', chapters.length > 0);
            console.log('[TTV-DEBUG] Chapter list updated');

            this.fillChapterForms(chapters.slice(0, MAX_CHAPTER_POST));
        },

        processContent: function(text) {
            try {
                this.showLoading('Đang tách chương...');
                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`);

                this.updateChapterList(chapters);

                const remainingChapters = chapters.slice(MAX_CHAPTER_POST);
                if (remainingChapters.length > 0) {
                    this.copyRemainingChapters(remainingChapters);
                }

                if (this.STATE.isAuto && chapters.length >= MAX_CHAPTER_POST) {
                    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ó ${chapters.length} chương)`, 'warning');
                    this.STATE.isAuto = false;
                }

            } catch (error) {
                console.error('[TTV-ERROR] Content processing error:', error);
                this.showNotification('Có lỗi khi xử lý nội dung', 'error');
            } finally {
                this.hideLoading();
                this.STATE.isProcessing = false;
                document.getElementById('ttv-auto').disabled = false;
                document.getElementById('ttv-manual').disabled = false;
            }
        },

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

            const chapterPattern = /^[\s\t]*[Cc]hương\s+(\d+)(?:\s*[::\.]|$)/;

            const chapterNumbers = new Map();

            function getChapterNumber(line) {
                const match = line.match(chapterPattern);
                return match ? parseInt(match[1]) : null;
            }

            for (let i = 0; i < lines.length; i++) {
                const line = lines[i];
                const chapterNum = getChapterNumber(line);

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

                    if (chapterNumbers.has(chapterNum)) {
                        console.log(`[TTV-DEBUG] Skip duplicate chapter ${chapterNum}`);
                        continue;
                    }

                    chapterNumbers.set(chapterNum, true);
                    currentChapter = [line];
                    console.log(`[TTV-DEBUG] Found chapter ${chapterNum}: ${line.trim()}`);
                } else if (currentChapter.length > 0) {
                    currentChapter.push(line);
                }
            }

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

            chapters.sort((a, b) => {
                const numA = getChapterNumber(a.split('\n')[0]) || 0;
                const numB = getChapterNumber(b.split('\n')[0]) || 0;
                return numA - numB;
            });

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

        fillChapterForms: function(chapters) {
            if (!chapters || chapters.length === 0) {
                console.log('[TTV-DEBUG] No chapters to fill');
                return;
            }

            console.log('[TTV-DEBUG] Filling chapter forms');
            const formContainer = this.createFormContainer();
            formContainer.innerHTML = '';

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

                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" value="${title.includes(':') ? title.split(':')[1].trim() : title}"/>
                            </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">${HEADER_SIGN}\n${content}\n${FOOTER_SIGN}</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>`;

                formContainer.insertAdjacentHTML('beforeend', formHtml);

                const textarea = formContainer.querySelector(`textarea[name="introduce[${this.STATE.chapterNumber}]"]`);
                if (textarea) {
                    const event = new Event('input', { bubbles: true });
                    textarea.dispatchEvent(event);
                }
            });

            console.log(`[TTV-DEBUG] Created ${chapters.length} chapter forms`);
            this.showNotification(`Đã điền ${chapters.length} chương vào form`, 'success');
        },

        copyRemainingChapters: function(chapters) {
            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;
            }

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

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

            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.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');
                    this.showNotification('Đang tải lại trang để tiếp tục đăng...', 'info');
                    setTimeout(() => window.location.reload(), 2000);
                }
            }, 3000);
        },

        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 === 'warning' ? '#fff3e0' : '#e8f5e9'};
                    color: ${type === 'error' ? '#c62828' : type === 'warning' ? '#ef6c00' : '#2e7d32'};
                    border: 1px solid ${type === 'error' ? '#ffcdd2' : type === 'warning' ? '#ffe0b2' : '#c8e6c9'};
                ">${message}</div>
            `;
        }
    };

    TTVManager.init();
})();