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.7
  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. // Chỉ lấy 10 chương đầu tiên
  327. if (chapters.length >= 10) {
  328. break;
  329. }
  330. }
  331.  
  332. // Kiểm tra dòng tiếp theo
  333. if (i + 1 < lines.length && chapterPattern.test(lines[i + 1])) {
  334. // Nếu dòng tiếp theo cũng là tiêu đề chương, gộp 2 dòng
  335. currentChapter.title = line + '\n' + lines[i + 1];
  336. // Lấy tên chương từ dòng đầu sau dấu :
  337. let name = line.split(':')[1]?.trim() || '';
  338. // Xóa dấu ., , hoặc ; ở đầu tên chương nếu có
  339. name = name.replace(/^[.,;'"]+/, '').trim();
  340. currentChapter.name = name;
  341. i++; // Bỏ qua dòng tiếp theo
  342. } else {
  343. currentChapter.title = line;
  344. // Lấy tên chương sau dấu :
  345. let name = line.split(':')[1]?.trim() || '';
  346. // Xóa dấu ., , hoặc ; ở đầu tên chương nếu có
  347. name = name.replace(/^[.,;'"]+/, '').trim();
  348. currentChapter.name = name;
  349. }
  350. } else {
  351. // Lưu tất cả nội dung không phải tiêu đề chương
  352. allContent.push(line);
  353. if (currentChapter.title) {
  354. currentChapter.content.push(line);
  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. return chapters;
  365. }
  366.  
  367. // Chọn chương để hiển thị
  368. function selectChapter(chapter, index) {
  369. const contentEditor = document.querySelector('.ttv-content-editor');
  370. contentEditor.value = chapter.title + '\n' + chapter.content.join('\n');
  371.  
  372. // Cập nhật số từ
  373. const wordCountSpan = document.querySelector('.ttv-word-count');
  374. if (wordCountSpan) {
  375. wordCountSpan.textContent = updateWordCount(contentEditor.value);
  376. }
  377.  
  378. // Highlight selected chapter
  379. const chapterItems = document.querySelectorAll('.ttv-chapter-item');
  380. chapterItems.forEach(item => item.classList.remove('selected'));
  381. chapterItems[index]?.classList.add('selected');
  382. }
  383.  
  384. // Hiển thị danh sách chương
  385. function displayChapters(chapters) {
  386. const chapterList = document.createElement('div');
  387. chapterList.className = 'ttv-chapter-list';
  388.  
  389. chapters.forEach((chapter, index) => {
  390. const chapterItem = document.createElement('div');
  391. chapterItem.className = 'ttv-chapter-item';
  392.  
  393. // Đánh dấu chương tự động tạo
  394. if (chapter.isAutoGenerated) {
  395. chapterItem.classList.add('auto-generated');
  396. }
  397.  
  398. // Tạo các phần tử con với định dạng riêng
  399. const titleDiv = document.createElement('div');
  400. titleDiv.className = 'chapter-title';
  401. titleDiv.textContent = chapter.title;
  402.  
  403. const nameDiv = document.createElement('div');
  404. nameDiv.className = 'chapter-name';
  405. nameDiv.textContent = `Tên chương: ${chapter.name}`;
  406.  
  407. const statsDiv = document.createElement('div');
  408. statsDiv.className = 'chapter-stats';
  409. statsDiv.textContent = `${chapter.content.length} dòng`;
  410.  
  411. // Thêm các phần tử vào item
  412. chapterItem.appendChild(titleDiv);
  413. chapterItem.appendChild(nameDiv);
  414. chapterItem.appendChild(statsDiv);
  415.  
  416. chapterItem.onclick = () => selectChapter(chapter, index);
  417. chapterList.appendChild(chapterItem);
  418.  
  419. // Select first chapter by default
  420. if (index === 0) {
  421. chapterItem.classList.add('selected');
  422. }
  423. });
  424.  
  425. const existingList = document.querySelector('.ttv-chapter-list');
  426. if (existingList) {
  427. existingList.remove();
  428. }
  429.  
  430. const contentEditor = document.querySelector('.ttv-content-editor');
  431. contentEditor.parentNode.insertBefore(chapterList, contentEditor);
  432. }
  433.  
  434. // Đọc nội dung file
  435. function readFileContent(file) {
  436. return new Promise((resolve, reject) => {
  437. const reader = new FileReader();
  438. reader.onload = (e) => resolve(e.target.result);
  439. reader.onerror = (e) => reject(new Error('Lỗi đọc file: ' + e.target.error));
  440. reader.readAsText(file, 'UTF-8');
  441. });
  442. }
  443.  
  444. // Đếm số từ và ký tự
  445. function updateWordCount(content) {
  446. const wordCount = content.trim().split(/\s+/).length;
  447. const charCount = content.length;
  448. return `${wordCount} t | ${charCount} ký tự`;
  449. }
  450.  
  451. // Xử lý khi chọn file
  452. async function handleFileSelect(event) {
  453. try {
  454. const file = event.target.files[0];
  455. if (!file) return;
  456.  
  457. // Đọc nội dung file
  458. const content = await readFileContent(file);
  459.  
  460. // Phân tích và tách chương
  461. const chapters = parseChapters(content);
  462. displayChapters(chapters);
  463.  
  464. // Điền vào khung soạn thảo nội dung chương đầu tiên nếu có
  465. if (chapters.length > 0) {
  466. const contentEditor = document.querySelector('.ttv-content-editor');
  467. if (contentEditor) {
  468. const firstChapter = chapters[0];
  469. contentEditor.value = firstChapter.title + '\n' + firstChapter.content.join('\n');
  470.  
  471. // Cập nhật số từ
  472. const wordCountSpan = document.querySelector('.ttv-word-count');
  473. if (wordCountSpan) {
  474. wordCountSpan.textContent = updateWordCount(contentEditor.value);
  475. }
  476.  
  477. showNotification(`Đã ti ${chapters.length} chương t file thành công!`);
  478. }
  479. } else {
  480. showNotification('Không tìm thấy chương nào trong file!', true);
  481. }
  482. } catch (error) {
  483. console.error('Lỗi xử lý file:', error);
  484. showNotification('Có lỗi xảy ra khi đọc file!', true);
  485. }
  486. }
  487.  
  488. // Chuyển nội dung từ khung soạn thảo sang form
  489. function transferContent() {
  490. const contentEditor = document.querySelector('.ttv-content-editor');
  491. const chapterNameInput = document.querySelector('input[name="chap_name[1]"]');
  492. const contentInput = document.querySelector('textarea[name="introduce[1]"]');
  493.  
  494. if (contentEditor && contentInput) {
  495. const content = contentEditor.value;
  496. const chapters = parseChapters(content);
  497.  
  498. if (chapters.length > 0) {
  499. // Điền tên chương vào input tên chương
  500. if (chapterNameInput && chapters[0].name) {
  501. chapterNameInput.value = chapters[0].name;
  502. }
  503.  
  504. // Điền nội dung vào textarea
  505. contentInput.value = chapters[0].content.join('\n');
  506.  
  507. showNotification('Đã chuyển nội dung sang form đăng chương!');
  508. } else {
  509. contentInput.value = contentEditor.value;
  510. showNotification('Đã chuyển toàn bộ nội dung sang form!');
  511. }
  512. } else {
  513. showNotification('Không tìm thấy form đăng chương!', true);
  514. }
  515. }
  516.  
  517. // Chuyển đổi giữa chế độ soạn thảo và xem trước
  518. function togglePreview() {
  519. const contentEditor = document.querySelector('.ttv-content-editor');
  520. const preview = document.querySelector('.ttv-preview');
  521. const previewBtn = document.querySelector('.ttv-preview-btn');
  522.  
  523. if (contentEditor.style.display !== 'none') {
  524. contentEditor.style.display = 'none';
  525. preview.style.display = 'block';
  526. preview.innerHTML = contentEditor.value.replace(/\n/g, '<br>');
  527. previewBtn.textContent = 'Soạn thảo';
  528. } else {
  529. contentEditor.style.display = 'block';
  530. preview.style.display = 'none';
  531. previewBtn.textContent = 'Xem trước';
  532. }
  533. }
  534.  
  535. // Chuyển đổi chế độ toàn màn hình
  536. function toggleFullscreen() {
  537. const panel = document.querySelector('.ttv-control-panel');
  538. const fullscreenBtn = document.querySelector('.ttv-fullscreen-btn');
  539.  
  540. panel.classList.toggle('fullscreen');
  541. fullscreenBtn.textContent = panel.classList.contains('fullscreen') ? 'Thu nhỏ' : 'Toàn màn hình';
  542. }
  543.  
  544. // Thêm panel điều khiển
  545. function addControlPanel() {
  546. // Tạo panel
  547. const panel = document.createElement('div');
  548. panel.className = 'ttv-control-panel';
  549.  
  550. // Thêm header
  551. const header = document.createElement('div');
  552. header.className = 'ttv-header';
  553. header.innerHTML = `
  554. <div>Son Tho Ni Dung</div>
  555. <div class="ttv-toolbar">
  556. <button class="ttv-preview-btn" onclick="togglePreview()">Xem trước</button>
  557. <button class="ttv-fullscreen-btn" onclick="toggleFullscreen()">Toàn màn hình</button>
  558. <button class="ttv-minimize">−</button>
  559. </div>
  560. `;
  561.  
  562. // Container cho các nút
  563. const buttonGroup = document.createElement('div');
  564. buttonGroup.className = 'ttv-button-group';
  565.  
  566. // Input chọn file
  567. const fileInput = document.createElement('input');
  568. fileInput.type = 'file';
  569. fileInput.accept = '.txt';
  570. fileInput.className = 'ttv-file-input';
  571. fileInput.id = 'ttv-file-input';
  572. fileInput.style.display = 'none';
  573. fileInput.onchange = handleFileSelect;
  574.  
  575. // Label cho input file
  576. const fileLabel = document.createElement('label');
  577. fileLabel.htmlFor = 'ttv-file-input';
  578. fileLabel.className = 'ttv-file-label';
  579. fileLabel.innerHTML = '<span>Chọn file txt chứa nội dung</span>';
  580.  
  581. // Khung soạn thảo nội dung
  582. const contentEditorLabel = document.createElement('div');
  583. contentEditorLabel.className = 'ttv-heading';
  584. contentEditorLabel.innerHTML = `
  585. Ni dung chương:
  586. <span class="ttv-word-count">0 t | 0 ký tự</span>
  587. `;
  588.  
  589. const contentEditor = document.createElement('textarea');
  590. contentEditor.className = 'ttv-content-editor';
  591. contentEditor.placeholder = 'Nhập hoặc dán nội dung chương vào đây...';
  592.  
  593. // Khung xem trước
  594. const preview = document.createElement('div');
  595. preview.className = 'ttv-preview';
  596.  
  597. // Cập nhật số từ khi nhập nội dung
  598. contentEditor.oninput = () => {
  599. const wordCountSpan = document.querySelector('.ttv-word-count');
  600. if (wordCountSpan) {
  601. wordCountSpan.textContent = updateWordCount(contentEditor.value);
  602. }
  603. };
  604.  
  605. // Xử lý khi paste nội dung
  606. contentEditor.onpaste = (e) => {
  607. // Cho phép paste hoàn tất
  608. setTimeout(() => {
  609. const content = contentEditor.value;
  610. const chapters = parseChapters(content);
  611. if (chapters.length > 0) {
  612. displayChapters(chapters);
  613. showNotification(`Đã tìm thy ${chapters.length} chương!`);
  614. }
  615. }, 0);
  616. };
  617.  
  618. // Nút chuyển nội dung
  619. const transferBtn = document.createElement('button');
  620. transferBtn.type = 'button';
  621. transferBtn.className = 'btn btn-warning';
  622. transferBtn.innerHTML = '<span>Chuyển nội dung sang form</span>';
  623. transferBtn.onclick = transferContent;
  624.  
  625. // Thêm các phần tử vào panel
  626. panel.appendChild(header);
  627. buttonGroup.appendChild(fileInput);
  628. buttonGroup.appendChild(fileLabel);
  629. buttonGroup.appendChild(contentEditorLabel);
  630. buttonGroup.appendChild(contentEditor);
  631. buttonGroup.appendChild(preview);
  632. buttonGroup.appendChild(transferBtn);
  633. panel.appendChild(buttonGroup);
  634.  
  635. document.body.appendChild(panel);
  636.  
  637. // Thêm xử lý sự kiện cho các nút trong toolbar
  638. const minimizeBtn = panel.querySelector('.ttv-minimize');
  639. minimizeBtn.onclick = () => {
  640. panel.classList.toggle('minimized');
  641. minimizeBtn.innerHTML = panel.classList.contains('minimized') ? '+' : '−';
  642. };
  643.  
  644. window.togglePreview = togglePreview;
  645. window.toggleFullscreen = toggleFullscreen;
  646. }
  647.  
  648. // Thêm control panel khi trang đã load
  649. window.addEventListener('load', function() {
  650. addControlPanel();
  651. });
  652. })();