TTV Auto Upload

Tự động điền form đăng chương trên tangthuvien.net với tính năng nâng cao

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

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         TTV Auto Upload
// @namespace    http://tampermonkey.net/
// @version      3.5
// @description  Tự động điền form đăng chương trên tangthuvien.net với tính năng nâng cao
// @author       HA
// @match        https://tangthuvien.net/dang-chuong/story/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // CSS cho thông báo và control panel
    const style = document.createElement('style');
    style.textContent = `
        .ttv-notification {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 10px 20px;
            background: #4CAF50;
            color: white;
            border-radius: 4px;
            z-index: 9999;
            display: none;
        }
        .ttv-error {
            background: #f44336;
        }
        .ttv-control-panel {
            position: fixed;
            top: 50px;
            right: 20px;
            background: white;
            padding: 25px;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.15);
            z-index: 9998;
            width: 500px;
            margin-bottom: 20px;
            transition: all 0.3s ease;
        }
        .ttv-control-panel.minimized {
            width: auto;
            height: auto;
            padding: 10px;
            opacity: 0.8;
            transform: translateX(calc(100% - 40px));
            transition: all 0.3s ease;
        }
        .ttv-control-panel.minimized:hover {
            opacity: 1;
            transform: translateX(0);
        }
        .ttv-control-panel.minimized .ttv-button-group,
        .ttv-control-panel.minimized .ttv-header {
            display: none;
        }
        .ttv-button-group {
            display: flex;
            flex-direction: column;
            gap: 15px;
        }
        .ttv-content-editor {
            width: 100%;
            height: 100px;
            margin: 6px 0;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 13px;
            line-height: 1.4;
            resize: vertical;
            transition: all 0.2s ease;
        }
        .ttv-content-editor:focus {
            border-color: #5bc0de;
            outline: none;
            box-shadow: 0 0 8px rgba(91,192,222,0.2);
        }
        .ttv-preview {
            display: none;
            width: 100%;
            height: 100px;
            margin: 6px 0;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 13px;
            line-height: 1.4;
            overflow-y: auto;
            background: #f9f9f9;
            transition: all 0.2s ease;
        }
        .ttv-heading {
            font-size: 15px;
            color: #555;
            margin: 12px 0;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .ttv-word-count {
            font-size: 12px;
            color: #888;
            padding: 3px 8px;
            background: #f5f5f5;
            border-radius: 4px;
            transition: all 0.2s ease;
        }
        .ttv-chapter-list {
            width: 100%;
            margin: 10px 0;
            max-height: 200px;
            overflow-y: auto;
            border: 1px solid #eee;
            border-radius: 6px;
            padding: 8px;
        }
        .ttv-chapter-item {
            padding: 8px;
            border-bottom: 1px solid #eee;
            cursor: pointer;
            transition: all 0.2s ease;
            line-height: 1.4;
            font-size: 12px;
            color: #666;
        }
        .ttv-chapter-item .chapter-title {
            font-weight: bold;
            margin-bottom: 4px;
        }
        .ttv-chapter-item .chapter-name {
            color: #888;
            padding-left: 10px;
            border-left: 2px solid #ddd;
            margin: 4px 0;
        }
        .ttv-chapter-item .chapter-stats {
            font-size: 11px;
            color: #999;
        }
        .ttv-chapter-item:last-child {
            border-bottom: none;
        }
        .ttv-chapter-item.selected {
            background: #f0f8ff;
            border-left: 2px solid #5bc0de;
        }
        .ttv-control-panel.fullscreen {
            position: fixed;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            width: 100%;
            height: 100%;
            border-radius: 0;
            z-index: 9999;
            padding: 15px;
            display: flex;
            flex-direction: column;
        }
        .ttv-control-panel.fullscreen .ttv-content-editor,
        .ttv-control-panel.fullscreen .ttv-preview {
            height: calc(100vh - 250px);
            margin: 15px 0;
            font-size: 16px;
        }
        .ttv-header {
            margin-bottom: 20px;
            padding-bottom: 15px;
            border-bottom: 2px solid #eee;
            font-weight: bold;
            font-size: 16px;
            color: #444;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        button.btn-warning {
            background: #f0ad4e;
            color: white;
            border: none;
            padding: 12px;
            border-radius: 6px;
            width: 100%;
            font-size: 14px;
            transition: all 0.2s ease;
        }
        button.btn-warning:hover {
            background: #ec971f;
            transform: translateY(-1px);
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        button.ttv-minimize {
            padding: 2px 8px;
            background: none;
            border: none;
            cursor: pointer;
            font-size: 16px;
            color: #666;
            transition: all 0.2s ease;
        }
        button.ttv-minimize:hover {
            color: #333;
        }
    `;
    document.head.appendChild(style);

    // Tạo div thông báo
    const notification = document.createElement('div');
    notification.className = 'ttv-notification';
    document.body.appendChild(notification);

    // Hiển thị thông báo
    function showNotification(message, isError = false) {
        notification.textContent = message;
        notification.className = 'ttv-notification' + (isError ? ' ttv-error' : '');
        notification.style.display = 'block';
        setTimeout(() => {
            notification.style.display = 'none';
        }, 3000);
    }

    // Parse chapters function update
    function parseChapters(content) {
        const lines = content.split('\n');
        const chapters = [];
        let currentChapter = {
            title: '',
            name: '',
            content: []
        };

        const chapterPattern = /^\s*Chương\s+\d+:/;
        let previousChapterTitle = '';

        // Find chapters with clear markers
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i].trim();

            if (chapterPattern.test(line)) {
                // Check if it's a duplicate header
                if (line !== previousChapterTitle) {
                    // Log for debug
                    console.log(`Tìm thấy chương mới: ${line}`);

                    // If there was a previous chapter, save it
                    if (currentChapter.title && currentChapter.content.length > 0) {
                        chapters.push({...currentChapter});
                        console.log(`Đã lưu chương: ${currentChapter.title}\nTên: ${currentChapter.name}\nSố dòng: ${currentChapter.content.length}`);
                        currentChapter = {title: '', name: '', content: []};
                    }

                    // Save current chapter title
                    currentChapter.title = line;
                    // Get chapter name after :
                    let name = line.split(':')[1]?.trim() || '';
                    // Remove punctuation from start and end of chapter name
                    name = name.replace(/^[.,;'"]+|[.,;'"]+$/g, '').trim();
                    currentChapter.name = name;
                    previousChapterTitle = line;

                    console.log(`Đang xử lý chương mới:\nTiêu đề: ${line}\nTên chương: ${name}`);
                } else {
                    console.log(`Bỏ qua tiêu đề trùng lặp: ${line}`);
                }
            } else {
                // Save all non-chapter-title content
                if (line) { // Only add non-empty lines
                    if (currentChapter.title) {
                        currentChapter.content.push(line);
                    }
                }
            }
        }

        // Add the last chapter if exists
        if (currentChapter.title && currentChapter.content.length > 0) {
            chapters.push({...currentChapter});
        }

        // Display chapters and auto fill forms
        if (chapters.length > 0) {
            displayChapters(chapters);
            // Auto fill forms
            setTimeout(() => {
                try {
                    transferContent();
                } catch (error) {
                    console.error('Lỗi khi tự động điền form:', error);
                    showNotification('Có lỗi xảy ra khi tự động điền form!', true);
                }
            }, 1000);
        }

        console.log(`Tổng số chương tìm thấy: ${chapters.length}`);
        return chapters;
    }

    // Thêm function createChapterHTML
    function createChapterHTML(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" 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="${dăngnhanhTTV.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="${dăngnhanhTTV.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="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[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea>
                </div>
            </div>
        </div>`;
    }

    // Update transferContent function to use createChapterHTML
    async function transferContent() {
        try {
            // Get all chapters
            const chapterItems = document.querySelectorAll('.ttv-chapter-item');
            if (!chapterItems.length) {
                throw new Error('Không tìm thấy chương nào để điền vào form');
            }

            // Find the form container
            const form = document.querySelector('form[name="postChapForm"]');
            if (!form) {
                throw new Error('Không tìm thấy form đăng chương');
            }

            // Get and update state values
            const chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
            const chap_vol_name = jQuery('.chap_vol_name').val() || '';
            dăngnhanhTTV.STATE.CHAP_STT = parseInt(jQuery('#chap_stt').val()) || 1;
            dăngnhanhTTV.STATE.CHAP_SERIAL = parseInt(jQuery('#chap_serial').val()) || 1;

            console.log(`Bắt đầu tạo ${chapterItems.length} form chương`);

            // Create forms for all chapters
            for (let i = 0; i < chapterItems.length; i++) {
                const formIndex = i + 1;
                console.log(`Đang tạo form cho chương ${formIndex}`);

                // Create new chapter form HTML
                const chapterHTML = createChapterHTML(formIndex);
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = chapterHTML;
                form.appendChild(tempDiv.firstElementChild);

                // Get chapter info
                const chapterItem = chapterItems[i];
                const chapterTitle = chapterItem.querySelector('.chapter-title').textContent;
                const chapterName = chapterItem.querySelector('.chapter-name').textContent.replace('Tên chương: ', '');
                const chapterNumber = chapterTitle.match(/Chương\s+(\d+)/)?.[1] || formIndex.toString();

                // Fill in the form fields
                const formFields = {
                    chapterName: form.querySelector(`input[name="chap_name[${formIndex}]"]`),
                    content: form.querySelector(`textarea[name="introduce[${formIndex}]"]`),
                    chapterNumber: form.querySelector(`input[name="chap_number[${formIndex}]"]`),
                    chapterOrder: form.querySelector(`input[name="chap_stt[${formIndex}]"]`),
                    volume: form.querySelector(`input[name="vol[${formIndex}]"]`),
                    volumeName: form.querySelector(`input[name="vol_name[${formIndex}]"]`),
                    advertisement: form.querySelector(`textarea[name="adv[${formIndex}]"]`)
                };

                // Verify all fields exist
                const missingFields = Object.entries(formFields)
                    .filter(([_, element]) => !element)
                    .map(([fieldName]) => fieldName);

                if (missingFields.length > 0) {
                    throw new Error(`Thiếu các trường ${missingFields.join(', ')} trong form ${formIndex}`);
                }

                try {
                    // Fill form fields
                    formFields.chapterName.value = chapterName;
                    formFields.chapterNumber.value = chapterNumber;
                    formFields.chapterOrder.value = chapterNumber;
                    formFields.volume.value = chap_vol;
                    formFields.volumeName.value = chap_vol_name;
                    formFields.content.value = chapterItem._content || '';
                    formFields.advertisement.value = '';

                    // Update state for next chapter
                    dăngnhanhTTV.STATE.CHAP_STT++;
                    dăngnhanhTTV.STATE.CHAP_SERIAL++;

                    console.log(`Đã điền form cho chương ${formIndex}:`, {
                        title: chapterTitle,
                        name: chapterName,
                        number: chapterNumber,
                        contentLength: formFields.content.value.length
                    });

                    // Trigger input event for content field to update character count
                    const inputEvent = new Event('input', { bubbles: true });
                    formFields.content.dispatchEvent(inputEvent);
                } catch (error) {
                    console.error(`Lỗi khi điền nội dung vào form ${formIndex}:`, error);
                    throw new Error(`Không thể điền nội dung vào form ${formIndex}: ${error.message}`);
                }
            }

            showNotification(`Đã tự động điền ${chapterItems.length} chương vào form!`);
            console.log(`Đã điền ${chapterItems.length} chương vào form`);

        } catch (error) {
            console.error('Lỗi khi điền form:', error);
            showNotification('Có lỗi xảy ra khi điền form: ' + error.message, true);
            throw error;
        }
    }

    // Chọn chương để hiển thị
    function selectChapter(chapter, index) {
        const contentEditor = document.querySelector('.ttv-content-editor');
        contentEditor.value = chapter.title + '\n' + chapter.content.join('\n');

        // Cập nhật số từ
        const wordCountSpan = document.querySelector('.ttv-word-count');
        if (wordCountSpan) {
            wordCountSpan.textContent = updateWordCount(contentEditor.value);
        }

        // Highlight selected chapter
        const chapterItems = document.querySelectorAll('.ttv-chapter-item');
        chapterItems.forEach(item => item.classList.remove('selected'));
        chapterItems[index]?.classList.add('selected');

        // Tự động điền form khi chọn chương
        try {
            transferContent();
        } catch (error) {
            console.error('Lỗi khi tự động điền form:', error);
            showNotification('Có lỗi xảy ra khi tự động điền form!', true);
        }
    }

    // Hiển thị danh sách chương
    function displayChapters(chapters) {
        const chapterList = document.createElement('div');
        chapterList.className = 'ttv-chapter-list';

        chapters.forEach((chapter, index) => {
            const chapterItem = document.createElement('div');
            chapterItem.className = 'ttv-chapter-item';

            // Store chapter content
            chapterItem._content = chapter.title + '\n' + chapter.content.join('\n');

            // Tạo các phần tử con với định dạng riêng
            const titleDiv = document.createElement('div');
            titleDiv.className = 'chapter-title';
            titleDiv.textContent = chapter.title;

            const nameDiv = document.createElement('div');
            nameDiv.className = 'chapter-name';
            nameDiv.textContent = `Tên chương: ${chapter.name}`;

            const statsDiv = document.createElement('div');
            statsDiv.className = 'chapter-stats';
            statsDiv.textContent = `${chapter.content.length} dòng`;

            // Thêm các phần tử vào item
            chapterItem.appendChild(titleDiv);
            chapterItem.appendChild(nameDiv);
            chapterItem.appendChild(statsDiv);

            chapterItem.onclick = () => selectChapter(chapter, index);
            chapterList.appendChild(chapterItem);

            // Select first chapter by default
            if (index === 0) {
                chapterItem.classList.add('selected');
            }
        });

        const existingList = document.querySelector('.ttv-chapter-list');
        if (existingList) {
            existingList.remove();
        }

        const contentEditor = document.querySelector('.ttv-content-editor');
        contentEditor.parentNode.insertBefore(chapterList, contentEditor);

        // Hiển thị thông báo về số chương tìm thấy
        showNotification(`Đã tìm thấy ${chapters.length} chương và tự động điền vào form.`);

        // Tự động chọn chương đầu tiên
        if (chapters.length > 0) {
            selectChapter(chapters[0], 0);
        }
    }

    // Đếm số từ và ký tự
    function updateWordCount(content) {
        const wordCount = content.trim().split(/\s+/).length;
        const charCount = content.length;
        return `${wordCount} từ | ${charCount} ký tự`;
    }

    // Chuyển đổi giữa chế độ soạn thảo và xem trước
    function togglePreview() {
        const contentEditor = document.querySelector('.ttv-content-editor');
        const preview = document.querySelector('.ttv-preview');
        const previewBtn = document.querySelector('.ttv-preview-btn');

        if (contentEditor.style.display !== 'none') {
            contentEditor.style.display = 'none';
            preview.style.display = 'block';
            preview.innerHTML = contentEditor.value.replace(/\n/g, '<br>');
            previewBtn.textContent = 'Soạn thảo';
        } else {
            contentEditor.style.display = 'block';
            preview.style.display = 'none';
            previewBtn.textContent = 'Xem trước';
        }
    }

    // Chuyển đổi chế độ toàn màn hình
    function toggleFullscreen() {
        const panel = document.querySelector('.ttv-control-panel');
        const fullscreenBtn = document.querySelector('.ttv-fullscreen-btn');
        panel.classList.toggle('fullscreen');
        fullscreenBtn.textContent = panel.classList.contains('fullscreen') ? 'Thu nhỏ' : 'Toàn màn hình';
    }

    // Thêm panel điều khiển
    function addControlPanel() {
        // Tạo panel
        const panel = document.createElement('div');
        panel.className = 'ttv-control-panel';

        // Thêm header
        const header = document.createElement('div');
        header.className = 'ttv-header';
        header.innerHTML = `
            <div>Soạn Thảo Nội Dung</div>
            <div class="ttv-toolbar">
                <button class="ttv-preview-btn" onclick="togglePreview()">Xem trước</button>
                <button class="ttv-fullscreen-btn" onclick="toggleFullscreen()">Toàn màn hình</button>
                <button class="ttv-minimize">−</button>
            </div>
        `;

        const buttonGroup = document.createElement('div');
        buttonGroup.className = 'ttv-button-group';

        // Khung soạn thảo nội dung
        const contentEditorLabel = document.createElement('div');
        contentEditorLabel.className = 'ttv-heading';
        contentEditorLabel.innerHTML = `
            Nội dung chương:
            <span class="ttv-word-count">0 từ | 0 ký tự</span>
        `;

        const contentEditor = document.createElement('textarea');
        contentEditor.className = 'ttv-content-editor';
        contentEditor.placeholder = 'Nhập hoặc dán nội dung chương vào đây...';

        // Khung xem trước
        const preview = document.createElement('div');
        preview.className = 'ttv-preview';

        // Cập nhật số từ khi nhập nội dung
        contentEditor.oninput = () => {
            const wordCountSpan = document.querySelector('.ttv-word-count');
            if (wordCountSpan) {
                wordCountSpan.textContent = updateWordCount(contentEditor.value);
            }
        };

        // Xử lý khi paste nội dung
        contentEditor.onpaste = (e) => {
            // Cho phép paste hoàn tất
            setTimeout(() => {
                const content = contentEditor.value;
                const chapters = parseChapters(content);
                if (chapters.length > 0) {
                    displayChapters(chapters);
                    // Tự động điền form sau khi hiển thị danh sách chương
                    setTimeout(() => {
                        try {
                            transferContent();
                        } catch (error) {
                            console.error('Lỗi khi tự động điền form:', error);
                            showNotification('Có lỗi xảy ra khi tự động điền form!', true);
                        }
                    }, 1000); // Đợi 1s để đảm bảo UI đã được cập nhật hoàn toàn
                }
            }, 0);
        };

        // Nút chuyển nội dung
        const transferBtn = document.createElement('button');
        transferBtn.type = 'button';
        transferBtn.className = 'btn btn-warning';
        transferBtn.innerHTML = '<span>Chuyển nội dung sang form</span>';
        transferBtn.onclick = transferContent;

        // Thêm các phần tử vào panel
        panel.appendChild(header);
        buttonGroup.appendChild(contentEditorLabel);
        buttonGroup.appendChild(contentEditor);
        buttonGroup.appendChild(preview);
        buttonGroup.appendChild(transferBtn);
        panel.appendChild(buttonGroup);

        document.body.appendChild(panel);

        // Thêm xử lý sự kiện cho các nút trong toolbar
        const minimizeBtn = panel.querySelector('.ttv-minimize');
        minimizeBtn.onclick = () => {
            panel.classList.toggle('minimized');
            minimizeBtn.innerHTML = panel.classList.contains('minimized') ? '+' : '−';
        };

        window.togglePreview = togglePreview;
        window.toggleFullscreen = toggleFullscreen;
    }

    // Thêm control panel khi trang đã load
    window.addEventListener('load', function() {
        addControlPanel();
    });
})();