TTV Auto Upload

Công cụ đăng chương đơn giản cho Tàng Thư Viện

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

  1. // ==UserScript==
  2. // @name TTV Auto Upload
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Công cụ đăng chương đơn giản cho Tàng Thư Viện
  6. // @author HA
  7. // @match https://tangthuvien.net/dang-chuong/story/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. const HEADER_SIGN = "";
  15. const FOOTER_SIGN = "";
  16. const MAX_CHAPTER_POST = 10;
  17.  
  18. const style = document.createElement('style');
  19. style.textContent = `
  20. #ttv-panel {
  21. position: fixed;
  22. top: 50px;
  23. right: 20px;
  24. background: white;
  25. padding: 20px;
  26. border-radius: 12px;
  27. box-shadow: 0 4px 20px rgba(0,0,0,0.15);
  28. width: 400px;
  29. z-index: 9998;
  30. max-height: 90vh;
  31. overflow-y: auto;
  32. }
  33. #ttv-chapters {
  34. width: 100%;
  35. margin-bottom: 15px;
  36. border: 1px solid #eee;
  37. border-radius: 8px;
  38. max-height: 300px;
  39. overflow-y: auto;
  40. background: #fafafa;
  41. }
  42. #ttv-content {
  43. width: 100%;
  44. height: 150px;
  45. margin-bottom: 15px;
  46. padding: 12px;
  47. border: 1px solid #ddd;
  48. border-radius: 8px;
  49. font-size: 14px;
  50. font-family: monospace;
  51. transition: border-color 0.2s;
  52. resize: vertical;
  53. }
  54. #ttv-content:focus {
  55. border-color: #4CAF50;
  56. outline: none;
  57. }
  58. .chapter-item {
  59. padding: 15px;
  60. border-bottom: 1px solid #eee;
  61. background: white;
  62. transition: all 0.2s;
  63. }
  64. .chapter-item:hover {
  65. background: #f5f5f5;
  66. }
  67. .chapter-item:last-child {
  68. border-bottom: none;
  69. }
  70. .chapter-title {
  71. font-weight: 600;
  72. margin-bottom: 8px;
  73. color: #333;
  74. font-size: 14px;
  75. }
  76. .chapter-stats {
  77. font-size: 12px;
  78. color: #666;
  79. display: flex;
  80. gap: 10px;
  81. align-items: center;
  82. }
  83. .chapter-warning {
  84. color: #ff0000;
  85. font-weight: 500;
  86. padding: 2px 6px;
  87. background: rgba(255,0,0,0.1);
  88. border-radius: 4px;
  89. }
  90. .chapter-long {
  91. color: #ff9800;
  92. font-weight: 500;
  93. padding: 2px 6px;
  94. background: rgba(255,152,0,0.1);
  95. border-radius: 4px;
  96. }
  97. .btn-group {
  98. display: flex;
  99. gap: 10px;
  100. margin-top: 15px;
  101. }
  102. #ttv-panel button {
  103. flex: 1;
  104. padding: 12px 15px;
  105. border: none;
  106. border-radius: 6px;
  107. cursor: pointer;
  108. font-weight: 600;
  109. font-size: 14px;
  110. transition: all 0.2s;
  111. position: relative;
  112. }
  113. #ttv-panel button:hover {
  114. opacity: 0.9;
  115. }
  116. #ttv-panel button:disabled {
  117. opacity: 0.6;
  118. cursor: not-allowed;
  119. }
  120. #ttv-panel button.processing:after {
  121. content: "";
  122. position: absolute;
  123. width: 20px;
  124. height: 20px;
  125. top: calc(50% - 10px);
  126. right: 10px;
  127. border: 2px solid rgba(255,255,255,0.3);
  128. border-top: 2px solid white;
  129. border-radius: 50%;
  130. animation: spin 1s linear infinite;
  131. }
  132. @keyframes spin {
  133. 0% { transform: rotate(0deg); }
  134. 100% { transform: rotate(360deg); }
  135. }
  136. .btn-auto {
  137. background: #4CAF50;
  138. color: white;
  139. }
  140. .btn-manual {
  141. background: #2196F3;
  142. color: white;
  143. }
  144. .loading-overlay {
  145. position: fixed;
  146. top: 0;
  147. left: 0;
  148. width: 100%;
  149. height: 100%;
  150. background: rgba(0, 0, 0, 0.5);
  151. display: flex;
  152. justify-content: center;
  153. align-items: center;
  154. z-index: 9999;
  155. }
  156. .loading-content {
  157. background: white;
  158. padding: 20px;
  159. border-radius: 10px;
  160. text-align: center;
  161. }
  162. .loading-spinner {
  163. width: 40px;
  164. height: 40px;
  165. margin: 0 auto 10px;
  166. border: 4px solid #f3f3f3;
  167. border-top: 4px solid #3498db;
  168. border-radius: 50%;
  169. animation: spin 1s linear infinite;
  170. }
  171. .chapter-character-count {
  172. text-align: right;
  173. font-size: 12px;
  174. margin-top: 5px;
  175. color: #666;
  176. }
  177. textarea[name^="introduce"].short-chapter {
  178. border: 2px solid #ff0000 !important;
  179. background-color: rgba(255,0,0,0.1) !important;
  180. animation: shortChapterBlink 1s infinite;
  181. }
  182. @keyframes shortChapterBlink {
  183. 0% { background-color: rgba(255,0,0,0.1); }
  184. 50% { background-color: rgba(255,0,0,0.2); }
  185. 100% { background-color: rgba(255,0,0,0.1); }
  186. }
  187. `;
  188. document.head.appendChild(style);
  189.  
  190. const TTVManager = {
  191. STATE: {
  192. chapterNumber: 1,
  193. chapterSTT: 1,
  194. chapterSerial: 1,
  195. isAuto: false,
  196. isProcessing: false
  197. },
  198.  
  199. init: function() {
  200. console.log('[TTV-DEBUG] Initializing script...');
  201. this.initializeChapterValues();
  202. this.createInterface();
  203. this.setupEventListeners();
  204. this.setupCharacterCounter();
  205. console.log('[TTV-DEBUG] Script initialized successfully');
  206. this.showNotification('Công cụ đã sẵn sàng', 'success');
  207. },
  208.  
  209. createInterface: function() {
  210. console.log('[TTV-DEBUG] Creating interface');
  211. const panel = document.createElement('div');
  212. panel.id = 'ttv-panel';
  213. panel.innerHTML = `
  214. <h3 style="margin: 0 0 15px; color: #333; text-align: center;">📝 ĐĂNG CHƯƠNG</h3>
  215. <div id="ttv-chapters"></div>
  216. <textarea id="ttv-content" placeholder="Dán nội dung vào đây để tự động tách chương..."></textarea>
  217. <div class="btn-group">
  218. <button class="btn-auto" id="ttv-auto">🔄 Đăng t động</button>
  219. <button class="btn-manual" id="ttv-manual">📝 Đăng th công</button>
  220. </div>
  221. <div id="ttv-notification" style="margin-top: 10px;"></div>
  222. `;
  223. document.body.appendChild(panel);
  224. },
  225.  
  226. initializeChapterValues: function() {
  227. try {
  228. const chap_number = parseInt(jQuery('#chap_number').val()) || 1;
  229. let chap_stt = parseInt(jQuery('.chap_stt1').val()) || 1;
  230. let chap_serial = parseInt(jQuery('.chap_serial').val()) || 1;
  231.  
  232. if (parseInt(jQuery('#chap_stt').val()) > chap_stt) {
  233. chap_stt = parseInt(jQuery('#chap_stt').val());
  234. }
  235. if (parseInt(jQuery('#chap_serial').val()) > chap_serial) {
  236. chap_serial = parseInt(jQuery('#chap_serial').val());
  237. }
  238.  
  239. this.STATE.chapterNumber = chap_number;
  240. this.STATE.chapterSTT = chap_stt;
  241. this.STATE.chapterSerial = chap_serial;
  242.  
  243. console.log('[TTV-DEBUG] Chapter values initialized:', this.STATE);
  244. } catch (e) {
  245. console.error('[TTV-ERROR] Error initializing chapter values:', e);
  246. }
  247. },
  248.  
  249. setupEventListeners: function() {
  250. const content = document.getElementById('ttv-content');
  251. const autoBtn = document.getElementById('ttv-auto');
  252. const manualBtn = document.getElementById('ttv-manual');
  253.  
  254. // Xử lý paste vào textarea
  255. content.addEventListener('paste', (e) => {
  256. e.preventDefault();
  257. const text = e.clipboardData.getData('text');
  258. content.value = text;
  259. this.processContent(text);
  260. });
  261.  
  262. // Xử lý input trực tiếp
  263. content.addEventListener('input', () => {
  264. const text = content.value;
  265. if (text) {
  266. this.processContent(text);
  267. }
  268. });
  269.  
  270. // Nút đăng tự động
  271. autoBtn.addEventListener('click', () => {
  272. if (this.STATE.isProcessing) return;
  273.  
  274. console.log('[TTV-DEBUG] Auto button clicked');
  275. this.STATE.isAuto = true;
  276. this.STATE.isProcessing = true;
  277. autoBtn.disabled = true;
  278. manualBtn.disabled = true;
  279. autoBtn.classList.add('processing');
  280.  
  281. const text = content.value;
  282. if (!text) {
  283. this.showNotification('Vui lòng nhập hoặc dán nội dung trước', 'error');
  284. this.STATE.isProcessing = false;
  285. autoBtn.disabled = false;
  286. manualBtn.disabled = false;
  287. autoBtn.classList.remove('processing');
  288. return;
  289. }
  290.  
  291. this.processContent(text);
  292. });
  293.  
  294. // Nút đăng thủ công
  295. manualBtn.addEventListener('click', () => {
  296. if (this.STATE.isProcessing) return;
  297.  
  298. console.log('[TTV-DEBUG] Manual button clicked');
  299. this.STATE.isAuto = false;
  300. this.STATE.isProcessing = true;
  301. manualBtn.disabled = true;
  302. autoBtn.disabled = true;
  303. manualBtn.classList.add('processing');
  304.  
  305. const text = content.value;
  306. if (!text) {
  307. this.showNotification('Vui lòng nhập hoặc dán nội dung trước', 'error');
  308. this.STATE.isProcessing = false;
  309. manualBtn.disabled = false;
  310. autoBtn.disabled = false;
  311. manualBtn.classList.remove('processing');
  312. return;
  313. }
  314.  
  315. this.processContent(text);
  316. });
  317. },
  318.  
  319. setupCharacterCounter: function() {
  320. document.addEventListener('input', (e) => {
  321. if (e.target.matches('textarea[name^="introduce"]')) {
  322. const text = e.target.value;
  323. const charCount = text.length;
  324. let counter = e.target.nextElementSibling;
  325.  
  326. if (!counter || !counter.classList.contains('chapter-character-count')) {
  327. counter = document.createElement('div');
  328. counter.className = 'chapter-character-count';
  329. e.target.parentNode.insertBefore(counter, e.target.nextSibling);
  330. }
  331.  
  332. if (charCount < 3000) {
  333. e.target.classList.add('short-chapter');
  334. counter.innerHTML = `<span style="color: #ff0000;">${charCount.toLocaleString()}/40.000 ký tự</span>`;
  335. } else {
  336. e.target.classList.remove('short-chapter');
  337. counter.innerHTML = `<span style="color: ${charCount > 40000 ? '#ff9800' : '#4caf50'}">${charCount.toLocaleString()}/40.000 ký tự</span>`;
  338. }
  339. }
  340. });
  341. },
  342.  
  343. updateChapterList: function(chapters) {
  344. console.log('[TTV-DEBUG] Updating chapter list');
  345. const chapterList = document.getElementById('ttv-chapters');
  346. let html = '';
  347.  
  348. chapters.forEach((chapter, index) => {
  349. const lines = chapter.split('\n');
  350. const title = lines.shift().trim();
  351. const content = lines.join('\n');
  352. const charCount = content.length;
  353.  
  354. html += `
  355. <div class="chapter-item">
  356. <div class="chapter-title">${title}</div>
  357. <div class="chapter-stats">
  358. <span>S ký tự: ${charCount.toLocaleString()}</span>
  359. ${charCount < 3000 ? '<span class="chapter-warning">⚠️ Thiếu</span>' : ''}
  360. ${charCount > 40000 ? '<span class="chapter-long">⚠️ Dài</span>' : ''}
  361. </div>
  362. </div>
  363. `;
  364. });
  365.  
  366. chapterList.innerHTML = html;
  367. },
  368.  
  369. processContent: function(text) {
  370. console.log('[TTV-DEBUG] Processing content, auto mode:', this.STATE.isAuto);
  371. if (!text) {
  372. this.showNotification('Không có nội dung để xử lý', 'error');
  373. return;
  374. }
  375.  
  376. const chapters = this.splitChapters(text);
  377. if (chapters.length === 0) {
  378. this.showNotification('Không tìm thấy chương nào', 'error');
  379. return;
  380. }
  381.  
  382. console.log(`[TTV-DEBUG] Found ${chapters.length} chapters`);
  383.  
  384. // Cập nhật danh sách chương
  385. this.updateChapterList(chapters);
  386.  
  387. // Lấy 10 chương đầu
  388. const chaptersToFill = chapters.slice(0, MAX_CHAPTER_POST);
  389. const remainingChapters = chapters.slice(MAX_CHAPTER_POST);
  390.  
  391. // Điền form
  392. this.fillChaptersToForm(chaptersToFill);
  393.  
  394. // Copy các chương còn lại vào clipboard
  395. if (remainingChapters.length > 0) {
  396. this.copyRemainingChapters(remainingChapters);
  397. }
  398.  
  399. // Nếu đang ở chế độ tự động và có đủ 10 chương, tự động đăng
  400. if (this.STATE.isAuto && chaptersToFill.length === 10) {
  401. this.showNotification('Sẽ tự động đăng sau 2 giây...', 'info');
  402. setTimeout(() => {
  403. this.submitChapters();
  404. }, 2000);
  405. } else if (this.STATE.isAuto) {
  406. this.showNotification(`Cn đủ 10 chương để t động đăng (hin có ${chaptersToFill.length} chương)`, 'warning');
  407. }
  408. },
  409.  
  410. splitChapters: function(text) {
  411. console.log('[TTV-DEBUG] Splitting chapters');
  412. const chapters = [];
  413. const lines = text.split('\n');
  414. let currentChapter = [];
  415. let lastTitle = null;
  416.  
  417. // Hỗ trợ nhiều định dạng tiêu đề chương phổ biến
  418. const chapterPatterns = [
  419. /^\s*Chương\s+\d+\s*:/i, // Chương X:
  420. /^\t+Chương\s+\d+\s*:/i, // [Tab]Chương X:
  421. /^\s{4,}Chương\s+\d+\s*:/i // [Spaces]Chương X:
  422. ];
  423.  
  424. // Map để nhóm các tiêu đề theo số chương
  425. const chapterGroups = new Map();
  426.  
  427. // Hàm kiểm tra xem một dòng có phải là tiêu đề chương
  428. function isChapterTitle(line) {
  429. return chapterPatterns.some(pattern => pattern.test(line));
  430. }
  431.  
  432. // Hàm trích xuất số chương từ tiêu đề
  433. function extractChapterNumber(line) {
  434. const match = line.match(/Chương\s+(\d+)\s*:/i);
  435. return match ? parseInt(match[1]) : 0;
  436. }
  437.  
  438. // Pass đầu tiên: thu thập tất cả các tiêu đề chương và nhóm theo số chương
  439. for (let i = 0; i < lines.length; i++) {
  440. const line = lines[i].trim();
  441. if (isChapterTitle(line)) {
  442. const chapterNum = extractChapterNumber(line);
  443. if (!chapterGroups.has(chapterNum)) {
  444. chapterGroups.set(chapterNum, []);
  445. }
  446. chapterGroups.get(chapterNum).push({
  447. lineIndex: i,
  448. title: lines[i], // Giữ nguyên định dạng gốc
  449. content: []
  450. });
  451. }
  452. }
  453.  
  454. // Pass thứ hai: xây dựng nội dung chương từ các tiêu đề đã được xác định
  455. const sortedChapters = Array.from(chapterGroups.entries());
  456. sortedChapters.sort((a, b) => a[0] - b[0]); // Sắp xếp theo số chương
  457.  
  458. for (let i = 0; i < sortedChapters.length; i++) {
  459. const [_chapterNum, chapterInfos] = sortedChapters[i];
  460. // Chọn tiêu đề đầu tiên nếu có nhiều tiêu đề trùng
  461. const chapterInfo = chapterInfos[0];
  462.  
  463. const nextChapterIndex = (i < sortedChapters.length - 1)
  464. ? sortedChapters[i+1][1][0].lineIndex
  465. : lines.length;
  466.  
  467. // Thu thập nội dung từ sau tiêu đề đến trước tiêu đề tiếp theo
  468. const chapterContent = [chapterInfo.title];
  469. for (let j = chapterInfo.lineIndex + 1; j < nextChapterIndex; j++) {
  470. const line = lines[j];
  471. // Bỏ qua các tiêu đề trùng lặp
  472. if (!isChapterTitle(line.trim())) {
  473. chapterContent.push(line);
  474. }
  475. }
  476.  
  477. chapters.push(chapterContent.join('\n'));
  478. }
  479.  
  480. console.log(`[TTV-DEBUG] Found ${chapters.length} chapters`);
  481. return chapters;
  482. },
  483.  
  484. fillChaptersToForm: function(chapters) {
  485. console.log('[TTV-DEBUG] Filling form with chapters');
  486. this.showLoading('Đang điền nội dung vào form...');
  487.  
  488. try {
  489. // Thêm form cho đủ số chương
  490. while (document.querySelectorAll('input[name^="chap_name"]').length < chapters.length) {
  491. this.addNewChapterForm();
  492. }
  493.  
  494. const titles = document.querySelectorAll('input[name^="chap_name"]');
  495. const contents = document.querySelectorAll('textarea[name^="introduce"]');
  496. const advs = document.querySelectorAll('textarea[name^="adv"]');
  497.  
  498. chapters.forEach((chapter, index) => {
  499. if (index >= titles.length) return;
  500.  
  501. const lines = chapter.split('\n');
  502. const title = lines.shift().trim();
  503. let chapterName = title.includes(':') ? title.split(':')[1].trim() : title;
  504. chapterName = chapterName || 'Vô đề';
  505.  
  506. titles[index].value = chapterName;
  507. contents[index].value = HEADER_SIGN + "\n" + lines.join('\n') + "\n" + FOOTER_SIGN;
  508. if (advs[index]) advs[index].value = '';
  509.  
  510. // Trigger character counter
  511. const event = new Event('input', { bubbles: true });
  512. contents[index].dispatchEvent(event);
  513. });
  514.  
  515. console.log(`[TTV-DEBUG] Filled ${chapters.length} chapters into form`);
  516. this.showNotification(`Đã đin ${chapters.length} chương vào form`, 'success');
  517. } catch (error) {
  518. console.error('[TTV-ERROR] Form filling error:', error);
  519. this.showNotification('Có lỗi khi điền nội dung vào form', 'error');
  520. } finally {
  521. this.hideLoading();
  522. }
  523. },
  524.  
  525. copyRemainingChapters: function(chapters) {
  526. console.log('[TTV-DEBUG] Copying remaining chapters to clipboard');
  527. try {
  528. const content = chapters.map(chapter => {
  529. const lines = chapter.trim().split('\n');
  530. if (lines[0] && !lines[0].startsWith('\t')) {
  531. lines[0] = '\t' + lines[0];
  532. }
  533. return lines.join('\n');
  534. }).join('\n\n');
  535.  
  536. navigator.clipboard.writeText(content)
  537. .then(() => {
  538. console.log(`[TTV-DEBUG] Copied ${chapters.length} chapters to clipboard`);
  539. this.showNotification(`Đã copy ${chapters.length} chương còn li vào clipboard`, 'info');
  540. })
  541. .catch(err => {
  542. console.error('[TTV-ERROR] Clipboard write error:', err);
  543. this.showNotification('Không thể copy vào clipboard', 'error');
  544. });
  545. } catch (error) {
  546. console.error('[TTV-ERROR] Copy process error:', error);
  547. this.showNotification('Có lỗi khi copy các chương còn lại', 'error');
  548. }
  549. },
  550.  
  551. submitChapters: function() {
  552. console.log('[TTV-DEBUG] Submitting chapters');
  553. const submitBtn = document.querySelector('button[type="submit"]');
  554. if (!submitBtn) {
  555. this.showNotification('Không tìm thấy nút đăng chương!', 'error');
  556. return;
  557. }
  558.  
  559. // Kiểm tra độ dài chương
  560. const shortChapters = Array.from(document.querySelectorAll('textarea[name^="introduce"]'))
  561. .filter(textarea => textarea.value.length < 3000);
  562.  
  563. if (shortChapters.length > 0) {
  564. this.showNotification(`Có ${shortChapters.length} chương chưa đủ 3000 ký tự`, 'error');
  565. return;
  566. }
  567.  
  568. // Đăng chương
  569. submitBtn.click();
  570. this.showNotification('Đang đăng chương...', 'info');
  571.  
  572. // Kiểm tra sau khi đăng
  573. setTimeout(() => {
  574. const remainingChapters = document.querySelectorAll('textarea[name^="introduce"]').length;
  575. console.log('[TTV-DEBUG] Chapters remaining after submit:', remainingChapters);
  576.  
  577. if (remainingChapters < 10 && this.STATE.isAuto) {
  578. console.log('[TTV-DEBUG] Less than 10 chapters remaining, stopping auto mode');
  579. this.STATE.isAuto = false;
  580. this.STATE.isProcessing = false;
  581. this.showNotification(`Còn ${remainingChapters} chương, dưới 10 chương nên đã dng t động`, 'warning');
  582. } else if (this.STATE.isAuto) {
  583. console.log('[TTV-DEBUG] Reloading page for next batch');
  584. setTimeout(() => window.location.reload(), 2000);
  585. }
  586. }, 3000);
  587. },
  588.  
  589. addNewChapterForm: function() {
  590. this.STATE.chapterNumber++;
  591. this.STATE.chapterSTT++;
  592. this.STATE.chapterSerial++;
  593.  
  594. const formHtml = `
  595. <div data-gen="MK_GEN" id="COUNT_CHAP_${this.STATE.chapterNumber}_MK">
  596. <div class="col-xs-12 form-group"></div>
  597. <div class="form-group">
  598. <label class="col-sm-2" for="chap_stt">STT</label>
  599. <div class="col-sm-8">
  600. <input class="form-control" required name="chap_stt[${this.STATE.chapterNumber}]" value="${this.STATE.chapterSTT}" placeholder="Số thứ tự của chương" type="text"/>
  601. </div>
  602. </div>
  603. <div class="form-group">
  604. <label class="col-sm-2" for="chap_number">Chương thứ..</label>
  605. <div class="col-sm-8">
  606. <input value="${this.STATE.chapterSerial}" required class="form-control" name="chap_number[${this.STATE.chapterNumber}]" placeholder="Chương thứ.. (1,2,3..)" type="text"/>
  607. </div>
  608. </div>
  609. <div class="form-group">
  610. <label class="col-sm-2" for="chap_name">Quyn số</label>
  611. <div class="col-sm-8">
  612. <input class="form-control" name="vol[${this.STATE.chapterNumber}]" value="1" placeholder="Quyển số" type="number" required/>
  613. </div>
  614. </div>
  615. <div class="form-group">
  616. <label class="col-sm-2" for="chap_name">Tên quyn</label>
  617. <div class="col-sm-8">
  618. <input class="form-control chap_vol_name" name="vol_name[${this.STATE.chapterNumber}]" placeholder="Tên quyển" type="text" />
  619. </div>
  620. </div>
  621. <div class="form-group">
  622. <label class="col-sm-2" for="chap_name">Tên chương</label>
  623. <div class="col-sm-8">
  624. <input required class="form-control" name="chap_name[${this.STATE.chapterNumber}]" placeholder="Tên chương" type="text"/>
  625. </div>
  626. </div>
  627. <div class="form-group">
  628. <label class="col-sm-2" for="introduce">Ni dung</label>
  629. <div class="col-sm-8">
  630. <textarea maxlength="75000" style="color:#000;font-weight: 400;" required class="form-control" name="introduce[${this.STATE.chapterNumber}]" rows="20" placeholder="Nội dung" type="text"></textarea>
  631. <div class="chapter-character-count"></div>
  632. </div>
  633. </div>
  634. <div class="form-group">
  635. <label class="col-sm-2" for="adv">Qung cáo</label>
  636. <div class="col-sm-8">
  637. <textarea maxlength="1000" class="form-control" name="adv[${this.STATE.chapterNumber}]" placeholder="Quảng cáo" type="text"></textarea>
  638. </div>
  639. </div>
  640. </div>`;
  641.  
  642. document.querySelector('#div_chapt_upload').insertAdjacentHTML('beforeend', formHtml);
  643. console.log(`[TTV-DEBUG] Added new chapter form #${this.STATE.chapterNumber}`);
  644. },
  645.  
  646. showLoading: function(message = 'Đang xử lý...') {
  647. const overlay = document.createElement('div');
  648. overlay.className = 'loading-overlay';
  649. overlay.innerHTML = `
  650. <div class="loading-content">
  651. <div class="loading-spinner"></div>
  652. <div>${message}</div>
  653. </div>
  654. `;
  655. document.body.appendChild(overlay);
  656. },
  657.  
  658. hideLoading: function() {
  659. const overlay = document.querySelector('.loading-overlay');
  660. if (overlay) overlay.remove();
  661. },
  662.  
  663. showNotification: function(message, type = 'info') {
  664. const notification = document.getElementById('ttv-notification');
  665. notification.innerHTML = `
  666. <div style="
  667. padding: 10px 15px;
  668. border-radius: 6px;
  669. background-color: ${type === 'error' ? '#ffebee' : type === 'success' ? '#e8f5e9' : type === 'warning' ? '#fff3e0' : '#e3f2fd'};
  670. color: ${type === 'error' ? '#c62828' : type === 'success' ? '#1b5e20' : type === 'warning' ? '#e65100' : '#0d47a1'};
  671. border: 1px solid ${type === 'error' ? '#ef9a9a' : type === 'success' ? '#a5d6a7' : type === 'warning' ? '#ffcc80' : '#90caf9'};
  672. ">
  673. ${message}
  674. </div>
  675. `;
  676. console.log(`[TTV-DEBUG] ${type.toUpperCase()}: ${message}`);
  677. }
  678. };
  679.  
  680. // Initialize script
  681. TTVManager.init();
  682. })();