TTV

Công cụ đăng chương hiện đại cho Tàng Thư Viện với UI/UX được tối ưu

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         TTV
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Công cụ đăng chương hiện đại cho Tàng Thư Viện với UI/UX được tối ưu
// @author       HA
// @match        https://tangthuvien.net/dang-chuong/story/*
// @match        https://tangthuvien.net/danh-sach-chuong/story/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @required     https://code.jquery.com/jquery-3.2.1.min.js
// ==/UserScript==

(function() {
    'use strict';
    console.log('[TTV-DEBUG] Script starting...');

    if (window.location.href.includes('/danh-sach-chuong/story/')) {
        const storyId = window.location.pathname.split('/').pop();
        setTimeout(() => {
            window.location.href = `https://tangthuvien.net/dang-chuong/story/${storyId}`;
        }, 3000);
        return;
    }

    const HEADER_SIGN = "";
    const FOOTER_SIGN = "";
    const MAX_CHAPTER_POST = 10;

    GM_addStyle(`
        /* Vị trí và giao diện của công cụ đăng nhanh */
        #modern-uploader {
            background: linear-gradient(145deg, #ffffff, #f5f8ff);
            padding: 28px;
            border-radius: 16px;
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
            position: fixed;
            right: 25px;
            top: 50%;
            transform: translateY(-50%);
            width: 450px;
            max-height: 92vh;
            overflow-y: auto;
            z-index: 1000;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            border: 1px solid #e0e0e0;
            transition: all 0.3s ease;
        }
        .notification-container {
            margin-top: 10px;
        }
        .notification-success {
            background-color: #d4edda;
            border-color: #c3e6cb;
            color: #155724;
            padding: 10px;
            border-radius: 5px;
        }
        .notification-error {
            background-color: #f8d7da;
            border-color: #f5c6cb;
            color: #721c24;
            padding: 10px;
            border-radius: 5px;
        }
    `);

    const dangNhanhTTV = {
        STATE: {
            CHAP_NUMBER: 1,
            CHAP_STT: 1,
            CHAP_SERIAL: 1,
            CHAP_NUMBER_ORIGINAL: 1,
            CHAP_STT_ORIGINAL: 1,
            CHAP_SERIAL_ORIGINAL: 1,
            AUTO_POST: false,
            TOTAL_CHAPTERS: 0,
            POSTED_CHAPTERS: 0
        },
        ELEMENTS: {
            qpContent: null,
            qpButtonSubmit: null,
            qpButtonRemoveEmpty: null,
            qpButtonReset: null,
            qpButtonPaste: null,
            qpButtonAutoPost: null,
            qpOptionLoop: null
        },
        init: function() {
            try {
                console.log('[TTV-DEBUG] Script initialization starting...');
                this.initializeChapterValues();
                this.createInterface();
                this.cacheElements();
                this.registerEvents();
                console.log('[TTV-DEBUG] Script initialized successfully');
                showNotification('Công cụ đã chạy thành công', 'success');
            } catch (e) {
                console.error('[TTV-ERROR] Initialization error:', e);
                showNotification('Có lỗi khi khởi tạo Script', 'error');
            }
        },
        initializeChapterValues: function() {
            try {
                const chap_number = parseInt(jQuery('#chap_number').val());
                let chap_stt = parseInt(jQuery('.chap_stt1').val());
                let chap_serial = parseInt(jQuery('.chap_serial').val());

                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.CHAP_NUMBER = this.STATE.CHAP_NUMBER_ORIGINAL = chap_number || 1;
                this.STATE.CHAP_STT = this.STATE.CHAP_STT_ORIGINAL = chap_stt || 1;
                this.STATE.CHAP_SERIAL = this.STATE.CHAP_SERIAL_ORIGINAL = chap_serial || 1;
            } catch (e) {
                console.error("Error initializing chapter values:", e);
            }
        },
        createInterface: function() {
            const html = `
                <div id="modern-uploader">
                    <div class="uploader-header">
                        <h3>📚 CÔNG CỤ ĐĂNG CHƯƠNG</h3>
                    </div>
                    <div class="form-group">
                        <textarea placeholder="Nội dung truyện (Dán vào đây để tự động tách chương)" id="qpContent" class="form-control" rows="5"></textarea>
                    </div>
                    <div class="button-container">
                        <button class="btn btn-primary" id="qpButtonPaste">📋 Dán nội dung</button>
                        <button class="btn btn-success" id="qpButtonSubmit">📤 Đăng chương</button>
                    </div>
                    <div class="notification-container"></div>
                </div>`;

            jQuery(".list-in-user").before(html);
        },
        cacheElements: function() {
            this.ELEMENTS.qpContent = jQuery("#qpContent");
            this.ELEMENTS.qpButtonSubmit = jQuery("#qpButtonSubmit");
            this.ELEMENTS.qpButtonPaste = jQuery("#qpButtonPaste");
        },
        registerEvents: function() {
            this.ELEMENTS.qpContent.on("paste", this.handlePaste.bind(this));
            this.ELEMENTS.qpButtonSubmit.on('click', this.submitChapters.bind(this));
            this.ELEMENTS.qpButtonPaste.on('click', this.handlePasteButton.bind(this));
            setupCharacterCounter();
        },
        handlePasteButton: function() {
            this.showLoading();
            navigator.clipboard.readText()
                .then(text => {
                    this.ELEMENTS.qpContent.val(text);
                    setTimeout(() => {
                        this.performAction();
                        this.hideLoading();
                    }, 100);
                })
                .catch(err => {
                    console.error('Không thể đọc dữ liệu từ clipboard:', err);
                    this.hideLoading();
                    showNotification('Không thể truy cập clipboard. Vui lòng dán trực tiếp vào ô nội dung.', 'error');
                });
        },
        handlePaste: function(e) {
            e.preventDefault();
            this.ELEMENTS.qpContent.val("");
            this.showLoading();
            const pastedText = e.originalEvent.clipboardData.getData('text');
            this.ELEMENTS.qpContent.val(pastedText);
            setTimeout(() => {
                this.performAction();
                this.hideLoading();
            }, 100);
        },
        performAction: function() {
            try {
                console.log("[TTV-DEBUG] Starting performAction");
                var text = this.ELEMENTS.qpContent.val();

                if (!text) {
                    showNotification('Không có nội dung để tách chương', 'error');
                    return 0;
                }

                var chapters = [];
                var lines = text.split('\n');
                var currentChapter = [];
                var lastTitle = null;

                for (let i = 0; i < lines.length; i++) {
                    let line = lines[i];
                    let isChapterTitle =
                        /^\t[Cc]hương\s*\d+\s*:/.test(line) ||
                        /^\s{4,}[Cc]hương\s*\d+\s*:/.test(line) ||
                        /^\t[Cc]hương\s*\d+(?!\S)/.test(line) ||
                        /^\s{4,}[Cc]hương\s*\d+(?!\S)/.test(line) ||
                        /^[Cc]hương\s*\d+\s*:/.test(line) ||
                        /^[Cc]hương\s*\d+(?!\S)/.test(line);

                    if (isChapterTitle) {
                        if (currentChapter.length > 0) {
                            chapters.push(currentChapter.join('\n'));
                        }
                        currentChapter = [line];
                        lastTitle = line;
                    } else if (currentChapter.length > 0) {
                        currentChapter.push(line);
                    }
                }

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

                const processedChapters = [];
                for (let i = 0; i < chapters.length; i++) {
                    const chapterLines = chapters[i].split('\n');
                    const title = chapterLines.shift().trim();
                    const chapterText = chapterLines.join('\n');

                    processedChapters.push({
                        displayTitle: title,
                        originalTitle: title,
                        displayContent: chapterText,
                        clipboardContent: chapterText,
                        length: chapterText.length
                    });
                }

                this.updateChapterList(processedChapters);
                showNotification(`Đã tách thành ${processedChapters.length} chương`, 'success');
                return processedChapters.length;
            } catch (error) {
                console.error("[TTV-ERROR] Error in performAction:", error);
                showNotification('Có lỗi xảy ra khi xử lý nội dung', 'error');
                return 0;
            }
        },
        updateChapterList: function(chapters) {
            jQuery('.chapter-detail[data-gen="MK_GEN"]').remove();
            chapters.forEach((chapter, index) => {
                const chapterElement = jQuery(this.createChapterHTML(index + 1));
                chapterElement.find('[name^="chap_name"]').val(chapter.displayTitle);
                chapterElement.find('[name^="introduce"]').val(chapter.displayContent);
                chapterElement.attr('data-original-title', chapter.originalTitle);
                chapterElement.attr('data-clipboard-content', chapter.clipboardContent);
                jQuery('form[name="postChapForm"]').append(chapterElement);
            });

            this.setupClipboardHandling();
            setupCharacterCounter();
        },
        setupClipboardHandling: function() {
            jQuery(document).on('copy', '.chapter-detail[data-gen="MK_GEN"]', function(e) {
                const originalTitle = jQuery(this).attr('data-original-title');
                const clipboardContent = jQuery(this).attr('data-clipboard-content');
                e.originalEvent.clipboardData.setData('text/plain', originalTitle + '\n' + clipboardContent);
                e.preventDefault();
            });
        },
        createChapterHTML: function(chapNum) {
            const chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
            const chap_vol_name = jQuery('.chap_vol_name').val() || '';
            return `
                <div data-gen="MK_GEN" class="chapter-detail" id="COUNT_CHAP_${chapNum}_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[${chapNum}]" value="${dangNhanhTTV.STATE.CHAP_STT}" 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="${dangNhanhTTV.STATE.CHAP_SERIAL}" required class="form-control" name="chap_number[${chapNum}]" 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[${chapNum}]" placeholder="Quyển số" type="number" value="${chap_vol}" 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[${chapNum}]" placeholder="Tên quyển" type="text" value="${chap_vol_name}" />
                        </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[${chapNum}]" 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[${chapNum}]" rows="20" placeholder="Nội dung" type="text"></textarea>
                            <div class="counter"></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[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea>
                        </div>
                    </div>
                </div>`;
        },
        submitChapters: function() {
            this.showLoading();
            document.querySelector('form[name="postChapForm"] button[type="submit"]').click();
            setTimeout(() => this.hideLoading(), 2000);
        },
        showLoading: function() {
            const loading = jQuery("<div>", {
                class: "loading-overlay",
                html: "<div class='loading-spinner'></div>"
            });
            jQuery("body").append(loading);
        },
        hideLoading: function() {
            jQuery(".loading-overlay").remove();
        }
    };

    function showNotification(message, type) {
        console.log(`[TTV-${type.toUpperCase()}] ${message}`);
        try {
            jQuery('#modern-uploader .notification-container').remove();
            const container = jQuery("<div>", {
                class: "notification-container"
            });
            const notification = jQuery("<div>", {
                class: `notification-${type}`
            }).text(message);
            container.append(notification);
            jQuery("#modern-uploader .button-container").after(container);
            notification.fadeIn(300);
        } catch (e) {
            console.error('[TTV-ERROR] Failed to show notification:', e);
            alert(message);
        }
    }

    function setupCharacterCounter() {
        jQuery('textarea[name^="introduce"]').each(function() {
            const counter = jQuery(this).next('.counter');
            jQuery(this).on('input', function() {
                const length = this.value.length;
                if (length < 3000) {
                    jQuery(this).addClass('short-chapter');
                    counter.html(`<span style="color: red;">${length.toLocaleString()} ký tự</span>`);
                } else {
                    jQuery(this).removeClass('short-chapter');
                    counter.html(`<span style="color: green;">${length.toLocaleString()} ký tự</span>`);
                }
            });
            jQuery(this).trigger('input');
        });
    }

    // Initialize when document is ready
    jQuery(document).ready(() => {
        console.log('[TTV-DEBUG] Document ready, initializing script...');
        dangNhanhTTV.init();
    });

})();