您需要先安装一个扩展,例如 篡改猴、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.1 // @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; display: none; } #ttv-chapters.has-chapters { display: block; } #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; } #ttv-panel button:hover { opacity: 0.9; } #ttv-panel button:disabled { opacity: 0.6; cursor: not-allowed; } .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; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .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.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 = ` <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); }, createFormContainer: function() { let formContainer = document.querySelector('#div_chapt_upload'); if (!formContainer) { formContainer = document.createElement('div'); formContainer.id = 'div_chapt_upload'; let parent = document.querySelector('.tab-content'); if (!parent) { parent = document.createElement('div'); parent.className = 'tab-content'; document.body.appendChild(parent); } parent.appendChild(formContainer); console.log('[TTV-DEBUG] Created form container'); } return formContainer; }, 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'); content.addEventListener('paste', (e) => { e.preventDefault(); console.log('[TTV-DEBUG] Content pasted'); const text = e.clipboardData.getData('text'); content.value = text; this.processContent(text); }); let inputTimer; content.addEventListener('input', () => { clearTimeout(inputTimer); inputTimer = setTimeout(() => { const text = content.value; if (text && text.length > 0) { console.log('[TTV-DEBUG] Processing input content'); this.processContent(text); } }, 500); }); 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; 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; return; } this.processContent(text); }); 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; 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; 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 class="short-chapters-warning">${charCount.toLocaleString()}/40.000 ký tự</span>`; } else { e.target.classList.remove('short-chapter'); counter.innerHTML = `<span style="color: ${charCount > 40000 ? '#fbbc05' : '#34a853'}">${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; chapterList.classList.toggle('has-chapters', chapters.length > 0); console.log('[TTV-DEBUG] Chapter list updated'); this.fillChapterForms(chapters.slice(0, MAX_CHAPTER_POST)); }, processContent: function(text) { try { this.showLoading('Đang tách chương...'); 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`); this.updateChapterList(chapters); const remainingChapters = chapters.slice(MAX_CHAPTER_POST); if (remainingChapters.length > 0) { this.copyRemainingChapters(remainingChapters); } if (this.STATE.isAuto && chapters.length >= MAX_CHAPTER_POST) { 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ó ${chapters.length} chương)`, 'warning'); this.STATE.isAuto = false; } } catch (error) { console.error('[TTV-ERROR] Content processing error:', error); this.showNotification('Có lỗi khi xử lý nội dung', 'error'); } finally { this.hideLoading(); this.STATE.isProcessing = false; document.getElementById('ttv-auto').disabled = false; document.getElementById('ttv-manual').disabled = false; } }, splitChapters: function(text) { console.log('[TTV-DEBUG] Starting chapter splitting...'); const chapters = []; const lines = text.split('\n'); let currentChapter = []; const chapterPattern = /^[\s\t]*[Cc]hương\s+(\d+)(?:\s*[::\.]|$)/; const chapterNumbers = new Map(); function getChapterNumber(line) { const match = line.match(chapterPattern); return match ? parseInt(match[1]) : null; } for (let i = 0; i < lines.length; i++) { const line = lines[i]; const chapterNum = getChapterNumber(line); if (chapterNum !== null) { if (currentChapter.length > 0) { chapters.push(currentChapter.join('\n')); } if (chapterNumbers.has(chapterNum)) { console.log(`[TTV-DEBUG] Skip duplicate chapter ${chapterNum}`); continue; } chapterNumbers.set(chapterNum, true); currentChapter = [line]; console.log(`[TTV-DEBUG] Found chapter ${chapterNum}: ${line.trim()}`); } else if (currentChapter.length > 0) { currentChapter.push(line); } } if (currentChapter.length > 0) { chapters.push(currentChapter.join('\n')); } chapters.sort((a, b) => { const numA = getChapterNumber(a.split('\n')[0]) || 0; const numB = getChapterNumber(b.split('\n')[0]) || 0; return numA - numB; }); console.log(`[TTV-DEBUG] Split complete. Found ${chapters.length} chapters`); return chapters; }, fillChapterForms: function(chapters) { if (!chapters || chapters.length === 0) { console.log('[TTV-DEBUG] No chapters to fill'); return; } console.log('[TTV-DEBUG] Filling chapter forms'); const formContainer = this.createFormContainer(); formContainer.innerHTML = ''; chapters.forEach((chapter, index) => { const lines = chapter.split('\n'); const title = lines.shift().trim(); const content = lines.join('\n'); 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" value="${title.includes(':') ? title.split(':')[1].trim() : title}"/> </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">${HEADER_SIGN}\n${content}\n${FOOTER_SIGN}</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>`; formContainer.insertAdjacentHTML('beforeend', formHtml); const textarea = formContainer.querySelector(`textarea[name="introduce[${this.STATE.chapterNumber}]"]`); if (textarea) { const event = new Event('input', { bubbles: true }); textarea.dispatchEvent(event); } }); console.log(`[TTV-DEBUG] Created ${chapters.length} chapter forms`); this.showNotification(`Đã điền ${chapters.length} chương vào form`, 'success'); }, copyRemainingChapters: function(chapters) { 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; } 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; } submitBtn.click(); this.showNotification('Đang đăng chương...', 'info'); 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.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'); this.showNotification('Đang tải lại trang để tiếp tục đăng...', 'info'); setTimeout(() => window.location.reload(), 2000); } }, 3000); }, 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 === 'warning' ? '#fff3e0' : '#e8f5e9'}; color: ${type === 'error' ? '#c62828' : type === 'warning' ? '#ef6c00' : '#2e7d32'}; border: 1px solid ${type === 'error' ? '#ffcdd2' : type === 'warning' ? '#ffe0b2' : '#c8e6c9'}; ">${message}</div> `; } }; TTVManager.init(); })();