TTV Auto Upload

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

目前為 2025-03-09 提交的版本,檢視 最新版本

// ==UserScript==
// @name         TTV Auto Upload
// @namespace    http://tampermonkey.net/
// @version      0.5
// @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;
        }
        #ttv-panel textarea {
            width: 100%;
            height: 150px;
            margin: 10px 0;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 8px;
            font-size: 14px;
            resize: vertical;
        }
        #ttv-panel .btn-group {
            display: flex;
            gap: 10px;
            margin-top: 15px;
        }
        #ttv-panel button {
            flex: 1;
            padding: 10px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 600;
            transition: all 0.2s;
        }
        #ttv-panel button:hover {
            opacity: 0.9;
        }
        #ttv-panel .btn-auto {
            background: #4CAF50;
            color: white;
        }
        #ttv-panel .btn-manual {
            background: #2196F3;
            color: white;
        }
        .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: {
            isAutoMode: false,
            chapterNumber: 1,
            chapterSTT: 1,
            chapterSerial: 1
        },

        init: function() {
            this.createInterface();
            this.setupEventListeners();
            this.setupCharacterCounter();
            console.log('[TTV] Script khởi động thành công');
        },

        createInterface: function() {
            const panel = document.createElement('div');
            panel.id = 'ttv-panel';
            panel.innerHTML = `
                <h3 style="margin: 0 0 15px; color: #333;">📝 ĐĂNG CHƯƠNG</h3>
                <textarea id="ttv-content" placeholder="Dán nội dung vào đây để 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 nhanh</button>
                </div>
                <div id="ttv-notification" style="margin-top: 10px;"></div>
            `;
            document.body.appendChild(panel);
        },

        setupEventListeners: function() {
            const content = document.getElementById('ttv-content');
            const autoBtn = document.getElementById('ttv-auto');
            const manualBtn = document.getElementById('ttv-manual');

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

            // Nút đăng tự động
            autoBtn.addEventListener('click', () => {
                this.STATE.isAutoMode = true;
                navigator.clipboard.readText()
                    .then(text => {
                        content.value = text;
                        this.processContent(text, true);
                    })
                    .catch(() => this.showNotification('Không thể đọc clipboard', 'error'));
            });

            // Nút đăng nhanh
            manualBtn.addEventListener('click', () => {
                this.STATE.isAutoMode = false;
                this.processContent(content.value, false);
            });
        },

        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: #f44336;">${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>`;
                    }
                }
            });
        },

        processContent: function(text, autoPost = false) {
            if (!text) {
                this.showNotification('Vui lòng nhập nội dung', '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;
            }

            // 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 chương còn lại vào clipboard
            if (remainingChapters.length > 0) {
                this.copyRemainingChapters(remainingChapters);
            }

            // Tự động đăng nếu ở chế độ tự động
            if (autoPost) {
                setTimeout(() => this.submitChapters(), 2000);
            }
        },

        splitChapters: function(text) {
            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) {
                    if (currentChapter.length > 0) {
                        chapters.push(currentChapter.join('\n'));
                        currentChapter = [line];
                        lastTitle = line;
                    } else {
                        currentChapter = [line];
                        lastTitle = line;
                    }
                } else if (currentChapter.length > 0) {
                    currentChapter.push(line);
                }
            }

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

            return chapters;
        },

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

            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(() => {
                    this.showNotification(`Đã copy ${chapters.length} chương còn lại vào clipboard`, 'info');
                });
            } catch (error) {
                console.error('[TTV] Lỗi copy clipboard:', error);
                this.showNotification('Không thể copy vào clipboard', 'error');
            }
        },

        submitChapters: function() {
            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 các 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;
            }

            // Đếm số chương
            const chapterCount = document.querySelectorAll('textarea[name^="introduce"]').length;
            if (chapterCount === 0) {
                this.showNotification('Không có chương nào để đăng', '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] Số chương còn lại:', remainingChapters);

                if (remainingChapters < 10 && this.STATE.isAutoMode) {
                    this.STATE.isAutoMode = false;
                    this.showNotification(`Còn ${remainingChapters} chương, dưới 10 chương nên đã tắt chế độ tự động`, 'warning');
                } else if (this.STATE.isAutoMode) {
                    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);
        },

        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] ${type.toUpperCase()}: ${message}`);
        }
    };

    // Khởi động script
    TTVManager.init();
})();