// ==UserScript==
// @name TTV
// @namespace http://tampermonkey.net/
// @version 1.0
// @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';
const headerSign = "";
const footerSign = "";
const MAX_CHAPTER_POST = 10;
const AUTO_SAVE_DELAY = 30000; // 30 seconds
let CHAP_NUMBER = 1;
let CHAP_STT = 1;
let CHAP_SERIAL = 1;
let CHAP_NUMBER_ORIGINAL = 1;
let CHAP_STT_ORIGINAL = 1;
let CHAP_SERIAL_ORIGINAL = 1;
// CSS tùy chỉnh chỉ cho UI mới
GM_addStyle(`
#modern-uploader {
position: fixed;
top: 10px;
right: 10px;
width: 400px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
z-index: 9999;
}
.notification {
position: fixed;
bottom: 1rem;
right: 1rem;
padding: 1rem;
border-radius: 0.5rem;
color: white;
opacity: 0;
transform: translateY(100%);
transition: all 0.3s;
z-index: 9999;
}
.notification.success { background-color: #22c55e; }
.notification.error { background-color: #ef4444; }
.notification.warning { background-color: #f59e0b; }
.notification.info { background-color: #3b82f6; }
.notification.show {
opacity: 1;
transform: translateY(0);
}
.loading-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-border {
border: 2px solid #ef4444 !important;
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgb(239 68 68 / 0.5); }
50% { box-shadow: 0 0 0 4px rgb(239 68 68 / 0.5); }
}
`);
// Thêm chương mới
function addNewChapter() {
if ((CHAP_NUMBER + 1) <= MAX_CHAPTER_POST) {
updateChapNumber(true);
const html = createChapterHTML(CHAP_NUMBER);
jQuery('#add-chap').before(html);
} else {
const chapterLeft = MAX_CHAPTER_POST - CHAP_NUMBER;
showNotification(`Chỉ có thể đăng tối đa ${MAX_CHAPTER_POST} chương một lần`, 'warning');
}
}
// Cập nhật số chương
function updateChapNumber(isAdd) {
try {
if (isAdd) {
let maxStt = 0;
let maxSerial = 0;
// Tìm giá trị lớn nhất trong tất cả các input hiện có
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);
});
// So sánh với giá trị từ DOM
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);
// Cập nhật biến đếm
CHAP_STT = maxStt + 1;
CHAP_SERIAL = maxSerial + 1;
CHAP_NUMBER++;
} else {
if (CHAP_NUMBER > CHAP_NUMBER_ORIGINAL) {
CHAP_NUMBER--;
}
if (CHAP_STT > CHAP_STT_ORIGINAL) {
CHAP_STT--;
}
if (CHAP_SERIAL > CHAP_SERIAL_ORIGINAL) {
CHAP_SERIAL--;
}
}
// Cập nhật các input ẩn
jQuery('#chap_number').val(CHAP_NUMBER);
jQuery('#chap_stt').val(CHAP_STT);
jQuery('#chap_serial').val(CHAP_SERIAL);
jQuery('#countNumberPost').text(CHAP_NUMBER);
} catch (e) {
console.log("Lỗi: " + e);
}
}
// Tạo HTML cho chương mới
function createChapterHTML(number) {
const chap_vol = parseInt(jQuery('.chap_vol').val());
const chap_vol_name = jQuery('.chap_vol_name').val();
return `
<div data-gen="MK_GEN" id="COUNT_CHAP_${number}_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[${number}]" value="${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="${CHAP_SERIAL}" required class="form-control" name="chap_number[${number}]"
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[${number}]"
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[${number}]"
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[${number}]"
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[${number}]" rows="20" placeholder="Nội dung" type="text"></textarea>
</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[${number}]"
placeholder="Quảng cáo" type="text"></textarea>
</div>
</div>
</div>`;
}
// Tạo giao diện chính
function createInterface() {
const container = document.createElement('div');
container.id = 'modern-uploader';
container.innerHTML = `
<div class="text-center mb-4">
<h3 class="text-xl font-bold">CÔNG CỤ ĐĂNG NHANH</h3>
<p id="short-chapter-warning" class="text-red-500 mt-2 hidden"></p>
</div>
<div class="form-group">
<textarea id="content-input" class="form-control" rows="5"
placeholder="Dán nội dung truyện vào đây để tự động tách chương"></textarea>
</div>
<div class="flex justify-between">
<div class="space-x-2">
<button class="btn btn-outline" id="remove-empty">Xóa chương trống</button>
</div>
<button class="btn btn-primary" id="submit-chapters">Đăng chương</button>
</div>
`;
document.body.appendChild(container);
setupEventListeners();
}
// Khởi tạo các chương
function initializeChapters() {
showLoading();
try {
// Khởi tạo các biến đếm chương
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());
}
CHAP_NUMBER = CHAP_NUMBER_ORIGINAL = chap_number;
CHAP_STT = CHAP_STT_ORIGINAL = chap_stt;
CHAP_SERIAL = CHAP_SERIAL_ORIGINAL = chap_serial;
// Thêm 9 chương mới
for(let i = 0; i < 9; i++) {
setTimeout(() => {
addNewChapter();
}, i * 100); // Delay để tránh lag
}
hideLoading();
showNotification('Đã tạo đủ 10 chương', 'success');
// Khôi phục bản nháp nếu có
loadDraft();
} catch (e) {
console.log("Lỗi: " + e);
hideLoading();
showNotification('Có lỗi khi tạo chương', 'error');
}
}
// Khởi tạo các event listener
function setupEventListeners() {
const contentInput = document.getElementById('content-input');
const removeEmptyBtn = document.getElementById('remove-empty');
const submitBtn = document.getElementById('submit-chapters');
// Xử lý paste nội dung
contentInput.addEventListener('paste', async (e) => {
e.preventDefault();
showLoading();
const text = e.clipboardData.getData('text');
const chapters = await splitChapters(text);
if (chapters.length > MAX_CHAPTER_POST) {
showNotification(`Chỉ có thể đăng tối đa ${MAX_CHAPTER_POST} chương một lúc`, 'warning');
hideLoading();
return;
}
fillChapterForms(chapters);
hideLoading();
showNotification(`Đã tách thành ${chapters.length} chương`, 'success');
});
// Xóa chương trống
removeEmptyBtn.addEventListener('click', removeEmptyChapters);
// Đăng chương
submitBtn.addEventListener('click', submitChapters);
}
// Tách chương thông minh
async function splitChapters(text) {
const chapters = [];
const lines = text.split('\n');
let currentChapter = [];
const chapterPattern = /^[Cc]hương\s+\d+\s*[::]/;
for (const line of lines) {
if (chapterPattern.test(line.trim())) {
if (currentChapter.length > 0) {
chapters.push(currentChapter.join('\n'));
currentChapter = [];
}
}
currentChapter.push(line);
}
if (currentChapter.length > 0) {
chapters.push(currentChapter.join('\n'));
}
// Xử lý chương dài
return chapters.reduce((acc, chapter) => {
if (chapter.length > 19000) {
const parts = splitLongChapter(chapter);
return [...acc, ...parts];
}
return [...acc, chapter];
}, []);
}
// Chia nhỏ chương dài
function splitLongChapter(chapter) {
const parts = [];
const lines = chapter.split('\n');
const title = lines[0];
const content = lines.slice(1).join('\n');
const partSize = 15000;
const numParts = Math.ceil(content.length / partSize);
for (let i = 0; i < numParts; i++) {
const start = i * partSize;
const end = Math.min((i + 1) * partSize, content.length);
const partContent = content.substring(start, end);
const partTitle = `${title} (Phần ${i + 1}/${numParts})`;
parts.push(`${partTitle}\n${partContent}`);
}
return parts;
}
// Điền nội dung vào form chương
function fillChapterForms(chapters) {
chapters.forEach((chapter, index) => {
const lines = chapter.split('\n');
const titleMatch = lines[0].match(/^[Cc]hương\s+(\d+)\s*[::]\s*(.*)/);
if (titleMatch) {
const chapterNumber = parseInt(titleMatch[1]);
const title = titleMatch[2].trim();
const content = lines.slice(1).join('\n');
// Tìm form chương tương ứng
const forms = document.querySelectorAll('[data-gen="MK_GEN"]');
if (forms[index]) {
const form = forms[index];
form.querySelector('input[name^="chap_stt"]').value = chapterNumber;
form.querySelector('input[name^="chap_number"]').value = chapterNumber;
form.querySelector('input[name^="chap_name"]').value = title;
form.querySelector('textarea[name^="introduce"]').value = headerSign + "\r\n" + content + "\r\n" + footerSign;
}
}
});
}
// Tự động lưu nháp
let autoSaveTimeout;
function saveDraft() {
clearTimeout(autoSaveTimeout);
autoSaveTimeout = setTimeout(() => {
const chapters = [];
document.querySelectorAll('[data-gen="MK_GEN"]').forEach(form => {
chapters.push({
stt: form.querySelector('input[name^="chap_stt"]').value,
number: form.querySelector('input[name^="chap_number"]').value,
title: form.querySelector('input[name^="chap_name"]').value,
content: form.querySelector('textarea[name^="introduce"]').value,
ad: form.querySelector('textarea[name^="adv"]').value
});
});
GM_setValue('chapter_draft', JSON.stringify({
chapters,
timestamp: new Date().getTime()
}));
showNotification('Đã tự động lưu nháp', 'info');
}, AUTO_SAVE_DELAY);
}
// Khôi phục bản nháp
function loadDraft() {
const draftData = GM_getValue('chapter_draft');
if (!draftData) return;
const draft = JSON.parse(draftData);
const timeDiff = (new Date().getTime() - draft.timestamp) / 1000 / 60; // Phút
if (timeDiff < 60 && confirm(`Phát hiện bản nháp từ ${Math.round(timeDiff)} phút trước. Bạn có muốn khôi phục?`)) {
draft.chapters.forEach((chapter, index) => {
const form = document.querySelectorAll('[data-gen="MK_GEN"]')[index];
if (!form) return;
form.querySelector('input[name^="chap_stt"]').value = chapter.stt;
form.querySelector('input[name^="chap_number"]').value = chapter.number;
form.querySelector('input[name^="chap_name"]').value = chapter.title;
form.querySelector('textarea[name^="introduce"]').value = chapter.content;
form.querySelector('textarea[name^="adv"]').value = chapter.ad;
});
showNotification('Đã khôi phục bản nháp', 'success');
}
}
// Xóa chương trống
function removeEmptyChapters() {
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++;
updateChapNumber(false);
}
});
if (removed > 0) {
showNotification(`Đã xóa ${removed} chương trống`, 'info');
}
}
// Đăng chương
function submitChapters() {
const forms = document.querySelectorAll('[data-gen="MK_GEN"]');
let hasError = false;
forms.forEach(form => {
const content = form.querySelector('textarea[name^="introduce"]').value;
if (content.length < 300) {
form.querySelector('textarea[name^="introduce"]').classList.add('error-border');
hasError = true;
}
});
if (hasError) {
showNotification('Có chương quá ngắn, vui lòng kiểm tra lại', 'error');
return;
}
showLoading();
// Submit form gốc
document.querySelector('form[name="postChapForm"] button[type="submit"]').click();
setTimeout(hideLoading, 2000);
}
// Hiển thị thông báo
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
requestAnimationFrame(() => {
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 3000);
});
}
// Hiển thị loading
function showLoading() {
const loading = document.createElement('div');
loading.className = 'loading-overlay';
loading.innerHTML = `
<div class="loading-spinner"></div>
`;
document.body.appendChild(loading);
}
function hideLoading() {
const loading = document.querySelector('.loading-overlay');
if (loading) {
loading.remove();
}
}
// Khởi tạo script
function init() {
createInterface();
initializeChapters();
}
init();
})();