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.1
  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. `;
  39. document.head.appendChild(style);
  40.  
  41. // State management
  42. const dăngnhanhTTV = {
  43. STATE: {
  44. CHAP_NUMBER: 1,
  45. CHAP_STT: 1,
  46. CHAP_SERIAL: 1,
  47. CHAP_NUMBER_ORIGINAL: 1,
  48. CHAP_STT_ORIGINAL: 1,
  49. CHAP_SERIAL_ORIGINAL: 1
  50. },
  51. initializeChapterValues: function() {
  52. try {
  53. const chap_number = parseInt(jQuery('#chap_number').val());
  54. let chap_stt = parseInt(jQuery('.chap_stt1').val());
  55. let chap_serial = parseInt(jQuery('.chap_serial').val());
  56.  
  57. if (parseInt(jQuery('#chap_stt').val()) > chap_stt) {
  58. chap_stt = parseInt(jQuery('#chap_stt').val());
  59. }
  60. if (parseInt(jQuery('#chap_serial').val()) > chap_serial) {
  61. chap_serial = parseInt(jQuery('#chap_serial').val());
  62. }
  63.  
  64. this.STATE.CHAP_NUMBER = this.STATE.CHAP_NUMBER_ORIGINAL = chap_number || 1;
  65. this.STATE.CHAP_STT = this.STATE.CHAP_STT_ORIGINAL = chap_stt || 1;
  66. this.STATE.CHAP_SERIAL = this.STATE.CHAP_SERIAL_ORIGINAL = chap_serial || 1;
  67. } catch (e) {
  68. console.error("Error initializing chapter values:", e);
  69. }
  70. }
  71. };
  72.  
  73. // UI Elements
  74. const notification = document.createElement('div');
  75. notification.className = 'ttv-notification';
  76. document.body.appendChild(notification);
  77.  
  78. // Hiển thị thông báo
  79. function showNotification(message, isError = false) {
  80. notification.textContent = message;
  81. notification.className = 'ttv-notification' + (isError ? ' ttv-error' : '');
  82. notification.style.display = 'block';
  83. setTimeout(() => notification.style.display = 'none', 3000);
  84. }
  85.  
  86. // Phân tích chương
  87. function parseChapters(content) {
  88. // 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
  89. const lines = content.split('\n');
  90. const chapters = [];
  91. let currentChapter = {title: '', name: '', content: []};
  92. // Hỗ trợ nhiều định dạng tiêu đề chương phổ biến
  93. const chapterPatterns = [
  94. /^\s*Chương\s+\d+\s*:/i, // Chương X:
  95. /^\t+Chương\s+\d+\s*:/i, // [Tab]Chương X:
  96. /^\s{4,}Chương\s+\d+\s*:/i // [Spaces]Chương X:
  97. ];
  98.  
  99. // Biến để kiểm soát tiêu đề trùng lặp
  100. let chapterTitles = new Set();
  101. let chapterNumbers = new Map(); // Lưu số chương và tiêu đề đã gặp
  102. let duplicateCount = 0;
  103.  
  104. // Mảng lưu nội dung của các dòng trống gần nhất
  105. let emptyLineBuffer = [];
  106. // Biến để theo dõi tiêu đề chương trước đó
  107. let previousChapterLine = '';
  108. let previousChapterNum = 0;
  109. // Khoảng cách dòng tối thiểu giữa các tiêu đề hợp lệ
  110. const MIN_LINES_BETWEEN_CHAPTERS = 5;
  111. // Số dòng đã qua kể từ tiêu đề cuối
  112. let linesSinceLastTitle = 0;
  113. // Map để nhóm các tiêu đề theo số chương
  114. let chapterGroups = new Map();
  115.  
  116. // Hàm kiểm tra xem một dòng có phải là tiêu đề chương không
  117. function isChapterTitle(line) {
  118. return chapterPatterns.some(pattern => pattern.test(line));
  119. }
  120.  
  121. // Hàm trích xuất số chương từ tiêu đề
  122. function extractChapterNumber(line) {
  123. const match = line.match(/Chương\s+(\d+)\s*:/i);
  124. return match ? parseInt(match[1]) : 0;
  125. }
  126.  
  127. // Hàm trích xuất tên chương từ tiêu đề
  128. function extractChapterName(line) {
  129. const parts = line.split(':');
  130. if (parts.length < 2) return '';
  131. return parts.slice(1).join(':').replace(/^[.,;'"]+|[.,;'"]+$/g, '').trim();
  132. }
  133.  
  134. // Pass đầu tiên: thu thập tất cả các tiêu đề chương và nhóm theo số chương
  135. for (let i = 0; i < lines.length; i++) {
  136. const line = lines[i].trim();
  137. if (isChapterTitle(line)) {
  138. const chapterNum = extractChapterNumber(line);
  139. const chapterName = extractChapterName(line);
  140. if (!chapterGroups.has(chapterNum)) {
  141. chapterGroups.set(chapterNum, []);
  142. }
  143. chapterGroups.get(chapterNum).push({
  144. lineIndex: i,
  145. title: lines[i], // Giữ nguyên định dạng gốc
  146. name: chapterName,
  147. length: chapterName.length
  148. });
  149. }
  150. }
  151. // Phân tích để loại bỏ các tiêu đề trùng lặp trong cùng một chương
  152. for (const [chapterNum, titles] of chapterGroups.entries()) {
  153. // Nếu chỉ có một tiêu đề cho số chương này, giữ lại
  154. if (titles.length === 1) {
  155. chapterNumbers.set(chapterNum, {
  156. title: titles[0].title,
  157. name: titles[0].name,
  158. lineIndex: titles[0].lineIndex
  159. });
  160. continue;
  161. }
  162. // Nếu có nhiều tiêu đề cho cùng một số chương
  163. // Chọn tiêu đề có tên dài nhất và xác định nhất
  164. titles.sort((a, b) => b.length - a.length);
  165. // Ưu tiên tiêu đề đầu tiên nếu các tiêu đề có độ dài tương đương
  166. if (titles[0].length > 0) {
  167. chapterNumbers.set(chapterNum, {
  168. title: titles[0].title,
  169. name: titles[0].name,
  170. lineIndex: titles[0].lineIndex
  171. });
  172. // Đánh dấu các tiêu đề còn lại là trùng lặp
  173. for (let i = 1; i < titles.length; i++) {
  174. duplicateCount++;
  175. }
  176. }
  177. }
  178. // Sắp xếp các chương theo thứ tự dòng trong văn bản
  179. const sortedChapters = Array.from(chapterNumbers.entries())
  180. .sort((a, b) => a[1].lineIndex - b[1].lineIndex);
  181. // Pass thứ hai: xây dựng nội dung chương từ các tiêu đề đã được xác định
  182. for (let i = 0; i < sortedChapters.length; i++) {
  183. const [chapterNum, chapterInfo] = sortedChapters[i];
  184. const nextChapterIndex = (i < sortedChapters.length - 1) ?
  185. sortedChapters[i+1][1].lineIndex : lines.length;
  186. const chapterContent = [];
  187. // Thu thập nội dung từ sau tiêu đề đến trước tiêu đề tiếp theo
  188. for (let j = chapterInfo.lineIndex + 1; j < nextChapterIndex; j++) {
  189. // Loại bỏ các tiêu đề trùng lặp đã được xác định
  190. const line = lines[j];
  191. const trimmedLine = line.trim();
  192. if (isChapterTitle(trimmedLine)) {
  193. const lineChapterNum = extractChapterNumber(trimmedLine);
  194. // Nếu đây là tiêu đề trùng lặp của chương hiện tại, bỏ qua
  195. if (lineChapterNum === chapterNum) {
  196. continue;
  197. }
  198. }
  199. chapterContent.push(lines[j]);
  200. }
  201. chapters.push({
  202. title: chapterInfo.title,
  203. name: chapterInfo.name,
  204. content: chapterContent
  205. });
  206. }
  207.  
  208. if (chapters.length > 0) {
  209. displayChapters(chapters);
  210. if (duplicateCount > 0) {
  211. showNotification(`Đã tìm thy ${chapters.length} chương và b qua ${duplicateCount} tiêu đề lp li.`);
  212. } else {
  213. showNotification(`Đã tìm thy ${chapters.length} chương và t động đin vào form.`);
  214. }
  215.  
  216. setTimeout(() => {
  217. try {
  218. transferContent();
  219. } catch (error) {
  220. console.error('Lỗi khi tự động điền form:', error);
  221. showNotification('Có lỗi xảy ra khi tự động điền form!', true);
  222. }
  223. }, 1000);
  224. }
  225.  
  226. return chapters;
  227. }
  228.  
  229. // Tạo HTML cho form chương
  230. function createChapterHTML(chapNum) {
  231. const chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
  232. const chap_vol_name = jQuery('.chap_vol_name').val() || '';
  233. return `
  234. <div data-gen="MK_GEN" id="COUNT_CHAP_${chapNum}_MK">
  235. <div class="col-xs-12 form-group"></div>
  236. <div class="form-group">
  237. <label class="col-sm-2" for="chap_stt">STT</label>
  238. <div class="col-sm-8">
  239. <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"/>
  240. </div>
  241. </div>
  242. <div class="form-group">
  243. <label class="col-sm-2" for="chap_number">Chương thứ..</label>
  244. <div class="col-sm-8">
  245. <input value="${dăngnhanhTTV.STATE.CHAP_SERIAL}" required class="form-control" name="chap_number[${chapNum}]" placeholder="Chương thứ.. (1,2,3..)" type="text"/>
  246. </div>
  247. </div>
  248. <div class="form-group">
  249. <label class="col-sm-2" for="chap_name">Quyn số</label>
  250. <div class="col-sm-8">
  251. <input class="form-control" name="vol[${chapNum}]" placeholder="Quyển số" type="number" value="${chap_vol}" required/>
  252. </div>
  253. </div>
  254. <div class="form-group">
  255. <label class="col-sm-2" for="chap_name">Tên quyn</label>
  256. <div class="col-sm-8">
  257. <input class="form-control chap_vol_name" name="vol_name[${chapNum}]" placeholder="Tên quyển" type="text" value="${chap_vol_name}" />
  258. </div>
  259. </div>
  260. <div class="form-group">
  261. <label class="col-sm-2" for="chap_name">Tên chương</label>
  262. <div class="col-sm-8">
  263. <input required class="form-control" name="chap_name[${chapNum}]" placeholder="Tên chương" type="text"/>
  264. </div>
  265. </div>
  266. <div class="form-group">
  267. <label class="col-sm-2" for="introduce">Ni dung</label>
  268. <div class="col-sm-8">
  269. <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>
  270. <div class="chapter-character-count"></div>
  271. </div>
  272. </div>
  273. <div class="form-group">
  274. <label class="col-sm-2" for="adv">Qung cáo</label>
  275. <div class="col-sm-8">
  276. <textarea maxlength="1000" class="form-control" name="adv[${chapNum}]" placeholder="Quảng cáo" type="text"></textarea>
  277. </div>
  278. </div>
  279. </div>`;
  280. }
  281.  
  282. // Chuyển nội dung vào form
  283. async function transferContent() {
  284. try {
  285. const chapterItems = document.querySelectorAll('.ttv-chapter-item');
  286. if (!chapterItems.length) {
  287. throw new Error('Không tìm thấy chương nào để điền vào form');
  288. }
  289.  
  290. // Kiểm tra số lượng chương và giới hạn tối đa
  291. if (chapterItems.length < 10) {
  292. showNotification(`Ch có ${chapterItems.length} chương - ít hơn 10 chương. Vn tiếp tc đăng.`, false);
  293. } else if (chapterItems.length > MAX_CHAPTER_POST) {
  294. 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);
  295. }
  296.  
  297. const form = document.querySelector('form[name="postChapForm"]');
  298. if (!form) {
  299. throw new Error('Không tìm thấy form đăng chương');
  300. }
  301.  
  302. let chap_vol = parseInt(jQuery('.chap_vol').val()) || 1;
  303. let chap_vol_name = jQuery('.chap_vol_name').val() || '';
  304.  
  305. let maxChapStt = 0;
  306. let maxChapSerial = 0;
  307.  
  308. const existingForms = form.querySelectorAll('[id^="COUNT_CHAP_"]');
  309.  
  310. existingForms.forEach(formElem => {
  311. const formIdMatch = formElem.id.match(/COUNT_CHAP_(\d+)_MK/);
  312. if (formIdMatch && formIdMatch[1]) {
  313. const formIndex = parseInt(formIdMatch[1]);
  314.  
  315. const sttInput = formElem.querySelector(`input[name="chap_stt[${formIndex}]"]`);
  316. if (sttInput && sttInput.value && !isNaN(parseInt(sttInput.value))) {
  317. const sttVal = parseInt(sttInput.value);
  318. if (sttVal > maxChapStt) {
  319. maxChapStt = sttVal;
  320. }
  321. }
  322.  
  323. const serialInput = formElem.querySelector(`input[name="chap_number[${formIndex}]"]`);
  324. if (serialInput && serialInput.value && !isNaN(parseInt(serialInput.value))) {
  325. const serialVal = parseInt(serialInput.value);
  326. if (serialVal > maxChapSerial) {
  327. maxChapSerial = serialVal;
  328. }
  329. }
  330. }
  331. });
  332.  
  333. // Đảm bảo STT và số chương tiếp theo form gốc
  334. if (maxChapStt > 0) {
  335. dăngnhanhTTV.STATE.CHAP_STT = maxChapStt;
  336. }
  337.  
  338. if (maxChapSerial > 0) {
  339. dăngnhanhTTV.STATE.CHAP_SERIAL = maxChapSerial;
  340. }
  341.  
  342. const existingFormCount = existingForms.length;
  343. // Giới hạn số lượng chương được đăng trong một lần
  344. const chaptersToProcess = Math.min(chapterItems.length, MAX_CHAPTER_POST);
  345.  
  346. for (let i = 0; i < chaptersToProcess; i++) {
  347. const formIndex = existingFormCount + i + 1;
  348.  
  349. // Tăng STT và Serial cho mỗi chương mới
  350. if (i > 0) {
  351. dăngnhanhTTV.STATE.CHAP_STT++;
  352. dăngnhanhTTV.STATE.CHAP_SERIAL++;
  353. }
  354.  
  355. const chapterHTML = createChapterHTML(formIndex);
  356. const tempDiv = document.createElement('div');
  357. tempDiv.innerHTML = chapterHTML;
  358. const newFormElement = tempDiv.firstElementChild;
  359.  
  360. if (!newFormElement) {
  361. throw new Error(`Không th to element form cho chương ${formIndex}`);
  362. }
  363.  
  364. form.appendChild(newFormElement);
  365.  
  366. const chapterItem = chapterItems[i];
  367. const titleElement = chapterItem.querySelector('.chapter-title');
  368. const nameElement = chapterItem.querySelector('.chapter-name');
  369.  
  370. if (!titleElement || !nameElement) {
  371. throw new Error(`Thiếu thông tin tiêu đề hoc tên cho chương ${formIndex}`);
  372. }
  373.  
  374. const chapterTitle = titleElement.textContent;
  375. const chapterName = nameElement.textContent.replace('Tên chương: ', '');
  376.  
  377. const formFields = {
  378. chapterName: form.querySelector(`input[name="chap_name[${formIndex}]"]`),
  379. content: form.querySelector(`textarea[name="introduce[${formIndex}]"]`),
  380. chapterNumber: form.querySelector(`input[name="chap_number[${formIndex}]"]`),
  381. chapterOrder: form.querySelector(`input[name="chap_stt[${formIndex}]"]`),
  382. volume: form.querySelector(`input[name="vol[${formIndex}]"]`),
  383. volumeName: form.querySelector(`input[name="vol_name[${formIndex}]"]`),
  384. advertisement: form.querySelector(`textarea[name="adv[${formIndex}]"]`)
  385. };
  386.  
  387. formFields.chapterName.value = chapterName;
  388. formFields.chapterNumber.value = dăngnhanhTTV.STATE.CHAP_SERIAL.toString();
  389. formFields.chapterOrder.value = dăngnhanhTTV.STATE.CHAP_STT.toString();
  390. formFields.volume.value = chap_vol;
  391. formFields.volumeName.value = chap_vol_name;
  392.  
  393. if (chapterItem._content) {
  394. // Chỉ lấy phần nội dung, không lấy phần tiêu đề
  395. // Đảm bảo giữ nguyên định dạng gốc của nội dung, không trim
  396. formFields.content.value = chapterItem._content;
  397. } else {
  398. formFields.content.value = '';
  399. }
  400.  
  401. formFields.advertisement.value = '';
  402.  
  403. const inputEvent = new Event('input', { bubbles: true });
  404. formFields.content.dispatchEvent(inputEvent);
  405. }
  406.  
  407. if (chaptersToProcess < chapterItems.length) {
  408. showNotification(`Đã t động đin ${chaptersToProcess}/${chapterItems.length} chương vào form! Chương còn li s được đăng ln sau.`);
  409. } else {
  410. showNotification(`Đã t động đin ${chaptersToProcess} chương vào form!`);
  411. }
  412.  
  413. } catch (error) {
  414. console.error('Lỗi khi điền form:', error);
  415. showNotification('Có lỗi xảy ra khi điền form: ' + error.message, true);
  416. }
  417. }
  418.  
  419. // Hiển thị danh sách chương
  420. function displayChapters(chapters) {
  421. const chapterList = document.createElement('div');
  422. chapterList.className = 'ttv-chapter-list';
  423.  
  424. chapters.forEach((chapter, index) => {
  425. const chapterItem = document.createElement('div');
  426. chapterItem.className = 'ttv-chapter-item';
  427. // Chỉ lấy phần nội dung dưới tiêu đề, không bao gồm tiêu đề
  428. chapterItem._content = chapter.content.join('\n');
  429.  
  430. // Trích xuất số chương để hiển thị
  431. const chapterMatch = chapter.title.match(/Chương\s+(\d+):/);
  432. const chapterNum = chapterMatch ? chapterMatch[1] : '?';
  433.  
  434. const titleDiv = document.createElement('div');
  435. titleDiv.className = 'chapter-title';
  436. titleDiv.textContent = chapter.title;
  437.  
  438. const nameDiv = document.createElement('div');
  439. nameDiv.className = 'chapter-name';
  440. nameDiv.textContent = `Tên chương: ${chapter.name}`;
  441.  
  442. const statsDiv = document.createElement('div');
  443. statsDiv.className = 'chapter-stats';
  444. statsDiv.textContent = `Chương ${chapterNum} | ${chapter.content.length} dòng`;
  445.  
  446. chapterItem.appendChild(titleDiv);
  447. chapterItem.appendChild(nameDiv);
  448. chapterItem.appendChild(statsDiv);
  449. chapterItem.onclick = () => selectChapter(chapter, index);
  450. chapterList.appendChild(chapterItem);
  451.  
  452. if (index === 0) {
  453. chapterItem.classList.add('selected');
  454. }
  455. });
  456.  
  457. const existingList = document.querySelector('.ttv-chapter-list');
  458. if (existingList) {
  459. existingList.remove();
  460. }
  461.  
  462. const contentEditor = document.querySelector('.ttv-content-editor');
  463. contentEditor.parentNode.insertBefore(chapterList, contentEditor);
  464.  
  465. if (chapters.length > 0) {
  466. selectChapter(chapters[0], 0);
  467. }
  468. }
  469.  
  470. // Chọn chương
  471. function selectChapter(chapter, index) {
  472. const contentEditor = document.querySelector('.ttv-content-editor');
  473. // Chỉ lấy nội dung, không lấy tiêu đề để tránh lặp lại
  474. contentEditor.value = chapter.content.join('\n');
  475.  
  476. const wordCountSpan = document.querySelector('.ttv-word-count');
  477. if (wordCountSpan) {
  478. wordCountSpan.textContent = updateWordCount(contentEditor.value);
  479. }
  480.  
  481. const chapterItems = document.querySelectorAll('.ttv-chapter-item');
  482. chapterItems.forEach(item => item.classList.remove('selected'));
  483. chapterItems[index]?.classList.add('selected');
  484.  
  485. try {
  486. transferContent();
  487. } catch (error) {
  488. console.error('Lỗi khi tự động điền form:', error);
  489. showNotification('Có lỗi xảy ra khi tự động điền form!', true);
  490. }
  491. }
  492.  
  493. // Tính số từ
  494. function updateWordCount(content) {
  495. const wordCount = content.trim().split(/\s+/).length;
  496. const charCount = content.length;
  497. return `${wordCount} t | ${charCount} ký tự`;
  498. }
  499.  
  500. // Xem trước
  501. function togglePreview() {
  502. const contentEditor = document.querySelector('.ttv-content-editor');
  503. const preview = document.querySelector('.ttv-preview');
  504. const previewBtn = document.querySelector('.ttv-preview-btn');
  505.  
  506. if (contentEditor.style.display !== 'none') {
  507. contentEditor.style.display = 'none';
  508. preview.style.display = 'block';
  509. preview.innerHTML = contentEditor.value.replace(/\n/g, '<br>');
  510. previewBtn.textContent = 'Soạn thảo';
  511. } else {
  512. contentEditor.style.display = 'block';
  513. preview.style.display = 'none';
  514. previewBtn.textContent = 'Xem trước';
  515. }
  516. }
  517.  
  518. // Toàn màn hình
  519. function toggleFullscreen() {
  520. const panel = document.querySelector('.ttv-control-panel');
  521. const fullscreenBtn = document.querySelector('.ttv-fullscreen-btn');
  522. panel.classList.toggle('fullscreen');
  523. fullscreenBtn.textContent = panel.classList.contains('fullscreen') ? 'Thu nhỏ' : 'Toàn màn hình';
  524. }
  525.  
  526. // Tạo panel điều khiển
  527. function addControlPanel() {
  528. const panel = document.createElement('div');
  529. panel.className = 'ttv-control-panel';
  530.  
  531. const header = document.createElement('div');
  532. header.className = 'ttv-header';
  533. header.innerHTML = `
  534. <div>Son Tho Ni Dung</div>
  535. <div class="ttv-toolbar">
  536. <button class="ttv-preview-btn" onclick="togglePreview()">Xem trước</button>
  537. <button class="ttv-fullscreen-btn" onclick="toggleFullscreen()">Toàn màn hình</button>
  538. <button class="ttv-minimize">−</button>
  539. </div>
  540. `;
  541.  
  542. const buttonGroup = document.createElement('div');
  543. buttonGroup.className = 'ttv-button-group';
  544.  
  545. const contentEditorLabel = document.createElement('div');
  546. contentEditorLabel.className = 'ttv-heading';
  547. contentEditorLabel.innerHTML = `
  548. Ni dung chương:
  549. <span class="ttv-word-count">0 t | 0 ký tự</span>
  550. `;
  551.  
  552. const contentEditor = document.createElement('textarea');
  553. contentEditor.className = 'ttv-content-editor';
  554. contentEditor.placeholder = 'Nhập hoặc dán nội dung chương vào đây...';
  555.  
  556. const preview = document.createElement('div');
  557. preview.className = 'ttv-preview';
  558.  
  559. contentEditor.oninput = () => {
  560. const wordCountSpan = document.querySelector('.ttv-word-count');
  561. if (wordCountSpan) {
  562. wordCountSpan.textContent = updateWordCount(contentEditor.value);
  563. }
  564. };
  565.  
  566. contentEditor.onpaste = (e) => {
  567. setTimeout(() => {
  568. const content = contentEditor.value;
  569. const chapters = parseChapters(content);
  570. if (chapters.length > 0) {
  571. displayChapters(chapters);
  572. setTimeout(() => {
  573. try {
  574. transferContent();
  575. } catch (error) {
  576. console.error('Lỗi khi tự động điền form:', error);
  577. showNotification('Có lỗi xảy ra khi tự động điền form!', true);
  578. }
  579. }, 1000);
  580. }
  581. }, 0);
  582. };
  583.  
  584. const transferBtn = document.createElement('button');
  585. transferBtn.type = 'button';
  586. transferBtn.className = 'btn btn-warning';
  587. transferBtn.innerHTML = '<span>Chuyển nội dung sang form</span>';
  588. transferBtn.onclick = transferContent;
  589.  
  590. panel.appendChild(header);
  591. buttonGroup.appendChild(contentEditorLabel);
  592. buttonGroup.appendChild(contentEditor);
  593. buttonGroup.appendChild(preview);
  594. buttonGroup.appendChild(transferBtn);
  595. panel.appendChild(buttonGroup);
  596.  
  597. document.body.appendChild(panel);
  598.  
  599. const minimizeBtn = panel.querySelector('.ttv-minimize');
  600. minimizeBtn.onclick = () => {
  601. panel.classList.toggle('minimized');
  602. minimizeBtn.innerHTML = panel.classList.contains('minimized') ? '+' : '−';
  603. };
  604.  
  605. window.togglePreview = togglePreview;
  606. window.toggleFullscreen = toggleFullscreen;
  607. }
  608.  
  609. window.addEventListener('load', function() {
  610. dăngnhanhTTV.initializeChapterValues();
  611. addControlPanel();
  612. });
  613. })();