TTV Auto Upload

Tự động điền form đăng chương trên tangthuvien.net

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

  1. // ==UserScript==
  2. // @name TTV Auto Upload
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.4
  5. // @description Tự động điền form đăng chương trên tangthuvien.net
  6. // @author HA
  7. // @match https://tangthuvien.net/dang-chuong/story/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // CSS tối giản
  15. const style = document.createElement('style');
  16. style.textContent = `
  17. .ttv-notification {position:fixed;top:20px;right:20px;padding:10px 20px;background:#4CAF50;color:white;border-radius:4px;z-index:9999;display:none}
  18. .ttv-error {background:#f44336}
  19. .ttv-control-panel {position:fixed;top:50px;right:20px;background:white;padding:25px;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.15);z-index:9998;width:500px;transition:all 0.3s ease}
  20. .ttv-control-panel.minimized {width:auto;height:auto;padding:10px;opacity:0.8;transform:translateX(calc(100% - 40px))}
  21. .ttv-control-panel.minimized:hover {opacity:1;transform:translateX(0)}
  22. .ttv-control-panel.minimized .ttv-button-group,.ttv-control-panel.minimized .ttv-header {display:none}
  23. .ttv-button-group {display:flex;flex-direction:column;gap:15px}
  24. .ttv-content-editor {width:100%;height:100px;margin:6px 0;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;resize:vertical}
  25. .ttv-preview {display:none;width:100%;height:100px;margin:6px 0;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;background:#f9f9f9}
  26. .ttv-heading {font-size:15px;color:#555;margin:12px 0;display:flex;justify-content:space-between;align-items:center}
  27. .ttv-word-count {font-size:12px;color:#888;padding:3px 8px;background:#f5f5f5;border-radius:4px}
  28. .ttv-chapter-list {width:100%;margin:10px 0;max-height:200px;overflow-y:auto;border:1px solid #eee;border-radius:6px;padding:8px}
  29. .ttv-chapter-item {padding:8px;border-bottom:1px solid #eee;cursor:pointer;font-size:12px;color:#666}
  30. .ttv-chapter-item .chapter-title {font-weight:bold;margin-bottom:4px}
  31. .ttv-chapter-item .chapter-name {color:#888;padding-left:10px;border-left:2px solid #ddd;margin:4px 0}
  32. .ttv-chapter-item .chapter-stats {font-size:11px;color:#999}
  33. .ttv-chapter-item:last-child {border-bottom:none}
  34. .ttv-chapter-item.selected {background:#f0f8ff;border-left:2px solid #5bc0de}
  35. button.btn-warning {background:#f0ad4e;color:white;border:none;padding:12px;border-radius:6px;width:100%;font-size:14px}
  36. button.btn-warning:hover {background:#ec971f}
  37. button.ttv-minimize {padding:2px 8px;background:none;border:none;cursor:pointer;font-size:16px;color:#666}
  38. .chapter-character-count {text-align:right;font-size:12px;margin-top:5px;color:#666}
  39. textarea[name^="introduce"].short-chapter {border:2px solid #ff0000 !important;background-color:rgba(255,0,0,0.1) !important}
  40. @keyframes shortChapterBlink {0%{background-color:rgba(255,0,0,0.1)}50%{background-color:rgba(255,0,0,0.2)}100%{background-color:rgba(255,0,0,0.1)}}
  41. .short-chapters-warning {color:#ff0000;font-weight:bold;animation:shortChapterBlink 1s infinite}
  42. `;
  43. document.head.appendChild(style);
  44. // Constants
  45. const HEADER_SIGN = "";
  46. const FOOTER_SIGN = "";
  47. const MAX_CHAPTER_POST = 10;
  48.  
  49. // State management
  50. const dăngnhanhTTV = {
  51. STATE: {
  52. CHAP_NUMBER: 1,
  53. CHAP_STT: 1,
  54. CHAP_SERIAL: 1,
  55. CHAP_NUMBER_ORIGINAL: 1,
  56. CHAP_STT_ORIGINAL: 1,
  57. CHAP_SERIAL_ORIGINAL: 1,
  58. AUTO_POST: false,
  59. TOTAL_CHAPTERS: 0,
  60. POSTED_CHAPTERS: 0
  61. },
  62. initializeChapterValues: function() {
  63. try {
  64. const chap_number = parseInt(jQuery('#chap_number').val());
  65. let chap_stt = parseInt(jQuery('.chap_stt1').val());
  66. let chap_serial = parseInt(jQuery('.chap_serial').val());
  67.  
  68. if (parseInt(jQuery('#chap_stt').val()) > chap_stt) {
  69. chap_stt = parseInt(jQuery('#chap_stt').val());
  70. }
  71. if (parseInt(jQuery('#chap_serial').val()) > chap_serial) {
  72. chap_serial = parseInt(jQuery('#chap_serial').val());
  73. }
  74.  
  75. this.STATE.CHAP_NUMBER = this.STATE.CHAP_NUMBER_ORIGINAL = chap_number || 1;
  76. this.STATE.CHAP_STT = this.STATE.CHAP_STT_ORIGINAL = chap_stt || 1;
  77. this.STATE.CHAP_SERIAL = this.STATE.CHAP_SERIAL_ORIGINAL = chap_serial || 1;
  78. } catch (e) {
  79. console.error("Error initializing chapter values:", e);
  80. }
  81. },
  82. addNewChapter: function() {
  83. if ((this.STATE.CHAP_NUMBER + 1) <= MAX_CHAPTER_POST) {
  84. this.updateChapNumber(true);
  85. const html = createChapterHTML(this.STATE.CHAP_NUMBER);
  86. jQuery('#add-chap').before(html);
  87. setupCharacterCounter();
  88. showNotification(`Đã thêm chương mi`);
  89. } else {
  90. showNotification(`Ch có th đăng ti đa ${MAX_CHAPTER_POST} chương mt ln`, true);
  91. }
  92. },
  93. updateChapNumber: function(isAdd) {
  94. try {
  95. if (isAdd) {
  96. let maxStt = 0;
  97. let maxSerial = 0;
  98. jQuery('input[name^="chap_stt"]').each(function() {
  99. const val = parseInt(jQuery(this).val()) || 0;
  100. maxStt = Math.max(maxStt, val);
  101. });
  102. jQuery('input[name^="chap_number"]').each(function() {
  103. const val = parseInt(jQuery(this).val()) || 0;
  104. maxSerial = Math.max(maxSerial, val);
  105. });
  106. const chapStt = parseInt(jQuery('.chap_stt1').val()) || 0;
  107. const chapSerial = parseInt(jQuery('.chap_serial').val()) || 0;
  108. maxStt = Math.max(maxStt, chapStt);
  109. maxSerial = Math.max(maxSerial, chapSerial);
  110. this.STATE.CHAP_STT = maxStt + 1;
  111. this.STATE.CHAP_SERIAL = maxSerial + 1;
  112. this.STATE.CHAP_NUMBER++;
  113. } else {
  114. if (this.STATE.CHAP_NUMBER > this.STATE.CHAP_NUMBER_ORIGINAL) {
  115. this.STATE.CHAP_NUMBER--;
  116. }
  117. if (this.STATE.CHAP_STT > this.STATE.CHAP_STT_ORIGINAL) {
  118. this.STATE.CHAP_STT--;
  119. }
  120. if (this.STATE.CHAP_SERIAL > this.STATE.CHAP_SERIAL_ORIGINAL) {
  121. this.STATE.CHAP_SERIAL--;
  122. }
  123. }
  124. jQuery('#chap_number').val(this.STATE.CHAP_NUMBER);
  125. jQuery('#chap_stt').val(this.STATE.CHAP_STT);
  126. jQuery('#chap_serial').val(this.STATE.CHAP_SERIAL);
  127. jQuery('#countNumberPost').text(this.STATE.CHAP_NUMBER);
  128. } catch (e) {
  129. console.log("Lỗi: " + e);
  130. }
  131. }
  132. };
  133.  
  134. // UI Elements
  135. const notification = document.createElement('div');
  136. notification.className = 'ttv-notification';
  137. document.body.appendChild(notification);
  138.  
  139. // Hiển thị thông báo
  140. function showNotification(message, isError = false) {
  141. notification.textContent = message;
  142. notification.className = 'ttv-notification' + (isError ? ' ttv-error' : '');
  143. notification.style.display = 'block';
  144. setTimeout(() => notification.style.display = 'none', 3000);
  145. }
  146.  
  147. // Phân tích chương
  148. function parseChapters(content) {
  149. // Giữ nguyên định dạng gốc của nội dung - không sửa đổi dấu cách hoặc xuống dòng
  150. const lines = content.split('\n');
  151. const chapters = [];
  152. let currentChapter = {title: '', name: '', content: []};
  153. // Hỗ trợ nhiều định dạng tiêu đề chương phổ biến
  154. const chapterPatterns = [
  155. /^\s*Chương\s+\d+\s*:/i, // Chương X:
  156. /^\t+Chương\s+\d+\s*:/i, // [Tab]Chương X:
  157. /^\s{4,}Chương\s+\d+\s*:/i // [Spaces]Chương X:
  158. ];
  159.  
  160. // Biến để kiểm soát tiêu đề trùng lặp
  161. let chapterTitles = new Set();
  162. let chapterNumbers = new Map(); // Lưu số chương và tiêu đề đã gặp
  163. let duplicateCount = 0;
  164.  
  165. // Mảng lưu nội dung của các dòng trống gần nhất
  166. let emptyLineBuffer = [];
  167. // Biến để theo dõi tiêu đề chương trước đó
  168. let previousChapterLine = '';
  169. let previousChapterNum = 0;
  170. // Khoảng cách dòng tối thiểu giữa các tiêu đề hợp lệ
  171. const MIN_LINES_BETWEEN_CHAPTERS = 5;
  172. // Số dòng đã qua kể từ tiêu đề cuối
  173. let linesSinceLastTitle = 0;
  174. // Map để nhóm các tiêu đề theo số chương
  175. let chapterGroups = new Map();
  176.  
  177. // Hàm kiểm tra xem một dòng có phải là tiêu đề chương không
  178. function isChapterTitle(line) {
  179. return chapterPatterns.some(pattern => pattern.test(line));
  180. }
  181.  
  182. // Hàm trích xuất số chương từ tiêu đề
  183. function extractChapterNumber(line) {
  184. const match = line.match(/Chương\s+(\d+)\s*:/i);
  185. return match ? parseInt(match[1]) : 0;
  186. }
  187.  
  188. // Hàm trích xuất tên chương từ tiêu đề
  189. function extractChapterName(line) {
  190. const parts = line.split(':');
  191. if (parts.length < 2) return '';
  192. return parts.slice(1).join(':').replace(/^[.,;'"]+|[.,;'"]+$/g, '').trim();
  193. }
  194.  
  195. // Hàm lấy mã chương dựa vào tiêu đề
  196. function getChapterCode(title) {
  197. // Lấy số chương + tên chương, bỏ qua các ký tự đặc biệt
  198. const match = title.match(/[Cc]hương\s*(\d+)\s*:/);
  199. if (!match) return title.trim();
  200. const chapterNum = match[1];
  201. return `chap_${chapterNum}`;
  202. }
  203.  
  204. // Pass đầu tiên: thu thập tất cả các tiêu đề chương và nhóm theo số chương
  205. for (let i = 0; i < lines.length; i++) {
  206. const line = lines[i].trim();
  207. if (isChapterTitle(line)) {
  208. const chapterNum = extractChapterNumber(line);
  209. const chapterName = extractChapterName(line);
  210. if (!chapterGroups.has(chapterNum)) {
  211. chapterGroups.set(chapterNum, []);
  212. }
  213. chapterGroups.get(chapterNum).push({
  214. lineIndex: i,
  215. title: lines[i], // Giữ nguyên định dạng gốc
  216. name: chapterName,
  217. length: chapterName.length
  218. });
  219. }
  220. }
  221. // Phân tích để loại bỏ các tiêu đề trùng lặp trong cùng một chương
  222. for (const [chapterNum, titles] of chapterGroups.entries()) {
  223. // Nếu chỉ có một tiêu đề cho số chương này, giữ lại
  224. if (titles.length === 1) {
  225. chapterNumbers.set(chapterNum, {
  226. title: titles[0].title,
  227. name: titles[0].name,
  228. lineIndex: titles[0].lineIndex
  229. });
  230. continue;
  231. }
  232. // Nếu có nhiều tiêu đề cho cùng một số chương
  233. // Chọn tiêu đề có tên dài nhất và xác định nhất
  234. titles.sort((a, b) => b.length - a.length);
  235. // Ưu tiên tiêu đề đầu tiên nếu các tiêu đề có độ dài tương đương
  236. if (titles[0].length > 0) {
  237. chapterNumbers.set(chapterNum, {
  238. title: titles[0].title,
  239. name: titles[0].name,
  240. lineIndex: titles[0].lineIndex
  241. });
  242. // Đánh dấu các tiêu đề còn lại là trùng lặp
  243. for (let i = 1; i < titles.length; i++) {
  244. duplicateCount++;
  245. }
  246. }
  247. }
  248. // Sắp xếp các chương theo thứ tự dòng trong văn bản
  249. const sortedChapters = Array.from(chapterNumbers.entries())
  250. .sort((a, b) => a[1].lineIndex - b[1].lineIndex);
  251. // Pass thứ hai: xây dựng nội dung chương từ các tiêu đề đã được xác định
  252. for (let i = 0; i < sortedChapters.length; i++) {
  253. const [chapterNum, chapterInfo] = sortedChapters[i];
  254. const nextChapterIndex = (i < sortedChapters.length - 1) ?
  255. sortedChapters[i+1][1].lineIndex : lines.length;
  256. const chapterContent = [];
  257. // Thu thập nội dung từ sau tiêu đề đến trước tiêu đề tiếp theo
  258. for (let j = chapterInfo.lineIndex + 1; j < nextChapterIndex; j++) {
  259. // Loại bỏ các tiêu đề trùng lặp đã được xác định
  260. const line = lines[j];
  261. const trimmedLine = line.trim();
  262. if (isChapterTitle(trimmedLine)) {
  263. const lineChapterNum = extractChapterNumber(trimmedLine);
  264. // Nếu đây là tiêu đề trùng lặp của chương hiện tại, bỏ qua
  265. if (lineChapterNum === chapterNum) {
  266. continue;
  267. }
  268. }
  269. chapterContent.push(lines[j]);
  270. }
  271. chapters.push({
  272. title: chapterInfo.title,
  273. name: chapterInfo.name,
  274. content: chapterContent
  275. });
  276. }
  277.  
  278. if (chapters.length > 0) {
  279. displayChapters(chapters);
  280. if (duplicateCount > 0) {
  281. showNotification(`Đã tìm thy ${chapters.length} chương và b qua ${duplicateCount} tiêu đề lp li.`);
  282. } else {
  283. showNotification(`Đã tìm thy ${chapters.length} chương và t động đin vào form.`);
  284. }
  285.  
  286. setTimeout(() => {
  287. try {
  288. transferContent();
  289. } catch (error) {
  290. console.error('Lỗi khi tự động điền form:', error);
  291. showNotification('Có lỗi xảy ra khi tự động điền form!', true);
  292. }
  293. }, 1000);
  294. }
  295.  
  296. return chapters;
  297. }
  298.  
  299. // Thiết lập bộ đếm ký tự
  300. function setupCharacterCounter() {
  301. jQuery(document).on("input", "textarea[name^='introduce']", function() {
  302. const text = jQuery(this).val();
  303. const charCount = text.length;
  304. let charCountElement = jQuery(this).next('.chapter-character-count');
  305. if (charCountElement.length === 0) {
  306. charCountElement = jQuery('<div class="chapter-character-count"></div>');
  307. jQuery(this).after(charCountElement);
  308. }
  309. if (charCount < 3000) {
  310. jQuery(this).addClass('short-chapter');
  311. charCountElement.html(`<span class="short-chapters-warning">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  312. } else {
  313. jQuery(this).removeClass('short-chapter');
  314. if (charCount > 40000) {
  315. charCountElement.html(`<span style="color: #fbbc05;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  316. } else {
  317. charCountElement.html(`<span style="color: #34a853;">${charCount.toLocaleString()}/40.000 ký tự</span>`);
  318. }
  319. }
  320. });
  321. }
  322.  
  323. // Tạo HTML cho form chương
  324. function createChapterHTML(chapNum) {
  325. const chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
  326. const chap_vol_name = jQuery('.chap_vol_name').val() || '';
  327. return `
  328. <div data-gen="MK_GEN" id="COUNT_CHAP_${chapNum}_MK">
  329. <div class="col-xs-12 form-group"></div>
  330. <div class="form-group">
  331. <label class="col-sm-2" for="chap_stt">STT</label>
  332. <div class="col-sm-8">
  333. <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"/>
  334. </div>
  335. </div>
  336. <div class="form-group">
  337. <label class="col-sm-2" for="chap_number">Chương thứ..</label>
  338. <div class="col-sm-8">
  339. <input value="${dăngnhanhTTV.STATE.CHAP_SERIAL}" required class="form-control" name="chap_number[${chapNum}]" placeholder="Chương thứ.. (1,2,3..)" type="text"/>
  340. </div>
  341. </div>
  342. <div class="form-group">
  343. <label class="col-sm-2" for="chap_name">Quyn số</label>
  344. <div class="col-sm-8">
  345. <input class="form-control" name="vol[${chapNum}]" placeholder="Quyển số" type="number" value="${chap_vol}" required/>
  346. </div>
  347. </div>
  348. <div class="form-group">
  349. <label class="col-sm-2" for="chap_name">Tên quyn</label>
  350. <div class="col-sm-8">
  351. <input class="form-control chap_vol_name" name="vol_name[${chapNum}]" placeholder="Tên quyển" type="text" value="${chap_vol_name}" />
  352. </div>
  353. </div>
  354. <div class="form-group">
  355. <label class="col-sm-2" for="chap_name">Tên chương</label>
  356. <div class="col-sm-8">
  357. <input required class="form-control" name="chap_name[${chapNum}]" placeholder="Tên chương" type="text"/>
  358. </div>
  359. </div>
  360. <div class="form-group">
  361. <label class="col-sm-2" for="introduce">Ni dung</label>
  362. <div class="col-sm-8">
  363. <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>
  364. <div class="chapter-character-count"></div>
  365. </div>
  366. </div>
  367. <div class="form-group">
  368. <label class="col-sm-2" for="adv">Qung cáo</label>
  369. <div class="col-sm-8">
  370. <textarea maxlength="1000" class="form-control" name="adv[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea>
  371. </div>
  372. </div>
  373. </div>`;
  374. }
  375.  
  376. // Chuyển nội dung vào form
  377. async function transferContent() {
  378. try {
  379. const chapterItems = document.querySelectorAll('.ttv-chapter-item');
  380. if (!chapterItems.length) {
  381. throw new Error('Không tìm thấy chương nào để điền vào form');
  382. }
  383.  
  384. // Kiểm tra số lượng chương và giới hạn tối đa
  385. if (chapterItems.length < 10) {
  386. showNotification(`Ch có ${chapterItems.length} chương - ít hơn 10 chương. Vn tiếp tc đăng.`, false);
  387. } else if (chapterItems.length > MAX_CHAPTER_POST) {
  388. showNotification(`Đã vượt quá gii hn ${MAX_CHAPTER_POST} chương cho mt ln đăng. Ch ${MAX_CHAPTER_POST} chương đầu s được đăng.`, true);
  389. }
  390.  
  391. const form = document.querySelector('form[name="postChapForm"]');
  392. if (!form) {
  393. throw new Error('Không tìm thấy form đăng chương');
  394. }
  395.  
  396. let chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
  397. let chap_vol_name = jQuery('.chap_vol_name').val() || '';
  398.  
  399. let maxChapStt = 0;
  400. let maxChapSerial = 0;
  401.  
  402. const existingForms = form.querySelectorAll('[id^="COUNT_CHAP_"]');
  403.  
  404. existingForms.forEach(formElem => {
  405. const formIdMatch = formElem.id.match(/COUNT_CHAP_(\d+)_MK/);
  406. if (formIdMatch && formIdMatch[1]) {
  407. const formIndex = parseInt(formIdMatch[1]);
  408.  
  409. const sttInput = formElem.querySelector(`input[name="chap_stt[${formIndex}]"]`);
  410. if (sttInput && sttInput.value && !isNaN(parseInt(sttInput.value))) {
  411. const sttVal = parseInt(sttInput.value);
  412. if (sttVal > maxChapStt) {
  413. maxChapStt = sttVal;
  414. }
  415. }
  416.  
  417. const serialInput = formElem.querySelector(`input[name="chap_number[${formIndex}]"]`);
  418. if (serialInput && serialInput.value && !isNaN(parseInt(serialInput.value))) {
  419. const serialVal = parseInt(serialInput.value);
  420. if (serialVal > maxChapSerial) {
  421. maxChapSerial = serialVal;
  422. }
  423. }
  424. }
  425. });
  426.  
  427. // Đảm bảo STT và số chương tiếp theo form gốc
  428. if (maxChapStt > 0) {
  429. dăngnhanhTTV.STATE.CHAP_STT = maxChapStt;
  430. }
  431.  
  432. if (maxChapSerial > 0) {
  433. dăngnhanhTTV.STATE.CHAP_SERIAL = maxChapSerial;
  434. }
  435.  
  436. const existingFormCount = existingForms.length;
  437. // Giới hạn số lượng chương được đăng trong một lần
  438. const chaptersToProcess = Math.min(chapterItems.length, MAX_CHAPTER_POST);
  439.  
  440. for (let i = 0; i < chaptersToProcess; i++) {
  441. const formIndex = existingFormCount + i + 1;
  442.  
  443. // Tăng STT và Serial cho mỗi chương mới
  444. if (i > 0) {
  445. dăngnhanhTTV.STATE.CHAP_STT++;
  446. dăngnhanhTTV.STATE.CHAP_SERIAL++;
  447. }
  448.  
  449. const chapterHTML = createChapterHTML(formIndex);
  450. const tempDiv = document.createElement('div');
  451. tempDiv.innerHTML = chapterHTML;
  452. const newFormElement = tempDiv.firstElementChild;
  453.  
  454. if (!newFormElement) {
  455. throw new Error(`Không th to element form cho chương ${formIndex}`);
  456. }
  457.  
  458. form.appendChild(newFormElement);
  459.  
  460. const chapterItem = chapterItems[i];
  461. const titleElement = chapterItem.querySelector('.chapter-title');
  462. const nameElement = chapterItem.querySelector('.chapter-name');
  463.  
  464. if (!titleElement || !nameElement) {
  465. throw new Error(`Thiếu thông tin tiêu đề hoc tên cho chương ${formIndex}`);
  466. }
  467.  
  468. const chapterTitle = titleElement.textContent;
  469. const chapterName = nameElement.textContent.replace('Tên chương: ', '');
  470.  
  471. const formFields = {
  472. chapterName: form.querySelector(`input[name="chap_name[${formIndex}]"]`),
  473. content: form.querySelector(`textarea[name="introduce[${formIndex}]"]`),
  474. chapterNumber: form.querySelector(`input[name="chap_number[${formIndex}]"]`),
  475. chapterOrder: form.querySelector(`input[name="chap_stt[${formIndex}]"]`),
  476. volume: form.querySelector(`input[name="vol[${formIndex}]"]`),
  477. volumeName: form.querySelector(`input[name="vol_name[${formIndex}]"]`),
  478. advertisement: form.querySelector(`textarea[name="adv[${formIndex}]"]`)
  479. };
  480.  
  481. formFields.chapterName.value = chapterName;
  482. formFields.chapterNumber.value = dăngnhanhTTV.STATE.CHAP_SERIAL.toString();
  483. formFields.chapterOrder.value = dăngnhanhTTV.STATE.CHAP_STT.toString();
  484. formFields.volume.value = chap_vol;
  485. formFields.volumeName.value = chap_vol_name;
  486.  
  487. if (chapterItem._content) {
  488. // Chỉ lấy phần nội dung, không lấy phần tiêu đề
  489. // Đảm bảo giữ nguyên định dạng gốc của nội dung, không trim
  490. formFields.content.value = HEADER_SIGN + "\r\n" + chapterItem._content + "\r\n" + FOOTER_SIGN;
  491. } else {
  492. formFields.content.value = '';
  493. }
  494.  
  495. formFields.advertisement.value = '';
  496.  
  497. const inputEvent = new Event('input', { bubbles: true });
  498. formFields.content.dispatchEvent(inputEvent);
  499. }
  500.  
  501. if (chaptersToProcess < chapterItems.length) {
  502. showNotification(`Đã t động đin ${chaptersToProcess}/${chapterItems.length} chương vào form! Chương còn li s được đăng ln sau.`);
  503. } else {
  504. showNotification(`Đã t động đin ${chaptersToProcess} chương vào form!`);
  505. }
  506.  
  507. } catch (error) {
  508. console.error('Lỗi khi điền form:', error);
  509. showNotification('Có lỗi xảy ra khi điền form: ' + error.message, true);
  510. }
  511. }
  512.  
  513. // Hiển thị danh sách chương
  514. function displayChapters(chapters) {
  515. const chapterList = document.createElement('div');
  516. chapterList.className = 'ttv-chapter-list';
  517.  
  518. chapters.forEach((chapter, index) => {
  519. const chapterItem = document.createElement('div');
  520. chapterItem.className = 'ttv-chapter-item';
  521. // Chỉ lấy phần nội dung dưới tiêu đề, không bao gồm tiêu đề
  522. chapterItem._content = chapter.content.join('\n');
  523.  
  524. // Trích xuất số chương để hiển thị
  525. const chapterMatch = chapter.title.match(/Chương\s+(\d+):/);
  526. const chapterNum = chapterMatch ? chapterMatch[1] : '?';
  527.  
  528. const titleDiv = document.createElement('div');
  529. titleDiv.className = 'chapter-title';
  530. titleDiv.textContent = chapter.title;
  531.  
  532. const nameDiv = document.createElement('div');
  533. nameDiv.className = 'chapter-name';
  534. nameDiv.textContent = `Tên chương: ${chapter.name}`;
  535.  
  536. const statsDiv = document.createElement('div');
  537. statsDiv.className = 'chapter-stats';
  538. statsDiv.textContent = `Chương ${chapterNum} | ${chapter.content.length} dòng | ${chapter.content.join('\n').length} ký tự`;
  539.  
  540. chapterItem.appendChild(titleDiv);
  541. chapterItem.appendChild(nameDiv);
  542. chapterItem.appendChild(statsDiv);
  543. chapterItem.onclick = () => selectChapter(chapter, index);
  544. chapterList.appendChild(chapterItem);
  545.  
  546. if (index === 0) {
  547. chapterItem.classList.add('selected');
  548. }
  549. });
  550.  
  551. const existingList = document.querySelector('.ttv-chapter-list');
  552. if (existingList) {
  553. existingList.remove();
  554. }
  555.  
  556. const contentEditor = document.querySelector('.ttv-content-editor');
  557. contentEditor.parentNode.insertBefore(chapterList, contentEditor);
  558.  
  559. if (chapters.length > 0) {
  560. selectChapter(chapters[0], 0);
  561. }
  562. }
  563.  
  564. // Chọn chương
  565. function selectChapter(chapter, index) {
  566. const contentEditor = document.querySelector('.ttv-content-editor');
  567. // Chỉ lấy nội dung, không lấy tiêu đề để tránh lặp lại
  568. contentEditor.value = chapter.content.join('\n');
  569.  
  570. const wordCountSpan = document.querySelector('.ttv-word-count');
  571. if (wordCountSpan) {
  572. wordCountSpan.textContent = updateWordCount(contentEditor.value);
  573. }
  574.  
  575. const chapterItems = document.querySelectorAll('.ttv-chapter-item');
  576. chapterItems.forEach(item => item.classList.remove('selected'));
  577. chapterItems[index]?.classList.add('selected');
  578.  
  579. try {
  580. transferContent();
  581. } catch (error) {
  582. console.error('Lỗi khi tự động điền form:', error);
  583. showNotification('Có lỗi xảy ra khi tự động điền form!', true);
  584. }
  585. }
  586.  
  587. // Tính số từ
  588. function updateWordCount(content) {
  589. const wordCount = content.trim().split(/\s+/).length;
  590. const charCount = content.length;
  591. return `${wordCount} t | ${charCount} ký tự`;
  592. }
  593.  
  594. // Xem trước
  595. function togglePreview() {
  596. const contentEditor = document.querySelector('.ttv-content-editor');
  597. const preview = document.querySelector('.ttv-preview');
  598. const previewBtn = document.querySelector('.ttv-preview-btn');
  599.  
  600. if (contentEditor.style.display !== 'none') {
  601. contentEditor.style.display = 'none';
  602. preview.style.display = 'block';
  603. preview.innerHTML = contentEditor.value.replace(/\n/g, '<br>');
  604. previewBtn.textContent = 'Soạn thảo';
  605. } else {
  606. contentEditor.style.display = 'block';
  607. preview.style.display = 'none';
  608. previewBtn.textContent = 'Xem trước';
  609. }
  610. }
  611.  
  612. // Toàn màn hình
  613. function toggleFullscreen() {
  614. const panel = document.querySelector('.ttv-control-panel');
  615. const fullscreenBtn = document.querySelector('.ttv-fullscreen-btn');
  616. panel.classList.toggle('fullscreen');
  617. fullscreenBtn.textContent = panel.classList.contains('fullscreen') ? 'Thu nhỏ' : 'Toàn màn hình';
  618. }
  619.  
  620. // Tạo panel điều khiển
  621. function addControlPanel() {
  622. const panel = document.createElement('div');
  623. panel.className = 'ttv-control-panel';
  624.  
  625. const header = document.createElement('div');
  626. header.className = 'ttv-header';
  627. header.innerHTML = `
  628. <div>Son Tho Ni Dung</div>
  629. <div class="ttv-toolbar">
  630. <button class="ttv-preview-btn" onclick="togglePreview()">Xem trước</button>
  631. <button class="ttv-fullscreen-btn" onclick="toggleFullscreen()">Toàn màn hình</button>
  632. <button class="ttv-minimize">−</button>
  633. </div>
  634. `;
  635.  
  636. const buttonGroup = document.createElement('div');
  637. buttonGroup.className = 'ttv-button-group';
  638.  
  639. const contentEditorLabel = document.createElement('div');
  640. contentEditorLabel.className = 'ttv-heading';
  641. contentEditorLabel.innerHTML = `
  642. Ni dung chương:
  643. <span class="ttv-word-count">0 t | 0 ký tự</span>
  644. `;
  645.  
  646. const contentEditor = document.createElement('textarea');
  647. contentEditor.className = 'ttv-content-editor';
  648. contentEditor.placeholder = 'Nhập hoặc dán nội dung chương vào đây...';
  649.  
  650. const preview = document.createElement('div');
  651. preview.className = 'ttv-preview';
  652.  
  653. contentEditor.oninput = () => {
  654. const wordCountSpan = document.querySelector('.ttv-word-count');
  655. if (wordCountSpan) {
  656. wordCountSpan.textContent = updateWordCount(contentEditor.value);
  657. }
  658. };
  659.  
  660. contentEditor.onpaste = (e) => {
  661. setTimeout(() => {
  662. const content = contentEditor.value;
  663. const chapters = parseChapters(content);
  664. if (chapters.length > 0) {
  665. displayChapters(chapters);
  666. setTimeout(() => {
  667. try {
  668. transferContent();
  669. } catch (error) {
  670. console.error('Lỗi khi tự động điền form:', error);
  671. showNotification('Có lỗi xảy ra khi tự động điền form!', true);
  672. }
  673. }, 1000);
  674. }
  675. }, 0);
  676. };
  677.  
  678. const buttonsContainer = document.createElement('div');
  679. buttonsContainer.style.display = 'flex';
  680. buttonsContainer.style.gap = '10px';
  681. buttonsContainer.style.marginTop = '10px';
  682.  
  683. const transferBtn = document.createElement('button');
  684. transferBtn.type = 'button';
  685. transferBtn.className = 'btn btn-warning';
  686. transferBtn.style.flex = '1';
  687. transferBtn.innerHTML = '<span>Chuyển nội dung sang form</span>';
  688. transferBtn.onclick = transferContent;
  689.  
  690. const addChapterBtn = document.createElement('button');
  691. addChapterBtn.type = 'button';
  692. addChapterBtn.className = 'btn btn-primary';
  693. addChapterBtn.style.flex = '1';
  694. addChapterBtn.innerHTML = '<span>Thêm chương mới</span>';
  695. addChapterBtn.onclick = () => dăngnhanhTTV.addNewChapter();
  696.  
  697. buttonsContainer.appendChild(transferBtn);
  698. buttonsContainer.appendChild(addChapterBtn);
  699.  
  700. panel.appendChild(header);
  701. buttonGroup.appendChild(contentEditorLabel);
  702. buttonGroup.appendChild(contentEditor);
  703. buttonGroup.appendChild(preview);
  704. buttonGroup.appendChild(buttonsContainer);
  705. panel.appendChild(buttonGroup);
  706.  
  707. document.body.appendChild(panel);
  708.  
  709. const minimizeBtn = panel.querySelector('.ttv-minimize');
  710. minimizeBtn.onclick = () => {
  711. panel.classList.toggle('minimized');
  712. minimizeBtn.innerHTML = panel.classList.contains('minimized') ? '+' : '−';
  713. };
  714.  
  715. window.togglePreview = togglePreview;
  716. window.toggleFullscreen = toggleFullscreen;
  717. }
  718.  
  719. window.addEventListener('load', function() {
  720. dăngnhanhTTV.initializeChapterValues();
  721. addControlPanel();
  722. setupCharacterCounter();
  723. });
  724. })();