您需要先安装一个扩展,例如 篡改猴、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 0.8 // @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-content { width: 100%; height: 150px; margin: 10px 0; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; transition: border-color 0.2s; resize: vertical; } #ttv-content:focus { border-color: #4CAF50; outline: none; } #ttv-chapters { width: 100%; margin: 10px 0; max-height: 300px; overflow-y: auto; border: 1px solid #eee; border-radius: 8px; background: #fafafa; } .chapter-item { padding: 12px; 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: 6px; color: #333; } .chapter-stats { font-size: 12px; color: #666; display: flex; gap: 10px; align-items: center; } .chapter-warning { color: #ff0000; font-weight: 500; } .chapter-long { color: #ff9800; font-weight: 500; } #ttv-panel .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; transition: all 0.2s; font-size: 14px; 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); } } #ttv-panel .btn-auto { background: #4CAF50; color: white; } #ttv-panel .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> <textarea id="ttv-content" placeholder="Dán nội dung vào đây để tự động tách chương..."></textarea> <div id="ttv-chapters"></div> <div class="btn-group"> <button class="btn-auto" id="ttv-auto">🔄 Đăng tự động 10 chươ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', () => { this.processContent(content.value); }); // 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'); navigator.clipboard.readText() .then(text => { if (!text) { this.showNotification('Không có nội dung trong clipboard', 'error'); return; } content.value = text; this.processContent(text); }) .catch(err => { console.error('[TTV-ERROR] Clipboard read error:', err); this.showNotification('Không thể đọc clipboard. Vui lòng cấp quyền truy cập clipboard và thử lại.', 'error'); }) .finally(() => { this.STATE.isProcessing = false; autoBtn.disabled = false; manualBtn.disabled = false; autoBtn.classList.remove('processing'); }); }); // 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); this.STATE.isProcessing = false; manualBtn.disabled = false; autoBtn.disabled = false; manualBtn.classList.remove('processing'); }); }, 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'); } }, 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(); }, splitChapters: function(text) { console.log('[TTV-DEBUG] Splitting chapters'); 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) { const currentChapterCode = getChapterCode(line); const lastTitleCode = lastTitle ? getChapterCode(lastTitle) : null; if (currentChapter.length > 0) { if (currentChapterCode !== lastTitleCode) { chapters.push(currentChapter.join('\n')); currentChapter = [line]; lastTitle = line; console.log(`[TTV-DEBUG] Found new chapter: ${currentChapterCode}`); } else { console.log(`[TTV-DEBUG] Skipped duplicate chapter: ${currentChapterCode}`); } } else { currentChapter = [line]; lastTitle = line; console.log(`[TTV-DEBUG] Started first chapter: ${currentChapterCode}`); } } else if (currentChapter.length > 0) { currentChapter.push(line); } } if (currentChapter.length > 0) { chapters.push(currentChapter.join('\n')); } 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}`); }, 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}`); } }; // Lấy mã chương dựa vào tiêu đề function getChapterCode(title) { const match = title.match(/[Cc]hương\s*(\d+)\s*:/); if (!match) return title.trim(); const chapterNum = match[1]; return `chap_${chapterNum}`; } // Initialize script TTVManager.init(); })();