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