TTV Chapter Manager

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

נכון ליום 09-03-2025. ראה הגרסה האחרונה.

// ==UserScript==
// @name         TTV Chapter Manager
// @namespace    http://tampermonkey.net/
// @version      0.9
// @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: 120px;
            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.processContent(text);
                } else {
                    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');
            });

            // 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.processContent(text);
                } else {
                    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');
            });
        },

        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;

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

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

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