// ==UserScript==
// @name TTV Auto Upload
// @namespace http://tampermonkey.net/
// @version 0.5
// @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;
}
#ttv-panel textarea {
width: 100%;
height: 150px;
margin: 10px 0;
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
resize: vertical;
}
#ttv-panel .btn-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
#ttv-panel button {
flex: 1;
padding: 10px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
#ttv-panel button:hover {
opacity: 0.9;
}
#ttv-panel .btn-auto {
background: #4CAF50;
color: white;
}
#ttv-panel .btn-manual {
background: #2196F3;
color: white;
}
.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: {
isAutoMode: false,
chapterNumber: 1,
chapterSTT: 1,
chapterSerial: 1
},
init: function() {
this.createInterface();
this.setupEventListeners();
this.setupCharacterCounter();
console.log('[TTV] Script khởi động thành công');
},
createInterface: function() {
const panel = document.createElement('div');
panel.id = 'ttv-panel';
panel.innerHTML = `
<h3 style="margin: 0 0 15px; color: #333;">📝 ĐĂNG CHƯƠNG</h3>
<textarea id="ttv-content" placeholder="Dán nội dung vào đây để 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 nhanh</button>
</div>
<div id="ttv-notification" style="margin-top: 10px;"></div>
`;
document.body.appendChild(panel);
},
setupEventListeners: function() {
const content = document.getElementById('ttv-content');
const autoBtn = document.getElementById('ttv-auto');
const manualBtn = document.getElementById('ttv-manual');
// Xử lý paste
content.addEventListener('paste', (e) => {
e.preventDefault();
const text = e.clipboardData.getData('text');
content.value = text;
this.processContent(text, false);
});
// Nút đăng tự động
autoBtn.addEventListener('click', () => {
this.STATE.isAutoMode = true;
navigator.clipboard.readText()
.then(text => {
content.value = text;
this.processContent(text, true);
})
.catch(() => this.showNotification('Không thể đọc clipboard', 'error'));
});
// Nút đăng nhanh
manualBtn.addEventListener('click', () => {
this.STATE.isAutoMode = false;
this.processContent(content.value, false);
});
},
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: #f44336;">${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>`;
}
}
});
},
processContent: function(text, autoPost = false) {
if (!text) {
this.showNotification('Vui lòng nhập nội dung', '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;
}
// 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 chương còn lại vào clipboard
if (remainingChapters.length > 0) {
this.copyRemainingChapters(remainingChapters);
}
// Tự động đăng nếu ở chế độ tự động
if (autoPost) {
setTimeout(() => this.submitChapters(), 2000);
}
},
splitChapters: function(text) {
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) {
if (currentChapter.length > 0) {
chapters.push(currentChapter.join('\n'));
currentChapter = [line];
lastTitle = line;
} else {
currentChapter = [line];
lastTitle = line;
}
} else if (currentChapter.length > 0) {
currentChapter.push(line);
}
}
if (currentChapter.length > 0) {
chapters.push(currentChapter.join('\n'));
}
return chapters;
},
fillChaptersToForm: function(chapters) {
// 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);
});
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(() => {
this.showNotification(`Đã copy ${chapters.length} chương còn lại vào clipboard`, 'info');
});
} catch (error) {
console.error('[TTV] Lỗi copy clipboard:', error);
this.showNotification('Không thể copy vào clipboard', 'error');
}
},
submitChapters: function() {
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 các 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;
}
// Đếm số chương
const chapterCount = document.querySelectorAll('textarea[name^="introduce"]').length;
if (chapterCount === 0) {
this.showNotification('Không có chương nào để đăng', '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] Số chương còn lại:', remainingChapters);
if (remainingChapters < 10 && this.STATE.isAutoMode) {
this.STATE.isAutoMode = false;
this.showNotification(`Còn ${remainingChapters} chương, dưới 10 chương nên đã tắt chế độ tự động`, 'warning');
} else if (this.STATE.isAutoMode) {
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);
},
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] ${type.toUpperCase()}: ${message}`);
}
};
// Khởi động script
TTVManager.init();
})();