// ==UserScript==
// @name TTV Auto Upload
// @namespace http://tampermonkey.net/
// @version 4.2
// @description Tự động điền form đăng chương trên tangthuvien.net
// @author HA
// @match https://tangthuvien.net/dang-chuong/story/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// CSS tối giản
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;transition:all 0.3s ease}
.ttv-control-panel.minimized {width:auto;height:auto;padding:10px;opacity:0.8;transform:translateX(calc(100% - 40px))}
.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;resize:vertical}
.ttv-preview {display:none;width:100%;height:100px;margin:6px 0;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;background:#f9f9f9}
.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}
.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;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}
button.btn-warning {background:#f0ad4e;color:white;border:none;padding:12px;border-radius:6px;width:100%;font-size:14px}
button.btn-warning:hover {background:#ec971f}
button.ttv-minimize {padding:2px 8px;background:none;border:none;cursor:pointer;font-size:16px;color:#666}
`;
document.head.appendChild(style);
// State management
const dăngnhanhTTV = {
STATE: {
CHAP_NUMBER: 1,
CHAP_STT: 1,
CHAP_SERIAL: 1,
CHAP_NUMBER_ORIGINAL: 1,
CHAP_STT_ORIGINAL: 1,
CHAP_SERIAL_ORIGINAL: 1
},
initializeChapterValues: function() {
try {
const chap_number = parseInt(jQuery('#chap_number').val());
let chap_stt = parseInt(jQuery('.chap_stt1').val());
let chap_serial = parseInt(jQuery('.chap_serial').val());
if (parseInt(jQuery('#chap_stt').val()) > chap_stt) {
chap_stt = parseInt(jQuery('#chap_stt').val());
}
if (parseInt(jQuery('#chap_serial').val()) > chap_serial) {
chap_serial = parseInt(jQuery('#chap_serial').val());
}
this.STATE.CHAP_NUMBER = this.STATE.CHAP_NUMBER_ORIGINAL = chap_number || 1;
this.STATE.CHAP_STT = this.STATE.CHAP_STT_ORIGINAL = chap_stt || 1;
this.STATE.CHAP_SERIAL = this.STATE.CHAP_SERIAL_ORIGINAL = chap_serial || 1;
} catch (e) {
console.error("Error initializing chapter values:", e);
}
}
};
// UI Elements
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);
}
// Phân tích chương
function parseChapters(content) {
const lines = content.split('\n');
const chapters = [];
let currentChapter = {title: '', name: '', content: []};
const chapterPattern = /^\s*Chương\s+\d+:/;
// Biến để kiểm soát tiêu đề trùng lặp
let chapterTitles = new Set();
let chapterNumbers = new Map(); // Lưu số chương đã gặp
let duplicateCount = 0;
// Mảng lưu nội dung của các dòng trống gần nhất
let emptyLineBuffer = [];
// Biến để theo dõi xem ta có đang ở trong một khối tiêu đề lặp không
let inDuplicatedTitleBlock = false;
// Theo dõi tiêu đề chương trước đó
let previousChapterLine = '';
// Khoảng cách dòng tối thiểu giữa các tiêu đề hợp lệ
const MIN_LINES_BETWEEN_CHAPTERS = 5;
// Số dòng đã qua kể từ tiêu đề cuối
let linesSinceLastTitle = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
linesSinceLastTitle++;
// Xử lý dòng trống
if (trimmedLine === '') {
emptyLineBuffer.push(line);
continue;
}
if (chapterPattern.test(trimmedLine)) {
// Trích xuất số chương từ tiêu đề
const chapterMatch = trimmedLine.match(/Chương\s+(\d+):/);
const chapterNum = chapterMatch ? parseInt(chapterMatch[1]) : 0;
const chapterName = trimmedLine.split(':')[1]?.trim() || '';
// Kiểm tra các điều kiện để xác định tiêu đề chương hợp lệ
const isPotentialChapter = linesSinceLastTitle > MIN_LINES_BETWEEN_CHAPTERS || chapters.length === 0;
const isFullTitle = chapterName.length > 2; // Tiêu đề đầy đủ phải có ít nhất 3 ký tự sau dấu :
const isDuplicateOfPrevious = trimmedLine === previousChapterLine;
if (isPotentialChapter && isFullTitle && !isDuplicateOfPrevious) {
// Lưu chương hiện tại nếu có
if (currentChapter.title && currentChapter.content.length > 0) {
chapters.push({...currentChapter});
currentChapter = {title: '', name: '', content: []};
}
// Khởi tạo chương mới
currentChapter.title = trimmedLine;
let name = chapterName;
name = name.replace(/^[.,;'"]+|[.,;'"]+$/g, '').trim();
currentChapter.name = name;
// Đánh dấu đã gặp tiêu đề và số chương này
chapterTitles.add(trimmedLine);
chapterNumbers.set(chapterNum, true);
// Đặt lại các biến theo dõi
inDuplicatedTitleBlock = false;
linesSinceLastTitle = 0;
previousChapterLine = trimmedLine;
emptyLineBuffer = [];
} else {
// Đây là một tiêu đề bị lặp, xử lý như nội dung chương thông thường
if (currentChapter.title) {
// Thêm các dòng trống đã lưu trữ
currentChapter.content.push(...emptyLineBuffer);
emptyLineBuffer = [];
// Thêm dòng hiện tại
currentChapter.content.push(line);
}
inDuplicatedTitleBlock = true;
duplicateCount++;
}
} else if (currentChapter.title) {
// Giữ nguyên định dạng gốc của dòng (không trim),
// bao gồm cả khoảng trắng, xuống dòng và định dạng đặc biệt
currentChapter.content.push(line);
}
}
if (currentChapter.title && currentChapter.content.length > 0) {
chapters.push({...currentChapter});
}
if (chapters.length > 0) {
displayChapters(chapters);
if (duplicateCount > 0) {
showNotification(`Đã tìm thấy ${chapters.length} chương và bỏ qua ${duplicateCount} tiêu đề lặp lại.`);
} else {
showNotification(`Đã tìm thấy ${chapters.length} chương và tự động điền vào 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);
}
return chapters;
}
// Tạo HTML cho form chương
function createChapterHTML(chapNum) {
const chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
const chap_vol_name = jQuery('.chap_vol_name').val() || '';
return `
<div data-gen="MK_GEN" id="COUNT_CHAP_${chapNum}_MK">
<div class="col-xs-12 form-group"></div>
<div class="form-group">
<label class="col-sm-2" for="chap_stt">STT</label>
<div class="col-sm-8">
<input class="form-control" required name="chap_stt[${chapNum}]" value="${dăngnhanhTTV.STATE.CHAP_STT}" placeholder="Số thứ tự của chương" type="text"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2" for="chap_number">Chương thứ..</label>
<div class="col-sm-8">
<input value="${dăngnhanhTTV.STATE.CHAP_SERIAL}" required class="form-control" name="chap_number[${chapNum}]" placeholder="Chương thứ.. (1,2,3..)" type="text"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2" for="chap_name">Quyển số</label>
<div class="col-sm-8">
<input class="form-control" name="vol[${chapNum}]" placeholder="Quyển số" type="number" value="${chap_vol}" required/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2" for="chap_name">Tên quyển</label>
<div class="col-sm-8">
<input class="form-control chap_vol_name" name="vol_name[${chapNum}]" placeholder="Tên quyển" type="text" value="${chap_vol_name}" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2" for="chap_name">Tên chương</label>
<div class="col-sm-8">
<input required class="form-control" name="chap_name[${chapNum}]" placeholder="Tên chương" type="text"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2" for="introduce">Nội dung</label>
<div class="col-sm-8">
<textarea maxlength="75000" style="color:#000;font-weight: 400;" required class="form-control" name="introduce[${chapNum}]" rows="20" placeholder="Nội dung" type="text"></textarea>
<div class="chapter-character-count"></div>
</div>
</div>
<div class="form-group">
<label class="col-sm-2" for="adv">Quảng cáo</label>
<div class="col-sm-8">
<textarea maxlength="1000" class="form-control" name="adv[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea>
</div>
</div>
</div>`;
}
// Chuyển nội dung vào form
async function transferContent() {
try {
const chapterItems = document.querySelectorAll('.ttv-chapter-item');
if (!chapterItems.length) {
throw new Error('Không tìm thấy chương nào để điền vào form');
}
const form = document.querySelector('form[name="postChapForm"]');
if (!form) {
throw new Error('Không tìm thấy form đăng chương');
}
let chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
let chap_vol_name = jQuery('.chap_vol_name').val() || '';
let maxChapStt = 0;
let maxChapSerial = 0;
const existingForms = form.querySelectorAll('[id^="COUNT_CHAP_"]');
existingForms.forEach(formElem => {
const formIdMatch = formElem.id.match(/COUNT_CHAP_(\d+)_MK/);
if (formIdMatch && formIdMatch[1]) {
const formIndex = parseInt(formIdMatch[1]);
const sttInput = formElem.querySelector(`input[name="chap_stt[${formIndex}]"]`);
if (sttInput && sttInput.value && !isNaN(parseInt(sttInput.value))) {
const sttVal = parseInt(sttInput.value);
if (sttVal > maxChapStt) {
maxChapStt = sttVal;
}
}
const serialInput = formElem.querySelector(`input[name="chap_number[${formIndex}]"]`);
if (serialInput && serialInput.value && !isNaN(parseInt(serialInput.value))) {
const serialVal = parseInt(serialInput.value);
if (serialVal > maxChapSerial) {
maxChapSerial = serialVal;
}
}
}
});
if (maxChapStt > 0) {
dăngnhanhTTV.STATE.CHAP_STT = maxChapStt;
}
if (maxChapSerial > 0) {
dăngnhanhTTV.STATE.CHAP_SERIAL = maxChapSerial;
}
const existingFormCount = existingForms.length;
for (let i = 0; i < chapterItems.length; i++) {
const formIndex = existingFormCount + i + 1;
dăngnhanhTTV.STATE.CHAP_STT++;
dăngnhanhTTV.STATE.CHAP_SERIAL++;
const chapterHTML = createChapterHTML(formIndex);
const tempDiv = document.createElement('div');
tempDiv.innerHTML = chapterHTML;
const newFormElement = tempDiv.firstElementChild;
if (!newFormElement) {
throw new Error(`Không thể tạo element form cho chương ${formIndex}`);
}
form.appendChild(newFormElement);
const chapterItem = chapterItems[i];
const titleElement = chapterItem.querySelector('.chapter-title');
const nameElement = chapterItem.querySelector('.chapter-name');
if (!titleElement || !nameElement) {
throw new Error(`Thiếu thông tin tiêu đề hoặc tên cho chương ${formIndex}`);
}
const chapterTitle = titleElement.textContent;
const chapterName = nameElement.textContent.replace('Tên chương: ', '');
const formFields = {
chapterName: form.querySelector(`input[name="chap_name[${formIndex}]"]`),
content: form.querySelector(`textarea[name="introduce[${formIndex}]"]`),
chapterNumber: form.querySelector(`input[name="chap_number[${formIndex}]"]`),
chapterOrder: form.querySelector(`input[name="chap_stt[${formIndex}]"]`),
volume: form.querySelector(`input[name="vol[${formIndex}]"]`),
volumeName: form.querySelector(`input[name="vol_name[${formIndex}]"]`),
advertisement: form.querySelector(`textarea[name="adv[${formIndex}]"]`)
};
formFields.chapterName.value = chapterName;
formFields.chapterNumber.value = dăngnhanhTTV.STATE.CHAP_SERIAL.toString();
formFields.chapterOrder.value = dăngnhanhTTV.STATE.CHAP_STT.toString();
formFields.volume.value = chap_vol;
formFields.volumeName.value = chap_vol_name;
if (chapterItem._content) {
// Đảm bảo giữ nguyên định dạng gốc của nội dung, không trim
formFields.content.value = chapterItem._content;
} else {
formFields.content.value = '';
}
formFields.advertisement.value = '';
const inputEvent = new Event('input', { bubbles: true });
formFields.content.dispatchEvent(inputEvent);
}
showNotification(`Đã tự động đ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);
}
}
// 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';
// Đảm bảo giữ nguyên định dạng gốc bằng cách không trim giá trị
chapterItem._content = chapter.title + '\n' + chapter.content.join('\n');
// Trích xuất số chương để hiển thị
const chapterMatch = chapter.title.match(/Chương\s+(\d+):/);
const chapterNum = chapterMatch ? chapterMatch[1] : '?';
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 = `Chương ${chapterNum} | ${chapter.content.length} dòng`;
chapterItem.appendChild(titleDiv);
chapterItem.appendChild(nameDiv);
chapterItem.appendChild(statsDiv);
chapterItem.onclick = () => selectChapter(chapter, index);
chapterList.appendChild(chapterItem);
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);
if (chapters.length > 0) {
selectChapter(chapters[0], 0);
}
}
// Chọn chương
function selectChapter(chapter, index) {
const contentEditor = document.querySelector('.ttv-content-editor');
// Giữ nguyên định dạng gốc của nội dung
contentEditor.value = chapter.title + '\n' + chapter.content.join('\n');
const wordCountSpan = document.querySelector('.ttv-word-count');
if (wordCountSpan) {
wordCountSpan.textContent = updateWordCount(contentEditor.value);
}
const chapterItems = document.querySelectorAll('.ttv-chapter-item');
chapterItems.forEach(item => item.classList.remove('selected'));
chapterItems[index]?.classList.add('selected');
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);
}
}
// Tính số từ
function updateWordCount(content) {
const wordCount = content.trim().split(/\s+/).length;
const charCount = content.length;
return `${wordCount} từ | ${charCount} ký tự`;
}
// 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';
}
}
// 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';
}
// Tạo panel điều khiển
function addControlPanel() {
const panel = document.createElement('div');
panel.className = 'ttv-control-panel';
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';
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...';
const preview = document.createElement('div');
preview.className = 'ttv-preview';
contentEditor.oninput = () => {
const wordCountSpan = document.querySelector('.ttv-word-count');
if (wordCountSpan) {
wordCountSpan.textContent = updateWordCount(contentEditor.value);
}
};
contentEditor.onpaste = (e) => {
setTimeout(() => {
const content = contentEditor.value;
const chapters = parseChapters(content);
if (chapters.length > 0) {
displayChapters(chapters);
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);
}
}, 0);
};
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;
panel.appendChild(header);
buttonGroup.appendChild(contentEditorLabel);
buttonGroup.appendChild(contentEditor);
buttonGroup.appendChild(preview);
buttonGroup.appendChild(transferBtn);
panel.appendChild(buttonGroup);
document.body.appendChild(panel);
const minimizeBtn = panel.querySelector('.ttv-minimize');
minimizeBtn.onclick = () => {
panel.classList.toggle('minimized');
minimizeBtn.innerHTML = panel.classList.contains('minimized') ? '+' : '−';
};
window.togglePreview = togglePreview;
window.toggleFullscreen = toggleFullscreen;
}
window.addEventListener('load', function() {
dăngnhanhTTV.initializeChapterValues();
addControlPanel();
});
})();