Greasy Fork is available in English.

TTV

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

Version au 10/03/2025. Voir la dernière version.

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

    var debugOutput = [];
    var isDebugMode = true;
    var chapterCount;
    var totalChapters = 0;
    var delayBetweenChapters = 1000; // Milliseconds
    var currentChapter = 0;
    var autoPostingInProgress = false;
    var countdownTimer;

    var customCSS = `
        .content-section {
            margin-bottom: 20px;
        }
        .custom-header {
            font-size: 18px;
            margin-bottom: 10px;
            font-weight: bold;
            color: #4CAF50;
        }
        .custom-textarea {
            width: 100%;
            height: 200px;
            padding: 10px;
            border-radius: 4px;
            border: 1px solid #ddd;
            font-family: 'Arial', sans-serif;
            resize: vertical;
        }
        .custom-input {
            width: 60px;
            padding: 8px;
            border-radius: 4px;
            border: 1px solid #ddd;
            margin-right: 10px;
        }
        .custom-button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-top: 10px;
            transition: background-color 0.3s;
        }
        .custom-button:hover {
            background-color: #45a049;
        }
        .button-group {
            display: flex;
            gap: 10px;
            margin-top: 10px;
        }
        .secondary-button {
            background-color: #2196F3;
            color: white;
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        .secondary-button:hover {
            background-color: #0b7dda;
        }
        .warning-button {
            background-color: #f44336;
        }
        .warning-button:hover {
            background-color: #d32f2f;
        }
        .config-section {
            display: flex;
            align-items: center;
            margin-bottom: 15px;
        }
        .config-label {
            margin-right: 10px;
            font-weight: bold;
        }
        .status-area {
            margin-top: 20px;
            padding: 10px;
            background-color: #f8f9fa;
            border-radius: 4px;
            border-left: 4px solid #4CAF50;
        }
        .debug-toggle {
            margin-top: 20px;
            display: flex;
            align-items: center;
        }
        .debug-toggle label {
            margin-left: 10px;
            cursor: pointer;
        }
        .debug-output {
            margin-top: 10px;
            padding: 10px;
            background-color: #f1f1f1;
            border-radius: 4px;
            font-family: monospace;
            height: 150px;
            overflow-y: auto;
            display: none;
        }
        .countdown {
            font-weight: bold;
            color: #f44336;
        }
        .progress-bar-container {
            width: 100%;
            height: 20px;
            background-color: #f1f1f1;
            border-radius: 10px;
            margin-top: 10px;
            overflow: hidden;
        }
        .progress-bar {
            height: 100%;
            background-color: #4CAF50;
            width: 0%;
            transition: width 0.5s;
        }
        .editor-content {
            white-space: pre-wrap;
            height: 300px;
            overflow-y: auto;
            border: 1px solid #ddd;
            padding: 10px;
            margin-top: 10px;
            font-family: monospace;
            background-color: #f9f9f9;
        }
        /* Tabs styling */
        .tabs {
            display: flex;
            margin-bottom: 20px;
            border-bottom: 1px solid #ddd;
        }
        .tab {
            padding: 10px 20px;
            cursor: pointer;
            background-color: #f1f1f1;
            border: 1px solid #ddd;
            border-bottom: none;
            margin-right: 5px;
            border-top-left-radius: 4px;
            border-top-right-radius: 4px;
        }
        .tab.active {
            background-color: white;
            border-bottom: 1px solid white;
            margin-bottom: -1px;
            font-weight: bold;
        }
        .tab-content {
            display: none;
        }
        .tab-content.active {
            display: block;
        }
    `;

    GM_addStyle(customCSS);

    // Create the main container
    var container = document.createElement('div');
    container.style.maxWidth = '800px';
    container.style.margin = '20px auto';
    container.style.padding = '20px';
    container.style.backgroundColor = 'white';
    container.style.borderRadius = '8px';
    container.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)';

    // Tabs container
    var tabsContainer = document.createElement('div');
    tabsContainer.className = 'tabs';

    // Create tabs
    var bulkTab = document.createElement('div');
    bulkTab.className = 'tab active';
    bulkTab.innerText = 'Đăng nhiều chương';
    bulkTab.onclick = function() { switchTab('bulk'); };

    var singleTab = document.createElement('div');
    singleTab.className = 'tab';
    singleTab.innerText = 'Đăng một chương';
    singleTab.onclick = function() { switchTab('single'); };

    tabsContainer.appendChild(bulkTab);
    tabsContainer.appendChild(singleTab);
    container.appendChild(tabsContainer);

    // Bulk tab content
    var bulkContent = document.createElement('div');
    bulkContent.className = 'tab-content active';
    bulkContent.id = 'bulk-tab';

    // Single tab content
    var singleContent = document.createElement('div');
    singleContent.className = 'tab-content';
    singleContent.id = 'single-tab';

    // Function to switch tabs
    function switchTab(tabName) {
        if (tabName === 'bulk') {
            bulkTab.className = 'tab active';
            singleTab.className = 'tab';
            bulkContent.className = 'tab-content active';
            singleContent.className = 'tab-content';
        } else {
            bulkTab.className = 'tab';
            singleTab.className = 'tab active';
            bulkContent.className = 'tab-content';
            singleContent.className = 'tab-content active';
        }
    }

    // Bulk tab content
    var contentSection = document.createElement('div');
    contentSection.className = 'content-section';

    var contentHeader = document.createElement('div');
    contentHeader.className = 'custom-header';
    contentHeader.innerText = 'Nội dung các chương';

    var contentTextarea = document.createElement('textarea');
    contentTextarea.className = 'custom-textarea';
    contentTextarea.placeholder = 'Dán nội dung các chương vào đây. Mỗi chương sẽ được tách ra dựa vào các định dạng:\n- "Chương + số:"\n- "tab + Chương + số:"\n- "Chương + số"';

    contentSection.appendChild(contentHeader);
    contentSection.appendChild(contentTextarea);
    bulkContent.appendChild(contentSection);

    // Configuration section
    var configSection = document.createElement('div');
    configSection.className = 'content-section';

    var configHeader = document.createElement('div');
    configHeader.className = 'custom-header';
    configHeader.innerText = 'Cấu hình';

    // Delimiter option
    var delimiterSection = document.createElement('div');
    delimiterSection.className = 'config-section';

    var delimiterLabel = document.createElement('div');
    delimiterLabel.className = 'config-label';
    delimiterLabel.innerText = 'Tách chương theo:';

    var delimiterChapter = document.createElement('input');
    delimiterChapter.type = 'radio';
    delimiterChapter.name = 'delimiter';
    delimiterChapter.id = 'delimiter-chapter';
    delimiterChapter.checked = true;

    var delimiterChapterLabel = document.createElement('label');
    delimiterChapterLabel.htmlFor = 'delimiter-chapter';
    delimiterChapterLabel.innerText = 'Chương';
    delimiterChapterLabel.style.marginRight = '15px';
    delimiterChapterLabel.style.marginLeft = '5px';

    var delimiterBlankLine = document.createElement('input');
    delimiterBlankLine.type = 'radio';
    delimiterBlankLine.name = 'delimiter';
    delimiterBlankLine.id = 'delimiter-blank';

    var delimiterBlankLineLabel = document.createElement('label');
    delimiterBlankLineLabel.htmlFor = 'delimiter-blank';
    delimiterBlankLineLabel.innerText = 'Dòng trống (2 dòng liên tiếp)';
    delimiterBlankLineLabel.style.marginLeft = '5px';

    delimiterSection.appendChild(delimiterLabel);
    delimiterSection.appendChild(delimiterChapter);
    delimiterSection.appendChild(delimiterChapterLabel);
    delimiterSection.appendChild(delimiterBlankLine);
    delimiterSection.appendChild(delimiterBlankLineLabel);

    // Chapters to post section
    var chapterCountSection = document.createElement('div');
    chapterCountSection.className = 'config-section';

    var chapterCountLabel = document.createElement('div');
    chapterCountLabel.className = 'config-label';
    chapterCountLabel.innerText = 'Số chương đăng:';

    var chapterCountInput = document.createElement('input');
    chapterCountInput.className = 'custom-input';
    chapterCountInput.type = 'number';
    chapterCountInput.min = '1';
    chapterCountInput.value = '1';

    chapterCountSection.appendChild(chapterCountLabel);
    chapterCountSection.appendChild(chapterCountInput);

    // Delay between posts section
    var delaySection = document.createElement('div');
    delaySection.className = 'config-section';

    var delayLabel = document.createElement('div');
    delayLabel.className = 'config-label';
    delayLabel.innerText = 'Khoảng cách (ms):';

    var delayInput = document.createElement('input');
    delayInput.className = 'custom-input';
    delayInput.type = 'number';
    delayInput.min = '1000';
    delayInput.value = '1000';
    delayInput.onchange = function() {
        delayBetweenChapters = parseInt(delayInput.value);
    };

    delaySection.appendChild(delayLabel);
    delaySection.appendChild(delayInput);

    configSection.appendChild(configHeader);
    configSection.appendChild(delimiterSection);
    configSection.appendChild(chapterCountSection);
    configSection.appendChild(delaySection);
    bulkContent.appendChild(configSection);

    // Button section
    var buttonSection = document.createElement('div');
    buttonSection.className = 'button-group';

    var separateButton = document.createElement('button');
    separateButton.className = 'custom-button';
    separateButton.innerText = 'Tách chương';
    separateButton.onclick = function() {
        separateChapters();
    };

    var postButton = document.createElement('button');
    postButton.className = 'custom-button';
    postButton.innerText = 'Đăng chương';
    postButton.onclick = function() {
        startAutomaticPosting();
    };

    var stopButton = document.createElement('button');
    stopButton.className = 'secondary-button warning-button';
    stopButton.innerText = 'Dừng đăng';
    stopButton.style.display = 'none';
    stopButton.onclick = function() {
        stopAutomaticPosting();
    };

    buttonSection.appendChild(separateButton);
    buttonSection.appendChild(postButton);
    buttonSection.appendChild(stopButton);
    bulkContent.appendChild(buttonSection);

    // Status area
    var statusSection = document.createElement('div');
    statusSection.className = 'status-area';
    statusSection.innerHTML = '<div>Trạng thái: <span id="status-text">Chờ lệnh</span> <span class="countdown" id="countdown"></span></div>';

    // Progress bar
    var progressContainer = document.createElement('div');
    progressContainer.className = 'progress-bar-container';
    progressContainer.style.display = 'none';

    var progressBar = document.createElement('div');
    progressBar.className = 'progress-bar';

    progressContainer.appendChild(progressBar);
    statusSection.appendChild(progressContainer);

    bulkContent.appendChild(statusSection);

    // Preview section
    var previewSection = document.createElement('div');
    previewSection.className = 'content-section';
    previewSection.style.display = 'none';

    var previewHeader = document.createElement('div');
    previewHeader.className = 'custom-header';
    previewHeader.innerText = 'Xem trước các chương';

    var previewContent = document.createElement('div');
    previewContent.className = 'editor-content';

    previewSection.appendChild(previewHeader);
    previewSection.appendChild(previewContent);
    bulkContent.appendChild(previewSection);

    // Debug section
    var debugSection = document.createElement('div');
    debugSection.className = 'content-section';

    var debugToggle = document.createElement('div');
    debugToggle.className = 'debug-toggle';

    var debugCheckbox = document.createElement('input');
    debugCheckbox.type = 'checkbox';
    debugCheckbox.id = 'debug-toggle';
    debugCheckbox.checked = true;
    debugCheckbox.onchange = function() {
        isDebugMode = debugCheckbox.checked;
        debugOutput.length = 0;
        updateDebugOutput();
        if (isDebugMode) {
            debugOutputArea.style.display = 'block';
        } else {
            debugOutputArea.style.display = 'none';
        }
    };

    var debugLabel = document.createElement('label');
    debugLabel.htmlFor = 'debug-toggle';
    debugLabel.innerText = 'Hiện thông tin debug';

    debugToggle.appendChild(debugCheckbox);
    debugToggle.appendChild(debugLabel);

    var debugOutputArea = document.createElement('div');
    debugOutputArea.className = 'debug-output';
    debugOutputArea.id = 'debug-output';
    debugOutputArea.style.display = 'block';

    debugSection.appendChild(debugToggle);
    debugSection.appendChild(debugOutputArea);
    bulkContent.appendChild(debugSection);

    // Single tab content (placeholder for now)
    var singleContentSection = document.createElement('div');
    singleContentSection.className = 'content-section';
    singleContentSection.innerHTML = `
        <div class="custom-header">Đăng một chương</div>
        <p>Chức năng này đang được phát triển.</p>
    `;
    singleContent.appendChild(singleContentSection);

    // Add tab contents to container
    container.appendChild(bulkContent);
    container.appendChild(singleContent);

    // Insert our custom UI before the editor
    var editorContainer = document.querySelector('.panel-body');
    if (editorContainer) {
        editorContainer.parentNode.insertBefore(container, editorContainer);
    } else {
        // If we're on the chapter list page, append to body
        document.body.appendChild(container);
    }

    // Function to update debug output
    function updateDebugOutput() {
        var outputArea = document.getElementById('debug-output');
        if (outputArea) {
            outputArea.innerHTML = debugOutput.join('<br>');
            outputArea.scrollTop = outputArea.scrollHeight;
        }
    }

    // Function to add debug message
    function debug(message) {
        if (isDebugMode) {
            debugOutput.push(message);
            updateDebugOutput();
        }
    }

    // Function to separate chapters
    function separateChapters() {
        var content = contentTextarea.value.trim();
        if (!content) {
            debug('Không có nội dung để tách.');
            return;
        }

        var chapters = [];

        if (delimiterChapter.checked) {
            // Split by chapter headings
            debug('Tách theo định dạng chương...');
            
            // Pattern matches: "Chương + number + optional :" or "tab + Chương + number + optional :"
            var chapterPattern = /(?:^|\n)(?:\t*)(?:[Cc]hương\s*\d+\s*:?)/g;
            
            var matches = content.split(chapterPattern);
            var chapterTitles = content.match(chapterPattern) || [];
            
            // Skip the first split if it's empty (happens when content starts with a chapter title)
            if (matches[0].trim() === '') {
                matches.shift();
            }
            
            // Combine chapter titles with content
            for (var i = 0; i < matches.length; i++) {
                if (i < chapterTitles.length) {
                    chapters.push(chapterTitles[i].trim() + '\n' + matches[i].trim());
                } else {
                    chapters.push(matches[i].trim());
                }
            }
        } else {
            // Split by blank lines (two consecutive newlines)
            debug('Tách theo dòng trống...');
            chapters = content.split(/\n\s*\n/).filter(Boolean).map(chapter => chapter.trim());
        }
        
        totalChapters = chapters.length;
        debug(`Đã tách được ${totalChapters} chương.`);
        
        // Show preview
        previewContent.innerHTML = '';
        for (var i = 0; i < Math.min(5, chapters.length); i++) {
            var chapterPreview = document.createElement('div');
            chapterPreview.style.marginBottom = '20px';
            chapterPreview.innerHTML = `<strong>Chương ${i+1}:</strong><br>${chapters[i].substring(0, 200)}${chapters[i].length > 200 ? '...' : ''}`;
            previewContent.appendChild(chapterPreview);
        }
        
        if (chapters.length > 5) {
            var moreIndicator = document.createElement('div');
            moreIndicator.innerText = `...còn ${chapters.length - 5} chương nữa...`;
            previewContent.appendChild(moreIndicator);
        }
        
        previewSection.style.display = 'block';
        
        // Store in session storage for accessing later
        sessionStorage.setItem('chapters', JSON.stringify(chapters));
        
        // Update status
        document.getElementById('status-text').innerText = `Đã tách ${totalChapters} chương, sẵn sàng đăng.`;
    }

    function startAutomaticPosting() {
        if (autoPostingInProgress) {
            debug('Đang đăng chương, vui lòng đợi...');
            return;
        }
        
        var chapters = JSON.parse(sessionStorage.getItem('chapters') || '[]');
        if (chapters.length === 0) {
            debug('Không có chương nào để đăng. Vui lòng tách chương trước.');
            return;
        }
        
        chapterCount = parseInt(chapterCountInput.value);
        if (isNaN(chapterCount) || chapterCount <= 0) {
            debug('Số chương không hợp lệ.');
            return;
        }
        
        if (chapterCount > chapters.length) {
            debug(`Số chương cần đăng (${chapterCount}) lớn hơn số chương đã tách (${chapters.length}). Đặt lại thành ${chapters.length}.`);
            chapterCount = chapters.length;
            chapterCountInput.value = chapters.length;
        }
        
        autoPostingInProgress = true;
        currentChapter = 0;
        
        // Update UI
        postButton.style.display = 'none';
        stopButton.style.display = 'inline-block';
        document.getElementById('status-text').innerText = 'Đang chuẩn bị đăng chương...';
        progressContainer.style.display = 'block';
        progressBar.style.width = '0%';
        
        // Start posting
        postNextChapter();
    }

    function stopAutomaticPosting() {
        autoPostingInProgress = false;
        clearTimeout(countdownTimer);
        
        // Update UI
        postButton.style.display = 'inline-block';
        stopButton.style.display = 'none';
        document.getElementById('status-text').innerText = 'Dừng đăng chương.';
        document.getElementById('countdown').innerText = '';
    }

    function postNextChapter() {
        if (!autoPostingInProgress || currentChapter >= chapterCount) {
            if (autoPostingInProgress) {
                document.getElementById('status-text').innerText = `Hoàn thành đăng ${currentChapter}/${chapterCount} chương.`;
                autoPostingInProgress = false;
                postButton.style.display = 'inline-block';
                stopButton.style.display = 'none';
            }
            return;
        }
        
        var chapters = JSON.parse(sessionStorage.getItem('chapters') || '[]');
        if (currentChapter >= chapters.length) {
            debug(`Đã hết chương để đăng (${currentChapter}/${chapters.length}).`);
            stopAutomaticPosting();
            return;
        }
        
        // Update progress
        var progress = (currentChapter / chapterCount) * 100;
        progressBar.style.width = progress + '%';
        document.getElementById('status-text').innerText = `Đang đăng chương ${currentChapter + 1}/${chapterCount}...`;
        
        var chaptersToFill = chapters;
        
        // Find editor fields
        debug(`Xử lý chương ${currentChapter + 1}...`);
        var titleInput = document.querySelector('input[name="chapter[name]"]');
        var contentEditor = document.querySelector('.trumbowyg-editor');
        
        if (titleInput && contentEditor) {
            try {
                // Get the raw content from chaptersToFill
                if (currentChapter < chaptersToFill.length) {
                    var content = chaptersToFill[currentChapter].split('\n');
                    var title = content.shift().trim();
                    // Lấy phần sau "Chương + số:" hoặc "Chương + số"
                    var chapterMatch = title.match(/[Cc]hương\s*\d+(\s*:)?/);
                    var chapterTitle = "";
                    
                    if (chapterMatch) {
                        // Lấy phần sau "Chương + số:" hoặc "Chương + số"
                        var matchedPart = chapterMatch[0];
                        var restOfTitle = "";

                        if (matchedPart.endsWith(':')) {
                            // Trường hợp "Chương + số:"
                            restOfTitle = title.substring(title.indexOf(matchedPart) + matchedPart.length).trim();
                        } else {
                            // Trường hợp "Chương + số" (không có dấu :)
                            var indexAfterNumber = title.indexOf(matchedPart) + matchedPart.length;
                            if (title.charAt(indexAfterNumber) === ':') {
                                // Có dấu ":" ngay sau số
                                restOfTitle = title.substring(indexAfterNumber + 1).trim();
                            } else {
                                // Không có dấu ":" sau số
                                restOfTitle = title.substring(indexAfterNumber).trim();
                            }
                        }
                        chapterTitle = restOfTitle || "Vô đề"; // Nếu không có phần sau, dùng "Vô đề"
                    } else {
                        // If no chapter format found, use the original title
                        chapterTitle = title;
                    }
                    
                    debugOutput.push(`\nFilling chapter ${currentChapter + 1}:`);
                    debugOutput.push(`Original title: ${title}`);
                    debugOutput.push(`Extracted title: ${chapterTitle}`);
                    
                    // Set the title
                    titleInput.value = chapterTitle;
                    
                    // Trigger input event to ensure any event listeners know the value has changed
                    var inputEvent = new Event('input', { bubbles: true });
                    titleInput.dispatchEvent(inputEvent);
                    
                    // Set content
                    contentEditor.innerHTML = content.join('<br>');
                    // Trigger input event for content
                    contentEditor.dispatchEvent(new Event('input', { bubbles: true }));
                    
                    debug(`Đã điền thông tin cho chương ${currentChapter + 1}.`);
                    
                    // Click submit after a short delay
                    setTimeout(function() {
                        var submitButton = document.querySelector('button[type="submit"]');
                        if (submitButton) {
                            submitButton.click();
                            debug(`Đã nhấn nút gửi cho chương ${currentChapter + 1}.`);
                            
                            // Increment counter and set timeout for next chapter
                            currentChapter++;
                            
                            if (currentChapter < chapterCount) {
                                // Start countdown for next chapter
                                var countdownSeconds = Math.floor(delayBetweenChapters / 1000);
                                updateCountdown(countdownSeconds);
                                
                                countdownTimer = setTimeout(postNextChapter, delayBetweenChapters);
                            } else {
                                setTimeout(function() {
                                    document.getElementById('status-text').innerText = `Hoàn thành đăng ${currentChapter}/${chapterCount} chương.`;
                                    autoPostingInProgress = false;
                                    postButton.style.display = 'inline-block';
                                    stopButton.style.display = 'none';
                                }, 1000);
                            }
                        } else {
                            debug('Không tìm thấy nút gửi.');
                            stopAutomaticPosting();
                        }
                    }, 500);
                } else {
                    debug('Không còn chương nào để đăng.');
                    stopAutomaticPosting();
                }
            } catch (e) {
                debug('Lỗi khi đăng chương: ' + e.message);
                stopAutomaticPosting();
            }
        } else {
            debug('Không tìm thấy các trường nhập liệu.');
            stopAutomaticPosting();
        }
    }

    function updateCountdown(seconds) {
        var countdownElement = document.getElementById('countdown');
        if (countdownElement) {
            if (seconds > 0) {
                countdownElement.innerText = `(Chương tiếp theo trong ${seconds}s)`;
                setTimeout(function() {
                    updateCountdown(seconds - 1);
                }, 1000);
            } else {
                countdownElement.innerText = '';
            }
        }
    }
})();