// ==UserScript==
// @name TTV Auto Upload
// @namespace http://tampermonkey.net/
// @version 1.1
// @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;
display: none;
}
#ttv-chapters.has-chapters {
display: block;
}
#ttv-content {
width: 100%;
height: 150px;
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;
}
#ttv-panel button:hover {
opacity: 0.9;
}
#ttv-panel button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.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;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.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.createFormContainer();
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() {
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);
},
createFormContainer: function() {
let formContainer = document.querySelector('#div_chapt_upload');
if (!formContainer) {
formContainer = document.createElement('div');
formContainer.id = 'div_chapt_upload';
let parent = document.querySelector('.tab-content');
if (!parent) {
parent = document.createElement('div');
parent.className = 'tab-content';
document.body.appendChild(parent);
}
parent.appendChild(formContainer);
console.log('[TTV-DEBUG] Created form container');
}
return formContainer;
},
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');
content.addEventListener('paste', (e) => {
e.preventDefault();
console.log('[TTV-DEBUG] Content pasted');
const text = e.clipboardData.getData('text');
content.value = text;
this.processContent(text);
});
let inputTimer;
content.addEventListener('input', () => {
clearTimeout(inputTimer);
inputTimer = setTimeout(() => {
const text = content.value;
if (text && text.length > 0) {
console.log('[TTV-DEBUG] Processing input content');
this.processContent(text);
}
}, 500);
});
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;
const text = content.value;
if (!text) {
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;
return;
}
this.processContent(text);
});
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;
const text = content.value;
if (!text) {
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;
return;
}
this.processContent(text);
});
},
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 class="short-chapters-warning">${charCount.toLocaleString()}/40.000 ký tự</span>`;
} else {
e.target.classList.remove('short-chapter');
counter.innerHTML = `<span style="color: ${charCount > 40000 ? '#fbbc05' : '#34a853'}">${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;
chapterList.classList.toggle('has-chapters', chapters.length > 0);
console.log('[TTV-DEBUG] Chapter list updated');
this.fillChapterForms(chapters.slice(0, MAX_CHAPTER_POST));
},
processContent: function(text) {
try {
this.showLoading('Đang tách chương...');
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`);
this.updateChapterList(chapters);
const remainingChapters = chapters.slice(MAX_CHAPTER_POST);
if (remainingChapters.length > 0) {
this.copyRemainingChapters(remainingChapters);
}
if (this.STATE.isAuto && chapters.length >= MAX_CHAPTER_POST) {
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ó ${chapters.length} chương)`, 'warning');
this.STATE.isAuto = false;
}
} catch (error) {
console.error('[TTV-ERROR] Content processing error:', error);
this.showNotification('Có lỗi khi xử lý nội dung', 'error');
} finally {
this.hideLoading();
this.STATE.isProcessing = false;
document.getElementById('ttv-auto').disabled = false;
document.getElementById('ttv-manual').disabled = false;
}
},
splitChapters: function(text) {
console.log('[TTV-DEBUG] Starting chapter splitting...');
const chapters = [];
const lines = text.split('\n');
let currentChapter = [];
const chapterPattern = /^[\s\t]*[Cc]hương\s+(\d+)(?:\s*[::\.]|$)/;
const chapterNumbers = new Map();
function getChapterNumber(line) {
const match = line.match(chapterPattern);
return match ? parseInt(match[1]) : null;
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const chapterNum = getChapterNumber(line);
if (chapterNum !== null) {
if (currentChapter.length > 0) {
chapters.push(currentChapter.join('\n'));
}
if (chapterNumbers.has(chapterNum)) {
console.log(`[TTV-DEBUG] Skip duplicate chapter ${chapterNum}`);
continue;
}
chapterNumbers.set(chapterNum, true);
currentChapter = [line];
console.log(`[TTV-DEBUG] Found chapter ${chapterNum}: ${line.trim()}`);
} else if (currentChapter.length > 0) {
currentChapter.push(line);
}
}
if (currentChapter.length > 0) {
chapters.push(currentChapter.join('\n'));
}
chapters.sort((a, b) => {
const numA = getChapterNumber(a.split('\n')[0]) || 0;
const numB = getChapterNumber(b.split('\n')[0]) || 0;
return numA - numB;
});
console.log(`[TTV-DEBUG] Split complete. Found ${chapters.length} chapters`);
return chapters;
},
fillChapterForms: function(chapters) {
if (!chapters || chapters.length === 0) {
console.log('[TTV-DEBUG] No chapters to fill');
return;
}
console.log('[TTV-DEBUG] Filling chapter forms');
const formContainer = this.createFormContainer();
formContainer.innerHTML = '';
chapters.forEach((chapter, index) => {
const lines = chapter.split('\n');
const title = lines.shift().trim();
const content = lines.join('\n');
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" value="${title.includes(':') ? title.split(':')[1].trim() : title}"/>
</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">${HEADER_SIGN}\n${content}\n${FOOTER_SIGN}</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>`;
formContainer.insertAdjacentHTML('beforeend', formHtml);
const textarea = formContainer.querySelector(`textarea[name="introduce[${this.STATE.chapterNumber}]"]`);
if (textarea) {
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
}
});
console.log(`[TTV-DEBUG] Created ${chapters.length} chapter forms`);
this.showNotification(`Đã điền ${chapters.length} chương vào form`, 'success');
},
copyRemainingChapters: function(chapters) {
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;
}
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;
}
submitBtn.click();
this.showNotification('Đang đăng chương...', 'info');
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.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');
this.showNotification('Đang tải lại trang để tiếp tục đăng...', 'info');
setTimeout(() => window.location.reload(), 2000);
}
}, 3000);
},
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 === 'warning' ? '#fff3e0' : '#e8f5e9'};
color: ${type === 'error' ? '#c62828' : type === 'warning' ? '#ef6c00' : '#2e7d32'};
border: 1px solid ${type === 'error' ? '#ffcdd2' : type === 'warning' ? '#ffe0b2' : '#c8e6c9'};
">${message}</div>
`;
}
};
TTVManager.init();
})();