长佩章节下载器

从长佩(gongzicp.com)下载章节文本

  1. // ==UserScript==
  2. // @name Changpei Chapter Downloader
  3. // @name:zh-CN 长佩章节下载器
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.2
  6. // @description Download chapter content from Changpei(gongzicp.com)
  7. // @description:zh-CN 从长佩(gongzicp.com)下载章节文本
  8. // @author oovz
  9. // @match *://*gongzicp.com/read-*.html
  10. // @grant none
  11. // @source https://gist.github.com/oovz/8c1c38607ed01cb594ebbd4913ff2c60
  12. // @source https://greasyfork.org/en/scripts/536172-changpei-chapter-downloader
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18. // Configure your selectors here
  19. const APP_WRAPPER_SELECTOR = '#app'; // for MutationObserver
  20. const TITLE_SELECTOR = 'div.title > div.name'; // for title
  21. const CONTENT_SELECTOR = 'div.h-reader > div.content'; // for content
  22. const NEXT_CHAPTER_BASE_SELECTOR = 'div#readPage div.item > a'; // for next chaper link
  23. const NEXT_ICON_IDENTIFIER = 'ic_next'; // for next chapter icon
  24. const AUTHOR_SAY_SELECTOR = 'div.h-reader div.postscript > div.value'; // for author say
  25.  
  26. // Internationalization
  27. const isZhCN = navigator.language.toLowerCase() === 'zh-cn' ||
  28. document.documentElement.lang.toLowerCase() === 'zh-cn';
  29. const i18n = {
  30. copyText: isZhCN ? '复制文本' : 'Copy Content',
  31. copiedText: isZhCN ? '已复制!' : 'Copied!',
  32. nextChapter: isZhCN ? '下一章' : 'Next Chapter',
  33. noNextChapter: isZhCN ? '没有下一章' : 'No Next Chapter',
  34. includeAuthorSay: isZhCN ? '包含作家说' : 'Include Author Say',
  35. excludeAuthorSay: isZhCN ? '排除作家说' : 'Exclude Author Say'
  36. };
  37.  
  38. // State variable for author say inclusion
  39. let includeAuthorSay = true;
  40.  
  41. // Create GUI elements
  42. const gui = document.createElement('div');
  43. gui.style.cssText = `
  44. position: fixed;
  45. bottom: 20px;
  46. right: 20px;
  47. background: white;
  48. padding: 15px;
  49. border: 1px solid #ccc;
  50. border-radius: 5px;
  51. box-shadow: 0 0 10px rgba(0,0,0,0.1);
  52. z-index: 9999;
  53. resize: both;
  54. overflow: visible;
  55. min-width: 350px;
  56. min-height: 250px;
  57. max-width: 100vw;
  58. max-height: 80vh;
  59. resize-origin: top-left;
  60. display: flex;
  61. flex-direction: column;
  62. `;
  63.  
  64. // Add CSS for custom resize handle at top-left
  65. const style = document.createElement('style');
  66. style.textContent = `
  67. @keyframes spin {
  68. to { transform: rotate(360deg); }
  69. }
  70.  
  71. .resize-handle {
  72. position: absolute;
  73. width: 14px;
  74. height: 14px;
  75. top: 0;
  76. left: 0;
  77. cursor: nwse-resize;
  78. z-index: 10000;
  79. background-color: #888;
  80. border-top-left-radius: 5px;
  81. border-right: 1px solid #ccc;
  82. border-bottom: 1px solid #ccc;
  83. }
  84.  
  85. .spinner-overlay {
  86. position: absolute;
  87. top: 0;
  88. left: 0;
  89. width: 100%;
  90. height: 100%;
  91. background-color: rgba(240, 240, 240, 0.8);
  92. display: none;
  93. justify-content: center;
  94. align-items: center;
  95. z-index: 10001;
  96. }
  97. `;
  98. document.head.appendChild(style);
  99.  
  100. // Create resize handle
  101. const resizeHandle = document.createElement('div');
  102. resizeHandle.className = 'resize-handle';
  103. const output = document.createElement('textarea');
  104. output.style.cssText = `
  105. width: 100%;
  106. flex: 1;
  107. margin-bottom: 8px;
  108. resize: none;
  109. overflow: auto;
  110. box-sizing: border-box;
  111. min-height: 180px;
  112. `;
  113. output.readOnly = true;
  114.  
  115. // Create button container for horizontal layout
  116. const buttonContainer = document.createElement('div');
  117. buttonContainer.style.cssText = `
  118. display: flex;
  119. justify-content: center;
  120. gap: 10px;
  121. margin-bottom: 2px;
  122. `;
  123.  
  124. // Create toggle author say button
  125. const toggleAuthorSayButton = document.createElement('button');
  126. toggleAuthorSayButton.textContent = includeAuthorSay ? i18n.excludeAuthorSay : i18n.includeAuthorSay;
  127. toggleAuthorSayButton.style.cssText = `
  128. padding: 4px 12px;
  129. cursor: pointer;
  130. background-color: #fbbc05; /* Yellow */
  131. color: white;
  132. border: none;
  133. border-radius: 15px;
  134. font-weight: bold;
  135. font-size: 0.9em;
  136. `;
  137.  
  138. const copyButton = document.createElement('button');
  139. copyButton.textContent = i18n.copyText;
  140. copyButton.style.cssText = `
  141. padding: 4px 12px;
  142. cursor: pointer;
  143. background-color: #4285f4;
  144. color: white;
  145. border: none;
  146. border-radius: 15px;
  147. font-weight: bold;
  148. font-size: 0.9em;
  149. `;
  150.  
  151. // Create next chapter button
  152. const nextChapterButton = document.createElement('button');
  153. nextChapterButton.textContent = i18n.nextChapter;
  154. nextChapterButton.style.cssText = `
  155. padding: 4px 12px;
  156. cursor: pointer;
  157. background-color: #34a853;
  158. color: white;
  159. border: none;
  160. border-radius: 15px;
  161. font-weight: bold;
  162. font-size: 0.9em;
  163. `;
  164.  
  165. // Add buttons to container
  166. buttonContainer.appendChild(toggleAuthorSayButton);
  167. buttonContainer.appendChild(copyButton);
  168. buttonContainer.appendChild(nextChapterButton);
  169.  
  170. // Create spinner overlay for better positioning
  171. const spinnerOverlay = document.createElement('div');
  172. spinnerOverlay.className = 'spinner-overlay';
  173. // Create spinner
  174. const spinner = document.createElement('div');
  175. spinner.style.cssText = `
  176. width: 30px;
  177. height: 30px;
  178. border: 4px solid rgba(0,0,0,0.1);
  179. border-radius: 50%;
  180. border-top-color: #333;
  181. animation: spin 1s ease-in-out infinite;
  182. `;
  183. spinnerOverlay.appendChild(spinner);
  184.  
  185. // Add elements to GUI
  186. gui.appendChild(resizeHandle);
  187. gui.appendChild(output);
  188. gui.appendChild(buttonContainer);
  189. gui.appendChild(spinnerOverlay);
  190. document.body.appendChild(gui);
  191.  
  192. // Custom resize functionality
  193. let isResizing = false;
  194. let originalWidth, originalHeight, originalX, originalY;
  195.  
  196. resizeHandle.addEventListener('mousedown', (e) => {
  197. e.preventDefault();
  198. isResizing = true;
  199. originalWidth = parseFloat(getComputedStyle(gui).width);
  200. originalHeight = parseFloat(getComputedStyle(gui).height);
  201. originalX = e.clientX;
  202. originalY = e.clientY;
  203. document.addEventListener('mousemove', resize);
  204. document.addEventListener('mouseup', stopResize);
  205. });
  206.  
  207. function resize(e) {
  208. if (!isResizing) return;
  209. const width = originalWidth - (e.clientX - originalX);
  210. const height = originalHeight - (e.clientY - originalY);
  211. if (width > 300 && width < window.innerWidth * 0.8) {
  212. gui.style.width = width + 'px';
  213. // Keep right position fixed and adjust left position
  214. gui.style.right = getComputedStyle(gui).right;
  215. }
  216. if (height > 250 && height < window.innerHeight * 0.8) {
  217. gui.style.height = height + 'px';
  218. // Keep bottom position fixed and adjust top position
  219. gui.style.bottom = getComputedStyle(gui).bottom;
  220. }
  221. }
  222.  
  223. function stopResize() {
  224. isResizing = false;
  225. document.removeEventListener('mousemove', resize);
  226. document.removeEventListener('mouseup', stopResize);
  227. }
  228. // Helper function to find the next chapter link
  229. function findNextChapterLink() {
  230. // Find all navigation links
  231. const navLinks = document.querySelectorAll(NEXT_CHAPTER_BASE_SELECTOR);
  232. console.log(`Found ${navLinks.length} navigation link candidates`);
  233. // Look for the link with the next chapter icon
  234. for (const link of navLinks) {
  235. const iconImg = link.querySelector('img.iconfont');
  236. if (iconImg) {
  237. console.log(`Found icon with src: ${iconImg.src}`);
  238. if (iconImg.src && iconImg.src.includes(NEXT_ICON_IDENTIFIER)) {
  239. console.log(`Found next chapter link: ${link.href}`);
  240. return link;
  241. }
  242. }
  243. }
  244. console.log('No next chapter link found');
  245. return null; // No next chapter link found
  246. }
  247.  
  248. // Legacy XPath extraction function (kept for fallback compatibility)
  249. function getElementsByXpath(xpath) {
  250. const results = [];
  251. const query = document.evaluate(
  252. xpath,
  253. document,
  254. null,
  255. XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  256. null
  257. );
  258. for (let i = 0; i < query.snapshotLength; i++) {
  259. const node = query.snapshotItem(i);
  260. if (node) {
  261. // Get full text content including children, preserving whitespace
  262. const textContent = node.textContent; // Keep original whitespace
  263. // Only push if the content is not just whitespace
  264. if (textContent && textContent.trim()) {
  265. results.push(textContent);
  266. }
  267. }
  268. }
  269. return results;
  270. }
  271. // Initial extraction
  272. function updateTitleOutput() {
  273. // Use querySelector with the new selector for title
  274. const titleElement = document.querySelector(TITLE_SELECTOR);
  275. if (titleElement) {
  276. // Extract direct text content, similar to XPath approach
  277. let directTextContent = '';
  278. for (let i = 0; i < titleElement.childNodes.length; i++) {
  279. const childNode = titleElement.childNodes[i];
  280. if (childNode.nodeType === Node.TEXT_NODE) {
  281. directTextContent += childNode.textContent;
  282. }
  283. }
  284. return directTextContent.trim();
  285. }
  286. return '';
  287. }
  288.  
  289. function updateContentOutput(includeAuthorSayFlag) {
  290. // Use querySelector to get the content container
  291. const contentContainer = document.querySelector(CONTENT_SELECTOR);
  292. let elements = [];
  293. if (contentContainer) {
  294. // Get all p elements within the content container
  295. const paragraphs = contentContainer.querySelectorAll('p');
  296. // Process each paragraph, excluding those with the "watermark" class
  297. paragraphs.forEach(p => {
  298. // Skip paragraphs with the "watermark" class
  299. if (!p.classList.contains('watermark')) {
  300. const textContent = p.textContent;
  301. // Only add paragraphs that have non-whitespace content
  302. if (textContent && textContent.trim()) {
  303. elements.push(textContent);
  304. }
  305. }
  306. });
  307. }
  308.  
  309. if (elements.length === 0) {
  310. console.error('no elements found for content, maybe using canvas');
  311. }
  312. // Join elements, do not trim here to preserve first line indentation
  313. let content = elements.join('\n');
  314.  
  315. // Append author say if requested
  316. if (includeAuthorSayFlag) {
  317. // Use querySelector for author say with the new selector
  318. const authorSayElement = document.querySelector(AUTHOR_SAY_SELECTOR);
  319. let authorSayContent = '';
  320. if (authorSayElement) {
  321. authorSayContent = authorSayElement.textContent.trim();
  322. }
  323. // Add author say content if it exists
  324. if (authorSayContent) {
  325. // Add separation if both content and author say exist
  326. if (content.trim()) {
  327. content += '\n\n---\n\n' + authorSayContent; // Add separator
  328. } else {
  329. content = authorSayContent;
  330. }
  331. }
  332. }
  333.  
  334. return content; // Return potentially leading-whitespace content
  335. }
  336.  
  337. // Async update function
  338. async function updateOutput() {
  339. // Show spinner overlay
  340. spinnerOverlay.style.display = 'flex';
  341. // Use setTimeout to make it async and not block the UI
  342. setTimeout(() => {
  343. try {
  344. const title = updateTitleOutput();
  345. const content = updateContentOutput(includeAuthorSay); // Pass the state
  346. output.value = title ? title + '\n\n' + content : content;
  347. } catch (error) {
  348. console.error('Error updating output:', error);
  349. } finally {
  350. // Hide spinner when done
  351. spinnerOverlay.style.display = 'none';
  352. }
  353. }, 0);
  354. }
  355.  
  356. // Run initial extraction
  357. updateOutput();
  358.  
  359. // Add event listener for toggle author say button
  360. toggleAuthorSayButton.addEventListener('click', () => {
  361. includeAuthorSay = !includeAuthorSay; // Toggle state
  362. toggleAuthorSayButton.textContent = includeAuthorSay ? i18n.excludeAuthorSay : i18n.includeAuthorSay;
  363. updateOutput(); // Update the content
  364. });
  365.  
  366. // Add event listener for copy button
  367. copyButton.addEventListener('click', () => {
  368. output.select();
  369. document.execCommand('copy');
  370. copyButton.textContent = i18n.copiedText;
  371. setTimeout(() => {
  372. copyButton.textContent = i18n.copyText;
  373. }, 1000);
  374. });
  375.  
  376. // Add event listener for next chapter button
  377. nextChapterButton.addEventListener('click', () => {
  378. // Find the next chapter link using our helper function
  379. const nextChapterLink = findNextChapterLink();
  380. if (nextChapterLink) {
  381. // Navigate to the next chapter
  382. window.location.href = nextChapterLink.href;
  383. } else {
  384. // Show a message if there's no next chapter
  385. nextChapterButton.textContent = i18n.noNextChapter;
  386. nextChapterButton.style.backgroundColor = '#ea4335';
  387. setTimeout(() => {
  388. nextChapterButton.textContent = i18n.nextChapter;
  389. nextChapterButton.style.backgroundColor = '#34a853';
  390. }, 2000);
  391. }
  392. });
  393.  
  394. // Find the content container element to observe (using the content selector)
  395. const contentElement = document.querySelector(APP_WRAPPER_SELECTOR);
  396. // Setup MutationObserver to watch for changes
  397. if (contentElement) {
  398. const observer = new MutationObserver(() => {
  399. updateOutput();
  400. });
  401. observer.observe(contentElement, {
  402. childList: true,
  403. subtree: true,
  404. characterData: true
  405. });
  406. // Also observe the document body for any structural changes that might affect the content
  407. observer.observe(document.body, {
  408. childList: true,
  409. subtree: false // Only direct children of body
  410. });
  411. } else {
  412. console.error('Content element not found. Cannot setup observer.');
  413. }
  414. })();