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