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