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 5.3
  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-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.  
  69. .button-container {
  70. display: flex;
  71. justify-content: space-between;
  72. align-items: center;
  73. gap: 15px;
  74. margin-top: 15px;
  75. }
  76. #modern-uploader .btn {
  77. padding: 10px 20px;
  78. border-radius: 6px;
  79. cursor: pointer;
  80. font-weight: 600;
  81. font-size: 14px;
  82. transition: all 0.2s ease;
  83. }
  84. #modern-uploader .form-control {
  85. width: 100%;
  86. padding: 15px;
  87. border: 1px solid #ddd;
  88. border-radius: 8px;
  89. margin-bottom: 15px;
  90. font-size: 16px;
  91. transition: border-color 0.2s ease;
  92. }
  93. #modern-uploader .form-control:focus {
  94. border-color: #4285f4;
  95. outline: none;
  96. }
  97. `);
  98. function showNotification(message, type) {
  99. jQuery('#modern-uploader .notification-container').remove();
  100. const container = jQuery("<div>", {
  101. class: "notification-container",
  102. css: {
  103. width: "100%",
  104. padding: "10px 0",
  105. marginTop: "10px",
  106. textAlign: "left",
  107. borderTop: "1px solid rgba(0,0,0,0.1)"
  108. }
  109. });
  110. const notification = jQuery("<div>", {
  111. class: `notification-${type}`,
  112. css: {
  113. backgroundColor: "#e8f5e9",
  114. color: "#000000",
  115. padding: "10px 15px",
  116. borderRadius: "8px",
  117. fontSize: "14px",
  118. fontWeight: "500",
  119. boxShadow: "0 4px 10px rgba(0,0,0,0.15)",
  120. display: "inline-block",
  121. maxWidth: "90%",
  122. margin: "0",
  123. wordBreak: "break-word",
  124. border: "1px solid #81c784"
  125. }
  126. });
  127. const lines = message.split('\n');
  128. lines.forEach((line, index) => {
  129. notification.append(jQuery("<div>").html(line));
  130. });
  131. container.append(notification);
  132. jQuery("#modern-uploader .button-container").after(container);
  133. notification.fadeIn(300);
  134. }
  135. function createChapterHTML(chapNum) {
  136. const chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
  137. const chap_vol_name = jQuery('.chap_vol_name').val() || '';
  138. return `
  139. <div data-gen="MK_GEN" id="COUNT_CHAP_${chapNum}_MK">
  140. <div class="col-xs-12 form-group"></div>
  141. <div class="form-group">
  142. <label class="col-sm-2" for="chap_stt">STT</label>
  143. <div class="col-sm-8">
  144. <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"/>
  145. </div>
  146. </div>
  147. <div class="form-group">
  148. <label class="col-sm-2" for="chap_number">Chương thứ..</label>
  149. <div class="col-sm-8">
  150. <input value="${dăngnhanhTTV.STATE.CHAP_SERIAL}" required class="form-control" name="chap_number[${chapNum}]" placeholder="Chương thứ.. (1,2,3..)" type="text"/>
  151. </div>
  152. </div>
  153. <div class="form-group">
  154. <label class="col-sm-2" for="chap_name">Quyn số</label>
  155. <div class="col-sm-8">
  156. <input class="form-control" name="vol[${chapNum}]" placeholder="Quyển số" type="number" value="${chap_vol}" required/>
  157. </div>
  158. </div>
  159. <div class="form-group">
  160. <label class="col-sm-2" for="chap_name">Tên quyn</label>
  161. <div class="col-sm-8">
  162. <input class="form-control chap_vol_name" name="vol_name[${chapNum}]" placeholder="Tên quyển" type="text" value="${chap_vol_name}" />
  163. </div>
  164. </div>
  165. <div class="form-group">
  166. <label class="col-sm-2" for="chap_name">Tên chương</label>
  167. <div class="col-sm-8">
  168. <input required class="form-control" name="chap_name[${chapNum}]" placeholder="Tên chương" type="text"/>
  169. </div>
  170. </div>
  171. <div class="form-group">
  172. <label class="col-sm-2" for="introduce">Ni dung</label>
  173. <div class="col-sm-8">
  174. <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>
  175. <div class="chapter-character-count"></div>
  176. </div>
  177. </div>
  178. <div class="form-group">
  179. <label class="col-sm-2" for="adv">Qung cáo</label>
  180. <div class="col-sm-8">
  181. <textarea maxlength="1000" class="form-control" name="adv[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea>
  182. </div>
  183. </div>
  184. </div>`;
  185. }
  186. function setupCharacterCounter() {
  187. jQuery(document).on("input", "[name^=introduce]", function() {
  188. const text = jQuery(this).val();
  189. const charCount = text.length;
  190. let charCountElement = jQuery(this).next('.chapter-character-count');
  191. if (charCountElement.length === 0) {
  192. charCountElement = jQuery('<div class="chapter-character-count"></div>');
  193. jQuery(this).after(charCountElement);
  194. }
  195. if(charCount < 3000) {
  196. jQuery(this).addClass('short-chapter');
  197. charCountElement.html(`<span class="short-chapters-warning">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  198. } else {
  199. jQuery(this).removeClass('short-chapter');
  200. if(charCount > 40000) {
  201. charCountElement.html(`<span style="color: #fbbc05;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  202. } else {
  203. charCountElement.html(`<span style="color: #34a853;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  204. }
  205. }
  206. });
  207. }
  208. function validateChapterLengths() {
  209. let hasError = false;
  210. jQuery('form[name="postChapForm"] .chapter-detail').each(function() {
  211. const form = this;
  212. const contentTextarea = form.querySelector('textarea[name^="introduce"]');
  213. const content = contentTextarea.value;
  214. if (content.length < 3000) {
  215. jQuery(contentTextarea).addClass('short-chapter');
  216. let warningIcon = form.querySelector('.warning-icon');
  217. if (!warningIcon) {
  218. warningIcon = document.createElement('div');
  219. warningIcon.className = 'warning-icon';
  220. warningIcon.innerHTML = '⚠️';
  221. contentTextarea.parentNode.appendChild(warningIcon);
  222. }
  223. hasError = true;
  224. } else {
  225. jQuery(contentTextarea).removeClass('short-chapter');
  226. const warningIcon = form.querySelector('.warning-icon');
  227. if (warningIcon) {
  228. warningIcon.remove();
  229. }
  230. }
  231. });
  232. return !hasError;
  233. }
  234. const dăngnhanhTTV = {
  235. STATE: {
  236. CHAP_NUMBER: 1,
  237. CHAP_STT: 1,
  238. CHAP_SERIAL: 1,
  239. CHAP_NUMBER_ORIGINAL: 1,
  240. CHAP_STT_ORIGINAL: 1,
  241. CHAP_SERIAL_ORIGINAL: 1,
  242. AUTO_POST: false,
  243. TOTAL_CHAPTERS: 0,
  244. POSTED_CHAPTERS: 0,
  245. PROCESSED_CHAPTERS: 0, // Added: Tracks processed chapters
  246. TARGET_CHAPTERS: 0 // Added: Tracks target chapters
  247. },
  248. ELEMENTS: {
  249. qpContent: null,
  250. qpButtonSubmit: null,
  251. qpButtonRemoveEmpty: null,
  252. qpButtonReset: null // Added element
  253. },
  254. init: function() {
  255. try {
  256. console.log('[TTV-DEBUG] Script bắt đầu khởi tạo...');
  257. console.log('[TTV-DEBUG] Phiên bản script: 3.0');
  258. this.initializeChapterValues();
  259. console.log('[TTV-DEBUG] Đã khởi tạo giá trị chương');
  260.  
  261. // Khôi phục trạng thái tự động đăng
  262. this.loadAutoPostState();
  263. console.log('[TTV-DEBUG] Đã khôi phục trạng thái tự động đăng');
  264.  
  265. this.createInterface();
  266. console.log('[TTV-DEBUG] Đã tạo giao diện');
  267. this.cacheElements();
  268. console.log('[TTV-DEBUG] Đã cache các elements');
  269. this.registerEvents();
  270. console.log('[TTV-DEBUG] Đã đăng ký các events');
  271. console.log('[TTV-DEBUG] Script đã khởi động thành công');
  272. showNotification('Công cụ đã chạy', 'success');
  273.  
  274. // Cập nhật hiển thị nút tự động đăng
  275. if (this.STATE.AUTO_POST) {
  276. this.ELEMENTS.qpButtonAutoPost.text(`🔄 (ON) ${this.STATE.POSTED_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS}`);
  277. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-warning').addClass('btn-info');
  278. }
  279.  
  280. // Khôi phục trạng thái chế độ tự động đăng
  281. const isAutoMode = localStorage.getItem('TTV_AUTO_MODE') === 'true';
  282. if (isAutoMode) {
  283. this.ELEMENTS.qpOptionLoop.prop('checked', true);
  284. this.toggleAutoMode(); // Áp dụng giao diện theo chế độ
  285. }
  286.  
  287. // Khởi tạo biến theo dõi số chương đã xử lý
  288. this.STATE.PROCESSED_CHAPTERS = 0;
  289. this.STATE.TARGET_CHAPTERS = 0;
  290. } catch (e) {
  291. console.error('[TTV-ERROR] Lỗi khởi tạo:', e);
  292. showNotification('Có lỗi khi khởi tạo Script', 'error');
  293. }
  294. },
  295.  
  296. loadAutoPostState: function() {
  297. // Khôi phục trạng thái tự động đăng từ localStorage
  298. const autoPost = localStorage.getItem('TTV_AUTO_POST') === 'true';
  299. this.STATE.AUTO_POST = autoPost;
  300.  
  301. if (autoPost) {
  302. this.STATE.TOTAL_CHAPTERS = parseInt(localStorage.getItem('TTV_TOTAL_CHAPTERS') || '0');
  303. this.STATE.POSTED_CHAPTERS = parseInt(localStorage.getItem('TTV_POSTED_CHAPTERS') || '0');
  304.  
  305. console.log(`[TTV-DEBUG] Khôi phc t động đăng: ${this.STATE.POSTED_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS}`);
  306. }
  307. },
  308. createInterface: function() {
  309. const html = `
  310. <div id="modern-uploader">
  311. <div class="text-center mb-4">
  312. <h3 style="color: #4285f4; margin-bottom: 15px; font-weight: 700; font-size: 18px;">📝 CÔNG C ĐĂNG NHANH</h3>
  313. </div>
  314. <div class="form-group">
  315. <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>
  316. </div>
  317. <div class="text-center mb-3">
  318. <label style="color: #bef385;"><input type="checkbox" id="qpOptionLoop" class="form-control" style="height:10px;width: 10px;display: inline-block;">Chế độ t động</label>
  319. </div>
  320. <div class="button-container" style="display: flex; justify-content: center; gap: 15px;">
  321. <button class="btn btn-primary" id="qpButtonPaste">📋 Paste</button>
  322. <button class="btn btn-success" id="qpButtonSubmit">📤 Đăng chương</button>
  323. <button class="btn btn-danger" id="qpButtonRemoveEmpty" style="display: none;">Ẩn chương trng</button>
  324. <button class="btn btn-warning" id="qpButtonAutoPost" style="display: none;">🔄 (OFF)</button>
  325. <button class="btn btn-secondary" id="qpButtonReset" style="display: none;">🔁 Reset</button>
  326. </div>
  327. <div class="notification-container"></div>
  328. </div>`;
  329.  
  330. jQuery(".list-in-user").before(html);
  331. },
  332. initializeChapterValues: function() {
  333. try {
  334. const chap_number = parseInt(jQuery('#chap_number').val());
  335. let chap_stt = parseInt(jQuery('.chap_stt1').val());
  336. let chap_serial = parseInt(jQuery('.chap_serial').val());
  337.  
  338. if (parseInt(jQuery('#chap_stt').val()) > chap_stt) {
  339. chap_stt = parseInt(jQuery('#chap_stt').val());
  340. }
  341. if (parseInt(jQuery('#chap_serial').val()) > chap_serial) {
  342. chap_serial = parseInt(jQuery('#chap_serial').val());
  343. }
  344.  
  345. this.STATE.CHAP_NUMBER = this.STATE.CHAP_NUMBER_ORIGINAL = chap_number || 1;
  346. this.STATE.CHAP_STT = this.STATE.CHAP_STT_ORIGINAL = chap_stt || 1;
  347. this.STATE.CHAP_SERIAL = this.STATE.CHAP_SERIAL_ORIGINAL = chap_serial || 1;
  348. } catch (e) {
  349. console.error("Error initializing chapter values:", e);
  350. }
  351. },
  352. cacheElements: function() {
  353. this.ELEMENTS.qpContent = jQuery("#qpContent");
  354. this.ELEMENTS.qpButtonSubmit = jQuery("#qpButtonSubmit");
  355. this.ELEMENTS.qpButtonRemoveEmpty = jQuery("#qpButtonRemoveEmpty");
  356. this.ELEMENTS.qpButtonPaste = jQuery("#qpButtonPaste");
  357. this.ELEMENTS.qpButtonAutoPost = jQuery("#qpButtonAutoPost");
  358. this.ELEMENTS.qpButtonReset = jQuery("#qpButtonReset"); // Added element caching
  359. this.ELEMENTS.qpOptionLoop = jQuery("#qpOptionLoop"); // Added checkbox caching
  360. },
  361. registerEvents: function() {
  362. this.ELEMENTS.qpContent.on("paste", this.handlePaste.bind(this));
  363. this.ELEMENTS.qpButtonRemoveEmpty.on('click', this.removeEmptyChapters.bind(this));
  364. this.ELEMENTS.qpButtonSubmit.on('click', this.submitChapters.bind(this));
  365. this.ELEMENTS.qpButtonPaste.on('click', this.handlePasteButton.bind(this));
  366. this.ELEMENTS.qpButtonAutoPost.on('click', this.toggleAutoPost.bind(this));
  367. this.ELEMENTS.qpButtonReset.on('click', this.resetAutoPost.bind(this)); //Added event listener
  368. this.ELEMENTS.qpOptionLoop.on('change', this.toggleAutoMode.bind(this)); // Added checkbox event
  369. setupCharacterCounter();
  370.  
  371. // Kiểm tra và bắt đầu tự động đăng nếu đã bật
  372. if (window.location.href.includes('/dang-chuong/story/')) {
  373. setTimeout(() => {
  374. if (this.STATE.AUTO_POST && this.STATE.POSTED_CHAPTERS < this.STATE.TOTAL_CHAPTERS) {
  375. this.runAutoPostSequence();
  376. }
  377. }, 2000);
  378. }
  379. },
  380. handlePasteButton: function() {
  381. this.showLoading();
  382. navigator.clipboard.readText()
  383. .then(text => {
  384. this.ELEMENTS.qpContent.val(text);
  385. setTimeout(() => {
  386. this.performAction();
  387. this.hideLoading();
  388. }, 100);
  389. })
  390. .catch(err => {
  391. console.error('Không thể đọc dữ liệu từ clipboard:', err);
  392. this.hideLoading();
  393. showNotification('Không thể truy cập clipboard. Vui lòng dán trực tiếp vào ô nội dung.', 'error');
  394. });
  395. },
  396. handlePaste: function(e) {
  397. e.preventDefault();
  398. this.ELEMENTS.qpContent.val("");
  399. this.showLoading();
  400. const pastedText = e.originalEvent.clipboardData.getData('text');
  401. this.ELEMENTS.qpContent.val(pastedText);
  402. setTimeout(() => {
  403. this.performAction();
  404. this.hideLoading();
  405. }, 100);
  406. },
  407. performAction: function() {
  408. try {
  409. console.log("Starting performAction");
  410. var text = this.ELEMENTS.qpContent.val();
  411.  
  412. if (!text) {
  413. showNotification('Không có nội dung để tách chương', 'error');
  414. return 0;
  415. }
  416. var debugOutput = [];
  417. var chapters = [];
  418. var lines = text.split('\n');
  419. var currentChapter = [];
  420. var lastTitle = null;
  421. debugOutput.push("=== Processing Text ===");
  422. debugOutput.push(`Total lines: ${lines.length}`);
  423. debugOutput.push("=== Line Analysis ===");
  424. function visualizeWhitespace(str) {
  425. return str.split('').map(c => {
  426. if (c === '\t') return '\\t';
  427. if (c === ' ') return '·';
  428. if (c === '\n') return '\\n';
  429. return c;
  430. }).join('');
  431. }
  432. for (let i = 0; i < lines.length; i++) {
  433. let line = lines[i];
  434. let isChapterTitle = /^\t[Cc]hương\s*\d+\s*:/.test(line) || /^\s{4,}[Cc]hương\s*\d+\s*:/.test(line);
  435. debugOutput.push(`Line ${i}: ${visualizeWhitespace(line.substring(0, 50))}${line.length > 50 ? '...' : ''}`);
  436. debugOutput.push(` Is chapter: ${isChapterTitle}`);
  437.  
  438. if (isChapterTitle) {
  439. // Trích xuất số chương từ tiêu đề
  440. const chapterMatch = line.match(/[Cc]hương\s*(\d+)\s*:/);
  441. const currentChapterNum = chapterMatch ? parseInt(chapterMatch[1]) : 0;
  442. const isDuplicateNumber = currentChapterNum > 0 && debugOutput.includes(`Found chapter number: ${currentChapterNum}`);
  443. // Kiểm tra nếu dòng tiếp theo cũng là tiêu đề chương và có chung số chương
  444. let isConsecutiveTitle = false;
  445. if (i < lines.length - 1) {
  446. const nextLine = lines[i + 1];
  447. const nextLineIsChapter = /^\t[Cc]hương\s*\d+\s*:/.test(nextLine) || /^\s{4,}[Cc]hương\s*\d+\s*:/.test(nextLine);
  448. if (nextLineIsChapter) {
  449. const nextChapterMatch = nextLine.match(/[Cc]hương\s*(\d+)\s*:/);
  450. const nextChapterNum = nextChapterMatch ? parseInt(nextChapterMatch[1]) : 0;
  451. isConsecutiveTitle = (nextChapterNum === currentChapterNum);
  452. if (isConsecutiveTitle) {
  453. debugOutput.push(` -> Detected consecutive title with same chapter number: ${currentChapterNum}`);
  454. }
  455. }
  456. }
  457. // Kiểm tra nếu dòng trước đó cũng là tiêu đề chương và vừa được xử lý
  458. const isAfterPreviousTitle = (i > 0 && line !== lastTitle && currentChapter.length <= 1);
  459. if (currentChapter.length > 0) {
  460. // Nếu không phải là tiêu đề liên tiếp hoặc sau một tiêu đề khác
  461. if (line !== lastTitle && !isConsecutiveTitle && !isAfterPreviousTitle) {
  462. // Kiểm tra số chương trùng
  463. if (isDuplicateNumber) {
  464. debugOutput.push(` -> Warning: Duplicate chapter number ${currentChapterNum} detected`);
  465. showNotification(`Cnh báo: S chương ${currentChapterNum} b trùng lp`, 'warning');
  466. }
  467. chapters.push(currentChapter.join('\n'));
  468. currentChapter = [line];
  469. lastTitle = line;
  470. debugOutput.push(` -> New chapter started`);
  471. if (currentChapterNum > 0) {
  472. debugOutput.push(`Found chapter number: ${currentChapterNum}`);
  473. }
  474. } else {
  475. if (isConsecutiveTitle) {
  476. debugOutput.push(` -> Ignoring consecutive title`);
  477. // Bỏ qua dòng này vì là tiêu đề liên tiếp
  478. continue;
  479. } else {
  480. debugOutput.push(` -> Skipped duplicate title`);
  481. }
  482. }
  483. } else {
  484. currentChapter = [line];
  485. lastTitle = line;
  486. debugOutput.push(` -> First chapter started`);
  487. if (currentChapterNum > 0) {
  488. debugOutput.push(`Found chapter number: ${currentChapterNum}`);
  489. }
  490. }
  491. } else if (currentChapter.length > 0) {
  492. currentChapter.push(line);
  493. }
  494. }
  495. if (currentChapter.length > 0) {
  496. chapters.push(currentChapter.join('\n'));
  497. debugOutput.push("-> Added final chapter");
  498. }
  499. debugOutput.push("=== Results ===");
  500. debugOutput.push(`Found ${chapters.length} chapters`);
  501. const processedChapters = [];
  502. for (let i = 0; i < chapters.length; i++) {
  503. const chapterLines = chapters[i].split('\n');
  504. const title = chapterLines.shift().trim();
  505. const chapterText = chapterLines.join('\n');
  506. const charCount = chapterText.length;
  507. debugOutput.push(`Chapter ${i+1} character count: ${charCount}`);
  508. if (charCount > 40000) {
  509. const parts = Math.ceil(charCount / 40000);
  510. debugOutput.push(`Splitting into ${parts} parts`);
  511. const charsPerPart = Math.ceil(charCount / parts);
  512. debugOutput.push(`Characters per part: ~${charsPerPart}`);
  513. let currentText = chapterText;
  514. let totalProcessed = 0;
  515. for (let part = 0; part < parts; part++) {
  516. const isLastPart = part === parts - 1;
  517. const targetSize = isLastPart ? currentText.length : charsPerPart;
  518. let endPos = Math.min(targetSize, currentText.length);
  519. if (!isLastPart && endPos < currentText.length) {
  520. const nextParagraph = currentText.indexOf('\n\n', endPos - 500);
  521. if (nextParagraph !== -1 && nextParagraph < endPos + 500) {
  522. endPos = nextParagraph + 2;
  523. } else {
  524. const sentenceEnd = Math.max(
  525. currentText.lastIndexOf('. ', endPos),
  526. currentText.lastIndexOf('! ', endPos),
  527. currentText.lastIndexOf('? ', endPos)
  528. );
  529. if (sentenceEnd !== -1 && sentenceEnd > endPos - 500) {
  530. endPos = sentenceEnd + 2;
  531. }
  532. }
  533. }
  534. const partContent = currentText.substring(0, endPos);
  535. totalProcessed += partContent.length;
  536. currentText = currentText.substring(endPos);
  537. let chapterTitle = title;
  538. if (title.includes(':')) {
  539. chapterTitle = title.substring(title.indexOf(':') + 1).trim();
  540. }
  541. let newTitle = `${title} (Phn ${part+1}/${parts})`;
  542. processedChapters.push(newTitle + '\n' + partContent);
  543. debugOutput.push(`Part ${part+1}: ${partContent.length} chars`);
  544. }
  545. debugOutput.push(`Total processed: ${totalProcessed}/${charCount} chars`);
  546. } else {
  547. processedChapters.push(chapters[i]);
  548. }
  549. }
  550. debugOutput.push(`After processing: ${processedChapters.length} chapters`);
  551. const chaptersToFill = processedChapters.slice(0, MAX_CHAPTER_POST);
  552. const remainingChapters = processedChapters.slice(MAX_CHAPTER_POST);
  553. var titles = jQuery("input[name^='chap_name']");
  554. var contents = jQuery("textarea[name^='introduce']");
  555. var advs = jQuery("textarea[name^='adv']");
  556. debugOutput.push(`Forms found: ${titles.length}`);
  557. if (processedChapters.length === 0) {
  558. showNotification('Không tìm thấy chương nào', 'error');
  559. jQuery('#debug-output').text(debugOutput.join('\n'));
  560. return;
  561. }
  562. if (remainingChapters.length > 0) {
  563. debugOutput.push(`${remainingChapters.length} chapters will be copied to clipboard`);
  564. }
  565. const neededForms = chaptersToFill.length - titles.length;
  566. if (neededForms > 0 && titles.length < MAX_CHAPTER_POST) {
  567. debugOutput.push(`Need to add ${neededForms} more forms`);
  568. for (let i = 0; i < neededForms && (titles.length + i) < MAX_CHAPTER_POST; i++) {
  569. this.addNewChapter();
  570. }
  571. titles = jQuery("input[name^='chap_name']");
  572. contents = jQuery("textarea[name^='introduce']");
  573. advs = jQuery("textarea[name^='adv']");
  574. }
  575. debugOutput.push(`Filling ${chaptersToFill.length} chapters into forms`);
  576. jQuery.each(titles, function(k, v) {
  577. if (k < chaptersToFill.length) {
  578. var content = chaptersToFill[k].split('\n');
  579. var title = content.shift().trim();
  580. var chapterTitle = title;
  581. if (title.includes(':')) {
  582. chapterTitle = title.substring(title.indexOf(':') + 1).trim();
  583. }
  584. debugOutput.push(`\nFilling chapter ${k + 1}:`);
  585. debugOutput.push(`Original title: ${title}`);
  586. debugOutput.push(`Extracted title: ${chapterTitle}`);
  587. debugOutput.push(`Content length: ${content.length} lines`);
  588. if (!chapterTitle || chapterTitle.trim() === '') {
  589. chapterTitle = "Vô đề";
  590. debugOutput.push(`Empty title detected, using default: ${chapterTitle}`);
  591. }
  592. titles[k].value = chapterTitle;
  593. contents[k].value = HEADER_SIGN + "\r\n" + content.join('\n') + "\r\n" + FOOTER_SIGN;
  594. if (advs[k]) advs[k].value = "";
  595. jQuery(contents[k]).trigger('input');
  596. }
  597. });
  598. if (remainingChapters.length > 0) {
  599. try {
  600. const clipboardContent = remainingChapters.map(chap => {
  601. const lines = chap.trim().split('\n');
  602. if (lines.length > 0) {
  603. if (!lines[0].startsWith('\t')) {
  604. lines[0] = '\t' + lines[0];
  605. }
  606. }
  607. return lines.join('\n');
  608. }).join('\n\n---CHAPTER_SEPARATOR---\n\n');
  609. let splitChapters = 0;
  610. let shortChapters = 0;
  611. let shortChapterDetails = [];
  612. let longChapterDetails = [];
  613. for (let i = 0; i < chapters.length; i++) {
  614. const chapterLines = chapters[i].split('\n');
  615. const title = chapterLines.shift().trim();
  616. const chapterText = chapterLines.join('\n');
  617. if (chapterText.length > 40000) {
  618. splitChapters++;
  619. const partsCount = Math.ceil(chapterText.length / 40000);
  620. longChapterDetails.push({
  621. title: title,
  622. parts: partsCount
  623. });
  624. }
  625. if (chapterText.length < 3000) {
  626. shortChapters++;
  627. shortChapterDetails.push({
  628. title: title,
  629. length: chapterText.length
  630. });
  631. }
  632. }
  633. const splittedChaptersCount = processedChapters.length - (chapters.length - splitChapters);
  634. let message = '';
  635. message = message.concat(`📝Đã x lý ${processedChapters.length} Chương\n`);
  636. message = message.concat(`📝Đã nhp ${Math.min(processedChapters.length, MAX_CHAPTER_POST)} Chương\n`);
  637.  
  638. if (remainingChapters.length > 0) {
  639. message = message.concat(`📋Đã lưu li ${remainingChapters.length} Chương\n`);
  640. }
  641. if (splitChapters > 0) {
  642. message = message.concat(`📑Có ${splitChapters} Chương đã chia thành ${splittedChaptersCount} Chương\n`);
  643. longChapterDetails.forEach(chapter => {
  644. let chapterName = chapter.title;
  645. if (chapterName.includes(':')) {
  646. chapterName = chapterName.trim();
  647. }
  648. message = message.concat(` - ${chapterName}: ${chapter.parts} Chương\n`);
  649. });
  650. }
  651. if (shortChapters > 0) {
  652. message = message.concat(`⚠️<span class="short-chapters-warning">Có ${shortChapters} chương dưới 3000 ký tự</span>\n`);
  653. shortChapterDetails.forEach(chapter => {
  654. let chapterName = chapter.title;
  655. if (chapterName.includes(':')) {
  656. chapterName = chapterName.trim();
  657. }
  658. message = message.concat(`<span class="short-chapters-warning"> - ${chapterName}: có ${chapter.length.toLocaleString()} ký tự</span>\n`);
  659. });
  660. }
  661. // Cập nhật số chương đã xử lý
  662. this.STATE.PROCESSED_CHAPTERS += processedChapters.length;
  663.  
  664. // Kiểm tra xem đã xử lý đủ số chương chưa
  665. if (this.STATE.TARGET_CHAPTERS > 0 && this.STATE.PROCESSED_CHAPTERS >= this.STATE.TARGET_CHAPTERS) {
  666. message = message.concat(`\n🎯 Đã đạt mc tiêu x lý ${this.STATE.PROCESSED_CHAPTERS}/${this.STATE.TARGET_CHAPTERS} chương`);
  667. // Xóa clipboard nếu đã đủ số chương
  668. if (navigator.clipboard && navigator.clipboard.writeText) {
  669. navigator.clipboard.writeText("");
  670. }
  671. }
  672.  
  673. showNotification(message, 'success');
  674. } catch (err) {
  675. console.error('Lỗi khi sao chép vào clipboard:', err);
  676. debugOutput.push(`Li khi sao chép vào clipboard: ${err.message}`);
  677. showNotification('Không thể sao chép vào clipboard. Vui lòng thử lại.', 'error');
  678. const manualCopyArea = document.createElement('div');
  679. manualCopyArea.style.position = 'fixed';
  680. manualCopyArea.style.top = '50%';
  681. manualCopyArea.style.left = '50%';
  682. manualCopyArea.style.transform = 'translate(-50%, -50%)';
  683. manualCopyArea.style.backgroundColor = 'white';
  684. manualCopyArea.style.padding = '20px';
  685. manualCopyArea.style.borderRadius = '8px';
  686. manualCopyArea.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
  687. manualCopyArea.style.zIndex = '10000';
  688. manualCopyArea.style.maxWidth = '80%';
  689. manualCopyArea.style.maxHeight = '80%';
  690. manualCopyArea.style.overflow = 'auto';
  691. manualCopyArea.innerHTML = `
  692. <h3 style="margin-top: 0;">Sao chép th công</h3>
  693. <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>
  694. <textarea style="width: 100%; height: 300px; padding: 10px;">${clipboardContent}</textarea>
  695. <div style="text-align: right; margin-top: 10px;">
  696. <button id="closeManualCopy" style="padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;">Đóng</button>
  697. </div>
  698. `;
  699. document.body.appendChild(manualCopyArea);
  700. document.getElementById('closeManualCopy').addEventListener('click', () => {
  701. document.body.removeChild(manualCopyArea);
  702. });
  703. }
  704. }
  705. this.ELEMENTS.qpButtonSubmit.removeClass("btn-disable").addClass("btn-success");
  706. this.ELEMENTS.qpContent.val("Đã thựchiện xong");
  707. if (remainingChapters.length === 0) {
  708. let splitChapters = 0;
  709. let shortChapters = 0;
  710. let shortChapterDetails = [];
  711. let longChapterDetails = [];
  712. for (let i = 0; i < chapters.length; i++) {
  713. const chapterLines = chapters[i].split('\n');
  714. const title = chapterLines.shift().trim();
  715. const chapterText = chapterLines.join('\n');
  716. if (chapterText.length > 40000) {
  717. splitChapters++;
  718. const partsCount = Math.ceil(chapterText.length / 40000);
  719. longChapterDetails.push({
  720. title: title,
  721. parts: partsCount
  722. });
  723. }
  724. if (chapterText.length < 3000) {
  725. shortChapters++;
  726. shortChapterDetails.push({
  727. title: title,
  728. length: chapterText.length
  729. });
  730. }
  731. }
  732. const splittedChaptersCount = processedChapters.length - (chapters.length - splitChapters);
  733. let message = '';
  734. message = message.concat(`📝Đã x lý ${processedChapters.length} Chương\n`);
  735. message = message.concat(`📝Đã nhp ${Math.min(processedChapters.length, MAX_CHAPTER_POST)} Chương\n`);
  736. if (splitChapters > 0) {
  737. message = message.concat(`📑Có ${splitChapters} Chương dài chia thành ${splittedChaptersCount} Chương\n`);
  738. longChapterDetails.forEach(chapter => {
  739. let chapterName = chapter.title;
  740. if (chapterName.includes(':')) {
  741. chapterName = chapterName.trim();
  742. }
  743. message = message.concat(` - ${chapterName}: ${chapter.parts} Chương\n`);
  744. });
  745. }
  746. if (shortChapters > 0) {
  747. message = message.concat(`⚠️<span class="short-chapters-warning">Có ${shortChapters} chương dưới 3000 ký tự</span>\n`);
  748. shortChapterDetails.forEach(chapter => {
  749. let chapterName = chapter.title;
  750. if (chapterName.includes(':')) {
  751. chapterName = chapterName.trim();
  752. }
  753. message = message.concat(`<span class="short-chapters-warning"> - ${chapterName}: có ${chapter.length.toLocaleString()} ký tự</span>\n`);
  754. });
  755. }
  756. showNotification(message, 'success');
  757. }
  758. jQuery('#debug-output').text(debugOutput.join('\n'));
  759. return processedChapters.length;
  760. } catch (e) {
  761. console.error("Error in performAction:", e);
  762. showNotification('Có lỗi khi tách chương', 'error');
  763. return 0;
  764. }
  765. },
  766. removeEmptyChapters: function() {
  767. const forms = document.querySelectorAll('[data-gen="MK_GEN"]');
  768. let removed = 0;
  769.  
  770. forms.forEach(form => {
  771. const content = form.querySelector('textarea[name^="introduce"]').value.trim();
  772. if (!content) {
  773. form.remove();
  774. removed++;
  775. this.updateChapNumber(false);
  776. }
  777. });
  778. showNotification(`Đã x lý ${forms.length} chương`, 'info');
  779. },
  780. toggleAutoPost: function() {
  781. this.STATE.AUTO_POST = !this.STATE.AUTO_POST;
  782.  
  783. if (this.STATE.AUTO_POST) {
  784. // Lưu trạng thái tự động đăng vào localStorage
  785. localStorage.setItem('TTV_AUTO_POST', 'true');
  786. this.STATE.TOTAL_CHAPTERS = parseInt(prompt("Nhập tổng số lần tự động đăng:", "10")) || 0;
  787. this.STATE.POSTED_CHAPTERS = parseInt(localStorage.getItem('TTV_POSTED_CHAPTERS') || '0');
  788. this.STATE.TARGET_CHAPTERS = this.STATE.TOTAL_CHAPTERS; // Set target chapters
  789.  
  790. localStorage.setItem('TTV_TOTAL_CHAPTERS', this.STATE.TOTAL_CHAPTERS.toString());
  791.  
  792. this.ELEMENTS.qpButtonAutoPost.text(`🔄 (ON) ${this.STATE.POSTED_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS}`);
  793. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-warning').addClass('btn-info');
  794.  
  795. showNotification(`Đã bt t động đăng (${this.STATE.POSTED_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS})`, 'success');
  796.  
  797. // Bắt đầu quy trình tự động
  798. if (window.location.href.includes('/dang-chuong/story/')) {
  799. setTimeout(() => this.runAutoPostSequence(), 2000);
  800. }
  801. } else {
  802. // Tắt tự động đăng
  803. localStorage.setItem('TTV_AUTO_POST', 'false');
  804. // Reset số lần đã đăng về 0
  805. this.STATE.POSTED_CHAPTERS = 0;
  806. localStorage.setItem('TTV_POSTED_CHAPTERS', '0');
  807. // Reset tổng số lần đăng về 0
  808. this.STATE.TOTAL_CHAPTERS = 0;
  809. localStorage.setItem('TTV_TOTAL_CHAPTERS', '0');
  810. this.STATE.TARGET_CHAPTERS = 0; // Reset target chapters
  811. this.ELEMENTS.qpButtonAutoPost.text('🔄 (OFF)');
  812. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-info').addClass('btn-warning');
  813. showNotification('Đã tắt tự động đăng và reset số lần đăng', 'info');
  814. }
  815. },
  816.  
  817. runAutoPostSequence: function() {
  818. // Kiểm tra trước khi chạy tự động
  819. if (this.STATE.POSTED_CHAPTERS >= this.STATE.TOTAL_CHAPTERS) {
  820. // Reset trạng thái tự động nếu đã hoàn thành
  821. this.STATE.AUTO_POST = false;
  822. localStorage.setItem('TTV_AUTO_POST', 'false');
  823. this.ELEMENTS.qpButtonAutoPost.text('🔄 (OFF)');
  824. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-info').addClass('btn-warning');
  825.  
  826. // Reset số lần đã đăng
  827. this.STATE.POSTED_CHAPTERS = 0;
  828. localStorage.setItem('TTV_POSTED_CHAPTERS', '0');
  829.  
  830. showNotification(`Đã hoàn thành t động đăng ${this.STATE.TOTAL_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS} chương và đã reset s ln đăng`, 'success');
  831. return;
  832. }
  833.  
  834. if (!this.STATE.AUTO_POST) {
  835. return;
  836. }
  837.  
  838. // Tự động nhấn nút Paste sau 2 giây
  839. setTimeout(() => {
  840. if (this.STATE.AUTO_POST) {
  841. this.handlePasteButton();
  842.  
  843. // Tự động nhấn nút Đăng chương sau 3 giây
  844. setTimeout(() => {
  845. if (this.STATE.AUTO_POST) {
  846. this.submitChapters();
  847. }
  848. }, 3000);
  849. }
  850. }, 2000);
  851. },
  852.  
  853. submitChapters: function() {
  854. if (!validateChapterLengths()) {
  855. return;
  856. }
  857. this.showLoading();
  858. document.querySelector('form[name="postChapForm"] button[type="submit"]').click();
  859.  
  860. if (this.STATE.AUTO_POST) {
  861. // Cập nhật số chương đã đăng
  862. this.STATE.POSTED_CHAPTERS++;
  863. localStorage.setItem('TTV_POSTED_CHAPTERS', this.STATE.POSTED_CHAPTERS.toString());
  864.  
  865. // Kiểm tra và tắt tự động đăng nếu đã đủ số chương
  866. if (this.STATE.POSTED_CHAPTERS >= this.STATE.TOTAL_CHAPTERS) {
  867. this.STATE.AUTO_POST = false;
  868. localStorage.setItem('TTV_AUTO_POST', 'false');
  869. this.ELEMENTS.qpButtonAutoPost.text('🔄 (OFF)');
  870. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-info').addClass('btn-warning');
  871.  
  872. // Reset số lần đã đăng
  873. this.STATE.POSTED_CHAPTERS = 0;
  874. localStorage.setItem('TTV_POSTED_CHAPTERS', '0');
  875.  
  876. setTimeout(() => {
  877. showNotification(`Đã hoàn thành t động đăng ${this.STATE.TOTAL_CHAPTERS}/${this.STATE.TOTAL_CHAPTERS} chương và đã reset s ln đăng`, 'success');
  878. }, 3000);
  879. }
  880. }
  881.  
  882. setTimeout(() => this.hideLoading(), 2000);
  883. },
  884. addNewChapter: function() {
  885. if ((this.STATE.CHAP_NUMBER + 1) <= MAX_CHAPTER_POST) {
  886. this.updateChapNumber(true);
  887. const html = createChapterHTML(this.STATE.CHAP_NUMBER);
  888. jQuery('#add-chap').before(html);
  889. setupCharacterCounter();
  890. } else {
  891. showNotification(`Ch có th đăng ti đa ${MAX_CHAPTER_POST} chương mt ln`, 'warning');
  892. }
  893. },
  894. updateChapNumber: function(isAdd) {
  895. try{
  896. if (isAdd) {
  897. let maxStt =0;
  898. let maxSerial = 0;
  899. jQuery('input[name^="chap_stt"]').each(function() {
  900. const val = parseInt(jQuery(this).val()) || 0;
  901. maxStt = Math.max(maxStt, val);
  902. });
  903. jQuery('input[name^="chap_number"]').each(function() {
  904. const val = parseInt(jQuery(this).val()) || 0;
  905. maxSerial = Math.max(maxSerial, val);
  906. });
  907. const chapStt = parseInt(jQuery('.chap_stt1').val()) || 0;
  908. const chapSerial = parseInt(jQuery('.chap_serial').val()) || 0;
  909. maxStt = Math.max(maxStt, chapStt);
  910. maxSerial = Math.max(maxSerial, chapSerial);
  911. this.STATE.CHAP_STT = maxStt + 1;
  912. this.STATE.CHAP_SERIAL = maxSerial + 1;
  913. this.STATE.CHAP_NUMBER++;
  914. } else {
  915. if (this.STATE.CHAP_NUMBER > this.STATE.CHAP_NUMBER_ORIGINAL) {
  916. this.STATE.CHAP_NUMBER--;
  917. }
  918. if (this.STATE.CHAP_STT > this.STATE.CHAP_STT_ORIGINAL) {
  919. this.STATE.CHAP_STT--;
  920. }
  921. if (this.STATE.CHAP_SERIAL > this.STATE.CHAP_SERIAL_ORIGINAL) {
  922. this.STATE.CHAP_SERIAL--;
  923. }
  924. }
  925. jQuery('#chap_number').val(this.STATE.CHAP_NUMBER);
  926. jQuery('#chap_stt').val(this.STATE.CHAP_STT);
  927. jQuery('#chap_serial').val(this.STATE.CHAP_SERIAL);
  928. jQuery('#countNumberPost').text(this.STATE.CHAP_NUMBER);
  929. } catch (e) {
  930. console.log("Lỗi: " + e);
  931. }
  932. },
  933. showLoading: function() {
  934. const loading = jQuery("<div>", {
  935. class: "loading-overlay",
  936. css: {
  937. position: "fixed",
  938. inset: 0,
  939. backgroundColor: "rgba(0, 0, 0, 0.5)",
  940. display: "flex",
  941. alignItems: "center",
  942. justifyContent: "center",
  943. zIndex: 9999
  944. },
  945. html: "<div class='loading-spinner'></div>"
  946. });
  947. jQuery("body").append(loading);
  948. },
  949. hideLoading: function() {
  950. jQuery(".loading-overlay").remove();
  951. },
  952. resetAutoPost: function() { // Added reset function
  953. this.STATE.TOTAL_CHAPTERS = 0;
  954. this.STATE.POSTED_CHAPTERS = 0;
  955. this.STATE.TARGET_CHAPTERS = 0; // Reset target chapters
  956. localStorage.removeItem('TTV_TOTAL_CHAPTERS');
  957. localStorage.removeItem('TTV_POSTED_CHAPTERS');
  958. this.ELEMENTS.qpButtonAutoPost.text('🔄 (OFF)');
  959. this.ELEMENTS.qpButtonAutoPost.removeClass('btn-info').addClass('btn-warning');
  960. showNotification('Đã reset số lần tự động đăng', 'info');
  961. },
  962. toggleAutoMode: function() {
  963. const isAutoMode = this.ELEMENTS.qpOptionLoop.is(':checked');
  964.  
  965. if (isAutoMode) {
  966. // Hiển thị nút tự động đăng và reset, ẩn nút paste và đăng chương
  967. this.ELEMENTS.qpButtonAutoPost.show();
  968. this.ELEMENTS.qpButtonReset.show();
  969. this.ELEMENTS.qpButtonPaste.hide();
  970. this.ELEMENTS.qpButtonSubmit.hide();
  971.  
  972. showNotification('Đã bật chế độ tự động', 'info');
  973. } else {
  974. // Hiển thị nút paste và đăng chương, ẩn nút tự động đăng và reset
  975. this.ELEMENTS.qpButtonAutoPost.hide();
  976. this.ELEMENTS.qpButtonReset.hide();
  977. this.ELEMENTS.qpButtonPaste.show();
  978. this.ELEMENTS.qpButtonSubmit.show();
  979.  
  980. showNotification('Đã tắt chế độ tự động', 'info');
  981. }
  982.  
  983. // Lưu trạng thái vào localStorage
  984. localStorage.setItem('TTV_AUTO_MODE', isAutoMode.toString());
  985. }
  986. };
  987. dăngnhanhTTV.init();
  988. })();