Công cụ đăng chương hiện đại cho Tàng Thư Viện với UI/UX được tối ưu
目前為
// ==UserScript==
// @name TTV
// @namespace http://tampermonkey.net/
// @version 3.1
// @description Công cụ đăng chương hiện đại cho Tàng Thư Viện với UI/UX được tối ưu
// @author HA
// @match https://tangthuvien.net/dang-chuong/story/*
// @match https://tangthuvien.net/danh-sach-chuong/story/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @required https://code.jquery.com/jquery-3.2.1.min.js
// ==/UserScript==
(function() {
'use strict';
console.log('[TTV-DEBUG] Script starting...');
if (window.location.href.includes('/danh-sach-chuong/story/')) {
const storyId = window.location.pathname.split('/').pop();
setTimeout(() => {
window.location.href = `https://tangthuvien.net/dang-chuong/story/${storyId}`;
}, 3000);
return;
}
const HEADER_SIGN = "";
const FOOTER_SIGN = "";
const MAX_CHAPTER_POST = 10;
GM_addStyle(`
/* Vị trí và giao diện của công cụ đăng nhanh */
#modern-uploader {
background: linear-gradient(145deg, #ffffff, #f5f8ff);
padding: 28px;
border-radius: 16px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
position: fixed;
right: 25px;
top: 50%;
transform: translateY(-50%);
width: 450px;
max-height: 92vh;
overflow-y: auto;
z-index: 1000;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
border: 1px solid #e0e0e0;
transition: all 0.3s ease;
}
.notification-container {
margin-top: 10px;
}
.notification-success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
padding: 10px;
border-radius: 5px;
}
.notification-error {
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
padding: 10px;
border-radius: 5px;
}
`);
const dangNhanhTTV = {
STATE: {
CHAP_NUMBER: 1,
CHAP_STT: 1,
CHAP_SERIAL: 1,
CHAP_NUMBER_ORIGINAL: 1,
CHAP_STT_ORIGINAL: 1,
CHAP_SERIAL_ORIGINAL: 1,
AUTO_POST: false,
TOTAL_CHAPTERS: 0,
POSTED_CHAPTERS: 0
},
ELEMENTS: {
qpContent: null,
qpButtonSubmit: null,
qpButtonRemoveEmpty: null,
qpButtonReset: null,
qpButtonPaste: null,
qpButtonAutoPost: null,
qpOptionLoop: null
},
init: function() {
try {
console.log('[TTV-DEBUG] Script initialization starting...');
this.initializeChapterValues();
this.createInterface();
this.cacheElements();
this.registerEvents();
console.log('[TTV-DEBUG] Script initialized successfully');
showNotification('Công cụ đã chạy thành công', 'success');
} catch (e) {
console.error('[TTV-ERROR] Initialization error:', e);
showNotification('Có lỗi khi khởi tạo Script', 'error');
}
},
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);
}
},
createInterface: function() {
const html = `
<div id="modern-uploader">
<div class="uploader-header">
<h3>📚 CÔNG CỤ ĐĂNG CHƯƠNG</h3>
</div>
<div class="form-group">
<textarea placeholder="Nội dung truyện (Dán vào đây để tự động tách chương)" id="qpContent" class="form-control" rows="5"></textarea>
</div>
<div class="button-container">
<button class="btn btn-primary" id="qpButtonPaste">📋 Dán nội dung</button>
<button class="btn btn-success" id="qpButtonSubmit">📤 Đăng chương</button>
</div>
<div class="notification-container"></div>
</div>`;
jQuery(".list-in-user").before(html);
},
cacheElements: function() {
this.ELEMENTS.qpContent = jQuery("#qpContent");
this.ELEMENTS.qpButtonSubmit = jQuery("#qpButtonSubmit");
this.ELEMENTS.qpButtonPaste = jQuery("#qpButtonPaste");
},
registerEvents: function() {
this.ELEMENTS.qpContent.on("paste", this.handlePaste.bind(this));
this.ELEMENTS.qpButtonSubmit.on('click', this.submitChapters.bind(this));
this.ELEMENTS.qpButtonPaste.on('click', this.handlePasteButton.bind(this));
setupCharacterCounter();
},
handlePasteButton: function() {
this.showLoading();
navigator.clipboard.readText()
.then(text => {
this.ELEMENTS.qpContent.val(text);
setTimeout(() => {
this.performAction();
this.hideLoading();
}, 100);
})
.catch(err => {
console.error('Không thể đọc dữ liệu từ clipboard:', err);
this.hideLoading();
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();
this.ELEMENTS.qpContent.val("");
this.showLoading();
const pastedText = e.originalEvent.clipboardData.getData('text');
this.ELEMENTS.qpContent.val(pastedText);
setTimeout(() => {
this.performAction();
this.hideLoading();
}, 100);
},
performAction: function() {
try {
console.log("[TTV-DEBUG] Starting performAction");
var text = this.ELEMENTS.qpContent.val();
if (!text) {
showNotification('Không có nội dung để tách chương', 'error');
return 0;
}
var chapters = [];
var lines = text.split('\n');
var currentChapter = [];
var lastTitle = null;
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) ||
/^\t[Cc]hương\s*\d+(?!\S)/.test(line) ||
/^\s{4,}[Cc]hương\s*\d+(?!\S)/.test(line) ||
/^[Cc]hương\s*\d+\s*:/.test(line) ||
/^[Cc]hương\s*\d+(?!\S)/.test(line);
if (isChapterTitle) {
if (currentChapter.length > 0) {
chapters.push(currentChapter.join('\n'));
}
currentChapter = [line];
lastTitle = line;
} else if (currentChapter.length > 0) {
currentChapter.push(line);
}
}
if (currentChapter.length > 0) {
chapters.push(currentChapter.join('\n'));
}
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');
processedChapters.push({
displayTitle: title,
originalTitle: title,
displayContent: chapterText,
clipboardContent: chapterText,
length: chapterText.length
});
}
this.updateChapterList(processedChapters);
showNotification(`Đã tách thành ${processedChapters.length} chương`, 'success');
return processedChapters.length;
} catch (error) {
console.error("[TTV-ERROR] Error in performAction:", error);
showNotification('Có lỗi xảy ra khi xử lý nội dung', 'error');
return 0;
}
},
updateChapterList: function(chapters) {
jQuery('.chapter-detail[data-gen="MK_GEN"]').remove();
chapters.forEach((chapter, index) => {
const chapterElement = jQuery(this.createChapterHTML(index + 1));
chapterElement.find('[name^="chap_name"]').val(chapter.displayTitle);
chapterElement.find('[name^="introduce"]').val(chapter.displayContent);
chapterElement.attr('data-original-title', chapter.originalTitle);
chapterElement.attr('data-clipboard-content', chapter.clipboardContent);
jQuery('form[name="postChapForm"]').append(chapterElement);
});
this.setupClipboardHandling();
setupCharacterCounter();
},
setupClipboardHandling: function() {
jQuery(document).on('copy', '.chapter-detail[data-gen="MK_GEN"]', function(e) {
const originalTitle = jQuery(this).attr('data-original-title');
const clipboardContent = jQuery(this).attr('data-clipboard-content');
e.originalEvent.clipboardData.setData('text/plain', originalTitle + '\n' + clipboardContent);
e.preventDefault();
});
},
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" class="chapter-detail" 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="${dangNhanhTTV.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="${dangNhanhTTV.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="counter"></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>`;
},
submitChapters: function() {
this.showLoading();
document.querySelector('form[name="postChapForm"] button[type="submit"]').click();
setTimeout(() => this.hideLoading(), 2000);
},
showLoading: function() {
const loading = jQuery("<div>", {
class: "loading-overlay",
html: "<div class='loading-spinner'></div>"
});
jQuery("body").append(loading);
},
hideLoading: function() {
jQuery(".loading-overlay").remove();
}
};
function showNotification(message, type) {
console.log(`[TTV-${type.toUpperCase()}] ${message}`);
try {
jQuery('#modern-uploader .notification-container').remove();
const container = jQuery("<div>", {
class: "notification-container"
});
const notification = jQuery("<div>", {
class: `notification-${type}`
}).text(message);
container.append(notification);
jQuery("#modern-uploader .button-container").after(container);
notification.fadeIn(300);
} catch (e) {
console.error('[TTV-ERROR] Failed to show notification:', e);
alert(message);
}
}
function setupCharacterCounter() {
jQuery('textarea[name^="introduce"]').each(function() {
const counter = jQuery(this).next('.counter');
jQuery(this).on('input', function() {
const length = this.value.length;
if (length < 3000) {
jQuery(this).addClass('short-chapter');
counter.html(`<span style="color: red;">${length.toLocaleString()} ký tự</span>`);
} else {
jQuery(this).removeClass('short-chapter');
counter.html(`<span style="color: green;">${length.toLocaleString()} ký tự</span>`);
}
});
jQuery(this).trigger('input');
});
}
// Initialize when document is ready
jQuery(document).ready(() => {
console.log('[TTV-DEBUG] Document ready, initializing script...');
dangNhanhTTV.init();
});
})();