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