您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tự động điền form đăng chương trên tangthuvien.net
当前为
// ==UserScript== // @name TTV Auto Upload // @namespace http://tampermonkey.net/ // @version 0.1 // @description Tự động điền form đăng chương trên tangthuvien.net // @author HA // @match https://tangthuvien.net/dang-chuong/story/* // @grant none // ==/UserScript== (function() { 'use strict'; // CSS tối giản const style = document.createElement('style'); style.textContent = ` .ttv-notification {position:fixed;top:20px;right:20px;padding:10px 20px;background:#4CAF50;color:white;border-radius:4px;z-index:9999;display:none} .ttv-error {background:#f44336} .ttv-control-panel {position:fixed;top:50px;right:20px;background:white;padding:25px;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.15);z-index:9998;width:500px;transition:all 0.3s ease} .ttv-control-panel.minimized {width:auto;height:auto;padding:10px;opacity:0.8;transform:translateX(calc(100% - 40px))} .ttv-control-panel.minimized:hover {opacity:1;transform:translateX(0)} .ttv-control-panel.minimized .ttv-button-group,.ttv-control-panel.minimized .ttv-header {display:none} .ttv-button-group {display:flex;flex-direction:column;gap:15px} .ttv-content-editor {width:100%;height:100px;margin:6px 0;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;resize:vertical} .ttv-preview {display:none;width:100%;height:100px;margin:6px 0;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;background:#f9f9f9} .ttv-heading {font-size:15px;color:#555;margin:12px 0;display:flex;justify-content:space-between;align-items:center} .ttv-word-count {font-size:12px;color:#888;padding:3px 8px;background:#f5f5f5;border-radius:4px} .ttv-chapter-list {width:100%;margin:10px 0;max-height:200px;overflow-y:auto;border:1px solid #eee;border-radius:6px;padding:8px} .ttv-chapter-item {padding:8px;border-bottom:1px solid #eee;cursor:pointer;font-size:12px;color:#666} .ttv-chapter-item .chapter-title {font-weight:bold;margin-bottom:4px} .ttv-chapter-item .chapter-name {color:#888;padding-left:10px;border-left:2px solid #ddd;margin:4px 0} .ttv-chapter-item .chapter-stats {font-size:11px;color:#999} .ttv-chapter-item:last-child {border-bottom:none} .ttv-chapter-item.selected {background:#f0f8ff;border-left:2px solid #5bc0de} button.btn-warning {background:#f0ad4e;color:white;border:none;padding:12px;border-radius:6px;width:100%;font-size:14px} button.btn-warning:hover {background:#ec971f} button.ttv-minimize {padding:2px 8px;background:none;border:none;cursor:pointer;font-size:16px;color:#666} `; document.head.appendChild(style); // State management const dăngnhanhTTV = { STATE: { CHAP_NUMBER: 1, CHAP_STT: 1, CHAP_SERIAL: 1, CHAP_NUMBER_ORIGINAL: 1, CHAP_STT_ORIGINAL: 1, CHAP_SERIAL_ORIGINAL: 1 }, 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); } } }; // UI Elements const notification = document.createElement('div'); notification.className = 'ttv-notification'; document.body.appendChild(notification); // Hiển thị thông báo function showNotification(message, isError = false) { notification.textContent = message; notification.className = 'ttv-notification' + (isError ? ' ttv-error' : ''); notification.style.display = 'block'; setTimeout(() => notification.style.display = 'none', 3000); } // Phân tích chương function parseChapters(content) { // Giữ nguyên định dạng gốc của nội dung - không sửa đổi dấu cách hoặc xuống dòng const lines = content.split('\n'); const chapters = []; let currentChapter = {title: '', name: '', content: []}; // 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: ]; // Biến để kiểm soát tiêu đề trùng lặp let chapterTitles = new Set(); let chapterNumbers = new Map(); // Lưu số chương và tiêu đề đã gặp let duplicateCount = 0; // Mảng lưu nội dung của các dòng trống gần nhất let emptyLineBuffer = []; // Biến để theo dõi tiêu đề chương trước đó let previousChapterLine = ''; let previousChapterNum = 0; // Khoảng cách dòng tối thiểu giữa các tiêu đề hợp lệ const MIN_LINES_BETWEEN_CHAPTERS = 5; // Số dòng đã qua kể từ tiêu đề cuối let linesSinceLastTitle = 0; // Map để nhóm các tiêu đề theo số chương let chapterGroups = new Map(); // Hàm kiểm tra xem một dòng có phải là tiêu đề chương khô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; } // Hàm trích xuất tên chương từ tiêu đề function extractChapterName(line) { const parts = line.split(':'); if (parts.length < 2) return ''; return parts.slice(1).join(':').replace(/^[.,;'"]+|[.,;'"]+$/g, '').trim(); } // 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); const chapterName = extractChapterName(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 name: chapterName, length: chapterName.length }); } } // Phân tích để loại bỏ các tiêu đề trùng lặp trong cùng một chương for (const [chapterNum, titles] of chapterGroups.entries()) { // Nếu chỉ có một tiêu đề cho số chương này, giữ lại if (titles.length === 1) { chapterNumbers.set(chapterNum, { title: titles[0].title, name: titles[0].name, lineIndex: titles[0].lineIndex }); continue; } // Nếu có nhiều tiêu đề cho cùng một số chương // Chọn tiêu đề có tên dài nhất và xác định nhất titles.sort((a, b) => b.length - a.length); // Ưu tiên tiêu đề đầu tiên nếu các tiêu đề có độ dài tương đương if (titles[0].length > 0) { chapterNumbers.set(chapterNum, { title: titles[0].title, name: titles[0].name, lineIndex: titles[0].lineIndex }); // Đánh dấu các tiêu đề còn lại là trùng lặp for (let i = 1; i < titles.length; i++) { duplicateCount++; } } } // Sắp xếp các chương theo thứ tự dòng trong văn bản const sortedChapters = Array.from(chapterNumbers.entries()) .sort((a, b) => a[1].lineIndex - b[1].lineIndex); // Pass thứ hai: xây dựng nội dung chương từ các tiêu đề đã được xác định for (let i = 0; i < sortedChapters.length; i++) { const [chapterNum, chapterInfo] = sortedChapters[i]; const nextChapterIndex = (i < sortedChapters.length - 1) ? sortedChapters[i+1][1].lineIndex : lines.length; const chapterContent = []; // Thu thập nội dung từ sau tiêu đề đến trước tiêu đề tiếp theo for (let j = chapterInfo.lineIndex + 1; j < nextChapterIndex; j++) { // Loại bỏ các tiêu đề trùng lặp đã được xác định const line = lines[j]; const trimmedLine = line.trim(); if (isChapterTitle(trimmedLine)) { const lineChapterNum = extractChapterNumber(trimmedLine); // Nếu đây là tiêu đề trùng lặp của chương hiện tại, bỏ qua if (lineChapterNum === chapterNum) { continue; } } chapterContent.push(lines[j]); } chapters.push({ title: chapterInfo.title, name: chapterInfo.name, content: chapterContent }); } if (chapters.length > 0) { displayChapters(chapters); if (duplicateCount > 0) { showNotification(`Đã tìm thấy ${chapters.length} chương và bỏ qua ${duplicateCount} tiêu đề lặp lại.`); } else { showNotification(`Đã tìm thấy ${chapters.length} chương và tự động điền vào form.`); } setTimeout(() => { try { transferContent(); } catch (error) { console.error('Lỗi khi tự động điền form:', error); showNotification('Có lỗi xảy ra khi tự động điền form!', true); } }, 1000); } return chapters; } // Tạo HTML cho form chương function createChapterHTML(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" 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="${dăngnhanhTTV.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="${dăngnhanhTTV.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="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[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea> </div> </div> </div>`; } // Chuyển nội dung vào form async function transferContent() { try { const chapterItems = document.querySelectorAll('.ttv-chapter-item'); if (!chapterItems.length) { throw new Error('Không tìm thấy chương nào để điền vào form'); } // Kiểm tra số lượng chương và giới hạn tối đa if (chapterItems.length < 10) { showNotification(`Chỉ có ${chapterItems.length} chương - ít hơn 10 chương. Vẫn tiếp tục đăng.`, false); } else if (chapterItems.length > MAX_CHAPTER_POST) { showNotification(`Đã vượt quá giới hạn ${MAX_CHAPTER_POST} chương cho một lần đăng. Chỉ ${MAX_CHAPTER_POST} chương đầu sẽ được đăng.`, true); } const form = document.querySelector('form[name="postChapForm"]'); if (!form) { throw new Error('Không tìm thấy form đăng chương'); } let chap_vol = parseInt(jQuery('.chap_vol').val()) || 1; let chap_vol_name = jQuery('.chap_vol_name').val() || ''; let maxChapStt = 0; let maxChapSerial = 0; const existingForms = form.querySelectorAll('[id^="COUNT_CHAP_"]'); existingForms.forEach(formElem => { const formIdMatch = formElem.id.match(/COUNT_CHAP_(\d+)_MK/); if (formIdMatch && formIdMatch[1]) { const formIndex = parseInt(formIdMatch[1]); const sttInput = formElem.querySelector(`input[name="chap_stt[${formIndex}]"]`); if (sttInput && sttInput.value && !isNaN(parseInt(sttInput.value))) { const sttVal = parseInt(sttInput.value); if (sttVal > maxChapStt) { maxChapStt = sttVal; } } const serialInput = formElem.querySelector(`input[name="chap_number[${formIndex}]"]`); if (serialInput && serialInput.value && !isNaN(parseInt(serialInput.value))) { const serialVal = parseInt(serialInput.value); if (serialVal > maxChapSerial) { maxChapSerial = serialVal; } } } }); // Đảm bảo STT và số chương tiếp theo form gốc if (maxChapStt > 0) { dăngnhanhTTV.STATE.CHAP_STT = maxChapStt; } if (maxChapSerial > 0) { dăngnhanhTTV.STATE.CHAP_SERIAL = maxChapSerial; } const existingFormCount = existingForms.length; // Giới hạn số lượng chương được đăng trong một lần const chaptersToProcess = Math.min(chapterItems.length, MAX_CHAPTER_POST); for (let i = 0; i < chaptersToProcess; i++) { const formIndex = existingFormCount + i + 1; // Tăng STT và Serial cho mỗi chương mới if (i > 0) { dăngnhanhTTV.STATE.CHAP_STT++; dăngnhanhTTV.STATE.CHAP_SERIAL++; } const chapterHTML = createChapterHTML(formIndex); const tempDiv = document.createElement('div'); tempDiv.innerHTML = chapterHTML; const newFormElement = tempDiv.firstElementChild; if (!newFormElement) { throw new Error(`Không thể tạo element form cho chương ${formIndex}`); } form.appendChild(newFormElement); const chapterItem = chapterItems[i]; const titleElement = chapterItem.querySelector('.chapter-title'); const nameElement = chapterItem.querySelector('.chapter-name'); if (!titleElement || !nameElement) { throw new Error(`Thiếu thông tin tiêu đề hoặc tên cho chương ${formIndex}`); } const chapterTitle = titleElement.textContent; const chapterName = nameElement.textContent.replace('Tên chương: ', ''); const formFields = { chapterName: form.querySelector(`input[name="chap_name[${formIndex}]"]`), content: form.querySelector(`textarea[name="introduce[${formIndex}]"]`), chapterNumber: form.querySelector(`input[name="chap_number[${formIndex}]"]`), chapterOrder: form.querySelector(`input[name="chap_stt[${formIndex}]"]`), volume: form.querySelector(`input[name="vol[${formIndex}]"]`), volumeName: form.querySelector(`input[name="vol_name[${formIndex}]"]`), advertisement: form.querySelector(`textarea[name="adv[${formIndex}]"]`) }; formFields.chapterName.value = chapterName; formFields.chapterNumber.value = dăngnhanhTTV.STATE.CHAP_SERIAL.toString(); formFields.chapterOrder.value = dăngnhanhTTV.STATE.CHAP_STT.toString(); formFields.volume.value = chap_vol; formFields.volumeName.value = chap_vol_name; if (chapterItem._content) { // Chỉ lấy phần nội dung, không lấy phần tiêu đề // Đảm bảo giữ nguyên định dạng gốc của nội dung, không trim formFields.content.value = chapterItem._content; } else { formFields.content.value = ''; } formFields.advertisement.value = ''; const inputEvent = new Event('input', { bubbles: true }); formFields.content.dispatchEvent(inputEvent); } if (chaptersToProcess < chapterItems.length) { showNotification(`Đã tự động điền ${chaptersToProcess}/${chapterItems.length} chương vào form! Chương còn lại sẽ được đăng ở lần sau.`); } else { showNotification(`Đã tự động điền ${chaptersToProcess} chương vào form!`); } } catch (error) { console.error('Lỗi khi điền form:', error); showNotification('Có lỗi xảy ra khi điền form: ' + error.message, true); } } // Hiển thị danh sách chương function displayChapters(chapters) { const chapterList = document.createElement('div'); chapterList.className = 'ttv-chapter-list'; chapters.forEach((chapter, index) => { const chapterItem = document.createElement('div'); chapterItem.className = 'ttv-chapter-item'; // Chỉ lấy phần nội dung dưới tiêu đề, không bao gồm tiêu đề chapterItem._content = chapter.content.join('\n'); // Trích xuất số chương để hiển thị const chapterMatch = chapter.title.match(/Chương\s+(\d+):/); const chapterNum = chapterMatch ? chapterMatch[1] : '?'; const titleDiv = document.createElement('div'); titleDiv.className = 'chapter-title'; titleDiv.textContent = chapter.title; const nameDiv = document.createElement('div'); nameDiv.className = 'chapter-name'; nameDiv.textContent = `Tên chương: ${chapter.name}`; const statsDiv = document.createElement('div'); statsDiv.className = 'chapter-stats'; statsDiv.textContent = `Chương ${chapterNum} | ${chapter.content.length} dòng`; chapterItem.appendChild(titleDiv); chapterItem.appendChild(nameDiv); chapterItem.appendChild(statsDiv); chapterItem.onclick = () => selectChapter(chapter, index); chapterList.appendChild(chapterItem); if (index === 0) { chapterItem.classList.add('selected'); } }); const existingList = document.querySelector('.ttv-chapter-list'); if (existingList) { existingList.remove(); } const contentEditor = document.querySelector('.ttv-content-editor'); contentEditor.parentNode.insertBefore(chapterList, contentEditor); if (chapters.length > 0) { selectChapter(chapters[0], 0); } } // Chọn chương function selectChapter(chapter, index) { const contentEditor = document.querySelector('.ttv-content-editor'); // Chỉ lấy nội dung, không lấy tiêu đề để tránh lặp lại contentEditor.value = chapter.content.join('\n'); const wordCountSpan = document.querySelector('.ttv-word-count'); if (wordCountSpan) { wordCountSpan.textContent = updateWordCount(contentEditor.value); } const chapterItems = document.querySelectorAll('.ttv-chapter-item'); chapterItems.forEach(item => item.classList.remove('selected')); chapterItems[index]?.classList.add('selected'); try { transferContent(); } catch (error) { console.error('Lỗi khi tự động điền form:', error); showNotification('Có lỗi xảy ra khi tự động điền form!', true); } } // Tính số từ function updateWordCount(content) { const wordCount = content.trim().split(/\s+/).length; const charCount = content.length; return `${wordCount} từ | ${charCount} ký tự`; } // Xem trước function togglePreview() { const contentEditor = document.querySelector('.ttv-content-editor'); const preview = document.querySelector('.ttv-preview'); const previewBtn = document.querySelector('.ttv-preview-btn'); if (contentEditor.style.display !== 'none') { contentEditor.style.display = 'none'; preview.style.display = 'block'; preview.innerHTML = contentEditor.value.replace(/\n/g, '<br>'); previewBtn.textContent = 'Soạn thảo'; } else { contentEditor.style.display = 'block'; preview.style.display = 'none'; previewBtn.textContent = 'Xem trước'; } } // Toàn màn hình function toggleFullscreen() { const panel = document.querySelector('.ttv-control-panel'); const fullscreenBtn = document.querySelector('.ttv-fullscreen-btn'); panel.classList.toggle('fullscreen'); fullscreenBtn.textContent = panel.classList.contains('fullscreen') ? 'Thu nhỏ' : 'Toàn màn hình'; } // Tạo panel điều khiển function addControlPanel() { const panel = document.createElement('div'); panel.className = 'ttv-control-panel'; const header = document.createElement('div'); header.className = 'ttv-header'; header.innerHTML = ` <div>Soạn Thảo Nội Dung</div> <div class="ttv-toolbar"> <button class="ttv-preview-btn" onclick="togglePreview()">Xem trước</button> <button class="ttv-fullscreen-btn" onclick="toggleFullscreen()">Toàn màn hình</button> <button class="ttv-minimize">−</button> </div> `; const buttonGroup = document.createElement('div'); buttonGroup.className = 'ttv-button-group'; const contentEditorLabel = document.createElement('div'); contentEditorLabel.className = 'ttv-heading'; contentEditorLabel.innerHTML = ` Nội dung chương: <span class="ttv-word-count">0 từ | 0 ký tự</span> `; const contentEditor = document.createElement('textarea'); contentEditor.className = 'ttv-content-editor'; contentEditor.placeholder = 'Nhập hoặc dán nội dung chương vào đây...'; const preview = document.createElement('div'); preview.className = 'ttv-preview'; contentEditor.oninput = () => { const wordCountSpan = document.querySelector('.ttv-word-count'); if (wordCountSpan) { wordCountSpan.textContent = updateWordCount(contentEditor.value); } }; contentEditor.onpaste = (e) => { setTimeout(() => { const content = contentEditor.value; const chapters = parseChapters(content); if (chapters.length > 0) { displayChapters(chapters); setTimeout(() => { try { transferContent(); } catch (error) { console.error('Lỗi khi tự động điền form:', error); showNotification('Có lỗi xảy ra khi tự động điền form!', true); } }, 1000); } }, 0); }; const transferBtn = document.createElement('button'); transferBtn.type = 'button'; transferBtn.className = 'btn btn-warning'; transferBtn.innerHTML = '<span>Chuyển nội dung sang form</span>'; transferBtn.onclick = transferContent; panel.appendChild(header); buttonGroup.appendChild(contentEditorLabel); buttonGroup.appendChild(contentEditor); buttonGroup.appendChild(preview); buttonGroup.appendChild(transferBtn); panel.appendChild(buttonGroup); document.body.appendChild(panel); const minimizeBtn = panel.querySelector('.ttv-minimize'); minimizeBtn.onclick = () => { panel.classList.toggle('minimized'); minimizeBtn.innerHTML = panel.classList.contains('minimized') ? '+' : '−'; }; window.togglePreview = togglePreview; window.toggleFullscreen = toggleFullscreen; } window.addEventListener('load', function() { dăngnhanhTTV.initializeChapterValues(); addControlPanel(); }); })();