您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
(我已经安装了用户样式管理器,让我安装!)
// ==UserScript==
// @name Changpei Chapter Downloader
// @name:zh-CN 长佩章节下载器
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Download chapter content from Changpei(gongzicp.com)
// @description:zh-CN 从长佩(gongzicp.com)下载章节文本
// @author oovz
// @match *://*gongzicp.com/read-*.html
// @grant none
// @source https://gist.github.com/oovz/8c1c38607ed01cb594ebbd4913ff2c60
// @source http://greasyfork.icu/en/scripts/536172-changpei-chapter-downloader
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Configure your selectors here
const APP_WRAPPER_SELECTOR = '#app'; // for MutationObserver
const TITLE_SELECTOR = 'div.title > div.name'; // for title
const CONTENT_SELECTOR = 'div.h-reader > div.content'; // for content
const NEXT_CHAPTER_BASE_SELECTOR = 'div#readPage div.item > a'; // for next chaper link
const NEXT_ICON_IDENTIFIER = 'ic_next'; // for next chapter icon
const AUTHOR_SAY_SELECTOR = 'div.h-reader div.postscript > div.value'; // for author say
// Internationalization
const isZhCN = navigator.language.toLowerCase() === 'zh-cn' ||
document.documentElement.lang.toLowerCase() === 'zh-cn';
const i18n = {
copyText: isZhCN ? '复制文本' : 'Copy Content',
copiedText: isZhCN ? '已复制!' : 'Copied!',
nextChapter: isZhCN ? '下一章' : 'Next Chapter',
noNextChapter: isZhCN ? '没有下一章' : 'No Next Chapter',
includeAuthorSay: isZhCN ? '包含作家说' : 'Include Author Say',
excludeAuthorSay: isZhCN ? '排除作家说' : 'Exclude Author Say'
};
// State variable for author say inclusion
let includeAuthorSay = true;
// Create GUI elements
const gui = document.createElement('div');
gui.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: white;
padding: 15px;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
z-index: 9999;
resize: both;
overflow: visible;
min-width: 350px;
min-height: 250px;
max-width: 100vw;
max-height: 80vh;
resize-origin: top-left;
display: flex;
flex-direction: column;
`;
// Add CSS for custom resize handle at top-left
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
to { transform: rotate(360deg); }
}
.resize-handle {
position: absolute;
width: 14px;
height: 14px;
top: 0;
left: 0;
cursor: nwse-resize;
z-index: 10000;
background-color: #888;
border-top-left-radius: 5px;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
.spinner-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(240, 240, 240, 0.8);
display: none;
justify-content: center;
align-items: center;
z-index: 10001;
}
`;
document.head.appendChild(style);
// Create resize handle
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
const output = document.createElement('textarea');
output.style.cssText = `
width: 100%;
flex: 1;
margin-bottom: 8px;
resize: none;
overflow: auto;
box-sizing: border-box;
min-height: 180px;
`;
output.readOnly = true;
// Create button container for horizontal layout
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 2px;
`;
// Create toggle author say button
const toggleAuthorSayButton = document.createElement('button');
toggleAuthorSayButton.textContent = includeAuthorSay ? i18n.excludeAuthorSay : i18n.includeAuthorSay;
toggleAuthorSayButton.style.cssText = `
padding: 4px 12px;
cursor: pointer;
background-color: #fbbc05; /* Yellow */
color: white;
border: none;
border-radius: 15px;
font-weight: bold;
font-size: 0.9em;
`;
const copyButton = document.createElement('button');
copyButton.textContent = i18n.copyText;
copyButton.style.cssText = `
padding: 4px 12px;
cursor: pointer;
background-color: #4285f4;
color: white;
border: none;
border-radius: 15px;
font-weight: bold;
font-size: 0.9em;
`;
// Create next chapter button
const nextChapterButton = document.createElement('button');
nextChapterButton.textContent = i18n.nextChapter;
nextChapterButton.style.cssText = `
padding: 4px 12px;
cursor: pointer;
background-color: #34a853;
color: white;
border: none;
border-radius: 15px;
font-weight: bold;
font-size: 0.9em;
`;
// Add buttons to container
buttonContainer.appendChild(toggleAuthorSayButton);
buttonContainer.appendChild(copyButton);
buttonContainer.appendChild(nextChapterButton);
// Create spinner overlay for better positioning
const spinnerOverlay = document.createElement('div');
spinnerOverlay.className = 'spinner-overlay';
// Create spinner
const spinner = document.createElement('div');
spinner.style.cssText = `
width: 30px;
height: 30px;
border: 4px solid rgba(0,0,0,0.1);
border-radius: 50%;
border-top-color: #333;
animation: spin 1s ease-in-out infinite;
`;
spinnerOverlay.appendChild(spinner);
// Add elements to GUI
gui.appendChild(resizeHandle);
gui.appendChild(output);
gui.appendChild(buttonContainer);
gui.appendChild(spinnerOverlay);
document.body.appendChild(gui);
// Custom resize functionality
let isResizing = false;
let originalWidth, originalHeight, originalX, originalY;
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
isResizing = true;
originalWidth = parseFloat(getComputedStyle(gui).width);
originalHeight = parseFloat(getComputedStyle(gui).height);
originalX = e.clientX;
originalY = e.clientY;
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResize);
});
function resize(e) {
if (!isResizing) return;
const width = originalWidth - (e.clientX - originalX);
const height = originalHeight - (e.clientY - originalY);
if (width > 300 && width < window.innerWidth * 0.8) {
gui.style.width = width + 'px';
// Keep right position fixed and adjust left position
gui.style.right = getComputedStyle(gui).right;
}
if (height > 250 && height < window.innerHeight * 0.8) {
gui.style.height = height + 'px';
// Keep bottom position fixed and adjust top position
gui.style.bottom = getComputedStyle(gui).bottom;
}
}
function stopResize() {
isResizing = false;
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stopResize);
}
// Helper function to find the next chapter link
function findNextChapterLink() {
// Find all navigation links
const navLinks = document.querySelectorAll(NEXT_CHAPTER_BASE_SELECTOR);
console.log(`Found ${navLinks.length} navigation link candidates`);
// Look for the link with the next chapter icon
for (const link of navLinks) {
const iconImg = link.querySelector('img.iconfont');
if (iconImg) {
console.log(`Found icon with src: ${iconImg.src}`);
if (iconImg.src && iconImg.src.includes(NEXT_ICON_IDENTIFIER)) {
console.log(`Found next chapter link: ${link.href}`);
return link;
}
}
}
console.log('No next chapter link found');
return null; // No next chapter link found
}
// Legacy XPath extraction function (kept for fallback compatibility)
function getElementsByXpath(xpath) {
const results = [];
const query = document.evaluate(
xpath,
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (let i = 0; i < query.snapshotLength; i++) {
const node = query.snapshotItem(i);
if (node) {
// Get full text content including children, preserving whitespace
const textContent = node.textContent; // Keep original whitespace
// Only push if the content is not just whitespace
if (textContent && textContent.trim()) {
results.push(textContent);
}
}
}
return results;
}
// Initial extraction
function updateTitleOutput() {
// Use querySelector with the new selector for title
const titleElement = document.querySelector(TITLE_SELECTOR);
if (titleElement) {
// Extract direct text content, similar to XPath approach
let directTextContent = '';
for (let i = 0; i < titleElement.childNodes.length; i++) {
const childNode = titleElement.childNodes[i];
if (childNode.nodeType === Node.TEXT_NODE) {
directTextContent += childNode.textContent;
}
}
return directTextContent.trim();
}
return '';
}
function updateContentOutput(includeAuthorSayFlag) {
// Use querySelector to get the content container
const contentContainer = document.querySelector(CONTENT_SELECTOR);
let elements = [];
if (contentContainer) {
// Get all p elements within the content container
const paragraphs = contentContainer.querySelectorAll('p');
// Process each paragraph, excluding those with the "watermark" class
paragraphs.forEach(p => {
// Skip paragraphs with the "watermark" class
if (!p.classList.contains('watermark')) {
const textContent = p.textContent;
// Only add paragraphs that have non-whitespace content
if (textContent && textContent.trim()) {
elements.push(textContent);
}
}
});
}
if (elements.length === 0) {
console.error('no elements found for content, maybe using canvas');
}
// Join elements, do not trim here to preserve first line indentation
let content = elements.join('\n');
// Append author say if requested
if (includeAuthorSayFlag) {
// Use querySelector for author say with the new selector
const authorSayElement = document.querySelector(AUTHOR_SAY_SELECTOR);
let authorSayContent = '';
if (authorSayElement) {
authorSayContent = authorSayElement.textContent.trim();
}
// Add author say content if it exists
if (authorSayContent) {
// Add separation if both content and author say exist
if (content.trim()) {
content += '\n\n---\n\n' + authorSayContent; // Add separator
} else {
content = authorSayContent;
}
}
}
return content; // Return potentially leading-whitespace content
}
// Async update function
async function updateOutput() {
// Show spinner overlay
spinnerOverlay.style.display = 'flex';
// Use setTimeout to make it async and not block the UI
setTimeout(() => {
try {
const title = updateTitleOutput();
const content = updateContentOutput(includeAuthorSay); // Pass the state
output.value = title ? title + '\n\n' + content : content;
} catch (error) {
console.error('Error updating output:', error);
} finally {
// Hide spinner when done
spinnerOverlay.style.display = 'none';
}
}, 0);
}
// Run initial extraction
updateOutput();
// Add event listener for toggle author say button
toggleAuthorSayButton.addEventListener('click', () => {
includeAuthorSay = !includeAuthorSay; // Toggle state
toggleAuthorSayButton.textContent = includeAuthorSay ? i18n.excludeAuthorSay : i18n.includeAuthorSay;
updateOutput(); // Update the content
});
// Add event listener for copy button
copyButton.addEventListener('click', () => {
output.select();
document.execCommand('copy');
copyButton.textContent = i18n.copiedText;
setTimeout(() => {
copyButton.textContent = i18n.copyText;
}, 1000);
});
// Add event listener for next chapter button
nextChapterButton.addEventListener('click', () => {
// Find the next chapter link using our helper function
const nextChapterLink = findNextChapterLink();
if (nextChapterLink) {
// Navigate to the next chapter
window.location.href = nextChapterLink.href;
} else {
// Show a message if there's no next chapter
nextChapterButton.textContent = i18n.noNextChapter;
nextChapterButton.style.backgroundColor = '#ea4335';
setTimeout(() => {
nextChapterButton.textContent = i18n.nextChapter;
nextChapterButton.style.backgroundColor = '#34a853';
}, 2000);
}
});
// Find the content container element to observe (using the content selector)
const contentElement = document.querySelector(APP_WRAPPER_SELECTOR);
// Setup MutationObserver to watch for changes
if (contentElement) {
const observer = new MutationObserver(() => {
updateOutput();
});
observer.observe(contentElement, {
childList: true,
subtree: true,
characterData: true
});
// Also observe the document body for any structural changes that might affect the content
observer.observe(document.body, {
childList: true,
subtree: false // Only direct children of body
});
} else {
console.error('Content element not found. Cannot setup observer.');
}
})();