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