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