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 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
    });

})();