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