TTV Chapter Manager

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

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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