// ==UserScript==
// @name TTV Chapter Manager
// @namespace http://tampermonkey.net/
// @version 0.9
// @description Công cụ đăng chương đơn giản cho Tàng Thư Viện
// @author HA
// @match https://tangthuvien.net/dang-chuong/story/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const HEADER_SIGN = "";
const FOOTER_SIGN = "";
const MAX_CHAPTER_POST = 10;
const style = document.createElement('style');
style.textContent = `
#ttv-panel {
position: fixed;
top: 50px;
right: 20px;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
width: 400px;
z-index: 9998;
max-height: 90vh;
overflow-y: auto;
}
#ttv-chapters {
width: 100%;
margin-bottom: 15px;
border: 1px solid #eee;
border-radius: 8px;
max-height: 300px;
overflow-y: auto;
background: #fafafa;
}
#ttv-content {
width: 100%;
height: 120px;
margin-bottom: 15px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
font-family: monospace;
transition: border-color 0.2s;
resize: vertical;
}
#ttv-content:focus {
border-color: #4CAF50;
outline: none;
}
.chapter-item {
padding: 15px;
border-bottom: 1px solid #eee;
background: white;
transition: all 0.2s;
}
.chapter-item:hover {
background: #f5f5f5;
}
.chapter-item:last-child {
border-bottom: none;
}
.chapter-title {
font-weight: 600;
margin-bottom: 8px;
color: #333;
font-size: 14px;
}
.chapter-stats {
font-size: 12px;
color: #666;
display: flex;
gap: 10px;
align-items: center;
}
.chapter-warning {
color: #ff0000;
font-weight: 500;
padding: 2px 6px;
background: rgba(255,0,0,0.1);
border-radius: 4px;
}
.chapter-long {
color: #ff9800;
font-weight: 500;
padding: 2px 6px;
background: rgba(255,152,0,0.1);
border-radius: 4px;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
#ttv-panel button {
flex: 1;
padding: 12px 15px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
position: relative;
}
#ttv-panel button:hover {
opacity: 0.9;
}
#ttv-panel button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
#ttv-panel button.processing:after {
content: "";
position: absolute;
width: 20px;
height: 20px;
top: calc(50% - 10px);
right: 10px;
border: 2px solid rgba(255,255,255,0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.btn-auto {
background: #4CAF50;
color: white;
}
.btn-manual {
background: #2196F3;
color: white;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-content {
background: white;
padding: 20px;
border-radius: 10px;
text-align: center;
}
.loading-spinner {
width: 40px;
height: 40px;
margin: 0 auto 10px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.chapter-character-count {
text-align: right;
font-size: 12px;
margin-top: 5px;
color: #666;
}
textarea[name^="introduce"].short-chapter {
border: 2px solid #ff0000 !important;
background-color: rgba(255,0,0,0.1) !important;
animation: shortChapterBlink 1s infinite;
}
@keyframes shortChapterBlink {
0% { background-color: rgba(255,0,0,0.1); }
50% { background-color: rgba(255,0,0,0.2); }
100% { background-color: rgba(255,0,0,0.1); }
}
`;
document.head.appendChild(style);
const TTVManager = {
STATE: {
chapterNumber: 1,
chapterSTT: 1,
chapterSerial: 1,
isAuto: false,
isProcessing: false
},
init: function() {
console.log('[TTV-DEBUG] Initializing script...');
this.initializeChapterValues();
this.createInterface();
this.setupEventListeners();
this.setupCharacterCounter();
console.log('[TTV-DEBUG] Script initialized successfully');
this.showNotification('Công cụ đã sẵn sàng', 'success');
},
createInterface: function() {
console.log('[TTV-DEBUG] Creating interface');
const panel = document.createElement('div');
panel.id = 'ttv-panel';
panel.innerHTML = `
<h3 style="margin: 0 0 15px; color: #333; text-align: center;">📝 ĐĂNG CHƯƠNG</h3>
<div id="ttv-chapters"></div>
<textarea id="ttv-content" placeholder="Dán nội dung vào đây để tự động tách chương..."></textarea>
<div class="btn-group">
<button class="btn-auto" id="ttv-auto">🔄 Đăng tự động</button>
<button class="btn-manual" id="ttv-manual">📝 Đăng thủ công</button>
</div>
<div id="ttv-notification" style="margin-top: 10px;"></div>
`;
document.body.appendChild(panel);
},
initializeChapterValues: function() {
try {
const chap_number = parseInt(jQuery('#chap_number').val()) || 1;
let chap_stt = parseInt(jQuery('.chap_stt1').val()) || 1;
let chap_serial = parseInt(jQuery('.chap_serial').val()) || 1;
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.chapterNumber = chap_number;
this.STATE.chapterSTT = chap_stt;
this.STATE.chapterSerial = chap_serial;
console.log('[TTV-DEBUG] Chapter values initialized:', this.STATE);
} catch (e) {
console.error('[TTV-ERROR] Error initializing chapter values:', e);
}
},
setupEventListeners: function() {
const content = document.getElementById('ttv-content');
const autoBtn = document.getElementById('ttv-auto');
const manualBtn = document.getElementById('ttv-manual');
// Xử lý paste vào textarea
content.addEventListener('paste', (e) => {
e.preventDefault();
const text = e.clipboardData.getData('text');
content.value = text;
this.processContent(text);
});
// Xử lý input trực tiếp
content.addEventListener('input', () => {
const text = content.value;
if (text) {
this.processContent(text);
}
});
// Nút đăng tự động
autoBtn.addEventListener('click', () => {
if (this.STATE.isProcessing) return;
console.log('[TTV-DEBUG] Auto button clicked');
this.STATE.isAuto = true;
this.STATE.isProcessing = true;
autoBtn.disabled = true;
manualBtn.disabled = true;
autoBtn.classList.add('processing');
const text = content.value;
if (text) {
this.processContent(text);
} else {
this.showNotification('Vui lòng nhập hoặc dán nội dung trước', 'error');
}
this.STATE.isProcessing = false;
autoBtn.disabled = false;
manualBtn.disabled = false;
autoBtn.classList.remove('processing');
});
// Nút đăng thủ công
manualBtn.addEventListener('click', () => {
if (this.STATE.isProcessing) return;
console.log('[TTV-DEBUG] Manual button clicked');
this.STATE.isAuto = false;
this.STATE.isProcessing = true;
manualBtn.disabled = true;
autoBtn.disabled = true;
manualBtn.classList.add('processing');
const text = content.value;
if (text) {
this.processContent(text);
} else {
this.showNotification('Vui lòng nhập hoặc dán nội dung trước', 'error');
}
this.STATE.isProcessing = false;
manualBtn.disabled = false;
autoBtn.disabled = false;
manualBtn.classList.remove('processing');
});
},
setupCharacterCounter: function() {
document.addEventListener('input', (e) => {
if (e.target.matches('textarea[name^="introduce"]')) {
const text = e.target.value;
const charCount = text.length;
let counter = e.target.nextElementSibling;
if (!counter || !counter.classList.contains('chapter-character-count')) {
counter = document.createElement('div');
counter.className = 'chapter-character-count';
e.target.parentNode.insertBefore(counter, e.target.nextSibling);
}
if (charCount < 3000) {
e.target.classList.add('short-chapter');
counter.innerHTML = `<span style="color: #ff0000;">${charCount.toLocaleString()}/40.000 ký tự</span>`;
} else {
e.target.classList.remove('short-chapter');
counter.innerHTML = `<span style="color: ${charCount > 40000 ? '#ff9800' : '#4caf50'}">${charCount.toLocaleString()}/40.000 ký tự</span>`;
}
}
});
},
updateChapterList: function(chapters) {
console.log('[TTV-DEBUG] Updating chapter list');
const chapterList = document.getElementById('ttv-chapters');
let html = '';
chapters.forEach((chapter, index) => {
const lines = chapter.split('\n');
const title = lines.shift().trim();
const content = lines.join('\n');
const charCount = content.length;
html += `
<div class="chapter-item">
<div class="chapter-title">${title}</div>
<div class="chapter-stats">
<span>Số ký tự: ${charCount.toLocaleString()}</span>
${charCount < 3000 ? '<span class="chapter-warning">⚠️ Thiếu</span>' : ''}
${charCount > 40000 ? '<span class="chapter-long">⚠️ Dài</span>' : ''}
</div>
</div>
`;
});
chapterList.innerHTML = html;
},
processContent: function(text) {
console.log('[TTV-DEBUG] Processing content, auto mode:', this.STATE.isAuto);
if (!text) {
this.showNotification('Không có nội dung để xử lý', 'error');
return;
}
const chapters = this.splitChapters(text);
if (chapters.length === 0) {
this.showNotification('Không tìm thấy chương nào', 'error');
return;
}
console.log(`[TTV-DEBUG] Found ${chapters.length} chapters`);
// Cập nhật danh sách chương
this.updateChapterList(chapters);
// Lấy 10 chương đầu
const chaptersToFill = chapters.slice(0, MAX_CHAPTER_POST);
const remainingChapters = chapters.slice(MAX_CHAPTER_POST);
// Điền form
this.fillChaptersToForm(chaptersToFill);
// Copy các chương còn lại vào clipboard
if (remainingChapters.length > 0) {
this.copyRemainingChapters(remainingChapters);
}
// Nếu đang ở chế độ tự động và có đủ 10 chương, tự động đăng
if (this.STATE.isAuto && chaptersToFill.length === 10) {
this.showNotification('Sẽ tự động đăng sau 2 giây...', 'info');
setTimeout(() => {
this.submitChapters();
}, 2000);
} else if (this.STATE.isAuto) {
this.showNotification(`Cần đủ 10 chương để tự động đăng (hiện có ${chaptersToFill.length} chương)`, 'warning');
}
},
splitChapters: function(text) {
console.log('[TTV-DEBUG] Splitting chapters');
const chapters = [];
const lines = text.split('\n');
let currentChapter = [];
let lastTitle = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const isChapterTitle = /^\t[Cc]hương\s*\d+\s*:/.test(line) || /^\s{4,}[Cc]hương\s*\d+\s*:/.test(line);
if (isChapterTitle) {
const currentChapterCode = getChapterCode(line);
const lastTitleCode = lastTitle ? getChapterCode(lastTitle) : null;
if (currentChapter.length > 0) {
// Kiểm tra nếu chương hiện tại khác chương trước đó
if (currentChapterCode !== lastTitleCode) {
chapters.push(currentChapter.join('\n'));
currentChapter = [line];
lastTitle = line;
console.log(`[TTV-DEBUG] Found new chapter: ${currentChapterCode}`);
} else {
console.log(`[TTV-DEBUG] Skipped duplicate chapter: ${currentChapterCode}`);
}
} else {
currentChapter = [line];
lastTitle = line;
console.log(`[TTV-DEBUG] Started first chapter: ${currentChapterCode}`);
}
} else if (currentChapter.length > 0) {
currentChapter.push(line);
}
}
if (currentChapter.length > 0) {
chapters.push(currentChapter.join('\n'));
}
return chapters;
},
fillChaptersToForm: function(chapters) {
console.log('[TTV-DEBUG] Filling form with chapters');
this.showLoading('Đang điền nội dung vào form...');
try {
// Thêm form cho đủ số chương
while (document.querySelectorAll('input[name^="chap_name"]').length < chapters.length) {
this.addNewChapterForm();
}
const titles = document.querySelectorAll('input[name^="chap_name"]');
const contents = document.querySelectorAll('textarea[name^="introduce"]');
const advs = document.querySelectorAll('textarea[name^="adv"]');
chapters.forEach((chapter, index) => {
if (index >= titles.length) return;
const lines = chapter.split('\n');
const title = lines.shift().trim();
let chapterName = title.includes(':') ? title.split(':')[1].trim() : title;
chapterName = chapterName || 'Vô đề';
titles[index].value = chapterName;
contents[index].value = HEADER_SIGN + "\n" + lines.join('\n') + "\n" + FOOTER_SIGN;
if (advs[index]) advs[index].value = '';
// Trigger character counter
const event = new Event('input', { bubbles: true });
contents[index].dispatchEvent(event);
});
console.log(`[TTV-DEBUG] Filled ${chapters.length} chapters into form`);
this.showNotification(`Đã điền ${chapters.length} chương vào form`, 'success');
} catch (error) {
console.error('[TTV-ERROR] Form filling error:', error);
this.showNotification('Có lỗi khi điền nội dung vào form', 'error');
} finally {
this.hideLoading();
}
},
copyRemainingChapters: function(chapters) {
console.log('[TTV-DEBUG] Copying remaining chapters to clipboard');
try {
const content = chapters.map(chapter => {
const lines = chapter.trim().split('\n');
if (lines[0] && !lines[0].startsWith('\t')) {
lines[0] = '\t' + lines[0];
}
return lines.join('\n');
}).join('\n\n');
navigator.clipboard.writeText(content)
.then(() => {
console.log(`[TTV-DEBUG] Copied ${chapters.length} chapters to clipboard`);
this.showNotification(`Đã copy ${chapters.length} chương còn lại vào clipboard`, 'info');
})
.catch(err => {
console.error('[TTV-ERROR] Clipboard write error:', err);
this.showNotification('Không thể copy vào clipboard', 'error');
});
} catch (error) {
console.error('[TTV-ERROR] Copy process error:', error);
this.showNotification('Có lỗi khi copy các chương còn lại', 'error');
}
},
submitChapters: function() {
console.log('[TTV-DEBUG] Submitting chapters');
const submitBtn = document.querySelector('button[type="submit"]');
if (!submitBtn) {
this.showNotification('Không tìm thấy nút đăng chương!', 'error');
return;
}
// Kiểm tra độ dài chương
const shortChapters = Array.from(document.querySelectorAll('textarea[name^="introduce"]'))
.filter(textarea => textarea.value.length < 3000);
if (shortChapters.length > 0) {
this.showNotification(`Có ${shortChapters.length} chương chưa đủ 3000 ký tự`, 'error');
return;
}
// Đăng chương
submitBtn.click();
this.showNotification('Đang đăng chương...', 'info');
// Kiểm tra sau khi đăng
setTimeout(() => {
const remainingChapters = document.querySelectorAll('textarea[name^="introduce"]').length;
console.log('[TTV-DEBUG] Chapters remaining after submit:', remainingChapters);
if (remainingChapters < 10 && this.STATE.isAuto) {
console.log('[TTV-DEBUG] Less than 10 chapters remaining, stopping auto mode');
this.STATE.isAuto = false;
this.STATE.isProcessing = false;
this.showNotification(`Còn ${remainingChapters} chương, dưới 10 chương nên đã dừng tự động`, 'warning');
} else if (this.STATE.isAuto) {
console.log('[TTV-DEBUG] Reloading page for next batch');
setTimeout(() => window.location.reload(), 2000);
}
}, 3000);
},
addNewChapterForm: function() {
this.STATE.chapterNumber++;
this.STATE.chapterSTT++;
this.STATE.chapterSerial++;
const formHtml = `
<div data-gen="MK_GEN" id="COUNT_CHAP_${this.STATE.chapterNumber}_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[${this.STATE.chapterNumber}]" value="${this.STATE.chapterSTT}" 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="${this.STATE.chapterSerial}" required class="form-control" name="chap_number[${this.STATE.chapterNumber}]" 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[${this.STATE.chapterNumber}]" value="1" placeholder="Quyển số" type="number" 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[${this.STATE.chapterNumber}]" placeholder="Tên quyển" type="text" />
</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[${this.STATE.chapterNumber}]" 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[${this.STATE.chapterNumber}]" 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[${this.STATE.chapterNumber}]" placeholder="Quảng cáo" type="text"></textarea>
</div>
</div>
</div>`;
document.querySelector('#div_chapt_upload').insertAdjacentHTML('beforeend', formHtml);
console.log(`[TTV-DEBUG] Added new chapter form #${this.STATE.chapterNumber}`);
},
showLoading: function(message = 'Đang xử lý...') {
const overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.innerHTML = `
<div class="loading-content">
<div class="loading-spinner"></div>
<div>${message}</div>
</div>
`;
document.body.appendChild(overlay);
},
hideLoading: function() {
const overlay = document.querySelector('.loading-overlay');
if (overlay) overlay.remove();
},
showNotification: function(message, type = 'info') {
const notification = document.getElementById('ttv-notification');
notification.innerHTML = `
<div style="
padding: 10px 15px;
border-radius: 6px;
background-color: ${type === 'error' ? '#ffebee' : type === 'success' ? '#e8f5e9' : type === 'warning' ? '#fff3e0' : '#e3f2fd'};
color: ${type === 'error' ? '#c62828' : type === 'success' ? '#1b5e20' : type === 'warning' ? '#e65100' : '#0d47a1'};
border: 1px solid ${type === 'error' ? '#ef9a9a' : type === 'success' ? '#a5d6a7' : type === 'warning' ? '#ffcc80' : '#90caf9'};
">
${message}
</div>
`;
console.log(`[TTV-DEBUG] ${type.toUpperCase()}: ${message}`);
}
};
// Lấy mã chương dựa vào tiêu đề
function getChapterCode(title) {
const match = title.match(/[Cc]hương\s*\d+\s*:/);
if (!match) return title.trim();
const chapterNum = match[1];
return `chap_${chapterNum}`;
}
// Initialize script
TTVManager.init();
})();