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

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