TTV Auto Upload

Tự động điền form đăng chương trên tangthuvien.net với tính năng nâng cao

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

  1. // ==UserScript==
  2. // @name TTV Auto Upload
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.8
  5. // @description Tự động điền form đăng chương trên tangthuvien.net với tính năng nâng cao
  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 cho thông báo và control panel
  15. const style = document.createElement('style');
  16. style.textContent = `
  17. .ttv-notification {
  18. position: fixed;
  19. top: 20px;
  20. right: 20px;
  21. padding: 10px 20px;
  22. background: #4CAF50;
  23. color: white;
  24. border-radius: 4px;
  25. z-index: 9999;
  26. display: none;
  27. }
  28. .ttv-error {
  29. background: #f44336;
  30. }
  31. .ttv-control-panel {
  32. position: fixed;
  33. top: 50px;
  34. right: 20px;
  35. background: white;
  36. padding: 25px;
  37. border-radius: 12px;
  38. box-shadow: 0 4px 20px rgba(0,0,0,0.15);
  39. z-index: 9998;
  40. width: 500px;
  41. margin-bottom: 20px;
  42. transition: all 0.3s ease;
  43. }
  44. .ttv-control-panel.minimized {
  45. width: auto;
  46. height: auto;
  47. padding: 10px;
  48. opacity: 0.8;
  49. transform: translateX(calc(100% - 40px));
  50. transition: all 0.3s ease;
  51. }
  52. .ttv-control-panel.minimized:hover {
  53. opacity: 1;
  54. transform: translateX(0);
  55. }
  56. .ttv-control-panel.minimized .ttv-button-group,
  57. .ttv-control-panel.minimized .ttv-header {
  58. display: none;
  59. }
  60. .ttv-button-group {
  61. display: flex;
  62. flex-direction: column;
  63. gap: 15px;
  64. }
  65. .ttv-content-editor {
  66. width: 100%;
  67. height: 100px;
  68. margin: 6px 0;
  69. padding: 8px;
  70. border: 1px solid #ddd;
  71. border-radius: 6px;
  72. font-size: 13px;
  73. line-height: 1.4;
  74. resize: vertical;
  75. transition: all 0.2s ease;
  76. }
  77. .ttv-content-editor:focus {
  78. border-color: #5bc0de;
  79. outline: none;
  80. box-shadow: 0 0 8px rgba(91,192,222,0.2);
  81. }
  82. .ttv-preview {
  83. display: none;
  84. width: 100%;
  85. height: 100px;
  86. margin: 6px 0;
  87. padding: 8px;
  88. border: 1px solid #ddd;
  89. border-radius: 6px;
  90. font-size: 13px;
  91. line-height: 1.4;
  92. overflow-y: auto;
  93. background: #f9f9f9;
  94. transition: all 0.2s ease;
  95. }
  96. .ttv-heading {
  97. font-size: 15px;
  98. color: #555;
  99. margin: 12px 0;
  100. display: flex;
  101. justify-content: space-between;
  102. align-items: center;
  103. }
  104. .ttv-word-count {
  105. font-size: 12px;
  106. color: #888;
  107. padding: 3px 8px;
  108. background: #f5f5f5;
  109. border-radius: 4px;
  110. transition: all 0.2s ease;
  111. }
  112. .ttv-chapter-list {
  113. width: 100%;
  114. margin: 10px 0;
  115. max-height: 200px;
  116. overflow-y: auto;
  117. border: 1px solid #eee;
  118. border-radius: 6px;
  119. padding: 8px;
  120. }
  121. .ttv-chapter-item {
  122. padding: 8px;
  123. border-bottom: 1px solid #eee;
  124. cursor: pointer;
  125. transition: all 0.2s ease;
  126. line-height: 1.4;
  127. font-size: 12px;
  128. color: #666;
  129. }
  130. .ttv-chapter-item .chapter-title {
  131. font-weight: bold;
  132. margin-bottom: 4px;
  133. }
  134. .ttv-chapter-item .chapter-name {
  135. color: #888;
  136. padding-left: 10px;
  137. border-left: 2px solid #ddd;
  138. margin: 4px 0;
  139. }
  140. .ttv-chapter-item .chapter-stats {
  141. font-size: 11px;
  142. color: #999;
  143. }
  144. .ttv-chapter-item:last-child {
  145. border-bottom: none;
  146. }
  147. .ttv-chapter-item.selected {
  148. background: #f0f8ff;
  149. border-left: 2px solid #5bc0de;
  150. }
  151. .ttv-control-panel.fullscreen {
  152. position: fixed;
  153. top: 0;
  154. right: 0;
  155. bottom: 0;
  156. left: 0;
  157. width: 100%;
  158. height: 100%;
  159. border-radius: 0;
  160. z-index: 9999;
  161. padding: 15px;
  162. display: flex;
  163. flex-direction: column;
  164. }
  165. .ttv-control-panel.fullscreen .ttv-content-editor,
  166. .ttv-control-panel.fullscreen .ttv-preview {
  167. height: calc(100vh - 250px);
  168. margin: 15px 0;
  169. font-size: 16px;
  170. }
  171. .ttv-header {
  172. margin-bottom: 20px;
  173. padding-bottom: 15px;
  174. border-bottom: 2px solid #eee;
  175. font-weight: bold;
  176. font-size: 16px;
  177. color: #444;
  178. display: flex;
  179. justify-content: space-between;
  180. align-items: center;
  181. }
  182. button.btn-warning {
  183. background: #f0ad4e;
  184. color: white;
  185. border: none;
  186. padding: 12px;
  187. border-radius: 6px;
  188. width: 100%;
  189. font-size: 14px;
  190. transition: all 0.2s ease;
  191. }
  192. button.btn-warning:hover {
  193. background: #ec971f;
  194. transform: translateY(-1px);
  195. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  196. }
  197. button.ttv-minimize {
  198. padding: 2px 8px;
  199. background: none;
  200. border: none;
  201. cursor: pointer;
  202. font-size: 16px;
  203. color: #666;
  204. transition: all 0.2s ease;
  205. }
  206. button.ttv-minimize:hover {
  207. color: #333;
  208. }
  209. .chapter-form {
  210. margin-bottom: 20px;
  211. padding: 15px;
  212. border: 1px solid #ddd;
  213. border-radius: 8px;
  214. }
  215. .chapter-form h3 {
  216. margin: 0 0 10px;
  217. font-size: 16px;
  218. color: #333;
  219. }
  220. .form-row {
  221. display: flex;
  222. gap: 10px;
  223. margin-bottom: 10px;
  224. }
  225. .form-row input {
  226. flex: 1;
  227. padding: 8px;
  228. border: 1px solid #ddd;
  229. border-radius: 4px;
  230. }
  231. .chapter-form textarea {
  232. width: 100%;
  233. min-height: 100px;
  234. padding: 8px;
  235. border: 1px solid #ddd;
  236. border-radius: 4px;
  237. resize: vertical;
  238. }
  239. .ttv-forms-container {
  240. max-height: 600px;
  241. overflow-y: auto;
  242. padding: 10px;
  243. margin: 10px 0;
  244. border: 1px solid #eee;
  245. border-radius: 8px;
  246. }
  247. `;
  248. document.head.appendChild(style);
  249.  
  250. // Tạo div thông báo
  251. const notification = document.createElement('div');
  252. notification.className = 'ttv-notification';
  253. document.body.appendChild(notification);
  254.  
  255. // Hiển thị thông báo
  256. function showNotification(message, isError = false) {
  257. notification.textContent = message;
  258. notification.className = 'ttv-notification' + (isError ? ' ttv-error' : '');
  259. notification.style.display = 'block';
  260. setTimeout(() => {
  261. notification.style.display = 'none';
  262. }, 3000);
  263. }
  264.  
  265. // Parse chapters function update
  266. function parseChapters(content) {
  267. const lines = content.split('\n');
  268. const chapters = [];
  269. let currentChapter = {
  270. title: '',
  271. name: '',
  272. content: []
  273. };
  274.  
  275. const chapterPattern = /^\s*Chương\s+\d+:/;
  276. let previousChapterTitle = '';
  277.  
  278. for (let i = 0; i < lines.length; i++) {
  279. const line = lines[i].trim();
  280.  
  281. if (chapterPattern.test(line)) {
  282. // Kiểm tra xem có phải là tiêu đề trùng lặp không
  283. if (line !== previousChapterTitle) {
  284. // Log để debug
  285. console.log(`Tìm thy chương mi: ${line}`);
  286.  
  287. // Nếu đã có chương trước đó, lưu lại
  288. if (currentChapter.title && currentChapter.content.length > 0) {
  289. chapters.push({...currentChapter});
  290. console.log(`Đã lưu chương: ${currentChapter.title}\nTên: ${currentChapter.name}\nS dòng: ${currentChapter.content.length}`);
  291. currentChapter = {title: '', name: '', content: []};
  292. }
  293.  
  294. // Lưu tiêu đề chương hiện tại
  295. currentChapter.title = line;
  296. // Lấy tên chương sau dấu :
  297. let name = line.split(':')[1]?.trim() || '';
  298. // Xóa dấu ., , hoặc ; ở đầu và cuối tên chương
  299. name = name.replace(/^[.,;'"]+|[.,;'"]+$/g, '').trim();
  300. currentChapter.name = name;
  301. previousChapterTitle = line;
  302.  
  303. console.log(`Đang x lý chương mi:\nTiêu đề: ${line}\nTên chương: ${name}`);
  304. } else {
  305. console.log(`B qua tiêu đề trùng lp: ${line}`);
  306. }
  307. } else {
  308. // Lưu tất cả nội dung không phải tiêu đề chương
  309. if (line) { // Chỉ thêm dòng không trống
  310. if (currentChapter.title) {
  311. currentChapter.content.push(line);
  312. }
  313. }
  314. }
  315. }
  316.  
  317. // Thêm chương cuối cùng nếu có
  318. if (currentChapter.title && currentChapter.content.length > 0) {
  319. chapters.push({...currentChapter});
  320. }
  321.  
  322. // Tự động tạo form và điền nội dung
  323. if (chapters.length > 0) {
  324. // Tạo số form bằng với số chương tìm thấy
  325. createChapterForms(chapters.length);
  326. // Hiển thị danh sách chương
  327. displayChapters(chapters);
  328. // Tự động điền form
  329. setTimeout(() => {
  330. try {
  331. transferContent();
  332. } catch (error) {
  333. console.error('Lỗi khi tự động điền form:', error);
  334. showNotification('Có lỗi xảy ra khi tự động điền form!', true);
  335. }
  336. }, 1000);
  337. }
  338.  
  339. console.log(`Tng s chương tìm thy: ${chapters.length}`);
  340. return chapters;
  341. }
  342.  
  343. // Chọn chương để hiển thị
  344. function selectChapter(chapter, index) {
  345. const contentEditor = document.querySelector('.ttv-content-editor');
  346. contentEditor.value = chapter.title + '\n' + chapter.content.join('\n');
  347.  
  348. // Cập nhật số từ
  349. const wordCountSpan = document.querySelector('.ttv-word-count');
  350. if (wordCountSpan) {
  351. wordCountSpan.textContent = updateWordCount(contentEditor.value);
  352. }
  353.  
  354. // Highlight selected chapter
  355. const chapterItems = document.querySelectorAll('.ttv-chapter-item');
  356. chapterItems.forEach(item => item.classList.remove('selected'));
  357. chapterItems[index]?.classList.add('selected');
  358.  
  359. // Tự động điền form khi chọn chương
  360. try {
  361. transferContent();
  362. } catch (error) {
  363. console.error('Lỗi khi tự động điền form:', error);
  364. showNotification('Có lỗi xảy ra khi tự động điền form!', true);
  365. }
  366. }
  367.  
  368. // Hiển thị danh sách chương
  369. function displayChapters(chapters) {
  370. const chapterList = document.createElement('div');
  371. chapterList.className = 'ttv-chapter-list';
  372.  
  373. chapters.forEach((chapter, index) => {
  374. const chapterItem = document.createElement('div');
  375. chapterItem.className = 'ttv-chapter-item';
  376.  
  377. // Store chapter content
  378. chapterItem._content = chapter.title + '\n' + chapter.content.join('\n');
  379.  
  380. // Tạo các phần tử con với định dạng riêng
  381. const titleDiv = document.createElement('div');
  382. titleDiv.className = 'chapter-title';
  383. titleDiv.textContent = chapter.title;
  384.  
  385. const nameDiv = document.createElement('div');
  386. nameDiv.className = 'chapter-name';
  387. nameDiv.textContent = `Tên chương: ${chapter.name}`;
  388.  
  389. const statsDiv = document.createElement('div');
  390. statsDiv.className = 'chapter-stats';
  391. statsDiv.textContent = `${chapter.content.length} dòng`;
  392.  
  393. // Thêm các phần tử vào item
  394. chapterItem.appendChild(titleDiv);
  395. chapterItem.appendChild(nameDiv);
  396. chapterItem.appendChild(statsDiv);
  397.  
  398. chapterItem.onclick = () => selectChapter(chapter, index);
  399. chapterList.appendChild(chapterItem);
  400.  
  401. // Select first chapter by default
  402. if (index === 0) {
  403. chapterItem.classList.add('selected');
  404. }
  405. });
  406.  
  407. const existingList = document.querySelector('.ttv-chapter-list');
  408. if (existingList) {
  409. existingList.remove();
  410. }
  411.  
  412. const contentEditor = document.querySelector('.ttv-content-editor');
  413. contentEditor.parentNode.insertBefore(chapterList, contentEditor);
  414.  
  415. // Hiển thị thông báo về số chương tìm thấy
  416. showNotification(`Đã tìm thy ${chapters.length} chương và t động đin vào form.${chapters.length >= 10 ? ' (Giới hạn 10 chương đầu tiên)' : ''}`);
  417.  
  418. // Tự động chọn chương đầu tiên
  419. if (chapters.length > 0) {
  420. selectChapter(chapters[0], 0);
  421. }
  422. }
  423.  
  424. // Đếm số từ và ký tự
  425. function updateWordCount(content) {
  426. const wordCount = content.trim().split(/\s+/).length;
  427. const charCount = content.length;
  428. return `${wordCount} t | ${charCount} ký tự`;
  429. }
  430.  
  431. // Chuyển nội dung từ khung soạn thảo sang form
  432. function transferContent() {
  433. try {
  434. // Get all chapters
  435. const chapterItems = document.querySelectorAll('.ttv-chapter-item');
  436.  
  437. // Fill each chapter's content into corresponding form
  438. chapterItems.forEach((chapterItem, index) => {
  439. const formIndex = index + 1;
  440. const formFields = {
  441. chapterName: document.querySelector(`input[name="chap_name[${formIndex}]"]`),
  442. content: document.querySelector(`textarea[name="introduce[${formIndex}]"]`),
  443. chapterNumber: document.querySelector(`input[name="chap_number[${formIndex}]"]`),
  444. chapterOrder: document.querySelector(`input[name="chap_stt[${formIndex}]"]`),
  445. volume: document.querySelector(`input[name="vol[${formIndex}]"]`)
  446. };
  447.  
  448. // Verify all form fields exist
  449. Object.entries(formFields).forEach(([fieldName, element]) => {
  450. if (!element) {
  451. throw new Error(`Không tìm thy trường "${fieldName}" cho chương ${formIndex}`);
  452. }
  453. });
  454.  
  455. // Get chapter info
  456. const chapterTitle = chapterItem.querySelector('.chapter-title').textContent;
  457. const chapterName = chapterItem.querySelector('.chapter-name').textContent.replace('Tên chương: ', '');
  458. const chapterNumber = chapterTitle.match(/Chương\s+(\d+)/)?.[1] || formIndex.toString();
  459.  
  460. // Fill form fields
  461. formFields.chapterName.value = chapterName;
  462. formFields.chapterNumber.value = chapterNumber;
  463. formFields.chapterOrder.value = chapterNumber;
  464. formFields.volume.value = '1';
  465.  
  466. // Get content for this chapter
  467. if (index === 0) { // If it's the first/selected chapter
  468. formFields.content.value = document.querySelector('.ttv-content-editor').value;
  469. } else {
  470. formFields.content.value = chapterItem._content || ''; // Use stored content
  471. }
  472.  
  473. console.log(`Đã đin form cho chương ${formIndex}:`, {
  474. title: chapterTitle,
  475. name: chapterName,
  476. number: chapterNumber,
  477. contentLength: formFields.content.value.length
  478. });
  479. });
  480.  
  481. showNotification(`Đã t động đin ${chapterItems.length} chương vào form!`);
  482. console.log(`Đã đin ${chapterItems.length} chương vào form`);
  483.  
  484. } catch (error) {
  485. console.error('Lỗi khi điền form:', error);
  486. showNotification('Có lỗi xảy ra khi điền form: ' + error.message, true);
  487. throw error;
  488. }
  489. }
  490.  
  491. // Chuyển đổi giữa chế độ soạn thảo và xem trước
  492. function togglePreview() {
  493. const contentEditor = document.querySelector('.ttv-content-editor');
  494. const preview = document.querySelector('.ttv-preview');
  495. const previewBtn = document.querySelector('.ttv-preview-btn');
  496.  
  497. if (contentEditor.style.display !== 'none') {
  498. contentEditor.style.display = 'none';
  499. preview.style.display = 'block';
  500. preview.innerHTML = contentEditor.value.replace(/\n/g, '<br>');
  501. previewBtn.textContent = 'Soạn thảo';
  502. } else {
  503. contentEditor.style.display = 'block';
  504. preview.style.display = 'none';
  505. previewBtn.textContent = 'Xem trước';
  506. }
  507. }
  508.  
  509. // Chuyển đổi chế độ toàn màn hình
  510. function toggleFullscreen() {
  511. const panel = document.querySelector('.ttv-control-panel');
  512. const fullscreenBtn = document.querySelector('.ttv-fullscreen-btn');
  513.  
  514. panel.classList.toggle('fullscreen');
  515. fullscreenBtn.textContent = panel.classList.contains('fullscreen') ? 'Thu nhỏ' : 'Toàn màn hình';
  516. }
  517.  
  518. // Function to create form fields for multiple chapters using website's template
  519. function createChapterForms(totalChapters) {
  520. try {
  521. // Always create 10 forms
  522. const TOTAL_FORMS = 10;
  523.  
  524. // Find the original form container
  525. const originalForm = document.querySelector('form');
  526. if (!originalForm) {
  527. throw new Error('Không tìm thấy form gốc trên trang web');
  528. }
  529.  
  530. // Get original chapter form template
  531. const originalChapterDiv = document.querySelector('div[id^="div-chapter"]');
  532. if (!originalChapterDiv) {
  533. throw new Error('Không tìm thấy mẫu form chương gốc');
  534. }
  535.  
  536. // Debug log template structure
  537. console.log('Template form structure:', {
  538. id: originalChapterDiv.id,
  539. fields: Array.from(originalChapterDiv.querySelectorAll('input, textarea')).map(el => ({
  540. name: el.getAttribute('name'),
  541. type: el.tagName.toLowerCase()
  542. }))
  543. });
  544.  
  545. // Clear any existing additional chapter divs
  546. const existingChapters = document.querySelectorAll('div[id^="div-chapter"]');
  547. console.log(`Xóa ${existingChapters.length - 1} form cũ`);
  548. existingChapters.forEach((div, index) => {
  549. if (index > 0) { // Keep the first one as template
  550. div.remove();
  551. }
  552. });
  553.  
  554. // Create new chapter divs based on template
  555. for (let i = 1; i <= TOTAL_FORMS; i++) {
  556. const newChapterDiv = originalChapterDiv.cloneNode(true);
  557. const newId = `div-chapter-${i}`;
  558. newChapterDiv.id = newId;
  559.  
  560. // Update all input fields with proper indices
  561. newChapterDiv.querySelectorAll('input, textarea').forEach(input => {
  562. const name = input.getAttribute('name');
  563. if (name) {
  564. const oldIndex = name.match(/\[(\d+)\]/)?.[1];
  565. const newName = name.replace(/\[\d+\]/, `[${i}]`);
  566. console.log(`Cp nht trường ${name} -> ${newName} trong form ${i}`);
  567.  
  568. // Update the index in the name attribute
  569. input.setAttribute('name', newName);
  570. // Clear any existing values
  571. input.value = '';
  572. }
  573. });
  574.  
  575. // Update any labels or headers
  576. newChapterDiv.querySelectorAll('label, .chapter-header').forEach(el => {
  577. const oldText = el.textContent;
  578. const newText = oldText.replace(/\d+/, i);
  579. console.log(`Cp nht label ${oldText} -> ${newText} trong form ${i}`);
  580. el.textContent = newText;
  581. });
  582.  
  583. // Append the new chapter div to the form
  584. originalForm.appendChild(newChapterDiv);
  585. console.log(`Đã to form chương ${i} vi ID ${newId}`);
  586. }
  587.  
  588. // After creating forms, make sure they're visible
  589. const allChapterDivs = document.querySelectorAll('div[id^="div-chapter"]');
  590. allChapterDivs.forEach(div => {
  591. div.style.display = 'block';
  592. });
  593.  
  594. showNotification(`Đã to ${TOTAL_FORMS} form chương, s đin ${totalChapters} chương vào form`);
  595.  
  596. } catch (error) {
  597. console.error('Lỗi khi tạo form chương:', error);
  598. showNotification('Có lỗi xảy ra khi tạo form chương: ' + error.message, true);
  599. throw error;
  600. }
  601. }
  602.  
  603.  
  604.  
  605. // Thêm panel điều khiển
  606. function addControlPanel() {
  607. // Tạo panel
  608. const panel = document.createElement('div');
  609. panel.className = 'ttv-control-panel';
  610.  
  611. // Thêm header
  612. const header = document.createElement('div');
  613. header.className = 'ttv-header';
  614. header.innerHTML = `
  615. <div>Son Tho Ni Dung</div>
  616. <div class="ttv-toolbar">
  617. <button class="ttv-preview-btn" onclick="togglePreview()">Xem trước</button>
  618. <button class="ttv-fullscreen-btn" onclick="toggleFullscreen()">Toàn màn hình</button>
  619. <button class="ttv-minimize">−</button>
  620. </div>
  621. `;
  622.  
  623. const buttonGroup = document.createElement('div');
  624. buttonGroup.className = 'ttv-button-group';
  625.  
  626. // Khung soạn thảo nội dung
  627. const contentEditorLabel = document.createElement('div');
  628. contentEditorLabel.className = 'ttv-heading';
  629. contentEditorLabel.innerHTML = `
  630. Ni dung chương:
  631. <span class="ttv-word-count">0 t | 0 ký tự</span>
  632. `;
  633.  
  634. const contentEditor = document.createElement('textarea');
  635. contentEditor.className = 'ttv-content-editor';
  636. contentEditor.placeholder = 'Nhập hoặc dán nội dung chương vào đây...';
  637.  
  638. // Khung xem trước
  639. const preview = document.createElement('div');
  640. preview.className = 'ttv-preview';
  641.  
  642. // Cập nhật số từ khi nhập nội dung
  643. contentEditor.oninput = () => {
  644. const wordCountSpan = document.querySelector('.ttv-word-count');
  645. if (wordCountSpan) {
  646. wordCountSpan.textContent = updateWordCount(contentEditor.value);
  647. }
  648. };
  649.  
  650. // Xử lý khi paste nội dung
  651. contentEditor.onpaste = (e) => {
  652. // Cho phép paste hoàn tất
  653. setTimeout(() => {
  654. const content = contentEditor.value;
  655. const chapters = parseChapters(content);
  656. if (chapters.length > 0) {
  657. displayChapters(chapters);
  658. // Tự động điền form sau khi hiển thị danh sách chương
  659. setTimeout(() => {
  660. try {
  661. transferContent();
  662. } catch (error) {
  663. console.error('Lỗi khi tự động điền form:', error);
  664. showNotification('Có lỗi xảy ra khi tự động điền form!', true);
  665. }
  666. }, 1000); // Đợi 1s để đảm bảo UI đã được cập nhật hoàn toàn
  667. }
  668. }, 0);
  669. };
  670.  
  671. // Nút chuyển nội dung
  672. const transferBtn = document.createElement('button');
  673. transferBtn.type = 'button';
  674. transferBtn.className = 'btn btn-warning';
  675. transferBtn.innerHTML = '<span>Chuyển nội dung sang form</span>';
  676. transferBtn.onclick = transferContent;
  677.  
  678. // Thêm các phần tử vào panel
  679. panel.appendChild(header);
  680. buttonGroup.appendChild(contentEditorLabel);
  681. buttonGroup.appendChild(contentEditor);
  682. buttonGroup.appendChild(preview);
  683. buttonGroup.appendChild(transferBtn);
  684. panel.appendChild(buttonGroup);
  685.  
  686. document.body.appendChild(panel);
  687.  
  688. // Thêm xử lý sự kiện cho các nút trong toolbar
  689. const minimizeBtn = panel.querySelector('.ttv-minimize');
  690. minimizeBtn.onclick = () => {
  691. panel.classList.toggle('minimized');
  692. minimizeBtn.innerHTML = panel.classList.contains('minimized') ? '+' : '−';
  693. };
  694.  
  695. window.togglePreview = togglePreview;
  696. window.toggleFullscreen = toggleFullscreen;
  697. }
  698.  
  699. // Thêm control panel khi trang đã load
  700. window.addEventListener('load', function() {
  701. addControlPanel();
  702. });
  703. })();