// ==UserScript==
// @name TTV Auto Upload
// @namespace http://tampermonkey.net/
// @version 2.7
// @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;
}
.chapter-form {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
}
.chapter-form h3 {
margin: 0 0 10px;
font-size: 16px;
color: #333;
}
.form-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.form-row input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.chapter-form textarea {
width: 100%;
min-height: 100px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
}
.ttv-forms-container {
max-height: 600px;
overflow-y: auto;
padding: 10px;
margin: 10px 0;
border: 1px solid #eee;
border-radius: 8px;
}
`;
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 = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (chapterPattern.test(line)) {
// Kiểm tra xem có phải là tiêu đề trùng lặp không
if (line !== previousChapterTitle) {
// Log để debug
console.log(`Tìm thấy chương mới: ${line}`);
// Nếu đã có chương trước đó, lưu lại
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: []};
}
// Lưu tiêu đề chương hiện tại
currentChapter.title = line;
// Lấy tên chương sau dấu :
let name = line.split(':')[1]?.trim() || '';
// Xóa dấu ., , hoặc ; ở đầu và cuối tên chương
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 {
// Lưu tất cả nội dung không phải tiêu đề chương
if (line) { // Chỉ thêm dòng không trống
if (currentChapter.title) {
currentChapter.content.push(line);
}
}
}
}
// Thêm chương cuối cùng nếu có
if (currentChapter.title && currentChapter.content.length > 0) {
chapters.push({...currentChapter});
}
// Tự động tạo form và điền nội dung
if (chapters.length > 0) {
// Tạo số form bằng với số chương tìm thấy
createChapterForms(chapters.length);
// Hiển thị danh sách chương
displayChapters(chapters);
// Tự động điền form
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;
}
// 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.${chapters.length >= 10 ? ' (Giới hạn 10 chương đầu tiên)' : ''}`);
// 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 nội dung từ khung soạn thảo sang form
function transferContent() {
try {
// Get all chapters
const chapterItems = document.querySelectorAll('.ttv-chapter-item');
// Fill each chapter's content into corresponding form
chapterItems.forEach((chapterItem, index) => {
const formIndex = index + 1;
const formFields = {
chapterName: document.querySelector(`input[name="chap_name[${formIndex}]"]`),
content: document.querySelector(`textarea[name="introduce[${formIndex}]"]`),
chapterNumber: document.querySelector(`input[name="chap_number[${formIndex}]"]`),
chapterOrder: document.querySelector(`input[name="chap_stt[${formIndex}]"]`),
volume: document.querySelector(`input[name="vol[${formIndex}]"]`)
};
// Verify all form fields exist
Object.entries(formFields).forEach(([fieldName, element]) => {
if (!element) {
throw new Error(`Không tìm thấy trường "${fieldName}" cho chương ${formIndex}`);
}
});
// Get chapter info
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 form fields
formFields.chapterName.value = chapterName;
formFields.chapterNumber.value = chapterNumber;
formFields.chapterOrder.value = chapterNumber;
formFields.volume.value = '1';
// Get content for this chapter
if (index === 0) { // If it's the first/selected chapter
formFields.content.value = document.querySelector('.ttv-content-editor').value;
} else {
formFields.content.value = chapterItem._content || ''; // Use stored content
}
console.log(`Đã điền form cho chương ${formIndex}:`, {
title: chapterTitle,
name: chapterName,
number: chapterNumber,
contentLength: formFields.content.value.length
});
});
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;
}
}
// 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';
}
// Function to create form fields for multiple chapters using website's template
function createChapterForms(totalChapters) {
try {
// Find the original form container
const originalForm = document.querySelector('form');
if (!originalForm) {
throw new Error('Không tìm thấy form gốc trên trang web');
}
// Get original chapter form template
const originalChapterDiv = document.querySelector('div[id^="div-chapter"]');
if (!originalChapterDiv) {
throw new Error('Không tìm thấy mẫu form chương gốc');
}
// Debug log template structure
console.log('Template form structure:', {
id: originalChapterDiv.id,
fields: Array.from(originalChapterDiv.querySelectorAll('input, textarea')).map(el => ({
name: el.getAttribute('name'),
type: el.tagName.toLowerCase()
}))
});
// Clear any existing additional chapter divs
const existingChapters = document.querySelectorAll('div[id^="div-chapter"]');
console.log(`Xóa ${existingChapters.length - 1} form cũ`);
existingChapters.forEach((div, index) => {
if (index > 0) { // Keep the first one as template
div.remove();
}
});
// Create new chapter divs based on template
for (let i = 1; i <= totalChapters; i++) {
const newChapterDiv = originalChapterDiv.cloneNode(true);
const newId = `div-chapter-${i}`;
newChapterDiv.id = newId;
// Update all input fields with proper indices
newChapterDiv.querySelectorAll('input, textarea').forEach(input => {
const name = input.getAttribute('name');
if (name) {
const oldIndex = name.match(/\[(\d+)\]/)?.[1];
const newName = name.replace(/\[\d+\]/, `[${i}]`);
console.log(`Cập nhật trường ${name} -> ${newName} trong form ${i}`);
// Update the index in the name attribute
input.setAttribute('name', newName);
// Clear any existing values
input.value = '';
}
});
// Update any labels or headers
newChapterDiv.querySelectorAll('label, .chapter-header').forEach(el => {
const oldText = el.textContent;
const newText = el.textContent.replace(/\d+/, i);
console.log(`Cập nhật label ${oldText} -> ${newText} trong form ${i}`);
el.textContent = newText;
});
// Append the new chapter div to the form
originalForm.appendChild(newChapterDiv);
console.log(`Đã tạo form chương ${i} với ID ${newId}`);
}
console.log(`Đã tạo ${totalChapters} form chương dựa trên mẫu của trang web`);
showNotification(`Đã tạo ${totalChapters} form chương`);
} catch (error) {
console.error('Lỗi khi tạo form chương:', error);
showNotification('Có lỗi xảy ra khi tạo form chương: ' + error.message, true);
throw error;
}
}
// 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();
});
})();