TTV Auto Upload

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

As of 09.03.2025. See ბოლო ვერსია.

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