TTV

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

As of 2025-03-06. See the latest version.

// ==UserScript==
// @name         TTV
// @namespace    http://tampermonkey.net/
// @version      1.0
// @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';

    const headerSign = "";
    const footerSign = "";
    const MAX_CHAPTER_POST = 10;
    const AUTO_SAVE_DELAY = 30000; // 30 seconds

    let CHAP_NUMBER = 1;
    let CHAP_STT = 1;
    let CHAP_SERIAL = 1;
    let CHAP_NUMBER_ORIGINAL = 1;
    let CHAP_STT_ORIGINAL = 1;
    let CHAP_SERIAL_ORIGINAL = 1;

    // CSS tùy chỉnh chỉ cho UI mới
    GM_addStyle(`
        #modern-uploader {
            position: fixed;
            top: 10px;
            right: 10px;
            width: 400px;
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
            z-index: 9999;
        }

        .notification {
            position: fixed;
            bottom: 1rem;
            right: 1rem;
            padding: 1rem;
            border-radius: 0.5rem;
            color: white;
            opacity: 0;
            transform: translateY(100%);
            transition: all 0.3s;
            z-index: 9999;
        }

        .notification.success { background-color: #22c55e; }
        .notification.error { background-color: #ef4444; }
        .notification.warning { background-color: #f59e0b; }
        .notification.info { background-color: #3b82f6; }

        .notification.show {
            opacity: 1;
            transform: translateY(0);
        }

        .loading-overlay {
            position: fixed;
            inset: 0;
            background: rgba(0, 0, 0, 0.5);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 9999;
        }

        .loading-spinner {
            width: 40px;
            height: 40px;
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3b82f6;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .error-border {
            border: 2px solid #ef4444 !important;
            animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
        }

        @keyframes pulse {
            0%, 100% { box-shadow: 0 0 0 0 rgb(239 68 68 / 0.5); }
            50% { box-shadow: 0 0 0 4px rgb(239 68 68 / 0.5); }
        }
    `);

    // Thêm chương mới
    function addNewChapter() {
        if ((CHAP_NUMBER + 1) <= MAX_CHAPTER_POST) {
            updateChapNumber(true);
            const html = createChapterHTML(CHAP_NUMBER);
            jQuery('#add-chap').before(html);
        } else {
            const chapterLeft = MAX_CHAPTER_POST - CHAP_NUMBER;
            showNotification(`Chỉ có thể đăng tối đa ${MAX_CHAPTER_POST} chương một lần`, 'warning');
        }
    }

    // Cập nhật số chương
    function updateChapNumber(isAdd) {
        try {
            if (isAdd) {
                let maxStt = 0;
                let maxSerial = 0;

                // Tìm giá trị lớn nhất trong tất cả các input hiện có
                jQuery('input[name^="chap_stt"]').each(function() {
                    const val = parseInt(jQuery(this).val()) || 0;
                    maxStt = Math.max(maxStt, val);
                });

                jQuery('input[name^="chap_number"]').each(function() {
                    const val = parseInt(jQuery(this).val()) || 0;
                    maxSerial = Math.max(maxSerial, val);
                });

                // So sánh với giá trị từ DOM
                const chapStt = parseInt(jQuery('.chap_stt1').val()) || 0;
                const chapSerial = parseInt(jQuery('.chap_serial').val()) || 0;

                maxStt = Math.max(maxStt, chapStt);
                maxSerial = Math.max(maxSerial, chapSerial);

                // Cập nhật biến đếm
                CHAP_STT = maxStt + 1;
                CHAP_SERIAL = maxSerial + 1;
                CHAP_NUMBER++;
            } else {
                if (CHAP_NUMBER > CHAP_NUMBER_ORIGINAL) {
                    CHAP_NUMBER--;
                }
                if (CHAP_STT > CHAP_STT_ORIGINAL) {
                    CHAP_STT--;
                }
                if (CHAP_SERIAL > CHAP_SERIAL_ORIGINAL) {
                    CHAP_SERIAL--;
                }
            }

            // Cập nhật các input ẩn
            jQuery('#chap_number').val(CHAP_NUMBER);
            jQuery('#chap_stt').val(CHAP_STT);
            jQuery('#chap_serial').val(CHAP_SERIAL);
            jQuery('#countNumberPost').text(CHAP_NUMBER);
        } catch (e) {
            console.log("Lỗi: " + e);
        }
    }

    // Tạo HTML cho chương mới
    function createChapterHTML(number) {
        const chap_vol = parseInt(jQuery('.chap_vol').val());
        const chap_vol_name = jQuery('.chap_vol_name').val();

        return `
            <div data-gen="MK_GEN" id="COUNT_CHAP_${number}_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[${number}]" value="${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="${CHAP_SERIAL}" required class="form-control" name="chap_number[${number}]" 
                            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[${number}]" 
                            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[${number}]" 
                            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[${number}]" 
                            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[${number}]" rows="20" placeholder="Nội dung" type="text"></textarea>
                    </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[${number}]" 
                            placeholder="Quảng cáo" type="text"></textarea>
                    </div>
                </div>
            </div>`;
    }

    // Tạo giao diện chính
    function createInterface() {
        const container = document.createElement('div');
        container.id = 'modern-uploader';
        container.innerHTML = `
            <div class="text-center mb-4">
                <h3 class="text-xl font-bold">CÔNG CỤ ĐĂNG NHANH</h3>
                <p id="short-chapter-warning" class="text-red-500 mt-2 hidden"></p>
            </div>

            <div class="form-group">
                <textarea id="content-input" class="form-control" rows="5" 
                    placeholder="Dán nội dung truyện vào đây để tự động tách chương"></textarea>
            </div>

            <div class="flex justify-between">
                <div class="space-x-2">
                    <button class="btn btn-outline" id="remove-empty">Xóa chương trống</button>
                </div>
                <button class="btn btn-primary" id="submit-chapters">Đăng chương</button>
            </div>
        `;

        document.body.appendChild(container);
        setupEventListeners();
    }

    // Khởi tạo các chương
    function initializeChapters() {
        showLoading();
        try {
            // Khởi tạo các biến đếm chương
            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());
            }

            CHAP_NUMBER = CHAP_NUMBER_ORIGINAL = chap_number;
            CHAP_STT = CHAP_STT_ORIGINAL = chap_stt;
            CHAP_SERIAL = CHAP_SERIAL_ORIGINAL = chap_serial;

            // Thêm 9 chương mới
            for(let i = 0; i < 9; i++) {
                setTimeout(() => {
                    addNewChapter();
                }, i * 100); // Delay để tránh lag
            }

            hideLoading();
            showNotification('Đã tạo đủ 10 chương', 'success');

            // Khôi phục bản nháp nếu có
            loadDraft();
        } catch (e) {
            console.log("Lỗi: " + e);
            hideLoading();
            showNotification('Có lỗi khi tạo chương', 'error');
        }
    }

    // Khởi tạo các event listener
    function setupEventListeners() {
        const contentInput = document.getElementById('content-input');
        const removeEmptyBtn = document.getElementById('remove-empty');
        const submitBtn = document.getElementById('submit-chapters');

        // Xử lý paste nội dung
        contentInput.addEventListener('paste', async (e) => {
            e.preventDefault();
            showLoading();

            const text = e.clipboardData.getData('text');
            const chapters = await splitChapters(text);

            if (chapters.length > MAX_CHAPTER_POST) {
                showNotification(`Chỉ có thể đăng tối đa ${MAX_CHAPTER_POST} chương một lúc`, 'warning');
                hideLoading();
                return;
            }

            fillChapterForms(chapters);
            hideLoading();
            showNotification(`Đã tách thành ${chapters.length} chương`, 'success');
        });

        // Xóa chương trống
        removeEmptyBtn.addEventListener('click', removeEmptyChapters);

        // Đăng chương
        submitBtn.addEventListener('click', submitChapters);
    }

    // Tách chương thông minh
    async function splitChapters(text) {
        const chapters = [];
        const lines = text.split('\n');
        let currentChapter = [];
        const chapterPattern = /^[Cc]hương\s+\d+\s*[::]/;

        for (const line of lines) {
            if (chapterPattern.test(line.trim())) {
                if (currentChapter.length > 0) {
                    chapters.push(currentChapter.join('\n'));
                    currentChapter = [];
                }
            }
            currentChapter.push(line);
        }

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

        // Xử lý chương dài
        return chapters.reduce((acc, chapter) => {
            if (chapter.length > 19000) {
                const parts = splitLongChapter(chapter);
                return [...acc, ...parts];
            }
            return [...acc, chapter];
        }, []);
    }

    // Chia nhỏ chương dài
    function splitLongChapter(chapter) {
        const parts = [];
        const lines = chapter.split('\n');
        const title = lines[0];
        const content = lines.slice(1).join('\n');

        const partSize = 15000;
        const numParts = Math.ceil(content.length / partSize);

        for (let i = 0; i < numParts; i++) {
            const start = i * partSize;
            const end = Math.min((i + 1) * partSize, content.length);
            const partContent = content.substring(start, end);

            const partTitle = `${title} (Phần ${i + 1}/${numParts})`;
            parts.push(`${partTitle}\n${partContent}`);
        }

        return parts;
    }

    // Điền nội dung vào form chương
    function fillChapterForms(chapters) {
        chapters.forEach((chapter, index) => {
            const lines = chapter.split('\n');
            const titleMatch = lines[0].match(/^[Cc]hương\s+(\d+)\s*[::]\s*(.*)/);

            if (titleMatch) {
                const chapterNumber = parseInt(titleMatch[1]);
                const title = titleMatch[2].trim();
                const content = lines.slice(1).join('\n');

                // Tìm form chương tương ứng
                const forms = document.querySelectorAll('[data-gen="MK_GEN"]');
                if (forms[index]) {
                    const form = forms[index];
                    form.querySelector('input[name^="chap_stt"]').value = chapterNumber;
                    form.querySelector('input[name^="chap_number"]').value = chapterNumber;
                    form.querySelector('input[name^="chap_name"]').value = title;
                    form.querySelector('textarea[name^="introduce"]').value = headerSign + "\r\n" + content + "\r\n" + footerSign;
                }
            }
        });
    }

    // Tự động lưu nháp
    let autoSaveTimeout;
    function saveDraft() {
        clearTimeout(autoSaveTimeout);
        autoSaveTimeout = setTimeout(() => {
            const chapters = [];
            document.querySelectorAll('[data-gen="MK_GEN"]').forEach(form => {
                chapters.push({
                    stt: form.querySelector('input[name^="chap_stt"]').value,
                    number: form.querySelector('input[name^="chap_number"]').value,
                    title: form.querySelector('input[name^="chap_name"]').value,
                    content: form.querySelector('textarea[name^="introduce"]').value,
                    ad: form.querySelector('textarea[name^="adv"]').value
                });
            });

            GM_setValue('chapter_draft', JSON.stringify({
                chapters,
                timestamp: new Date().getTime()
            }));

            showNotification('Đã tự động lưu nháp', 'info');
        }, AUTO_SAVE_DELAY);
    }

    // Khôi phục bản nháp
    function loadDraft() {
        const draftData = GM_getValue('chapter_draft');
        if (!draftData) return;

        const draft = JSON.parse(draftData);
        const timeDiff = (new Date().getTime() - draft.timestamp) / 1000 / 60; // Phút

        if (timeDiff < 60 && confirm(`Phát hiện bản nháp từ ${Math.round(timeDiff)} phút trước. Bạn có muốn khôi phục?`)) {
            draft.chapters.forEach((chapter, index) => {
                const form = document.querySelectorAll('[data-gen="MK_GEN"]')[index];
                if (!form) return;

                form.querySelector('input[name^="chap_stt"]').value = chapter.stt;
                form.querySelector('input[name^="chap_number"]').value = chapter.number;
                form.querySelector('input[name^="chap_name"]').value = chapter.title;
                form.querySelector('textarea[name^="introduce"]').value = chapter.content;
                form.querySelector('textarea[name^="adv"]').value = chapter.ad;
            });

            showNotification('Đã khôi phục bản nháp', 'success');
        }
    }

    // Xóa chương trống
    function removeEmptyChapters() {
        const forms = document.querySelectorAll('[data-gen="MK_GEN"]');
        let removed = 0;

        forms.forEach(form => {
            const content = form.querySelector('textarea[name^="introduce"]').value.trim();
            if (!content) {
                form.remove();
                removed++;
                updateChapNumber(false);
            }
        });

        if (removed > 0) {
            showNotification(`Đã xóa ${removed} chương trống`, 'info');
        }
    }

    // Đăng chương
    function submitChapters() {
        const forms = document.querySelectorAll('[data-gen="MK_GEN"]');
        let hasError = false;

        forms.forEach(form => {
            const content = form.querySelector('textarea[name^="introduce"]').value;
            if (content.length < 300) {
                form.querySelector('textarea[name^="introduce"]').classList.add('error-border');
                hasError = true;
            }
        });

        if (hasError) {
            showNotification('Có chương quá ngắn, vui lòng kiểm tra lại', 'error');
            return;
        }

        showLoading();
        // Submit form gốc
        document.querySelector('form[name="postChapForm"] button[type="submit"]').click();
        setTimeout(hideLoading, 2000);
    }

    // Hiển thị thông báo
    function showNotification(message, type = 'info') {
        const notification = document.createElement('div');
        notification.className = `notification ${type}`;
        notification.textContent = message;

        document.body.appendChild(notification);
        requestAnimationFrame(() => {
            notification.classList.add('show');
            setTimeout(() => {
                notification.classList.remove('show');
                setTimeout(() => notification.remove(), 300);
            }, 3000);
        });
    }

    // Hiển thị loading
    function showLoading() {
        const loading = document.createElement('div');
        loading.className = 'loading-overlay';
        loading.innerHTML = `
            <div class="loading-spinner"></div>
        `;
        document.body.appendChild(loading);
    }

    function hideLoading() {
        const loading = document.querySelector('.loading-overlay');
        if (loading) {
            loading.remove();
        }
    }

    // Khởi tạo script
    function init() {
        createInterface();
        initializeChapters();
    }

    init();
})();