您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Công cụ đăng chương đơn giản cho Tàng Thư Viện
当前为
- // ==UserScript==
- // @name TTV Auto Upload
- // @namespace http://tampermonkey.net/
- // @version 1.0
- // @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;
- }
- #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;
- position: relative;
- }
- #ttv-panel button:hover {
- opacity: 0.9;
- }
- #ttv-panel button:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- #ttv-panel button.processing:after {
- content: "";
- position: absolute;
- width: 20px;
- height: 20px;
- top: calc(50% - 10px);
- right: 10px;
- border: 2px solid rgba(255,255,255,0.3);
- border-top: 2px solid white;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- }
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- .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;
- }
- .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.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() {
- console.log('[TTV-DEBUG] Creating interface');
- 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);
- },
- 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');
- // Xử lý paste vào textarea
- content.addEventListener('paste', (e) => {
- e.preventDefault();
- const text = e.clipboardData.getData('text');
- content.value = text;
- this.processContent(text);
- });
- // Xử lý input trực tiếp
- content.addEventListener('input', () => {
- const text = content.value;
- if (text) {
- this.processContent(text);
- }
- });
- // Nút đăng tự động
- 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;
- autoBtn.classList.add('processing');
- 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;
- autoBtn.classList.remove('processing');
- return;
- }
- this.processContent(text);
- });
- // Nút đăng thủ công
- 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;
- manualBtn.classList.add('processing');
- 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;
- manualBtn.classList.remove('processing');
- 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 style="color: #ff0000;">${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>`;
- }
- }
- });
- },
- 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;
- },
- processContent: function(text) {
- console.log('[TTV-DEBUG] Processing content, auto mode:', this.STATE.isAuto);
- if (!text) {
- this.showNotification('Không có nội dung để xử lý', '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;
- }
- console.log(`[TTV-DEBUG] Found ${chapters.length} chapters`);
- // Cập nhật danh sách chương
- this.updateChapterList(chapters);
- // 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 các chương còn lại vào clipboard
- if (remainingChapters.length > 0) {
- this.copyRemainingChapters(remainingChapters);
- }
- // Nếu đang ở chế độ tự động và có đủ 10 chương, tự động đăng
- if (this.STATE.isAuto && chaptersToFill.length === 10) {
- 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ó ${chaptersToFill.length} chương)`, 'warning');
- }
- },
- splitChapters: function(text) {
- console.log('[TTV-DEBUG] Splitting chapters');
- const chapters = [];
- const lines = text.split('\n');
- let currentChapter = [];
- let lastTitle = null;
- // Hỗ trợ nhiều định dạng tiêu đề chương phổ biến
- const chapterPatterns = [
- /^\s*Chương\s+\d+\s*:/i, // Chương X:
- /^\t+Chương\s+\d+\s*:/i, // [Tab]Chương X:
- /^\s{4,}Chương\s+\d+\s*:/i // [Spaces]Chương X:
- ];
- // Map để nhóm các tiêu đề theo số chương
- const chapterGroups = new Map();
- // Hàm kiểm tra xem một dòng có phải là tiêu đề chương
- function isChapterTitle(line) {
- return chapterPatterns.some(pattern => pattern.test(line));
- }
- // Hàm trích xuất số chương từ tiêu đề
- function extractChapterNumber(line) {
- const match = line.match(/Chương\s+(\d+)\s*:/i);
- return match ? parseInt(match[1]) : 0;
- }
- // Pass đầu tiên: thu thập tất cả các tiêu đề chương và nhóm theo số chương
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i].trim();
- if (isChapterTitle(line)) {
- const chapterNum = extractChapterNumber(line);
- if (!chapterGroups.has(chapterNum)) {
- chapterGroups.set(chapterNum, []);
- }
- chapterGroups.get(chapterNum).push({
- lineIndex: i,
- title: lines[i], // Giữ nguyên định dạng gốc
- content: []
- });
- }
- }
- // Pass thứ hai: xây dựng nội dung chương từ các tiêu đề đã được xác định
- const sortedChapters = Array.from(chapterGroups.entries());
- sortedChapters.sort((a, b) => a[0] - b[0]); // Sắp xếp theo số chương
- for (let i = 0; i < sortedChapters.length; i++) {
- const [_chapterNum, chapterInfos] = sortedChapters[i];
- // Chọn tiêu đề đầu tiên nếu có nhiều tiêu đề trùng
- const chapterInfo = chapterInfos[0];
- const nextChapterIndex = (i < sortedChapters.length - 1)
- ? sortedChapters[i+1][1][0].lineIndex
- : lines.length;
- // Thu thập nội dung từ sau tiêu đề đến trước tiêu đề tiếp theo
- const chapterContent = [chapterInfo.title];
- for (let j = chapterInfo.lineIndex + 1; j < nextChapterIndex; j++) {
- const line = lines[j];
- // Bỏ qua các tiêu đề trùng lặp
- if (!isChapterTitle(line.trim())) {
- chapterContent.push(line);
- }
- }
- chapters.push(chapterContent.join('\n'));
- }
- console.log(`[TTV-DEBUG] Found ${chapters.length} chapters`);
- return chapters;
- },
- fillChaptersToForm: function(chapters) {
- console.log('[TTV-DEBUG] Filling form with chapters');
- this.showLoading('Đang điền nội dung vào form...');
- try {
- // 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);
- });
- console.log(`[TTV-DEBUG] Filled ${chapters.length} chapters into form`);
- this.showNotification(`Đã điền ${chapters.length} chương vào form`, 'success');
- } catch (error) {
- console.error('[TTV-ERROR] Form filling error:', error);
- this.showNotification('Có lỗi khi điền nội dung vào form', 'error');
- } finally {
- this.hideLoading();
- }
- },
- copyRemainingChapters: function(chapters) {
- console.log('[TTV-DEBUG] Copying remaining chapters to clipboard');
- 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;
- }
- // Kiểm tra độ dài 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;
- }
- // Đă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-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.STATE.isProcessing = 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');
- 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);
- console.log(`[TTV-DEBUG] Added new chapter form #${this.STATE.chapterNumber}`);
- },
- 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 === '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-DEBUG] ${type.toUpperCase()}: ${message}`);
- }
- };
- // Initialize script
- TTVManager.init();
- })();