您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Công cụ đăng chương hiện đại cho Tàng Thư Viện với UI/UX được tối ưu
当前为
- // ==UserScript==
- // @name TTV Auto Upload
- // @namespace http://tampermonkey.net/
- // @version 6.2
- // @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';
- if (window.location.href.includes('/danh-sach-chuong/story/')) {
- const storyId = window.location.pathname.split('/').pop();
- setTimeout(() => {
- window.location.href = `https://tangthuvien.net/dang-chuong/story/${storyId}`;
- }, 3000);
- return;
- }
- const HEADER_SIGN = "";
- const FOOTER_SIGN = "";
- const MAX_CHAPTER_POST = 10;
- GM_addStyle(`
- #modern-uploader {
- background-color: white;
- padding: 20px;
- border-radius: 10px;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
- position: fixed;
- right: 20px;
- top: 50%;
- transform: translateY(-50%);
- width: 400px;
- max-height: 90vh;
- overflow-y: auto;
- z-index: 1000;
- }
- @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); }
- }
- textarea[name^="introduce"] {
- transition: all 0.3s ease;
- }
- textarea[name^="introduce"].short-chapter {
- animation: shortChapterBlink 1s infinite;
- border: 2px solid #ff0000 !important;
- background-color: rgba(255, 0, 0, 0.1) !important;
- }
- .chapter-character-count {
- text-align: right;
- font-size: 12px;
- margin-top: 5px;
- color: #666;
- }
- .short-chapters-warning {
- color: #ff0000;
- font-weight: bold;
- animation: shortChapterBlink 1s infinite;
- }
- .button-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 15px;
- margin-top: 15px;
- }
- #modern-uploader .btn {
- padding: 10px 20px;
- border-radius: 6px;
- cursor: pointer;
- font-weight: 600;
- font-size: 14px;
- transition: all 0.2s ease;
- }
- #modern-uploader .form-control {
- width: 100%;
- padding: 15px;
- border: 1px solid #ddd;
- border-radius: 8px;
- margin-bottom: 15px;
- font-size: 16px;
- transition: border-color 0.2s ease;
- }
- #modern-uploader .form-control:focus {
- border-color: #4285f4;
- outline: none;
- }
- `);
- 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,
- AUTO_MODE: false
- },
- ELEMENTS: {
- qpContent: null,
- qpButtonPaste: null,
- qpOptionAuto: null
- },
- init: function() {
- try {
- console.log('[TTV-DEBUG] Script bắt đầu khởi tạo...');
- this.initializeChapterValues();
- this.createInterface();
- this.cacheElements();
- this.registerEvents();
- console.log('[TTV-DEBUG] Script đã khởi động thành công');
- showNotification('Công cụ đã chạy', 'success');
- // Khôi phục trạng thái tự động
- const isAutoMode = localStorage.getItem('TTV_AUTO_MODE') === 'true';
- if (isAutoMode) {
- this.ELEMENTS.qpOptionAuto.prop('checked', true);
- this.STATE.AUTO_MODE = true;
- this.handlePasteButton(); // Tự động paste nếu đang ở chế độ tự động
- }
- } catch (e) {
- console.error('[TTV-ERROR] Lỗi khởi tạo:', e);
- showNotification('Có lỗi khi khởi tạo Script', 'error');
- }
- },
- createInterface: function() {
- const html = `
- <div id="modern-uploader">
- <div class="text-center mb-4">
- <h3 style="color: #4285f4; margin-bottom: 15px; font-weight: 700; font-size: 18px;">📝 CÔNG CỤ ĐĂNG NHANH</h3>
- </div>
- <div class="form-group">
- <textarea placeholder="Nội dung truyện (Dán vào đây để tự động tách chương)" id="qpContent" class="form-control" rows="5"></textarea>
- </div>
- <div class="text-center mb-3">
- <label style="color: #bef385;">
- <input type="checkbox" id="qpOptionAuto" class="form-control" style="height:10px;width: 10px;display: inline-block;">
- Chế độ tự động
- </label>
- </div>
- <div class="button-container" style="display: flex; justify-content: center; gap: 15px;">
- <button class="btn btn-primary" id="qpButtonPaste">📋 Paste</button>
- </div>
- <div class="notification-container"></div>
- </div>`;
- jQuery(".list-in-user").before(html);
- },
- 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);
- }
- },
- cacheElements: function() {
- this.ELEMENTS.qpContent = jQuery("#qpContent");
- this.ELEMENTS.qpButtonPaste = jQuery("#qpButtonPaste");
- this.ELEMENTS.qpOptionAuto = jQuery("#qpOptionAuto");
- },
- registerEvents: function() {
- this.ELEMENTS.qpContent.on("paste", this.handlePaste.bind(this));
- this.ELEMENTS.qpButtonPaste.on('click', this.handlePasteButton.bind(this));
- this.ELEMENTS.qpOptionAuto.on('change', this.toggleAutoMode.bind(this));
- setupCharacterCounter();
- },
- toggleAutoMode: function() {
- this.STATE.AUTO_MODE = this.ELEMENTS.qpOptionAuto.prop('checked');
- localStorage.setItem('TTV_AUTO_MODE', this.STATE.AUTO_MODE);
- if (this.STATE.AUTO_MODE) {
- showNotification('Đã bật chế độ tự động - sẽ tự động paste và đăng chương', 'info');
- // Tự động paste khi bật chế độ tự động
- setTimeout(() => {
- this.handlePasteButton();
- }, 100);
- } else {
- showNotification('Đã tắt chế độ tự động', 'info');
- }
- },
- handlePasteButton: function() {
- this.showLoading();
- navigator.clipboard.readText()
- .then(text => {
- this.ELEMENTS.qpContent.val(text);
- setTimeout(() => {
- this.performAction();
- this.hideLoading();
- }, 100);
- })
- .catch(err => {
- console.error('Không thể đọc dữ liệu từ clipboard:', err);
- this.hideLoading();
- showNotification('Không thể truy cập clipboard. Vui lòng dán trực tiếp vào ô nội dung.', 'error');
- });
- },
- handlePaste: function(e) {
- e.preventDefault();
- this.ELEMENTS.qpContent.val("");
- this.showLoading();
- const pastedText = e.originalEvent.clipboardData.getData('text');
- this.ELEMENTS.qpContent.val(pastedText);
- setTimeout(() => {
- this.performAction();
- this.hideLoading();
- }, 100);
- },
- performAction: function() {
- try {
- console.log("[TTV-DEBUG] Bắt đầu performAction");
- var text = this.ELEMENTS.qpContent.val();
- if (!text) {
- showNotification('Không có nội dung để tách chương', 'error');
- return;
- }
- // Xử lý tách chương và điền form
- var chapters = this.splitChapters(text);
- if (chapters.length === 0) {
- showNotification('Không tìm thấy chương nào', 'error');
- return;
- }
- // Lấy 10 chương đầu để điền vào form
- const chaptersToFill = chapters.slice(0, MAX_CHAPTER_POST);
- const remainingChapters = chapters.slice(MAX_CHAPTER_POST);
- // Điền 10 chương đầu vào form
- this.fillChaptersToForm(chaptersToFill);
- console.log(`[TTV-DEBUG] Đã điền ${chaptersToFill.length} chương vào form`);
- // Copy các chương còn lại vào clipboard nếu có
- if (remainingChapters.length > 0) {
- this.copyRemainingChapters(remainingChapters);
- console.log(`[TTV-DEBUG] Đã copy ${remainingChapters.length} chương vào clipboard`);
- }
- // Nếu đang ở chế độ tự động, đợi 2 giây rồi đăng
- if (this.STATE.AUTO_MODE) {
- showNotification('Sẽ tự động đăng sau 2 giây...', 'info');
- setTimeout(() => {
- this.submitChapters();
- }, 2000);
- }
- } catch (error) {
- console.error('[TTV-ERROR] Lỗi xử lý chương:', error);
- showNotification('Có lỗi khi xử lý các chương. Vui lòng thử lại.', 'error');
- }
- },
- splitChapters: function(text) {
- var chapters = [];
- var lines = text.split('\n');
- var currentChapter = [];
- var lastTitle = null;
- for (let i = 0; i < lines.length; i++) {
- let line = lines[i];
- let isChapterTitle = /^\t[Cc]hương\s*\d+\s*:/.test(line) || /^\s{4,}[Cc]hương\s*\d+\s*:/.test(line);
- if (isChapterTitle) {
- if (currentChapter.length > 0) {
- if (line !== lastTitle) {
- chapters.push(currentChapter.join('\n'));
- currentChapter = [line];
- lastTitle = line;
- }
- } else {
- currentChapter = [line];
- lastTitle = line;
- }
- } else if (currentChapter.length > 0) {
- currentChapter.push(line);
- }
- }
- if (currentChapter.length > 0) {
- chapters.push(currentChapter.join('\n'));
- }
- return chapters;
- },
- fillChaptersToForm: function(chapters) {
- var titles = jQuery("input[name^='chap_name']");
- var contents = jQuery("textarea[name^='introduce']");
- var advs = jQuery("textarea[name^='adv']");
- // Thêm form cho đủ số chương cần thiết
- const neededForms = chapters.length - titles.length;
- if (neededForms > 0 && titles.length < MAX_CHAPTER_POST) {
- for (let i = 0; i < neededForms && (titles.length + i) < MAX_CHAPTER_POST; i++) {
- this.addNewChapter();
- }
- titles = jQuery("input[name^='chap_name']");
- contents = jQuery("textarea[name^='introduce']");
- advs = jQuery("textarea[name^='adv']");
- }
- // Điền nội dung vào form
- jQuery.each(titles, function(k, v) {
- if (k < chapters.length) {
- var content = chapters[k].split('\n');
- var title = content.shift().trim();
- var chapterTitle = title;
- if (title.includes(':')) {
- chapterTitle = title.substring(title.indexOf(':') + 1).trim();
- }
- if (!chapterTitle || chapterTitle.trim() === '') {
- chapterTitle = "Vô đề";
- }
- titles[k].value = chapterTitle;
- contents[k].value = HEADER_SIGN + "\r\n" + content.join('\n') + "\r\n" + FOOTER_SIGN;
- if (advs[k]) advs[k].value = "";
- jQuery(contents[k]).trigger('input');
- }
- });
- showNotification(`Đã điền ${chapters.length} chương vào form`, 'success');
- },
- copyRemainingChapters: function(chapters) {
- try {
- const clipboardContent = chapters.map(chap => {
- const lines = chap.trim().split('\n');
- if (lines.length > 0 && !lines[0].startsWith('\t')) {
- lines[0] = '\t' + lines[0];
- }
- return lines.join('\n');
- }).join('\n\n---CHAPTER_SEPARATOR---\n\n');
- navigator.clipboard.writeText(clipboardContent)
- .then(() => {
- showNotification(`Đã copy ${chapters.length} chương còn lại vào clipboard`, 'success');
- })
- .catch(() => {
- showNotification('Không thể copy vào clipboard', 'error');
- });
- } catch (error) {
- console.error('Lỗi copy clipboard:', error);
- showNotification('Có lỗi khi copy các chương còn lại', 'error');
- }
- },
- submitChapters: function() {
- // Kiểm tra nút submit
- const postButton = jQuery('button[type="submit"]');
- if (!postButton.length) {
- showNotification('Không tìm thấy nút đăng chương!', 'error');
- return;
- }
- // Kiểm tra độ dài chương
- if (!validateChapterLengths()) {
- showNotification('Có chương chưa đủ độ dài tối thiểu (3000 ký tự)!', 'error');
- return;
- }
- // Đăng chương
- postButton.click();
- showNotification('Đang đăng chương...', 'info');
- // Nếu đang ở chế độ tự động, reload trang sau 5 giây
- if (this.STATE.AUTO_MODE) {
- setTimeout(() => {
- window.location.reload();
- }, 5000);
- }
- },
- addNewChapter: function() {
- if ((this.STATE.CHAP_NUMBER + 1) <= MAX_CHAPTER_POST) {
- this.STATE.CHAP_NUMBER++;
- this.STATE.CHAP_STT++;
- this.STATE.CHAP_SERIAL++;
- var html = createChapterHTML(this.STATE.CHAP_NUMBER);
- jQuery('#div_chapt_upload').append(html);
- }
- },
- showLoading: function() {
- jQuery(".loading-overlay").remove();
- var loading = jQuery("<div>", {
- class: "loading-overlay",
- css: {
- position: "fixed",
- top: "0",
- left: "0",
- width: "100%",
- height: "100%",
- backgroundColor: "rgba(0, 0, 0, 0.5)",
- zIndex: "9999",
- display: "flex",
- justifyContent: "center",
- alignItems: "center"
- }
- });
- loading.append(`
- <div style="
- background-color: white;
- padding: 20px;
- border-radius: 10px;
- text-align: center;
- ">
- <div style="
- border: 4px solid #f3f3f3;
- border-top: 4px solid #3498db;
- border-radius: 50%;
- width: 40px;
- height: 40px;
- margin: 0 auto 10px;
- animation: spin 1s linear infinite;
- "></div>
- <div>Đang xử lý...</div>
- </div>
- `);
- jQuery("body").append(loading);
- jQuery("head").append(`
- <style>
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- </style>
- `);
- },
- hideLoading: function() {
- jQuery(".loading-overlay").remove();
- }
- };
- function showNotification(message, type) {
- jQuery('#modern-uploader .notification-container').remove();
- const container = jQuery("<div>", {
- class: "notification-container",
- css: {
- width: "100%",
- padding: "10px 0",
- marginTop: "10px",
- textAlign: "left",
- borderTop: "1px solid rgba(0,0,0,0.1)"
- }
- });
- const notification = jQuery("<div>", {
- class: `notification-${type}`,
- css: {
- backgroundColor: type === 'success' ? "#e8f5e9" : (type === 'error' ? "#ffebee" : "#fff8e1"),
- color: type === 'success' ? "#000000" : (type === 'error' ? "#d32f2f" : "#ff9800"),
- padding: "10px 15px",
- borderRadius: "8px",
- fontSize: "14px",
- fontWeight: "500",
- boxShadow: "0 4px 10px rgba(0,0,0,0.15)",
- display: "inline-block",
- maxWidth: "90%",
- margin: "0",
- wordBreak: "break-word",
- border: type === 'success' ? "1px solid #81c784" : (type === 'error' ? "1px solid #d32f2f" : "1px solid #ff9800")
- }
- });
- const lines = message.split('\n');
- lines.forEach((line, index) => {
- notification.append(jQuery("<div>").html(line));
- });
- container.append(notification);
- jQuery("#modern-uploader .button-container").after(container);
- notification.fadeIn(300);
- }
- 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>`;
- }
- function setupCharacterCounter() {
- jQuery(document).on("input", "[name^=introduce]", function() {
- const text = jQuery(this).val();
- const charCount = text.length;
- let charCountElement = jQuery(this).next('.chapter-character-count');
- if (charCountElement.length === 0) {
- charCountElement = jQuery('<div class="chapter-character-count"></div>');
- jQuery(this).after(charCountElement);
- }
- if(charCount < 3000) {
- jQuery(this).addClass('short-chapter');
- charCountElement.html(`<span class="short-chapters-warning">${charCount.toLocaleString()}/40.000 ký tự</span>`);
- } else {
- jQuery(this).removeClass('short-chapter');
- if(charCount > 40000) {
- charCountElement.html(`<span style="color: #fbbc05;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
- } else {
- charCountElement.html(`<span style="color: #34a853;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
- }
- }
- });
- }
- function validateChapterLengths() {
- let hasError = false;
- jQuery('form[name="postChapForm"] .chapter-detail').each(function() {
- const form = this;
- const contentTextarea = form.querySelector('textarea[name^="introduce"]');
- const content = contentTextarea.value;
- if (content.length < 3000) {
- jQuery(contentTextarea).addClass('short-chapter');
- let warningIcon = form.querySelector('.warning-icon');
- if (!warningIcon) {
- warningIcon = document.createElement('div');
- warningIcon.className = 'warning-icon';
- warningIcon.innerHTML = '⚠️';
- contentTextarea.parentNode.appendChild(warningIcon);
- }
- hasError = true;
- } else {
- jQuery(contentTextarea).removeClass('short-chapter');
- const warningIcon = form.querySelector('.warning-icon');
- if (warningIcon) {
- warningIcon.remove();
- }
- }
- });
- return !hasError;
- }
- dăngnhanhTTV.init();
- })();