// ==UserScript==
// @name TTV Auto Upload
// @namespace http://tampermonkey.net/
// @version 1.3
// @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/*
// @match https://tangthuvien.net/danh-sach-chuong/story/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
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-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
#ttv-header h3 {
color: #2196F3;
margin: 0;
font-size: 18px;
font-weight: 600;
}
#ttv-stats {
display: flex;
gap: 10px;
align-items: center;
font-size: 14px;
color: #666;
}
#ttv-stats .stat {
padding: 4px 8px;
border-radius: 6px;
background: #f5f5f5;
}
#ttv-content {
width: 100%;
height: 120px;
margin-bottom: 15px;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
font-family: monospace;
transition: all 0.2s ease;
resize: vertical;
background: #fff;
}
#ttv-content:focus {
border-color: #2196F3;
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
outline: none;
}
.btn-group {
display: flex;
gap: 10px;
margin: 15px 0;
}
#ttv-panel button {
flex: 1;
padding: 12px 15px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
position: relative;
color: white;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#ttv-panel button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#ttv-panel button:active {
transform: translateY(0);
}
#ttv-panel button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-auto {
background: linear-gradient(45deg, #4CAF50, #45a049);
}
.btn-manual {
background: linear-gradient(45deg, #2196F3, #1e88e5);
}
#ttv-panel button.processing {
pointer-events: none;
}
#ttv-panel button.processing:after {
content: "";
position: absolute;
width: 20px;
height: 20px;
right: 10px;
border: 2px solid rgba(255,255,255,0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
#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;
}
.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: #f44336;
font-weight: 500;
padding: 2px 6px;
background: rgba(244, 67, 54, 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;
}
#ttv-notification {
margin-top: 10px;
padding: 10px 15px;
border-radius: 8px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
opacity: 0;
transition: opacity 0.3s ease;
}
#ttv-notification.show {
opacity: 1;
}
#ttv-notification.success {
background-color: #e8f5e9;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
#ttv-notification.error {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
#ttv-notification.warning {
background-color: #fff3e0;
color: #ef6c00;
border: 1px solid #ffe0b2;
}
#ttv-notification.info {
background-color: #e3f2fd;
color: #1565c0;
border: 1px solid #bbdefb;
}
.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;
opacity: 0;
transition: opacity 0.3s ease;
}
.loading-overlay.show {
opacity: 1;
}
.loading-content {
background: white;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #2196F3;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
.chapter-character-count {
text-align: right;
font-size: 12px;
margin-top: 5px;
color: #666;
}
textarea[name^="introduce"].short-chapter {
border: 2px solid #f44336 !important;
background-color: rgba(244, 67, 54, 0.05) !important;
animation: shortChapterBlink 1s infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes shortChapterBlink {
0% { background-color: rgba(244, 67, 54, 0.05); }
50% { background-color: rgba(244, 67, 54, 0.1); }
100% { background: rgba(244, 67, 54, 0.05); }
}
.chapter-counter {
position: absolute;
top: -8px;
right: -8px;
background: #ff5722;
color: white;
border-radius: 12px;
padding: 2px 6px;
font-size: 12px;
font-weight: bold;
}
`;
document.head.appendChild(style);
const TTVManager = {
STATE: {
chapterNumber: 1,
chapterSTT: 1,
chapterSerial: 1,
isAuto: false,
isProcessing: false,
totalChapters: 0,
processedChapters: 0
},
init: function() {
console.log('[TTV-DEBUG] Initializing script...');
this.createFormContainer = this.createFormContainer.bind(this);
this.initializeChapterValues = this.initializeChapterValues.bind(this);
this.createInterface = this.createInterface.bind(this);
this.setupEventListeners = this.setupEventListeners.bind(this);
this.setupCharacterCounter = this.setupCharacterCounter.bind(this);
this.showNotification = this.showNotification.bind(this);
this.showLoading = this.showLoading.bind(this);
this.hideLoading = this.hideLoading.bind(this);
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 = `
<div id="ttv-header">
<h3>📝 ĐĂNG CHƯƠNG</h3>
<div id="ttv-stats">
<span class="stat" id="total-chapters">0 chương</span>
<span class="stat" id="processed-chapters">0/0</span>
</div>
</div>
<div id="ttv-chapters"></div>
<textarea
id="ttv-content"
placeholder="Dán nội dung vào đây để tự động tách chương...
Hỗ trợ các định dạng:
- Chương 1: ...
- [Tab]Chương 2: ...
- Chương 3: ..."
></textarea>
<div class="btn-group">
<button class="btn-auto" id="ttv-auto">
<span>🔄 Tự động đăng</span>
<div class="chapter-counter">0/10</div>
</button>
<button class="btn-manual" id="ttv-manual">
<span>📝 Đăng thủ công</span>
</button>
</div>
<div id="ttv-notification"></div>
`;
document.body.appendChild(panel);
},
updateStats: function() {
document.getElementById('total-chapters').textContent =
`${this.STATE.totalChapters} chương`;
document.getElementById('processed-chapters').textContent =
`${this.STATE.processedChapters}/${this.STATE.totalChapters}`;
document.querySelector('.chapter-counter').textContent =
`${Math.min(this.STATE.totalChapters, 10)}/10`;
},
createChapterHTML: function(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>`;
},
setupCharacterCounter: function() {
jQuery(document).on("input", "[name^=introduce]", function() {
const text = jQuery(this).val();
const charCount = text.length;
let charCountElement = jQuery(this).next('.chapter-character-count');
if (charCountElement.length === 0) {
charCountElement = jQuery('<div class="chapter-character-count"></div>');
jQuery(this).after(charCountElement);
}
if(charCount < 3000) {
jQuery(this).addClass('short-chapter');
charCountElement.html(`<span class="short-chapters-warning">${charCount.toLocaleString()}/40.000 ký tự</span>`);
} else {
jQuery(this).removeClass('short-chapter');
if(charCount > 40000) {
charCountElement.html(`<span style="color: #fbbc05;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
} else {
charCountElement.html(`<span style="color: #34a853;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
}
}
});
},
validateChapterLengths: function() {
let hasError = false;
jQuery('form[name="postChapForm"] .chapter-detail').each(function() {
const form = this;
const contentTextarea = form.querySelector('textarea[name^="introduce"]');
const content = contentTextarea.value;
if (content.length < 3000) {
jQuery(contentTextarea).addClass('short-chapter');
let warningIcon = form.querySelector('.warning-icon');
if (!warningIcon) {
warningIcon = document.createElement('div');
warningIcon.className = 'warning-icon';
warningIcon.innerHTML = '⚠️';
contentTextarea.parentNode.appendChild(warningIcon);
}
hasError = true;
} else {
jQuery(contentTextarea).removeClass('short-chapter');
const warningIcon = form.querySelector('.warning-icon');
if (warningIcon) {
warningIcon.remove();
}
}
});
return !hasError;
},
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.chapterNumber = this.STATE.chapterNumberOriginal = chap_number || 1;
this.STATE.chapterSTT = this.STATE.chapterSTTOriginal = chap_stt || 1;
this.STATE.chapterSerial = this.STATE.chapterSerialOriginal = chap_serial || 1;
} catch (e) {
console.error("Error initializing chapter values:", e);
}
},
showNotification: function(message, type = 'info') {
const notification = document.getElementById('ttv-notification');
let icon = '';
switch(type) {
case 'success': icon = '✅'; break;
case 'error': icon = '❌'; break;
case 'warning': icon = '⚠️'; break;
case 'info': icon = 'ℹ️'; break;
}
notification.className = type;
notification.innerHTML = `
<span class="notification-icon">${icon}</span>
${message}
`;
notification.classList.add('show');
if (type === 'success' || type === 'info') {
setTimeout(() => {
notification.classList.remove('show');
}, 5000);
}
},
showLoading: function(message = 'Đang xử lý...') {
let overlay = document.querySelector('.loading-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.innerHTML = `
<div class="loading-content">
<div class="loading-spinner"></div>
<div class="loading-message">${message}</div>
</div>
`;
document.body.appendChild(overlay);
setTimeout(() => overlay.classList.add('show'), 0);
}
},
hideLoading: function() {
const overlay = document.querySelector('.loading-overlay');
if (overlay) {
overlay.classList.remove('show');
setTimeout(() => overlay.remove(), 300);
}
},
// ...rest of the functions remain largely unchanged... (handlePaste, performAction, etc.)
updateChapNumber: function(isAdd) {
try{
if (isAdd) {
let maxStt =0;
let maxSerial = 0;
jQuery('input[name^="chap_stt"]').each(function() {
const val = parseInt(jQuery(this).val()) || 0;
maxStt = Math.max(maxStt, val);
});
jQuery('input[name^="chap_number"]').each(function() {
const val = parseInt(jQuery(this).val()) || 0;
maxSerial = Math.max(maxSerial, val);
});
const chapStt = parseInt(jQuery('.chap_stt1').val()) || 0;
const chapSerial = parseInt(jQuery('.chap_serial').val()) || 0;
maxStt = Math.max(maxStt, chapStt);
maxSerial = Math.max(maxSerial, chapSerial);
this.STATE.chapterSTT = maxStt + 1;
this.STATE.chapterSerial = maxSerial + 1;
this.STATE.chapterNumber++;
} else {
if (this.STATE.chapterNumber > this.STATE.chapterNumberOriginal) {
this.STATE.chapterNumber--;
}
if (this.STATE.chapterSTT > this.STATE.chapterSTTOriginal) {
this.STATE.chapterSTT--;
}
if (this.STATE.chapterSerial > this.STATE.chapterSerialOriginal) {
this.STATE.chapterSerial--;
}
}
jQuery('#chap_number').val(this.STATE.chapterNumber);
jQuery('#chap_stt').val(this.STATE.chapterSTT);
jQuery('#chap_serial').val(this.STATE.chapterSerial);
jQuery('#countNumberPost').text(this.STATE.chapterNumber);
} catch (e) {
console.log("Lỗi: " + e);
}
},
// ...rest of the functions remain largely unchanged... (removeEmptyChapters, toggleAutoPost, runAutoPostSequence, submitChapters, addNewChapter, resetAutoPost, toggleAutoMode)
setupEventListeners: function() {
jQuery('#ttv-content').on('paste', this.handlePaste.bind(this));
jQuery('#ttv-manual').on('click', this.submitChapters.bind(this));
jQuery('#ttv-auto').on('click', this.toggleAutoPost.bind(this));
jQuery('#qpButtonRemoveEmpty').on('click', this.removeEmptyChapters.bind(this));
jQuery('#qpButtonReset').on('click', this.resetAutoPost.bind(this));
jQuery('#qpOptionLoop').on('change', this.toggleAutoMode.bind(this));
this.setupCharacterCounter();
if (window.location.href.includes('/dang-chuong/story/')) {
setTimeout(() => {
if (this.STATE.isAuto && this.STATE.processedChapters < this.STATE.totalChapters) {
this.runAutoPostSequence();
}
}, 2000);
}
},
handlePasteButton: function() {
this.showLoading();
navigator.clipboard.readText()
.then(text => {
jQuery('#ttv-content').val(text);
setTimeout(() => {
this.performAction();
this.hideLoading();
}, 100);
})
.catch(err => {
console.error('Không thể đọc dữ liệu từ clipboard:', err);
this.hideLoading();
this.showNotification('Không thể truy cập clipboard. Vui lòng dán trực tiếp vào ô nội dung.', 'error');
});
},
handlePaste: function(e) {
e.preventDefault();
jQuery('#ttv-content').val("");
this.showLoading();
const pastedText = e.originalEvent.clipboardData.getData('text');
jQuery('#ttv-content').val(pastedText);
setTimeout(() => {
this.performAction();
this.hideLoading();
}, 100);
},
performAction: function() {
try {
console.log("Starting performAction");
var text = jQuery('#ttv-content').val();
if (!text) {
this.showNotification('Không có nội dung để tách chương', 'error');
return 0;
}
var debugOutput = [];
var chapters = [];
var lines = text.split('\n');
var currentChapter = [];
var lastTitle = null;
debugOutput.push("=== Processing Text ===");
debugOutput.push(`Total lines: ${lines.length}`);
debugOutput.push("=== Line Analysis ===");
function visualizeWhitespace(str) {
return str.split('').map(c => {
if (c === '\t') return '\\t';
if (c === ' ') return '·';
if (c === '\n') return '\\n';
return c;
}).join('');
}
// Hàm lấy mã chương dựa vào tiêu đề
function getChapterCode(title) {
// Lấy số chương + tên chương, bỏ qua các ký tự đặc biệt
const match = title.match(/[Cc]hương\s*(\d+)\s*:/);
if (!match) return title.trim();
const chapterNum = match[1];
return `chap_${chapterNum}`;
}
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
let isChapterTitle = /^\t[Cc]hương\s*\d+\s*:/.test(line) || /^\s{4,}[Cc]hương\s*\d+\s*:/.test(line);
debugOutput.push(`Line ${i}: ${visualizeWhitespace(line.substring(0, 50))}${line.length > 50 ? '...' : ''}`);
debugOutput.push(` Is chapter: ${isChapterTitle}`);
if (isChapterTitle) {
// Lấy mã chương để so sánh
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;
debugOutput.push(` -> New chapter started: ${currentChapterCode}`);
} else {
debugOutput.push(` -> Skipped duplicate chapter: ${currentChapterCode}`);
// Không cần thêm dòng này vào chapter hiện tại vì nó là tiêu đề trùng lặp
}
} else {
currentChapter = [line];
lastTitle = line;
debugOutput.push(` -> First chapter started: ${currentChapterCode}`);
}
} else if (currentChapter.length > 0) {
currentChapter.push(line);
}
}
if (currentChapter.length > 0) {
chapters.push(currentChapter.join('\n'));
debugOutput.push("-> Added final chapter");
}
debugOutput.push("=== Results ===");
debugOutput.push(`Found ${chapters.length} chapters`);
const processedChapters = [];
for (let i = 0; i < chapters.length; i++) {
const chapterLines = chapters[i].split('\n');
const title = chapterLines.shift().trim();
const chapterText = chapterLines.join('\n');
const charCount = chapterText.length;
debugOutput.push(`Chapter ${i+1} character count: ${charCount}`);
if (charCount > 40000) {
const parts = Math.ceil(charCount / 40000);
debugOutput.push(`Splitting into ${parts} parts`);
const charsPerPart = Math.ceil(charCount / parts);
debugOutput.push(`Characters per part: ~${charsPerPart}`);
let currentText = chapterText;
let totalProcessed = 0;
for (let part = 0; part < parts; part++) {
const isLastPart = part === parts - 1;
const targetSize = isLastPart ? currentText.length : charsPerPart;
let endPos = Math.min(targetSize, currentText.length);
if (!isLastPart && endPos < currentText.length) {
const nextParagraph = currentText.indexOf('\n\n', endPos - 500);
if (nextParagraph !== -1 && nextParagraph < endPos + 500) {
endPos = nextParagraph + 2;
} else {
const sentenceEnd = Math.max(
currentText.lastIndexOf('. ', endPos),
currentText.lastIndexOf('! ', endPos),
currentText.lastIndexOf('? ', endPos)
);
if (sentenceEnd !== -1 && sentenceEnd > endPos - 500) {
endPos = sentenceEnd + 2;
}
}
}
const partContent = currentText.substring(0, endPos);
totalProcessed += partContent.length;
currentText = currentText.substring(endPos);
let chapterTitle = title;
if (title.includes(':')) {
chapterTitle = title.substring(title.indexOf(':') + 1).trim();
}
let newTitle = `${title} (Phần ${part+1}/${parts})`;
processedChapters.push(newTitle + '\n' + partContent);
debugOutput.push(`Part ${part+1}: ${partContent.length} chars`);
}
debugOutput.push(`Total processed: ${totalProcessed}/${charCount} chars`);
} else {
processedChapters.push(chapters[i]);
}
}
debugOutput.push(`After processing: ${processedChapters.length} chapters`);
const chaptersToFill = processedChapters.slice(0, MAX_CHAPTER_POST);
const remainingChapters = processedChapters.slice(MAX_CHAPTER_POST);
var titles = jQuery("input[name^='chap_name']");
var contents = jQuery("textarea[name^='introduce']");
var advs = jQuery("textarea[name^='adv']");
debugOutput.push(`Forms found: ${titles.length}`);
if (processedChapters.length === 0) {
this.showNotification('Không tìm thấy chương nào', 'error');
jQuery('#debug-output').text(debugOutput.join('\n'));
return;
}
if (remainingChapters.length > 0) {
debugOutput.push(`${remainingChapters.length} chapters will be copied to clipboard`);
}
const neededForms = chaptersToFill.length - titles.length;
if (neededForms > 0 && titles.length < MAX_CHAPTER_POST) {
debugOutput.push(`Need to add ${neededForms} more forms`);
for (let i = 0; i < neededForms && (titles.length + i) < MAX_CHAPTER_POST; i++) {
this.addNewChapter();
}
titles = jQuery("input[name^='chap_name']");
contents = jQuery("textarea[name^='introduce']");
advs = jQuery("textarea[name^='adv']");
}
debugOutput.push(`Filling ${chaptersToFill.length} chapters into forms`);
jQuery.each(titles, function(k, v) {
if (k < chaptersToFill.length) {
var content = chaptersToFill[k].split('\n');
var title = content.shift().trim();
var chapterTitle = title;
if (title.includes(':')) {
chapterTitle = title.substring(title.indexOf(':') + 1).trim();
}
debugOutput.push(`\nFilling chapter ${k + 1}:`);
debugOutput.push(`Original title: ${title}`);
debugOutput.push(`Extracted title: ${chapterTitle}`);
debugOutput.push(`Content length: ${content.length} lines`);
if (!chapterTitle || chapterTitle.trim() === '') {
chapterTitle = "Vô đề";
debugOutput.push(`Empty title detected, using default: ${chapterTitle}`);
}
titles[k].value = chapterTitle;
contents[k].value = HEADER_SIGN + "\r\n" + content.join('\n') + "\r\n" + FOOTER_SIGN;
if (advs[k]) advs[k].value = "";
jQuery(contents[k]).trigger('input');
}
});
if (remainingChapters.length > 0) {
try {
const clipboardContent = remainingChapters.map(chap => {
const lines = chap.trim().split('\n');
if (lines.length > 0) {
if (!lines[0].startsWith('\t')) {
lines[0] = '\t' + lines[0];
}
}
return lines.join('\n');
}).join('\n\n---CHAPTER_SEPARATOR---\n\n');
let splitChapters = 0;
let shortChapters = 0;
let shortChapterDetails = [];
let longChapterDetails = [];
for (let i = 0; i < chapters.length; i++) {
const chapterLines = chapters[i].split('\n');
const title = chapterLines.shift().trim();
const chapterText = chapterLines.join('\n');
if (chapterText.length > 40000) {
splitChapters++;
const partsCount = Math.ceil(chapterText.length / 40000);
longChapterDetails.push({
title: title,
parts: partsCount
});
}
if (chapterText.length < 3000) {
shortChapters++;
shortChapterDetails.push({
title: title,
length: chapterText.length
});
}
}
const splittedChaptersCount = processedChapters.length - (chapters.length - splitChapters);
let message = '';
message = message.concat(`📝Đã xử lý ${processedChapters.length} Chương\n`);
message = message.concat(`📝Đã nhập ${Math.min(processedChapters.length, MAX_CHAPTER_POST)} Chương\n`);
if (remainingChapters.length > 0) {
message = message.concat(`📋Đã lưu lại ${remainingChapters.length} Chương\n`);
}
if (splitChapters > 0) {
message = message.concat(`📑Có ${splitChapters} Chương đã chia thành ${splittedChaptersCount} Chương\n`);
longChapterDetails.forEach(chapter => {
let chapterName = chapter.title;
if (chapterName.includes(':')) {
chapterName = chapterName.trim();
}
message = message.concat(` - ${chapterName}: ${chapter.parts} Chương\n`);
});
}
if (shortChapters > 0) {
message = message.concat(`⚠️<span class="short-chapters-warning">Có ${shortChapters} chương dưới 3000 ký tự</span>\n`);
shortChapterDetails.forEach(chapter => {
let chapterName = chapter.title;
if (chapterName.includes(':')) {
chapterName = chapterName.trim();
}
message = message.concat(`<span class="short-chapters-warning"> - ${chapterName}: có ${chapter.length.toLocaleString()} ký tự</span>\n`);
});
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(clipboardContent)
.then(() => {
debugOutput.push(`📋Đã lưu lại ${remainingChapters.length} Chương\n`);
this.showNotification(message, remainingChapters.length > 0 ? 'warning' : 'success');
})
.catch(err => {
throw err;
});
} else {
const tempTextarea = document.createElement('textarea');
tempTextarea.style.position = 'fixed';
tempTextarea.style.top = '0';
tempTextarea.style.left = '0';
tempTextarea.style.width = '2em';
tempTextarea.style.height = '2em';
tempTextarea.style.opacity = '0';
tempTextarea.style.pointerEvents = 'none';
tempTextarea.value = clipboardContent;
document.body.appendChild(tempTextarea);
tempTextarea.focus();
tempTextarea.select();
const successful = document.execCommand('copy');
document.body.removeChild(tempTextarea);
if (!successful) {
throw new Error('Không thể sao chép vào clipboard');
}
debugOutput.push(`Đã sao chép ${remainingChapters.length} chương vào clipboard (execCommand)`);
this.showNotification(message, 'success');
}
} catch (err) {
console.error('Lỗi khi sao chép vào clipboard:', err);
debugOutput.push(`Lỗi khi sao chép vào clipboard: ${err.message}`);
this.showNotification('Không thể sao chép vào clipboard. Vui lòng thử lại.', 'error');
const manualCopyArea = document.createElement('div');
manualCopyArea.style.position = 'fixed';
manualCopyArea.style.top = '50%';
manualCopyArea.style.left = '50%';
manualCopyArea.style.transform = 'translate(-50%, -50%)';
manualCopyArea.style.backgroundColor = 'white';
manualCopyArea.style.padding = '20px';
manualCopyArea.style.borderRadius = '8px';
manualCopyArea.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
manualCopyArea.style.zIndex = '10000';
manualCopyArea.style.maxWidth = '80%';
manualCopyArea.style.maxHeight = '80%';
manualCopyArea.style.overflow = 'auto';
manualCopyArea.innerHTML = `
<h3 style="margin-top: 0;">Sao chép thủ công</h3>
<p>Không thể sao chép tự động. Vui lòng chọn toàn bộ nội dung bên dưới và sao chép (Ctrl+C):</p>
<textarea style="width: 100%; height: 300px; padding: 10px;">${clipboardContent}</textarea>
<div style="text-align: right; margin-top: 10px;">
<button id="closeManualCopy" style="padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;">Đóng</button>
</div>
`;
document.body.appendChild(manualCopyArea);
document.getElementById('closeManualCopy').addEventListener('click', () => {
document.body.removeChild(manualCopyArea);
});
}
}
jQuery('#qpButtonSubmit').removeClass("btn-disable").addClass("btn-success");
jQuery('#ttv-content').val("Đã xử lý xong");
if (remainingChapters.length === 0) {
let splitChapters = 0;
let shortChapters = 0;
let shortChapterDetails = [];
let longChapterDetails = [];
for (let i = 0; i < chapters.length; i++) {
const chapterLines = chapters[i].split('\n');
const title = chapterLines.shift().trim();
const chapterText = chapterLines.join('\n');
if (chapterText.length > 40000) {
splitChapters++;
const partsCount = Math.ceil(chapterText.length / 40000);
longChapterDetails.push({
title: title,
parts: partsCount
});
}
if (chapterText.length < 3000) {
shortChapters++;
shortChapterDetails.push({
title: title,
length: chapterText.length
});
}
}
const splittedChaptersCount = processedChapters.length - (chapters.length - splitChapters);
let message = '';
message = message.concat(`📝Đã xử lý ${processedChapters.length} Chương\n`);
message = message.concat(`📝Đã nhập ${Math.min(processedChapters.length, MAX_CHAPTER_POST)} Chương\n`);
if (splitChapters > 0) {
message = message.concat(`📑Có ${splitChapters} Chương dài chia thành ${splittedChaptersCount} Chương\n`);
longChapterDetails.forEach(chapter => {
let chapterName = chapter.title;
if (chapterName.includes(':')) {
chapterName = chapterName.trim();
}
message = message.concat(` - ${chapterName}: ${chapter.parts} Chương\n`);
});
}
if (shortChapters > 0) {
message = message.concat(`⚠️<span class="short-chapters-warning">Có ${shortChapters} chương dưới 3000 ký tự</span>\n`);
shortChapterDetails.forEach(chapter => {
let chapterName = chapter.title;
if (chapterName.includes(':')) {
chapterName = chapterName.trim();
}
message = message.concat(`<span class="short-chapters-warning"> - ${chapterName}: có ${chapter.length.toLocaleString()} ký tự</span>\n`);
});
}
this.showNotification(message, 'success');
}
jQuery('#debug-output').text(debugOutput.join('\n'));
return processedChapters.length;
} catch (e) {
console.error("Error in performAction:", e);
this.showNotification('Có lỗi khi tách chương', 'error');
return 0;
}
},
removeEmptyChapters: function() {
const forms = document.querySelectorAll('[data-gen="MK_GEN"]');
let removed = 0;
forms.forEach(form => {
const content = form.querySelector('textarea[name^="introduce"]').value.trim();
if (!content) {
form.remove();
removed++;
this.updateChapNumber(false);
}
});
this.showNotification(`Đã xử lý ${forms.length} chương`, 'info');
},
toggleAutoPost: function() {
this.STATE.isAuto = !this.STATE.isAuto;
if (this.STATE.isAuto) {
this.STATE.totalChapters = parseInt(prompt("Nhập tổng số lần tự động đăng:", "10")) || 0;
this.STATE.processedChapters = parseInt(localStorage.getItem('TTV_POSTED_CHAPTERS') || '0');
localStorage.setItem('TTV_TOTAL_CHAPTERS', this.STATE.totalChapters.toString());
this.updateStats();
this.showNotification(`Đã bật tự động đăng (${this.STATE.processedChapters}/${this.STATE.totalChapters})`, 'success');
if (window.location.href.includes('/dang-chuong/story/')) {
setTimeout(() => this.runAutoPostSequence(), 2000);
}
} else {
localStorage.setItem('TTV_AUTO_POST', 'false');
this.STATE.processedChapters = 0;
localStorage.setItem('TTV_POSTED_CHAPTERS', '0');
this.STATE.totalChapters = 0;
localStorage.setItem('TTV_TOTAL_CHAPTERS', '0');
this.updateStats();
this.showNotification('Đã tắt tự động đăng và reset số lần đăng', 'info');
}
},
runAutoPostSequence: function() {
if (this.STATE.processedChapters >= this.STATE.totalChapters) {
this.STATE.isAuto = false;
localStorage.setItem('TTV_AUTO_POST', 'false');
this.STATE.processedChapters = 0;
localStorage.setItem('TTV_POSTED_CHAPTERS', '0');
this.STATE.totalChapters = 0;
localStorage.setItem('TTV_TOTAL_CHAPTERS', '0');
this.updateStats();
this.showNotification(`Đã hoàn thành tự động đăng ${this.STATE.totalChapters}/${this.STATE.totalChapters} chương và đã reset số lần đăng`, 'success');
return;
}
if (!this.STATE.isAuto) {
return;
}
setTimeout(() => {
if (this.STATE.isAuto) {
this.handlePasteButton();
setTimeout(() => {
if (this.STATE.isAuto) {
this.submitChapters();
}
}, 3000);
}
}, 2000);
},
submitChapters: function() {
if (!this.validateChapterLengths()) {
return;
}
this.showLoading();
document.querySelector('form[name="postChapForm"] button[type="submit"]').click();
if (this.STATE.isAuto) {
this.STATE.processedChapters++;
localStorage.setItem('TTV_POSTED_CHAPTERS', this.STATE.processedChapters.toString());
if (this.STATE.processedChapters >= this.STATE.totalChapters) {
this.STATE.isAuto = false;
localStorage.setItem('TTV_AUTO_POST', 'false');
this.STATE.processedChapters = 0;
localStorage.setItem('TTV_POSTED_CHAPTERS', '0');
this.STATE.totalChapters = 0;
localStorage.setItem('TTV_TOTAL_CHAPTERS', '0');
this.updateStats();
setTimeout(() => {
this.showNotification(`Đã hoàn thành tự động đăng ${this.STATE.totalChapters}/${this.STATE.totalChapters} chương và đã reset số lần đăng`, 'success');
}, 3000);
}
}
setTimeout(() => this.hideLoading(), 2000);
},
addNewChapter: function() {
if ((this.STATE.chapterNumber + 1) <= MAX_CHAPTER_POST) {
this.updateChapNumber(true);
const html = this.createChapterHTML(this.STATE.chapterNumber);
jQuery('#add-chap').before(html);
this.setupCharacterCounter();
} else {
this.showNotification(`Chỉ có thể đăng tối đa ${MAX_CHAPTER_POST} chương một lần`, 'warning');
}
},
createFormContainer: function() {
const container = document.createElement('div');
container.id = 'ttv-chapter-forms';
document.body.appendChild(container);
},
resetAutoPost: function() {
this.STATE.totalChapters = 0;
this.STATE.processedChapters = 0;
localStorage.removeItem('TTV_TOTAL_CHAPTERS');
localStorage.removeItem('TTV_POSTED_CHAPTERS');
this.updateStats();
this.showNotification('Đã reset số lần tự động đăng', 'info');
},
toggleAutoMode: function() {
const isAutoMode = jQuery('#qpOptionLoop').is(':checked');
if (isAutoMode) {
jQuery('#qpButtonAutoPost').show();
jQuery('#qpButtonReset').show();
jQuery('#qpButtonPaste').hide();
jQuery('#qpButtonSubmit').hide();
this.showNotification('Đã bật chế độ tự động', 'info');
} else {
jQuery('#qpButtonAutoPost').hide();
jQuery('#qpButtonReset').hide();
jQuery('#qpButtonPaste').show();
jQuery('#qpButtonSubmit').show();
this.showNotification('Đã tắt chế độ tự động', 'info');
}
localStorage.setItem('TTV_AUTO_MODE', isAutoMode.toString());
}
};
const MAX_CHAPTER_POST = 10;
const HEADER_SIGN = "/***";
const FOOTER_SIGN = "***/";
TTVManager.init();
})();