TTV Auto Upload

Công cụ đăng chương hiện đại cho Tàng Thư Viện với UI/UX được tối ưu

当前为 2025-03-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name TTV Auto Upload
  3. // @namespace http://tampermonkey.net/
  4. // @version 6.2
  5. // @description Công cụ đăng chương hiện đại cho Tàng Thư Viện với UI/UX được tối ưu
  6. // @author HA
  7. // @match https://tangthuvien.net/dang-chuong/story/*
  8. // @match https://tangthuvien.net/danh-sach-chuong/story/*
  9. // @grant GM_addStyle
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @required https://code.jquery.com/jquery-3.2.1.min.js
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17. if (window.location.href.includes('/danh-sach-chuong/story/')) {
  18. const storyId = window.location.pathname.split('/').pop();
  19. setTimeout(() => {
  20. window.location.href = `https://tangthuvien.net/dang-chuong/story/${storyId}`;
  21. }, 3000);
  22. return;
  23. }
  24.  
  25. const HEADER_SIGN = "";
  26. const FOOTER_SIGN = "";
  27. const MAX_CHAPTER_POST = 10;
  28.  
  29. GM_addStyle(`
  30. #modern-uploader {
  31. background-color: white;
  32. padding: 20px;
  33. border-radius: 10px;
  34. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
  35. position: fixed;
  36. right: 20px;
  37. top: 50%;
  38. transform: translateY(-50%);
  39. width: 400px;
  40. max-height: 90vh;
  41. overflow-y: auto;
  42. z-index: 1000;
  43. }
  44. @keyframes shortChapterBlink {
  45. 0% { background-color: rgba(255, 0, 0, 0.1); }
  46. 50% { background-color: rgba(255, 0, 0, 0.2); }
  47. 100% { background-color: rgba(255, 0, 0, 0.1); }
  48. }
  49. textarea[name^="introduce"] {
  50. transition: all 0.3s ease;
  51. }
  52. textarea[name^="introduce"].short-chapter {
  53. animation: shortChapterBlink 1s infinite;
  54. border: 2px solid #ff0000 !important;
  55. background-color: rgba(255, 0, 0, 0.1) !important;
  56. }
  57. .chapter-character-count {
  58. text-align: right;
  59. font-size: 12px;
  60. margin-top: 5px;
  61. color: #666;
  62. }
  63. .short-chapters-warning {
  64. color: #ff0000;
  65. font-weight: bold;
  66. animation: shortChapterBlink 1s infinite;
  67. }
  68. .button-container {
  69. display: flex;
  70. justify-content: space-between;
  71. align-items: center;
  72. gap: 15px;
  73. margin-top: 15px;
  74. }
  75. #modern-uploader .btn {
  76. padding: 10px 20px;
  77. border-radius: 6px;
  78. cursor: pointer;
  79. font-weight: 600;
  80. font-size: 14px;
  81. transition: all 0.2s ease;
  82. }
  83. #modern-uploader .form-control {
  84. width: 100%;
  85. padding: 15px;
  86. border: 1px solid #ddd;
  87. border-radius: 8px;
  88. margin-bottom: 15px;
  89. font-size: 16px;
  90. transition: border-color 0.2s ease;
  91. }
  92. #modern-uploader .form-control:focus {
  93. border-color: #4285f4;
  94. outline: none;
  95. }
  96. `);
  97.  
  98. const dăngnhanhTTV = {
  99. STATE: {
  100. CHAP_NUMBER: 1,
  101. CHAP_STT: 1,
  102. CHAP_SERIAL: 1,
  103. CHAP_NUMBER_ORIGINAL: 1,
  104. CHAP_STT_ORIGINAL: 1,
  105. CHAP_SERIAL_ORIGINAL: 1,
  106. AUTO_MODE: false
  107. },
  108.  
  109. ELEMENTS: {
  110. qpContent: null,
  111. qpButtonPaste: null,
  112. qpOptionAuto: null
  113. },
  114.  
  115. init: function() {
  116. try {
  117. console.log('[TTV-DEBUG] Script bắt đầu khởi tạo...');
  118. this.initializeChapterValues();
  119. this.createInterface();
  120. this.cacheElements();
  121. this.registerEvents();
  122. console.log('[TTV-DEBUG] Script đã khởi động thành công');
  123. showNotification('Công cụ đã chạy', 'success');
  124.  
  125. // Khôi phục trạng thái tự động
  126. const isAutoMode = localStorage.getItem('TTV_AUTO_MODE') === 'true';
  127. if (isAutoMode) {
  128. this.ELEMENTS.qpOptionAuto.prop('checked', true);
  129. this.STATE.AUTO_MODE = true;
  130. this.handlePasteButton(); // Tự động paste nếu đang ở chế độ tự động
  131. }
  132. } catch (e) {
  133. console.error('[TTV-ERROR] Lỗi khởi tạo:', e);
  134. showNotification('Có lỗi khi khởi tạo Script', 'error');
  135. }
  136. },
  137.  
  138. createInterface: function() {
  139. const html = `
  140. <div id="modern-uploader">
  141. <div class="text-center mb-4">
  142. <h3 style="color: #4285f4; margin-bottom: 15px; font-weight: 700; font-size: 18px;">📝 CÔNG C ĐĂNG NHANH</h3>
  143. </div>
  144. <div class="form-group">
  145. <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>
  146. </div>
  147. <div class="text-center mb-3">
  148. <label style="color: #bef385;">
  149. <input type="checkbox" id="qpOptionAuto" class="form-control" style="height:10px;width: 10px;display: inline-block;">
  150. Chế độ t động
  151. </label>
  152. </div>
  153. <div class="button-container" style="display: flex; justify-content: center; gap: 15px;">
  154. <button class="btn btn-primary" id="qpButtonPaste">📋 Paste</button>
  155. </div>
  156. <div class="notification-container"></div>
  157. </div>`;
  158.  
  159. jQuery(".list-in-user").before(html);
  160. },
  161.  
  162. initializeChapterValues: function() {
  163. try {
  164. const chap_number = parseInt(jQuery('#chap_number').val());
  165. let chap_stt = parseInt(jQuery('.chap_stt1').val());
  166. let chap_serial = parseInt(jQuery('.chap_serial').val());
  167.  
  168. if (parseInt(jQuery('#chap_stt').val()) > chap_stt) {
  169. chap_stt = parseInt(jQuery('#chap_stt').val());
  170. }
  171. if (parseInt(jQuery('#chap_serial').val()) > chap_serial) {
  172. chap_serial = parseInt(jQuery('#chap_serial').val());
  173. }
  174.  
  175. this.STATE.CHAP_NUMBER = this.STATE.CHAP_NUMBER_ORIGINAL = chap_number || 1;
  176. this.STATE.CHAP_STT = this.STATE.CHAP_STT_ORIGINAL = chap_stt || 1;
  177. this.STATE.CHAP_SERIAL = this.STATE.CHAP_SERIAL_ORIGINAL = chap_serial || 1;
  178. } catch (e) {
  179. console.error("Error initializing chapter values:", e);
  180. }
  181. },
  182.  
  183. cacheElements: function() {
  184. this.ELEMENTS.qpContent = jQuery("#qpContent");
  185. this.ELEMENTS.qpButtonPaste = jQuery("#qpButtonPaste");
  186. this.ELEMENTS.qpOptionAuto = jQuery("#qpOptionAuto");
  187. },
  188.  
  189. registerEvents: function() {
  190. this.ELEMENTS.qpContent.on("paste", this.handlePaste.bind(this));
  191. this.ELEMENTS.qpButtonPaste.on('click', this.handlePasteButton.bind(this));
  192. this.ELEMENTS.qpOptionAuto.on('change', this.toggleAutoMode.bind(this));
  193. setupCharacterCounter();
  194. },
  195.  
  196. toggleAutoMode: function() {
  197. this.STATE.AUTO_MODE = this.ELEMENTS.qpOptionAuto.prop('checked');
  198. localStorage.setItem('TTV_AUTO_MODE', this.STATE.AUTO_MODE);
  199.  
  200. if (this.STATE.AUTO_MODE) {
  201. showNotification('Đã bật chế độ tự động - sẽ tự động paste và đăng chương', 'info');
  202. // Tự động paste khi bật chế độ tự động
  203. setTimeout(() => {
  204. this.handlePasteButton();
  205. }, 100);
  206. } else {
  207. showNotification('Đã tắt chế độ tự động', 'info');
  208. }
  209. },
  210.  
  211. handlePasteButton: function() {
  212. this.showLoading();
  213. navigator.clipboard.readText()
  214. .then(text => {
  215. this.ELEMENTS.qpContent.val(text);
  216. setTimeout(() => {
  217. this.performAction();
  218. this.hideLoading();
  219. }, 100);
  220. })
  221. .catch(err => {
  222. console.error('Không thể đọc dữ liệu từ clipboard:', err);
  223. this.hideLoading();
  224. showNotification('Không thể truy cập clipboard. Vui lòng dán trực tiếp vào ô nội dung.', 'error');
  225. });
  226. },
  227.  
  228. handlePaste: function(e) {
  229. e.preventDefault();
  230. this.ELEMENTS.qpContent.val("");
  231. this.showLoading();
  232. const pastedText = e.originalEvent.clipboardData.getData('text');
  233. this.ELEMENTS.qpContent.val(pastedText);
  234. setTimeout(() => {
  235. this.performAction();
  236. this.hideLoading();
  237. }, 100);
  238. },
  239.  
  240. performAction: function() {
  241. try {
  242. console.log("[TTV-DEBUG] Bắt đầu performAction");
  243. var text = this.ELEMENTS.qpContent.val();
  244.  
  245. if (!text) {
  246. showNotification('Không có nội dung để tách chương', 'error');
  247. return;
  248. }
  249.  
  250. // Xử lý tách chương và điền form
  251. var chapters = this.splitChapters(text);
  252. if (chapters.length === 0) {
  253. showNotification('Không tìm thấy chương nào', 'error');
  254. return;
  255. }
  256.  
  257. // Lấy 10 chương đầu để điền vào form
  258. const chaptersToFill = chapters.slice(0, MAX_CHAPTER_POST);
  259. const remainingChapters = chapters.slice(MAX_CHAPTER_POST);
  260.  
  261. // Điền 10 chương đầu vào form
  262. this.fillChaptersToForm(chaptersToFill);
  263. console.log(`[TTV-DEBUG] Đã đin ${chaptersToFill.length} chương vào form`);
  264.  
  265. // Copy các chương còn lại vào clipboard nếu có
  266. if (remainingChapters.length > 0) {
  267. this.copyRemainingChapters(remainingChapters);
  268. console.log(`[TTV-DEBUG] Đã copy ${remainingChapters.length} chương vào clipboard`);
  269. }
  270.  
  271. // Nếu đang ở chế độ tự động, đợi 2 giây rồi đăng
  272. if (this.STATE.AUTO_MODE) {
  273. showNotification('Sẽ tự động đăng sau 2 giây...', 'info');
  274. setTimeout(() => {
  275. this.submitChapters();
  276. }, 2000);
  277. }
  278.  
  279. } catch (error) {
  280. console.error('[TTV-ERROR] Lỗi xử lý chương:', error);
  281. showNotification('Có lỗi khi xử lý các chương. Vui lòng thử lại.', 'error');
  282. }
  283. },
  284.  
  285. splitChapters: function(text) {
  286. var chapters = [];
  287. var lines = text.split('\n');
  288. var currentChapter = [];
  289. var lastTitle = null;
  290.  
  291. for (let i = 0; i < lines.length; i++) {
  292. let line = lines[i];
  293. let isChapterTitle = /^\t[Cc]hương\s*\d+\s*:/.test(line) || /^\s{4,}[Cc]hương\s*\d+\s*:/.test(line);
  294.  
  295. if (isChapterTitle) {
  296. if (currentChapter.length > 0) {
  297. if (line !== lastTitle) {
  298. chapters.push(currentChapter.join('\n'));
  299. currentChapter = [line];
  300. lastTitle = line;
  301. }
  302. } else {
  303. currentChapter = [line];
  304. lastTitle = line;
  305. }
  306. } else if (currentChapter.length > 0) {
  307. currentChapter.push(line);
  308. }
  309. }
  310.  
  311. if (currentChapter.length > 0) {
  312. chapters.push(currentChapter.join('\n'));
  313. }
  314.  
  315. return chapters;
  316. },
  317.  
  318. fillChaptersToForm: function(chapters) {
  319. var titles = jQuery("input[name^='chap_name']");
  320. var contents = jQuery("textarea[name^='introduce']");
  321. var advs = jQuery("textarea[name^='adv']");
  322.  
  323. // Thêm form cho đủ số chương cần thiết
  324. const neededForms = chapters.length - titles.length;
  325. if (neededForms > 0 && titles.length < MAX_CHAPTER_POST) {
  326. for (let i = 0; i < neededForms && (titles.length + i) < MAX_CHAPTER_POST; i++) {
  327. this.addNewChapter();
  328. }
  329. titles = jQuery("input[name^='chap_name']");
  330. contents = jQuery("textarea[name^='introduce']");
  331. advs = jQuery("textarea[name^='adv']");
  332. }
  333.  
  334. // Điền nội dung vào form
  335. jQuery.each(titles, function(k, v) {
  336. if (k < chapters.length) {
  337. var content = chapters[k].split('\n');
  338. var title = content.shift().trim();
  339. var chapterTitle = title;
  340. if (title.includes(':')) {
  341. chapterTitle = title.substring(title.indexOf(':') + 1).trim();
  342. }
  343. if (!chapterTitle || chapterTitle.trim() === '') {
  344. chapterTitle = "Vô đề";
  345. }
  346. titles[k].value = chapterTitle;
  347. contents[k].value = HEADER_SIGN + "\r\n" + content.join('\n') + "\r\n" + FOOTER_SIGN;
  348. if (advs[k]) advs[k].value = "";
  349. jQuery(contents[k]).trigger('input');
  350. }
  351. });
  352.  
  353. showNotification(`Đã đin ${chapters.length} chương vào form`, 'success');
  354. },
  355.  
  356. copyRemainingChapters: function(chapters) {
  357. try {
  358. const clipboardContent = chapters.map(chap => {
  359. const lines = chap.trim().split('\n');
  360. if (lines.length > 0 && !lines[0].startsWith('\t')) {
  361. lines[0] = '\t' + lines[0];
  362. }
  363. return lines.join('\n');
  364. }).join('\n\n---CHAPTER_SEPARATOR---\n\n');
  365.  
  366. navigator.clipboard.writeText(clipboardContent)
  367. .then(() => {
  368. showNotification(`Đã copy ${chapters.length} chương còn li vào clipboard`, 'success');
  369. })
  370. .catch(() => {
  371. showNotification('Không thể copy vào clipboard', 'error');
  372. });
  373. } catch (error) {
  374. console.error('Lỗi copy clipboard:', error);
  375. showNotification('Có lỗi khi copy các chương còn lại', 'error');
  376. }
  377. },
  378.  
  379. submitChapters: function() {
  380. // Kiểm tra nút submit
  381. const postButton = jQuery('button[type="submit"]');
  382. if (!postButton.length) {
  383. showNotification('Không tìm thấy nút đăng chương!', 'error');
  384. return;
  385. }
  386.  
  387. // Kiểm tra độ dài chương
  388. if (!validateChapterLengths()) {
  389. showNotification('Có chương chưa đủ độ dài tối thiểu (3000 ký tự)!', 'error');
  390. return;
  391. }
  392.  
  393. // Đăng chương
  394. postButton.click();
  395. showNotification('Đang đăng chương...', 'info');
  396.  
  397. // Nếu đang ở chế độ tự động, reload trang sau 5 giây
  398. if (this.STATE.AUTO_MODE) {
  399. setTimeout(() => {
  400. window.location.reload();
  401. }, 5000);
  402. }
  403. },
  404.  
  405. addNewChapter: function() {
  406. if ((this.STATE.CHAP_NUMBER + 1) <= MAX_CHAPTER_POST) {
  407. this.STATE.CHAP_NUMBER++;
  408. this.STATE.CHAP_STT++;
  409. this.STATE.CHAP_SERIAL++;
  410. var html = createChapterHTML(this.STATE.CHAP_NUMBER);
  411. jQuery('#div_chapt_upload').append(html);
  412. }
  413. },
  414.  
  415. showLoading: function() {
  416. jQuery(".loading-overlay").remove();
  417. var loading = jQuery("<div>", {
  418. class: "loading-overlay",
  419. css: {
  420. position: "fixed",
  421. top: "0",
  422. left: "0",
  423. width: "100%",
  424. height: "100%",
  425. backgroundColor: "rgba(0, 0, 0, 0.5)",
  426. zIndex: "9999",
  427. display: "flex",
  428. justifyContent: "center",
  429. alignItems: "center"
  430. }
  431. });
  432. loading.append(`
  433. <div style="
  434. background-color: white;
  435. padding: 20px;
  436. border-radius: 10px;
  437. text-align: center;
  438. ">
  439. <div style="
  440. border: 4px solid #f3f3f3;
  441. border-top: 4px solid #3498db;
  442. border-radius: 50%;
  443. width: 40px;
  444. height: 40px;
  445. margin: 0 auto 10px;
  446. animation: spin 1s linear infinite;
  447. "></div>
  448. <divang x lý...</div>
  449. </div>
  450. `);
  451. jQuery("body").append(loading);
  452. jQuery("head").append(`
  453. <style>
  454. @keyframes spin {
  455. 0% { transform: rotate(0deg); }
  456. 100% { transform: rotate(360deg); }
  457. }
  458. </style>
  459. `);
  460. },
  461.  
  462. hideLoading: function() {
  463. jQuery(".loading-overlay").remove();
  464. }
  465. };
  466.  
  467. function showNotification(message, type) {
  468. jQuery('#modern-uploader .notification-container').remove();
  469. const container = jQuery("<div>", {
  470. class: "notification-container",
  471. css: {
  472. width: "100%",
  473. padding: "10px 0",
  474. marginTop: "10px",
  475. textAlign: "left",
  476. borderTop: "1px solid rgba(0,0,0,0.1)"
  477. }
  478. });
  479. const notification = jQuery("<div>", {
  480. class: `notification-${type}`,
  481. css: {
  482. backgroundColor: type === 'success' ? "#e8f5e9" : (type === 'error' ? "#ffebee" : "#fff8e1"),
  483. color: type === 'success' ? "#000000" : (type === 'error' ? "#d32f2f" : "#ff9800"),
  484. padding: "10px 15px",
  485. borderRadius: "8px",
  486. fontSize: "14px",
  487. fontWeight: "500",
  488. boxShadow: "0 4px 10px rgba(0,0,0,0.15)",
  489. display: "inline-block",
  490. maxWidth: "90%",
  491. margin: "0",
  492. wordBreak: "break-word",
  493. border: type === 'success' ? "1px solid #81c784" : (type === 'error' ? "1px solid #d32f2f" : "1px solid #ff9800")
  494.  
  495. }
  496. });
  497. const lines = message.split('\n');
  498. lines.forEach((line, index) => {
  499. notification.append(jQuery("<div>").html(line));
  500. });
  501. container.append(notification);
  502. jQuery("#modern-uploader .button-container").after(container);
  503. notification.fadeIn(300);
  504. }
  505.  
  506. function createChapterHTML(chapNum) {
  507. const chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
  508. const chap_vol_name = jQuery('.chap_vol_name').val() || '';
  509. return `
  510. <div data-gen="MK_GEN" id="COUNT_CHAP_${chapNum}_MK">
  511. <div class="col-xs-12 form-group"></div>
  512. <div class="form-group">
  513. <label class="col-sm-2" for="chap_stt">STT</label>
  514. <div class="col-sm-8">
  515. <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"/>
  516. </div>
  517. </div>
  518. <div class="form-group">
  519. <label class="col-sm-2" for="chap_number">Chương thứ..</label>
  520. <div class="col-sm-8">
  521. <input value="${dăngnhanhTTV.STATE.CHAP_SERIAL}" required class="form-control" name="chap_number[${chapNum}]" placeholder="Chương thứ.. (1,2,3..)" type="text"/>
  522. </div>
  523. </div>
  524. <div class="form-group">
  525. <label class="col-sm-2" for="chap_name">Quyn số</label>
  526. <div class="col-sm-8">
  527. <input class="form-control" name="vol[${chapNum}]" placeholder="Quyển số" type="number" value="${chap_vol}" required/>
  528. </div>
  529. </div>
  530. <div class="form-group">
  531. <label class="col-sm-2" for="chap_name">Tên quyn</label>
  532. <div class="col-sm-8">
  533. <input class="form-control chap_vol_name" name="vol_name[${chapNum}]" placeholder="Tên quyển" type="text" value="${chap_vol_name}" />
  534. </div>
  535. </div>
  536. <div class="form-group">
  537. <label class="col-sm-2" for="chap_name">Tên chương</label>
  538. <div class="col-sm-8">
  539. <input required class="form-control" name="chap_name[${chapNum}]" placeholder="Tên chương" type="text"/>
  540. </div>
  541. </div>
  542. <div class="form-group">
  543. <label class="col-sm-2" for="introduce">Ni dung</label>
  544. <div class="col-sm-8">
  545. <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>
  546. <div class="chapter-character-count"></div>
  547. </div>
  548. </div>
  549. <div class="form-group">
  550. <label class="col-sm-2" for="adv">Qung cáo</label>
  551. <div class="col-sm-8">
  552. <textarea maxlength="1000" class="form-control" name="adv[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea>
  553. </div>
  554. </div>
  555. </div>`;
  556. }
  557.  
  558. function setupCharacterCounter() {
  559. jQuery(document).on("input", "[name^=introduce]", function() {
  560. const text = jQuery(this).val();
  561. const charCount = text.length;
  562. let charCountElement = jQuery(this).next('.chapter-character-count');
  563. if (charCountElement.length === 0) {
  564. charCountElement = jQuery('<div class="chapter-character-count"></div>');
  565. jQuery(this).after(charCountElement);
  566. }
  567. if(charCount < 3000) {
  568. jQuery(this).addClass('short-chapter');
  569. charCountElement.html(`<span class="short-chapters-warning">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  570. } else {
  571. jQuery(this).removeClass('short-chapter');
  572. if(charCount > 40000) {
  573. charCountElement.html(`<span style="color: #fbbc05;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  574. } else {
  575. charCountElement.html(`<span style="color: #34a853;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  576. }
  577. }
  578. });
  579. }
  580.  
  581. function validateChapterLengths() {
  582. let hasError = false;
  583. jQuery('form[name="postChapForm"] .chapter-detail').each(function() {
  584. const form = this;
  585. const contentTextarea = form.querySelector('textarea[name^="introduce"]');
  586. const content = contentTextarea.value;
  587. if (content.length < 3000) {
  588. jQuery(contentTextarea).addClass('short-chapter');
  589. let warningIcon = form.querySelector('.warning-icon');
  590. if (!warningIcon) {
  591. warningIcon = document.createElement('div');
  592. warningIcon.className = 'warning-icon';
  593. warningIcon.innerHTML = '⚠️';
  594. contentTextarea.parentNode.appendChild(warningIcon);
  595. }
  596. hasError = true;
  597. } else {
  598. jQuery(contentTextarea).removeClass('short-chapter');
  599. const warningIcon = form.querySelector('.warning-icon');
  600. if (warningIcon) {
  601. warningIcon.remove();
  602. }
  603. }
  604. });
  605. return !hasError;
  606. }
  607.  
  608. dăngnhanhTTV.init();
  609. })();