TTV

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

  1. // ==UserScript==
  2. // @name TTV
  3. // @namespace http://tampermonkey.net/
  4. // @version 4.0
  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. GM_addStyle(`
  29. /* Vị trí và giao diện của công cụ đăng nhanh */
  30. #modern-uploader {
  31. background: linear-gradient(145deg, #ffffff, #f5f8ff);
  32. padding: 28px;
  33. border-radius: 16px;
  34. box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
  35. position: fixed;
  36. right: 25px;
  37. top: 50%;
  38. transform: translateY(-50%);
  39. width: 450px;
  40. max-height: 92vh;
  41. overflow-y: auto;
  42. z-index: 1000;
  43. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  44. border: 1px solid #e0e0e0;
  45. transition: all 0.3s ease;
  46. }
  47. #modern-uploader:hover {
  48. box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15);
  49. }
  50. #modern-uploader::-webkit-scrollbar {
  51. width: 8px;
  52. }
  53. #modern-uploader::-webkit-scrollbar-track {
  54. background: #f5f5f5;
  55. border-radius: 8px;
  56. }
  57. #modern-uploader::-webkit-scrollbar-thumb {
  58. background: linear-gradient(180deg, #4285f4, #34a853);
  59. border-radius: 8px;
  60. }
  61. #modern-uploader::-webkit-scrollbar-thumb:hover {
  62. background: linear-gradient(180deg, #1a73e8, #27833c);
  63. }
  64. @keyframes shortChapterBlink {
  65. 0% { background-color: rgba(255, 0, 0, 0.1); }
  66. 50% { background-color: rgba(255, 0, 0, 0.2); }
  67. 100% { background-color: rgba(255, 0, 0, 0.1); }
  68. }
  69. textarea[name^="introduce"] {
  70. transition: all 0.3s ease;
  71. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  72. line-height: 1.6;
  73. }
  74. textarea[name^="introduce"].short-chapter {
  75. animation: shortChapterBlink 1s infinite;
  76. border: 2px solid #ff0000 !important;
  77. background-color: rgba(255, 0, 0, 0.1) !important;
  78. }
  79. .chapter-character-count {
  80. text-align: right;
  81. font-size: 12px;
  82. margin-top: 10px;
  83. color: #666;
  84. font-weight: 500;
  85. }
  86. .short-chapters-warning {
  87. color: #ff0000;
  88. font-weight: bold;
  89. animation: shortChapterBlink 1s infinite;
  90. }
  91.  
  92. .button-container {
  93. display: flex;
  94. justify-content: center;
  95. align-items: center;
  96. gap: 14px;
  97. margin-top: 24px;
  98. flex-wrap: wrap;
  99. }
  100. #modern-uploader .btn {
  101. padding: 12px 20px;
  102. border-radius: 10px;
  103. cursor: pointer;
  104. font-weight: 600;
  105. font-size: 14px;
  106. transition: all 0.3s ease;
  107. border: none;
  108. box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  109. display: flex;
  110. align-items: center;
  111. gap: 6px;
  112. }
  113. #modern-uploader .btn:hover {
  114. transform: translateY(-3px);
  115. box-shadow: 0 6px 10px rgba(0,0,0,0.15);
  116. }
  117. #modern-uploader .btn:active {
  118. transform: translateY(-1px);
  119. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  120. }
  121. #modern-uploader .btn-primary {
  122. background: linear-gradient(140deg, #4285f4, #1a73e8);
  123. color: white;
  124. }
  125. #modern-uploader .btn-success {
  126. background: linear-gradient(140deg, #34a853, #27833c);
  127. color: white;
  128. }
  129. #modern-uploader .btn-danger {
  130. background: linear-gradient(140deg, #ea4335, #d32f2f);
  131. color: white;
  132. }
  133. #modern-uploader .btn-secondary {
  134. background: linear-gradient(140deg, #9e9e9e, #757575);
  135. color: white;
  136. }
  137. #modern-uploader .btn-warning {
  138. background: linear-gradient(140deg, #fbbc05, #f5a623);
  139. color: #212529;
  140. }
  141. #modern-uploader .btn-info {
  142. background: linear-gradient(140deg, #17a2b8, #0097a7);
  143. color: white;
  144. }
  145. #modern-uploader .form-control {
  146. width: 100%;
  147. padding: 16px;
  148. border: 1px solid #ddd;
  149. border-radius: 10px;
  150. margin-bottom: 18px;
  151. font-size: 15px;
  152. transition: all 0.3s ease;
  153. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  154. }
  155. #modern-uploader .form-control:focus {
  156. border-color: #4285f4;
  157. outline: none;
  158. box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.2);
  159. }
  160. #modern-uploader .loading-overlay {
  161. position: fixed;
  162. inset: 0;
  163. background-color: rgba(0, 0, 0, 0.7);
  164. display: flex;
  165. align-items: center;
  166. justify-content: center;
  167. z-index: 9999;
  168. backdrop-filter: blur(5px);
  169. }
  170. #modern-uploader .loading-spinner {
  171. width: 60px;
  172. height: 60px;
  173. border: 5px solid rgba(255, 255, 255, 0.3);
  174. border-radius: 50%;
  175. border-top-color: #ffffff;
  176. animation: spin 1s linear infinite;
  177. box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
  178. }
  179. @keyframes spin {
  180. to { transform: rotate(360deg); }
  181. }
  182. /* Checkbox tự động */
  183. #modern-uploader .checkbox-container {
  184. display: flex;
  185. justify-content: center;
  186. align-items: center;
  187. margin-bottom: 20px;
  188. background: linear-gradient(145deg, #f8f9fa, #f1f3f5);
  189. border-radius: 12px;
  190. padding: 14px;
  191. border: 1px solid #e9ecef;
  192. position: relative;
  193. overflow: hidden;
  194. }
  195. #modern-uploader .checkbox-container::before {
  196. content: '';
  197. position: absolute;
  198. width: 100%;
  199. height: 3px;
  200. bottom: 0;
  201. left: 0;
  202. background: linear-gradient(90deg, #4285f4, #34a853, #fbbc05, #ea4335);
  203. opacity: 0.7;
  204. }
  205. #qpOptionLoop {
  206. margin-right: 10px;
  207. width: 18px;
  208. height: 18px;
  209. cursor: pointer;
  210. accent-color: #4285f4;
  211. }
  212. /* Header styling */
  213. #modern-uploader .text-center {
  214. text-align: center;
  215. }
  216. #modern-uploader .uploader-header {
  217. margin-bottom: 25px;
  218. position: relative;
  219. }
  220. #modern-uploader .uploader-header::after {
  221. content: '';
  222. position: absolute;
  223. bottom: -10px;
  224. left: 50%;
  225. transform: translateX(-50%);
  226. width: 80%;
  227. height: 3px;
  228. background: linear-gradient(90deg, transparent, #4285f4, transparent);
  229. border-radius: 3px;
  230. }
  231. #modern-uploader h3 {
  232. color: #4285f4;
  233. font-weight: 700;
  234. font-size: 22px;
  235. display: flex;
  236. align-items: center;
  237. justify-content: center;
  238. gap: 10px;
  239. margin: 0;
  240. padding: 0;
  241. letter-spacing: 0.5px;
  242. }
  243. /* Notification styling */
  244. #modern-uploader .notification-container {
  245. width: 100%;
  246. padding: 14px 0;
  247. margin-top: 18px;
  248. text-align: left;
  249. border-top: 1px solid rgba(0,0,0,0.06);
  250. }
  251. #modern-uploader .notification-success,
  252. #modern-uploader .notification-error,
  253. #modern-uploader .notification-warning,
  254. #modern-uploader .notification-info {
  255. padding: 14px 18px;
  256. border-radius: 12px;
  257. font-size: 14px;
  258. font-weight: 500;
  259. box-shadow: 0 4px 12px rgba(0,0,0,0.08);
  260. display: inline-block;
  261. max-width: 100%;
  262. margin: 0;
  263. word-break: break-word;
  264. animation: fadeInUp 0.4s ease;
  265. line-height: 1.5;
  266. }
  267. @keyframes fadeInUp {
  268. from { opacity: 0; transform: translateY(15px); }
  269. to { opacity: 1; transform: translateY(0); }
  270. }
  271. #modern-uploader .notification-success {
  272. background: linear-gradient(145deg, #e8f5e9, #c8e6c9);
  273. color: #2e7d32;
  274. border-left: 4px solid #4caf50;
  275. }
  276. #modern-uploader .notification-error {
  277. background: linear-gradient(145deg, #ffebee, #ffcdd2);
  278. color: #c62828;
  279. border-left: 4px solid #f44336;
  280. }
  281. #modern-uploader .notification-warning {
  282. background: linear-gradient(145deg, #fff8e1, #ffecb3);
  283. color: #f57f17;
  284. border-left: 4px solid #ffb300;
  285. }
  286. #modern-uploader .notification-info {
  287. background: linear-gradient(145deg, #e3f2fd, #bbdefb);
  288. color: #1565c0;
  289. border-left: 4px solid #2196f3;
  290. }
  291. /* Google-style waves */
  292. @keyframes wave-animation {
  293. 0% {transform: translateX(-100%);}
  294. 100% {transform: translateX(100%);}
  295. }
  296. #modern-uploader .wave-bottom {
  297. position: absolute;
  298. bottom: 0;
  299. left: 0;
  300. width: 100%;
  301. height: 5px;
  302. overflow: hidden;
  303. z-index: 1;
  304. }
  305. #modern-uploader .wave-bottom::after {
  306. content: '';
  307. position: absolute;
  308. width: 200%;
  309. height: 100%;
  310. background: linear-gradient(90deg, transparent, rgba(66, 133, 244, 0.3), transparent);
  311. animation: wave-animation 2s infinite linear;
  312. }
  313. `);
  314.  
  315. function showNotification(message, type) {
  316. jQuery('#modern-uploader .notification-container').remove();
  317. const container = jQuery("<div>", {
  318. class: "notification-container"
  319. });
  320. const notification = jQuery("<div>", {
  321. class: `notification-${type}`
  322. });
  323. const lines = message.split('\n');
  324. lines.forEach((line, index) => {
  325. notification.append(jQuery("<div>").html(line));
  326. });
  327. container.append(notification);
  328. jQuery("#modern-uploader .button-container").after(container);
  329. notification.fadeIn(300);
  330. }
  331.  
  332. function createChapterHTML(chapNum) {
  333. const chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
  334. const chap_vol_name = jQuery('.chap_vol_name').val() || '';
  335. return `
  336. <div data-gen="MK_GEN" id="COUNT_CHAP_${chapNum}_MK">
  337. <div class="col-xs-12 form-group"></div>
  338. <div class="form-group">
  339. <label class="col-sm-2" for="chap_stt">STT</label>
  340. <div class="col-sm-8">
  341. <input class="form-control" required name="chap_stt[${chapNum}]" value="${dangNhanhTTV.STATE.CHAP_STT}" placeholder="Số thứ tự của chương" type="text"/>
  342. </div>
  343. </div>
  344. <div class="form-group">
  345. <label class="col-sm-2" for="chap_number">Chương thứ..</label>
  346. <div class="col-sm-8">
  347. <input value="${dangNhanhTTV.STATE.CHAP_SERIAL}" required class="form-control" name="chap_number[${chapNum}]" placeholder="Chương thứ.. (1,2,3..)" type="text"/>
  348. </div>
  349. </div>
  350. <div class="form-group">
  351. <label class="col-sm-2" for="chap_name">Quyn số</label>
  352. <div class="col-sm-8">
  353. <input class="form-control" name="vol[${chapNum}]" placeholder="Quyển số" type="number" value="${chap_vol}" required/>
  354. </div>
  355. </div>
  356. <div class="form-group">
  357. <label class="col-sm-2" for="chap_name">Tên quyn</label>
  358. <div class="col-sm-8">
  359. <input class="form-control chap_vol_name" name="vol_name[${chapNum}]" placeholder="Tên quyển" type="text" value="${chap_vol_name}" />
  360. </div>
  361. </div>
  362. <div class="form-group">
  363. <label class="col-sm-2" for="chap_name">Tên chương</label>
  364. <div class="col-sm-8">
  365. <input required class="form-control" name="chap_name[${chapNum}]" placeholder="Tên chương" type="text"/>
  366. </div>
  367. </div>
  368. <div class="form-group">
  369. <label class="col-sm-2" for="introduce">Ni dung</label>
  370. <div class="col-sm-8">
  371. <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>
  372. <div class="chapter-character-count"></div>
  373. </div>
  374. </div>
  375. <div class="form-group">
  376. <label class="col-sm-2" for="adv">Qung cáo</label>
  377. <div class="col-sm-8">
  378. <textarea maxlength="1000" class="form-control" name="adv[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea>
  379. </div>
  380. </div>
  381. </div>`;
  382. }
  383.  
  384. function setupCharacterCounter() {
  385. jQuery(document).on("input", "[name^=introduce]", function() {
  386. const text = jQuery(this).val();
  387. const charCount = text.length;
  388. let charCountElement = jQuery(this).next('.chapter-character-count');
  389. if (charCountElement.length === 0) {
  390. charCountElement = jQuery('<div class="chapter-character-count"></div>');
  391. jQuery(this).after(charCountElement);
  392. }
  393. if(charCount < 3000) {
  394. jQuery(this).addClass('short-chapter');
  395. charCountElement.html(`<span class="short-chapters-warning">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  396. } else {
  397. jQuery(this).removeClass('short-chapter');
  398. if(charCount > 40000) {
  399. charCountElement.html(`<span style="color: #fbbc05;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  400. } else {
  401. charCountElement.html(`<span style="color: #34a853;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  402. }
  403. }
  404. });
  405. }
  406.  
  407. function validateChapterLengths() {
  408. let hasError = false;
  409. jQuery('form[name="postChapForm"] .chapter-detail').each(function() {
  410. const form = this;
  411. const contentTextarea = form.querySelector('textarea[name^="introduce"]');
  412. const content = contentTextarea.value;
  413. if (content.length < 3000) {
  414. jQuery(contentTextarea).addClass('short-chapter');
  415. let warningIcon = form.querySelector('.warning-icon');
  416. if (!warningIcon) {
  417. warningIcon = document.createElement('div');
  418. warningIcon.className = 'warning-icon';
  419. warningIcon.innerHTML = '⚠️';
  420. contentTextarea.parentNode.appendChild(warningIcon);
  421. }
  422. hasError = true;
  423. } else {
  424. jQuery(contentTextarea).removeClass('short-chapter');
  425. const warningIcon = form.querySelector('.warning-icon');
  426. if (warningIcon) {
  427. warningIcon.remove();
  428. }
  429. }
  430. });
  431. return !hasError;
  432. }
  433.  
  434. const dangNhanhTTV = {
  435. STATE: {
  436. CHAP_NUMBER: 1,
  437. CHAP_STT: 1,
  438. CHAP_SERIAL: 1,
  439. CHAP_NUMBER_ORIGINAL: 1,
  440. CHAP_STT_ORIGINAL: 1,
  441. CHAP_SERIAL_ORIGINAL: 1,
  442. AUTO_POST: false,
  443. TOTAL_CHAPTERS: 0,
  444. POSTED_CHAPTERS: 0
  445. },
  446. ELEMENTS: {
  447. qpContent: null,
  448. qpButtonSubmit: null,
  449. qpButtonRemoveEmpty: null,
  450. qpButtonReset: null,
  451. qpButtonPaste: null,
  452. qpButtonAutoPost: null,
  453. qpOptionLoop: null
  454. },
  455. init: function() {
  456. try {
  457. console.log('[TTV-DEBUG] Script bắt đầu khởi tạo...');
  458. console.log('[TTV-DEBUG] Phiên bản script: 3.0');
  459. this.initializeChapterValues();
  460. console.log('[TTV-DEBUG] Đã khởi tạo giá trị chương');
  461.  
  462. // Khôi phục trạng thái tự động đăng
  463. this.loadAutoPostState();
  464. console.log('[TTV-DEBUG] Đã khôi phục trạng thái tự động đăng');
  465.  
  466. this.createInterface();
  467. console.log('[TTV-DEBUG] Đã tạo giao diện');
  468. this.cacheElements();
  469. console.log('[TTV-DEBUG] Đã cache các elements');
  470. this.registerEvents();
  471. console.log('[TTV-DEBUG] Đã đăng ký các events');
  472. console.log('[TTV-DEBUG] Script đã khởi động thành công');
  473. showNotification('Công cụ đã chạy thành công', 'success');
  474.  
  475. // Cập nhật hiển thị nút tự động đăng
  476. if (this.STATE.AUTO_POST) {
  477. this.ELEMENTS.qpButtonAutoPost.text(`🔄 T ĐỘNG (${this.STATE.POSTED_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS})`);
  478. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-warning').addClass('btn-info');
  479. }
  480.  
  481. // Khôi phục trạng thái chế độ tự động đăng
  482. const isAutoMode = localStorage.getItem('TTV_AUTO_MODE') === 'true';
  483. if (isAutoMode) {
  484. this.ELEMENTS.qpOptionLoop.prop('checked', true);
  485. this.toggleAutoMode(); // Áp dụng giao diện theo chế độ
  486. }
  487. } catch (e) {
  488. console.error('[TTV-ERROR] Lỗi khởi tạo:', e);
  489. showNotification('Có lỗi khi khởi tạo Script', 'error');
  490. }
  491. },
  492.  
  493. loadAutoPostState: function() {
  494. // Khôi phục trạng thái tự động đăng từ localStorage
  495. const autoPost = localStorage.getItem('TTV_AUTO_POST') === 'true';
  496. this.STATE.AUTO_POST = autoPost;
  497.  
  498. if (autoPost) {
  499. this.STATE.TOTAL_CHAPTERS = parseInt(localStorage.getItem('TTV_TOTAL_CHAPTERS') || '0');
  500. this.STATE.POSTED_CHAPTERS = parseInt(localStorage.getItem('TTV_POSTED_CHAPTERS') || '0');
  501.  
  502. console.log(`[TTV-DEBUG] Khôi phc t động đăng: ${this.STATE.POSTED_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS}`);
  503. }
  504. },
  505.  
  506. createInterface: function() {
  507. const html = `
  508. <div id="modern-uploader">
  509. <div class="uploader-header">
  510. <h3>📚 CÔNG C ĐĂNG CHƯƠNG</h3>
  511. </div>
  512. <div class="form-group">
  513. <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>
  514. </div>
  515. <div class="checkbox-container">
  516. <input type="checkbox" id="qpOptionLoop">
  517. <label for="qpOptionLoop" style="color: #1a73e8; font-weight: 600; margin-left: 8px;">Chế độ t động đăng</label>
  518. </div>
  519. <div class="button-container">
  520. <button class="btn btn-primary" id="qpButtonPaste">📋 Dán ni dung</button>
  521. <button class="btn btn-success" id="qpButtonSubmit">📤 Đăng chương</button>
  522. <button class="btn btn-danger" id="qpButtonRemoveEmpty" style="display: none;">🗑️ n chương trng</button>
  523. <button class="btn btn-warning" id="qpButtonAutoPost" style="display: none;">🔄 T ĐỘNG (TT)</button>
  524. <button class="btn btn-secondary" id="qpButtonReset" style="display: none;">🔁 Reset đếm</button>
  525. </div>
  526. <div class="notification-container"></div>
  527. <div class="wave-bottom"></div>
  528. </div>`;
  529.  
  530. jQuery(".list-in-user").before(html);
  531. },
  532.  
  533. initializeChapterValues: function() {
  534. try {
  535. const chap_number = parseInt(jQuery('#chap_number').val());
  536. let chap_stt = parseInt(jQuery('.chap_stt1').val());
  537. let chap_serial = parseInt(jQuery('.chap_serial').val());
  538.  
  539. if (parseInt(jQuery('#chap_stt').val()) > chap_stt) {
  540. chap_stt = parseInt(jQuery('#chap_stt').val());
  541. }
  542. if (parseInt(jQuery('#chap_serial').val()) > chap_serial) {
  543. chap_serial = parseInt(jQuery('#chap_serial').val());
  544. }
  545.  
  546. this.STATE.CHAP_NUMBER = this.STATE.CHAP_NUMBER_ORIGINAL = chap_number || 1;
  547. this.STATE.CHAP_STT = this.STATE.CHAP_STT_ORIGINAL = chap_stt || 1;
  548. this.STATE.CHAP_SERIAL = this.STATE.CHAP_SERIAL_ORIGINAL = chap_serial || 1;
  549. } catch (e) {
  550. console.error("Error initializing chapter values:", e);
  551. }
  552. },
  553.  
  554. cacheElements: function() {
  555. this.ELEMENTS.qpContent = jQuery("#qpContent");
  556. this.ELEMENTS.qpButtonSubmit = jQuery("#qpButtonSubmit");
  557. this.ELEMENTS.qpButtonRemoveEmpty = jQuery("#qpButtonRemoveEmpty");
  558. this.ELEMENTS.qpButtonPaste = jQuery("#qpButtonPaste");
  559. this.ELEMENTS.qpButtonAutoPost = jQuery("#qpButtonAutoPost");
  560. this.ELEMENTS.qpButtonReset = jQuery("#qpButtonReset");
  561. this.ELEMENTS.qpOptionLoop = jQuery("#qpOptionLoop");
  562. },
  563.  
  564. registerEvents: function() {
  565. this.ELEMENTS.qpContent.on("paste", this.handlePaste.bind(this));
  566. this.ELEMENTS.qpButtonRemoveEmpty.on('click', this.removeEmptyChapters.bind(this));
  567. this.ELEMENTS.qpButtonSubmit.on('click', this.submitChapters.bind(this));
  568. this.ELEMENTS.qpButtonPaste.on('click', this.handlePasteButton.bind(this));
  569. this.ELEMENTS.qpButtonAutoPost.on('click', this.toggleAutoPost.bind(this));
  570. this.ELEMENTS.qpButtonReset.on('click', this.resetAutoPost.bind(this));
  571. this.ELEMENTS.qpOptionLoop.on('change', this.toggleAutoMode.bind(this));
  572. setupCharacterCounter();
  573.  
  574. // Kiểm tra và bắt đầu tự động đăng nếu đã bật
  575. if (window.location.href.includes('/dang-chuong/story/')) {
  576. setTimeout(() => {
  577. if (this.STATE.AUTO_POST && this.STATE.POSTED_CHAPTERS < this.STATE.TOTAL_CHAPTERS) {
  578. this.runAutoPostSequence();
  579. }
  580. }, 2000);
  581. }
  582. },
  583.  
  584. handlePasteButton: function() {
  585. this.showLoading();
  586. navigator.clipboard.readText()
  587. .then(text => {
  588. this.ELEMENTS.qpContent.val(text);
  589. setTimeout(() => {
  590. this.performAction();
  591. this.hideLoading();
  592. }, 100);
  593. })
  594. .catch(err => {
  595. console.error('Không thể đọc dữ liệu từ clipboard:', err);
  596. this.hideLoading();
  597. showNotification('Không thể truy cập clipboard. Vui lòng dán trực tiếp vào ô nội dung.', 'error');
  598. });
  599. },
  600.  
  601. handlePaste: function(e) {
  602. e.preventDefault();
  603. this.ELEMENTS.qpContent.val("");
  604. this.showLoading();
  605. const pastedText = e.originalEvent.clipboardData.getData('text');
  606. this.ELEMENTS.qpContent.val(pastedText);
  607. setTimeout(() => {
  608. this.performAction();
  609. this.hideLoading();
  610. }, 100);
  611. },
  612.  
  613. performAction: function() {
  614. try {
  615. console.log("Starting performAction");
  616. var text = this.ELEMENTS.qpContent.val();
  617.  
  618. if (!text) {
  619. showNotification('Không có nội dung để tách chương', 'error');
  620. return 0;
  621. }
  622. var debugOutput = [];
  623. var chapters = [];
  624. var lines = text.split('\n');
  625. var currentChapter = [];
  626. var lastTitle = null;
  627. debugOutput.push("=== Processing Text ===");
  628. debugOutput.push(`Total lines: ${lines.length}`);
  629. debugOutput.push("=== Line Analysis ===");
  630. function visualizeWhitespace(str) {
  631. return str.split('').map(c => {
  632. if (c === '\t') return '\\t';
  633. if (c === ' ') return '·';
  634. if (c === '\n') return '\\n';
  635. return c;
  636. }).join('');
  637. }
  638.  
  639. // Hàm lấy mã chương dựa vào tiêu đề
  640. function getChapterCode(title) {
  641. // Lấy số chương + tên chương, bỏ qua các ký tự đặc biệt
  642. const match = title.match(/[Cc]hương\s*(\d+)\s*:/);
  643. if (!match) return title.trim();
  644. const chapterNum = match[1];
  645. return `chap_${chapterNum}`;
  646. }
  647.  
  648. for (let i = 0; i < lines.length; i++) {
  649. let line = lines[i];
  650. let isChapterTitle =
  651. /^\t[Cc]hương\s*\d+\s*:/.test(line) || // tab + Chương + số:
  652. /^\s{4,}[Cc]hương\s*\d+\s*:/.test(line) || // spaces + Chương + số:
  653. /^\t[Cc]hương\s*\d+(?!\S)/.test(line) || // tab + Chương + số (không có dấu :)
  654. /^\s{4,}[Cc]hương\s*\d+(?!\S)/.test(line) || // spaces + Chương + số (không có dấu :)
  655. /^[Cc]hương\s*\d+\s*:/.test(line) || // Chương + số:
  656. /^[Cc]hương\s*\d+(?!\S)/.test(line); // Chương + số (không có dấu :)
  657. debugOutput.push(`Line ${i}: ${visualizeWhitespace(line.substring(0, 50))}${line.length > 50 ? '...' : ''}`);
  658. debugOutput.push(` Is chapter: ${isChapterTitle}`);
  659.  
  660. if (isChapterTitle) {
  661. // Lấy mã chương để so sánh
  662. const currentChapterCode = getChapterCode(line);
  663. const lastTitleCode = lastTitle ? getChapterCode(lastTitle) : null;
  664.  
  665. if (currentChapter.length > 0) {
  666. // Kiểm tra nếu chương hiện tại khác chương trước đó
  667. if (currentChapterCode !== lastTitleCode) {
  668. chapters.push(currentChapter.join('\n'));
  669. currentChapter = [line];
  670. lastTitle = line;
  671. debugOutput.push(` -> New chapter started: ${currentChapterCode}`);
  672. } else {
  673. debugOutput.push(` -> Skipped duplicate chapter: ${currentChapterCode}`);
  674. // Không cần thêm dòng này vào chapter hiện tại vì nó là tiêu đề trùng lặp
  675. }
  676. } else {
  677. currentChapter = [line];
  678. lastTitle = line;
  679. debugOutput.push(` -> First chapter started: ${currentChapterCode}`);
  680. }
  681. } else if (currentChapter.length > 0) {
  682. currentChapter.push(line);
  683. }
  684. }
  685. if (currentChapter.length > 0) {
  686. chapters.push(currentChapter.join('\n'));
  687. debugOutput.push("-> Added final chapter");
  688. }
  689. debugOutput.push("=== Results ===");
  690. debugOutput.push(`Found ${chapters.length} chapters`);
  691. const processedChapters = [];
  692. for (let i = 0; i < chapters.length; i++) {
  693. const chapterLines = chapters[i].split('\n');
  694. const title = chapterLines.shift().trim();
  695. const chapterText = chapterLines.join('\n');
  696. const charCount = chapterText.length;
  697. debugOutput.push(`Chapter ${i+1} character count: ${charCount}`);
  698. if (charCount > 40000) {
  699. const parts = Math.ceil(charCount / 40000);
  700. debugOutput.push(`Splitting into ${parts} parts`);
  701. const charsPerPart = Math.ceil(charCount / parts);
  702. debugOutput.push(`Characters per part: ~${charsPerPart}`);
  703. let currentText = chapterText;
  704. let totalProcessed = 0;
  705. for (let part = 0; part < parts; part++) {
  706. const isLastPart = part === parts - 1;
  707. const targetSize = isLastPart ? currentText.length : charsPerPart;
  708. let endPos = Math.min(targetSize, currentText.length);
  709. if (!isLastPart && endPos < currentText.length) {
  710. const nextParagraph = currentText.indexOf('\n\n', endPos - 500);
  711. if (nextParagraph !== -1 && nextParagraph < endPos + 500) {
  712. endPos = nextParagraph + 2;
  713. } else {
  714. const sentenceEnd = Math.max(
  715. currentText.lastIndexOf('. ', endPos),
  716. currentText.lastIndexOf('! ', endPos),
  717. currentText.lastIndexOf('? ', endPos)
  718. );
  719. if (sentenceEnd !== -1 && sentenceEnd > endPos - 500) {
  720. endPos = sentenceEnd + 2;
  721. }
  722. }
  723. }
  724. const partContent = currentText.substring(0, endPos);
  725. totalProcessed += partContent.length;
  726. currentText = currentText.substring(endPos);
  727. let chapterTitle = title;
  728. // Create new title with part indicator, ensure it's only added once
  729. let newTitle;
  730. if (title.includes('(Phần ')) {
  731. // Title already has a part indicator, replace it
  732. newTitle = title.replace(/\(Phần \d+\/\d+\)/, `(Phn ${part+1}/${parts})`);
  733. } else {
  734. newTitle = `${title} (Phn ${part+1}/${parts})`;
  735. }
  736. processedChapters.push(newTitle + '\n' + partContent);
  737. debugOutput.push(`Part ${part+1}: ${partContent.length} chars`);
  738. }
  739. debugOutput.push(`Total processed: ${totalProcessed}/${charCount} chars`);
  740. } else {
  741. processedChapters.push(chapters[i]);
  742. }
  743. }
  744. debugOutput.push(`After processing: ${processedChapters.length} chapters`);
  745. const chaptersToFill = processedChapters.slice(0, MAX_CHAPTER_POST);
  746. const remainingChapters = processedChapters.slice(MAX_CHAPTER_POST);
  747. var titles = jQuery("input[name^='chap_name']");
  748. var contents = jQuery("textarea[name^='introduce']");
  749. var advs = jQuery("textarea[name^='adv']");
  750. debugOutput.push(`Forms found: ${titles.length}`);
  751. if (processedChapters.length === 0) {
  752. showNotification('Không tìm thấy chương nào', 'error');
  753. jQuery('#debug-output').text(debugOutput.join('\n'));
  754. return;
  755. }
  756. if (remainingChapters.length > 0) {
  757. debugOutput.push(`${remainingChapters.length} chapters will be copied to clipboard`);
  758. }
  759. const neededForms = chaptersToFill.length - titles.length;
  760. if (neededForms > 0 && titles.length < MAX_CHAPTER_POST) {
  761. debugOutput.push(`Need to add ${neededForms} more forms`);
  762. for (let i = 0; i < neededForms && (titles.length + i) < MAX_CHAPTER_POST; i++) {
  763. this.addNewChapter();
  764. }
  765. titles = jQuery("input[name^='chap_name']");
  766. contents = jQuery("textarea[name^='introduce']");
  767. advs = jQuery("textarea[name^='adv']");
  768. }
  769. debugOutput.push(`Filling ${chaptersToFill.length} chapters into forms`);
  770. jQuery.each(titles, function(k, v) {
  771. if (k < chaptersToFill.length) {
  772. var content = chaptersToFill[k].split('\n');
  773. var title = content.shift().trim();
  774.  
  775. // Tìm định dạng "Chương + số:" hoặc "Chương + số"
  776. var chapterMatch = title.match(/[Cc]hương\s*\d+(\s*:)?/);
  777. var chapterTitle = "";
  778.  
  779. if (chapterMatch) {
  780. // Lấy phần sau "Chương + số:" hoặc "Chương + số"
  781. var matchedPart = chapterMatch[0];
  782. if (matchedPart.endsWith(':')) {
  783. // Nếu có dấu ":", lấy phần sau dấu ":"
  784. chapterTitle = title.substring(title.indexOf(':') + 1).trim();
  785. } else {
  786. // Nếu không có dấu ":", lấy phần sau "Chương + số"
  787. chapterTitle = title.substring(matchedPart.length).trim();
  788. }
  789.  
  790. // Nếu không có nội dung sau "Chương + số", sử dụng "Vô đề"
  791. if (!chapterTitle) {
  792. chapterTitle = "Vô đề";
  793. }
  794. } else {
  795. // Nếu không tìm thấy định dạng "Chương + số"
  796. chapterTitle = title;
  797. }
  798.  
  799. debugOutput.push(`\nFilling chapter ${k + 1}:`);
  800. debugOutput.push(`Original title: ${title}`);
  801. debugOutput.push(`Extracted title: ${chapterTitle}`);
  802. debugOutput.push(`Content length: ${content.length} lines`);
  803. if (!chapterTitle || chapterTitle.trim() === '') {
  804. chapterTitle = "Vô đề";
  805. debugOutput.push(`Empty title detected, using default: ${chapterTitle}`);
  806. }
  807. titles[k].value = chapterTitle;
  808. contents[k].value = HEADER_SIGN + "\r\n" + content.join('\n') + "\r\n" + FOOTER_SIGN;
  809. if (advs[k]) advs[k].value = "";
  810. jQuery(contents[k]).trigger('input');
  811. }
  812. });
  813. if (remainingChapters.length > 0) {
  814. try {
  815. const clipboardContent = remainingChapters.map(chap => {
  816. const lines = chap.trim().split('\n');
  817. if (lines.length > 0) {
  818. // Xử lý dòng đầu tiên để đảm bảo đúng định dạng
  819. let firstLine = lines[0];
  820.  
  821. // Loại bỏ tab hoặc khoảng trắng ở đầu
  822. firstLine = firstLine.replace(/^[\t\s]+/, '');
  823.  
  824. // Kiểm tra xem đã có định dạng Chương + số chưa
  825. const chapterMatch = firstLine.match(/^([Cc]hương\s*\d+)/);
  826. if (chapterMatch) {
  827. // Nếu không có dấu ":" thì thêm vào
  828. if (!firstLine.includes(':')) {
  829. firstLine = chapterMatch[0] + ': ' + firstLine.substring(chapterMatch[0].length).trim();
  830. }
  831. }
  832.  
  833. // Thêm tab vào đầu
  834. lines[0] = '\t' + firstLine;
  835. }
  836. return lines.join('\n');
  837. }).join('\n\n------\n\n');
  838. let splitChapters = 0;
  839. let shortChapters = 0;
  840. let shortChapterDetails = [];
  841. let longChapterDetails = [];
  842. for (let i = 0; i < chapters.length; i++) {
  843. const chapterLines = chapters[i].split('\n');
  844. const title = chapterLines.shift().trim();
  845. const chapterText = chapterLines.join('\n');
  846. if (chapterText.length > 40000) {
  847. splitChapters++;
  848. const partsCount = Math.ceil(chapterText.length / 40000);
  849. longChapterDetails.push({
  850. title: title,
  851. parts: partsCount
  852. });
  853. }
  854. if (chapterText.length < 3000) {
  855. shortChapters++;
  856. shortChapterDetails.push({
  857. title: title,
  858. length: chapterText.length
  859. });
  860. }
  861. }
  862. const splittedChaptersCount = processedChapters.length - (chapters.length - splitChapters);
  863. let message = '';
  864. message = message.concat(`📝 Đã x lý ${processedChapters.length} Chương\n`);
  865. message = message.concat(`📝 Đã nhp ${Math.min(processedChapters.length, MAX_CHAPTER_POST)} Chương\n`);
  866.  
  867. if (remainingChapters.length > 0) {
  868. message = message.concat(`📋 Đã lưu li ${remainingChapters.length} Chương\n`);
  869. }
  870. if (splitChapters > 0) {
  871. message = message.concat(`📑 Có ${splitChapters} Chương đã chia thành ${splittedChaptersCount} Chương\n`);
  872. longChapterDetails.forEach(chapter => {
  873. let chapterName = chapter.title;
  874. if (chapterName.includes(':')) {
  875. chapterName = chapterName.trim();
  876. }
  877. message = message.concat(` - ${chapterName}: ${chapter.parts} Chương\n`);
  878. });
  879. }
  880. if (shortChapters > 0) {
  881. message = message.concat(`⚠️<span class="short-chapters-warning">Có ${shortChapters} chương dưới 3000 ký tự</span>\n`);
  882. shortChapterDetails.forEach(chapter => {
  883. let chapterName = chapter.title;
  884. if (chapterName.includes(':')) {
  885. chapterName = chapterName.trim();
  886. }
  887. message = message.concat(`<span class="short-chapters-warning"> - ${chapterName}: có ${chapter.length.toLocaleString()} ký tự</span>\n`);
  888. });
  889. }
  890. if (navigator.clipboard && navigator.clipboard.writeText) {
  891. navigator.clipboard.writeText(clipboardContent)
  892. .then(() => {
  893. debugOutput.push(`📋Đã lưu li ${remainingChapters.length} Chương\n`);
  894. showNotification(message, remainingChapters.length > 0 ? 'warning' : 'success');
  895. })
  896. .catch(err => {
  897. throw err;
  898. });
  899. } else {
  900. const tempTextarea = document.createElement('textarea');
  901. tempTextarea.style.position = 'fixed';
  902. tempTextarea.style.top = '0';
  903. tempTextarea.style.left = '0';
  904. tempTextarea.style.width = '2em';
  905. tempTextarea.style.height = '2em';
  906. tempTextarea.style.opacity = '0';
  907. tempTextarea.style.pointerEvents = 'none';
  908. tempTextarea.value = clipboardContent;
  909. document.body.appendChild(tempTextarea);
  910. tempTextarea.focus();
  911. tempTextarea.select();
  912. const successful = document.execCommand('copy');
  913. document.body.removeChild(tempTextarea);
  914. if (!successful) {
  915. throw new Error('Không thể sao chép vào clipboard');
  916. }
  917. debugOutput.push(`Đã sao chép ${remainingChapters.length} chương vào clipboard (execCommand)`);
  918. showNotification(message, 'success');
  919. }
  920. } catch (err) {
  921. console.error('Lỗi khi sao chép vào clipboard:', err);
  922. debugOutput.push(`Li khi sao chép vào clipboard: ${err.message}`);
  923. showNotification('Không thể sao chép vào clipboard. Vui lòng thử lại.', 'error');
  924. const manualCopyArea = document.createElement('div');
  925. manualCopyArea.style.position = 'fixed';
  926. manualCopyArea.style.top = '50%';
  927. manualCopyArea.style.left = '50%';
  928. manualCopyArea.style.transform = 'translate(-50%, -50%)';
  929. manualCopyArea.style.backgroundColor = 'white';
  930. manualCopyArea.style.padding = '20px';
  931. manualCopyArea.style.borderRadius = '8px';
  932. manualCopyArea.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
  933. manualCopyArea.style.zIndex = '10000';
  934. manualCopyArea.style.maxWidth = '80%';
  935. manualCopyArea.style.maxHeight = '80%';
  936. manualCopyArea.style.overflow = 'auto';
  937. manualCopyArea.innerHTML = `
  938. <h3 style="margin-top: 0;">Sao chép th công</h3>
  939. <p>Không th sao chép t động. Vui lòng chn toàn b ni dung bên dưới và sao chép (Ctrl+C):</p>
  940. <textarea style="width: 100%; height: 300px; padding: 10px;">${clipboardContent}</textarea>
  941. <div style="text-align: right; margin-top: 10px;">
  942. <button id="closeManualCopy" style="padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;">Đóng</button>
  943. </div>
  944. `;
  945. document.body.appendChild(manualCopyArea);
  946. document.getElementById('closeManualCopy').addEventListener('click', () => {
  947. document.body.removeChild(manualCopyArea);
  948. });
  949. }
  950. }
  951. this.ELEMENTS.qpButtonSubmit.removeClass("btn-disable").addClass("btn-success");
  952. this.ELEMENTS.qpContent.val("Đã xử lý xong");
  953. if (remainingChapters.length === 0) {
  954. let splitChapters = 0;
  955. let shortChapters = 0;
  956. let shortChapterDetails = [];
  957. let longChapterDetails = [];
  958. for (let i = 0; i < chapters.length; i++) {
  959. const chapterLines = chapters[i].split('\n');
  960. const title = chapterLines.shift().trim();
  961. const chapterText = chapterLines.join('\n');
  962. if (chapterText.length > 40000) {
  963. splitChapters++;
  964. const partsCount = Math.ceil(chapterText.length / 40000);
  965. longChapterDetails.push({
  966. title: title,
  967. parts: partsCount
  968. });
  969. }
  970. if (chapterText.length < 3000) {
  971. shortChapters++;
  972. shortChapterDetails.push({
  973. title: title,
  974. length: chapterText.length
  975. });
  976. }
  977. }
  978. const splittedChaptersCount = processedChapters.length - (chapters.length - splitChapters);
  979. let message = '';
  980. message = message.concat(`📝 Đã x lý ${processedChapters.length} Chương\n`);
  981. message = message.concat(`📝 Đã nhp ${Math.min(processedChapters.length, MAX_CHAPTER_POST)} Chương\n`);
  982. if (splitChapters > 0) {
  983. message = message.concat(`📑 Có ${splitChapters} Chương dài chia thành ${splittedChaptersCount} Chương\n`);
  984. longChapterDetails.forEach(chapter => {
  985. let chapterName = chapter.title;
  986. if (chapterName.includes(':')) {
  987. chapterName = chapterName.trim();
  988. }
  989. message = message.concat(` - ${chapterName}: ${chapter.parts} Chương\n`);
  990. });
  991. }
  992. if (shortChapters > 0) {
  993. message = message.concat(`⚠️<span class="short-chapters-warning">Có ${shortChapters} chương dưới 3000 ký tự</span>\n`);
  994. shortChapterDetails.forEach(chapter => {
  995. let chapterName = chapter.title;
  996. if (chapterName.includes(':')) {
  997. chapterName = chapterName.trim();
  998. }
  999. message = message.concat(`<span class="short-chapters-warning"> - ${chapterName}: có ${chapter.length.toLocaleString()} ký tự</span>\n`);
  1000. });
  1001. }
  1002. showNotification(message, 'success');
  1003. }
  1004. jQuery('#debug-output').text(debugOutput.join('\n'));
  1005. return processedChapters.length;
  1006. } catch (e) {
  1007. console.error("Error in performAction:", e);
  1008. showNotification('Có lỗi khi tách chương', 'error');
  1009. return 0;
  1010. }
  1011. },
  1012.  
  1013. removeEmptyChapters: function() {
  1014. const forms = document.querySelectorAll('[data-gen="MK_GEN"]');
  1015. let removed = 0;
  1016.  
  1017. forms.forEach(form => {
  1018. const content = form.querySelector('textarea[name^="introduce"]').value.trim();
  1019. if (!content) {
  1020. form.remove();
  1021. removed++;
  1022. this.updateChapNumber(false);
  1023. }
  1024. });
  1025. showNotification(`🧹 Đã x lý ${forms.length} chương, xóa ${removed} chương trng`, 'info');
  1026. },
  1027.  
  1028. toggleAutoPost: function() {
  1029. this.STATE.AUTO_POST = !this.STATE.AUTO_POST;
  1030.  
  1031. if (this.STATE.AUTO_POST) {
  1032. // Lưu trạng thái tự động đăng vào localStorage
  1033. localStorage.setItem('TTV_AUTO_POST', 'true');
  1034. this.STATE.TOTAL_CHAPTERS = parseInt(prompt("Nhập tổng số lần tự động đăng:", "10")) || 0;
  1035. this.STATE.POSTED_CHAPTERS = parseInt(localStorage.getItem('TTV_POSTED_CHAPTERS') || '0');
  1036.  
  1037. localStorage.setItem('TTV_TOTAL_CHAPTERS', this.STATE.TOTAL_CHAPTERS.toString());
  1038.  
  1039. this.ELEMENTS.qpButtonAutoPost.text(`🔄 T ĐỘNG (${this.STATE.POSTED_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS})`);
  1040. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-warning').addClass('btn-info');
  1041.  
  1042. showNotification(`✅ Đã bt t động đăng (${this.STATE.POSTED_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS})`, 'success');
  1043.  
  1044. // Bắt đầu quy trình tự động
  1045. if (window.location.href.includes('/dang-chuong/story/')) {
  1046. setTimeout(() => this.runAutoPostSequence(), 2000);
  1047. }
  1048. } else {
  1049. // Tắt tự động đăng
  1050. localStorage.setItem('TTV_AUTO_POST', 'false');
  1051. // Reset số lần đã đăng về 0
  1052. this.STATE.POSTED_CHAPTERS = 0;
  1053. localStorage.setItem('TTV_POSTED_CHAPTERS', '0');
  1054. // Reset tổng số lần đăng về 0
  1055. this.STATE.TOTAL_CHAPTERS = 0;
  1056. localStorage.setItem('TTV_TOTAL_CHAPTERS', '0');
  1057. this.ELEMENTS.qpButtonAutoPost.text('🔄 TỰ ĐỘNG (TẮT)');
  1058. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-info').addClass('btn-warning');
  1059. showNotification('❌ Đã tắt tự động đăng và reset số lần đăng', 'info');
  1060. }
  1061. },
  1062.  
  1063. runAutoPostSequence: function() {
  1064. // Kiểm tra trước khi chạy tự động
  1065. if (this.STATE.POSTED_CHAPTERS >= this.STATE.TOTAL_CHAPTERS) {
  1066. // Reset trạng thái tự động nếu đã hoàn thành
  1067. this.STATE.AUTO_POST = false;
  1068. localStorage.setItem('TTV_AUTO_POST', 'false');
  1069. this.ELEMENTS.qpButtonAutoPost.text('🔄 TỰ ĐỘNG (TẮT)');
  1070. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-info').addClass('btn-warning');
  1071.  
  1072. // Reset số lần đã đăng
  1073. this.STATE.POSTED_CHAPTERS = 0;
  1074. localStorage.setItem('TTV_POSTED_CHAPTERS', '0');
  1075.  
  1076. showNotification(`🎉 Đã hoàn thành t động đăng ${this.STATE.TOTAL_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS} chương và đã reset s ln đăng`, 'success');
  1077. return;
  1078. }
  1079.  
  1080. if (!this.STATE.AUTO_POST) {
  1081. return;
  1082. }
  1083.  
  1084. // Tự động nhấn nút Paste sau 2 giây
  1085. setTimeout(() => {
  1086. if (this.STATE.AUTO_POST) {
  1087. this.handlePasteButton();
  1088.  
  1089. // Tự động nhấn nút Đăng chương sau 3 giây
  1090. setTimeout(() => {
  1091. if (this.STATE.AUTO_POST) {
  1092. this.submitChapters();
  1093. }
  1094. }, 3000);
  1095. }
  1096. }, 2000);
  1097. },
  1098.  
  1099. submitChapters: function() {
  1100. if (!validateChapterLengths()) {
  1101. showNotification('⚠️ Có chương có độ dài dưới 3000 ký tự. Vui lòng kiểm tra lại.', 'error');
  1102. return;
  1103. }
  1104. this.showLoading();
  1105. document.querySelector('form[name="postChapForm"] button[type="submit"]').click();
  1106.  
  1107. if (this.STATE.AUTO_POST) {
  1108. // Cập nhật số chương đã đăng
  1109. this.STATE.POSTED_CHAPTERS++;
  1110. localStorage.setItem('TTV_POSTED_CHAPTERS', this.STATE.POSTED_CHAPTERS.toString());
  1111.  
  1112. // Kiểm tra và tắt tự động đăng nếu đã đủ số chương
  1113. if (this.STATE.POSTED_CHAPTERS >= this.STATE.TOTAL_CHAPTERS) {
  1114. this.STATE.AUTO_POST = false;
  1115. localStorage.setItem('TTV_AUTO_POST', 'false');
  1116. this.ELEMENTS.qpButtonAutoPost.text('🔄 TỰ ĐỘNG (TẮT)');
  1117. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-info').addClass('btn-warning');
  1118.  
  1119. // Reset số lần đã đăng
  1120. this.STATE.POSTED_CHAPTERS = 0;
  1121. localStorage.setItem('TTV_POSTED_CHAPTERS', '0');
  1122.  
  1123. setTimeout(() => {
  1124. showNotification(`🎉 Đã hoàn thành t động đăng ${this.STATE.TOTAL_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS} chương và đã reset s ln đăng`, 'success');
  1125. }, 3000);
  1126. } else {
  1127. // Cập nhật hiển thị số chương đã đăng
  1128. this.ELEMENTS.qpButtonAutoPost.text(`🔄 T ĐỘNG (${this.STATE.POSTED_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS})`);
  1129. }
  1130. }
  1131.  
  1132. setTimeout(() => this.hideLoading(), 2000);
  1133. },
  1134.  
  1135. addNewChapter: function() {
  1136. if ((this.STATE.CHAP_NUMBER + 1) <= MAX_CHAPTER_POST) {
  1137. this.updateChapNumber(true);
  1138. const html = createChapterHTML(this.STATE.CHAP_NUMBER);
  1139. jQuery('#add-chap').before(html);
  1140. setupCharacterCounter();
  1141. } else {
  1142. showNotification(`⚠️ Ch có th đăng ti đa ${MAX_CHAPTER_POST} chương mt ln`, 'warning');
  1143. }
  1144. },
  1145.  
  1146. updateChapNumber: function(isAdd) {
  1147. try{
  1148. if (isAdd) {
  1149. let maxStt = 0;
  1150. let maxSerial = 0;
  1151. jQuery('input[name^="chap_stt"]').each(function() {
  1152. const val = parseInt(jQuery(this).val()) || 0;
  1153. maxStt = Math.max(maxStt, val);
  1154. });
  1155. jQuery('input[name^="chap_number"]').each(function() {
  1156. const val = parseInt(jQuery(this).val()) || 0;
  1157. maxSerial = Math.max(maxSerial, val);
  1158. });
  1159. const chapStt = parseInt(jQuery('.chap_stt1').val()) || 0;
  1160. const chapSerial = parseInt(jQuery('.chap_serial').val()) || 0;
  1161. maxStt = Math.max(maxStt, chapStt);
  1162. maxSerial = Math.max(maxSerial, chapSerial);
  1163. this.STATE.CHAP_STT = maxStt + 1;
  1164. this.STATE.CHAP_SERIAL = maxSerial + 1;
  1165. this.STATE.CHAP_NUMBER++;
  1166. } else {
  1167. if (this.STATE.CHAP_NUMBER > this.STATE.CHAP_NUMBER_ORIGINAL) {
  1168. this.STATE.CHAP_NUMBER--;
  1169. }
  1170. if (this.STATE.CHAP_STT > this.STATE.CHAP_STT_ORIGINAL) {
  1171. this.STATE.CHAP_STT--;
  1172. }
  1173. if (this.STATE.CHAP_SERIAL > this.STATE.CHAP_SERIAL_ORIGINAL) {
  1174. this.STATE.CHAP_SERIAL--;
  1175. }
  1176. }
  1177. jQuery('#chap_number').val(this.STATE.CHAP_NUMBER);
  1178. jQuery('#chap_stt').val(this.STATE.CHAP_STT);
  1179. jQuery('#chap_serial').val(this.STATE.CHAP_SERIAL);
  1180. jQuery('#countNumberPost').text(this.STATE.CHAP_NUMBER);
  1181. } catch (e) {
  1182. console.log("Lỗi: " + e);
  1183. }
  1184. },
  1185.  
  1186. showLoading: function() {
  1187. const loading = jQuery("<div>", {
  1188. class: "loading-overlay",
  1189. html: "<div class='loading-spinner'></div>"
  1190. });
  1191. jQuery("body").append(loading);
  1192. },
  1193.  
  1194. hideLoading: function() {
  1195. jQuery(".loading-overlay").remove();
  1196. },
  1197.  
  1198. resetAutoPost: function() {
  1199. this.STATE.TOTAL_CHAPTERS = 0;
  1200. this.STATE.POSTED_CHAPTERS = 0;
  1201. localStorage.removeItem('TTV_TOTAL_CHAPTERS');
  1202. localStorage.removeItem('TTV_POSTED_CHAPTERS');
  1203. this.ELEMENTS.qpButtonAutoPost.text('🔄 TỰ ĐỘNG (TẮT)');
  1204. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-info').addClass('btn-warning');
  1205. showNotification('🔄 Đã reset số lần tự động đăng', 'info');
  1206. },
  1207.  
  1208. toggleAutoMode: function() {
  1209. const isAutoMode = this.ELEMENTS.qpOptionLoop.is(':checked');
  1210.  
  1211. if (isAutoMode) {
  1212. // Hiển thị nút tự động đăng và reset, ẩn nút paste và đăng chương
  1213. this.ELEMENTS.qpButtonAutoPost.show();
  1214. this.ELEMENTS.qpButtonReset.show();
  1215. this.ELEMENTS.qpButtonPaste.hide();
  1216. this.ELEMENTS.qpButtonSubmit.hide();
  1217.  
  1218. showNotification('🔄 Đã bật chế độ tự động', 'info');
  1219. } else {
  1220. // Hiển thị nút paste và đăng chương, ẩn nút tự động đăng và reset
  1221. this.ELEMENTS.qpButtonAutoPost.hide();
  1222. this.ELEMENTS.qpButtonReset.hide();
  1223. this.ELEMENTS.qpButtonPaste.show();
  1224. this.ELEMENTS.qpButtonSubmit.show();
  1225.  
  1226. showNotification('❌ Đã tắt chế độ tự động', 'info');
  1227. }
  1228.  
  1229. // Lưu trạng thái vào localStorage
  1230. localStorage.setItem('TTV_AUTO_MODE', isAutoMode.toString());
  1231. }
  1232. };
  1233. dangNhanhTTV.init();
  1234. })();