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