TTV Auto Upload

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         TTV Auto Upload
// @namespace    http://tampermonkey.net/
// @version      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();
})();