Công cụ đăng chương hiện đại cho Tàng Thư Viện với UI/UX được tối ưu
目前為
// ==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 = '';
}
}
}
})();