您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Copies the chapter title and content from Kakuyomu, navigates to next/previous chapters, downloads as .txt, shows stats, includes customizable settings, bookmarks recent chapters, and supports hotkeys
// ==UserScript== // @name Kakuyomu Chapter Copier, Navigator, Downloader with Settings and Bookmarks // @namespace http://tampermonkey.net/ // @version 1.7 // @description Copies the chapter title and content from Kakuyomu, navigates to next/previous chapters, downloads as .txt, shows stats, includes customizable settings, bookmarks recent chapters, and supports hotkeys // @author Baconana-chan // @match https://kakuyomu.jp/works/*/episodes/* // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function() { 'use strict'; // Load settings const defaultSettings = { showNotifications: true, notificationDuration: 3, textFormat: 'plain' // Options: 'plain', 'numbered' }; let settings = GM_getValue('settings', defaultSettings); // Load recent chapters const recentChapters = GM_getValue('recentChapters', []); function saveRecentChapter() { const titleElement = document.querySelector('.widget-episodeTitle'); const title = titleElement ? titleElement.innerText.trim() : 'No Title'; const url = window.location.href; const newChapter = { title, url }; // Remove duplicates and add new chapter const updatedChapters = recentChapters.filter(ch => ch.url !== url); updatedChapters.unshift(newChapter); if (updatedChapters.length > 5) updatedChapters.pop(); GM_setValue('recentChapters', updatedChapters); } saveRecentChapter(); // Function to save settings function saveSettings() { GM_setValue('settings', settings); } // Function to create a styled button function createButton(text, rightPosition, topPosition, parent = document.body) { const button = document.createElement('button'); button.innerText = text; button.style.padding = '12px 20px'; button.style.backgroundColor = parent === document.body ? '#4CAF50' : '#2196F3'; button.style.color = 'white'; button.style.border = parent === document.body ? '2px solid #45a049' : 'none'; button.style.borderRadius = '8px'; button.style.cursor = 'pointer'; button.style.fontSize = '14px'; button.style.fontWeight = 'bold'; button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)'; button.style.transition = 'transform 0.2s'; if (parent === document.body) { button.style.position = 'fixed'; button.style.top = topPosition; button.style.right = rightPosition; button.style.zIndex = '99999'; } else { button.style.margin = '5px 0'; button.style.width = '100%'; } button.addEventListener('mouseover', () => { button.style.transform = 'scale(1.05)'; }); button.addEventListener('mouseout', () => { button.style.transform = 'scale(1)'; }); parent.appendChild(button); return button; } // Function to create a custom notification function showNotification(message) { if (!settings.showNotifications) return; const notification = document.createElement('div'); notification.innerText = message; notification.style.position = 'fixed'; notification.style.bottom = '20px'; notification.style.right = '20px'; notification.style.zIndex = '100000'; notification.style.padding = '15px 20px'; notification.style.backgroundColor = '#333'; notification.style.color = 'white'; notification.style.borderRadius = '8px'; notification.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)'; notification.style.opacity = '0'; notification.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; notification.style.transform = 'translateY(20px)'; notification.style.maxWidth = '300px'; notification.style.fontSize = '14px'; document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = '1'; notification.style.transform = 'translateY(0)'; }, 100); setTimeout(() => { notification.style.opacity = '0'; notification.style.transform = 'translateY(20px)'; setTimeout(() => notification.remove(), 500); }, settings.notificationDuration * 1000); } // Function to get chapter text and stats function getChapterText() { const titleElement = document.querySelector('.widget-episodeTitle'); const title = titleElement ? titleElement.innerText.trim() : 'No Title'; const contentElement = document.querySelector('.widget-episodeBody'); if (!contentElement) { return { title: null, content: null, stats: null }; } const paragraphs = contentElement.querySelectorAll('p'); const contentArray = Array.from(paragraphs) .map((p, index) => { if (p.classList.contains('blank')) return ''; const text = p.innerText.trim(); return settings.textFormat === 'numbered' ? `[${index + 1}] ${text}` : text; }) .filter(text => text !== ''); const content = contentArray.join('\n\n'); // Calculate stats const wordCount = contentArray.join(' ').split(/\s+/).filter(word => word.length > 0).length; const charCount = contentArray.join('').length; const paragraphCount = contentArray.length; return { title, content, stats: { wordCount, charCount, paragraphCount } }; } // Create settings panel function createSettingsPanel() { const panel = document.createElement('div'); panel.style.position = 'fixed'; panel.style.top = '10px'; panel.style.right = '180px'; panel.style.zIndex = '100000'; panel.style.backgroundColor = '#fff'; panel.style.border = '2px solid #ccc'; panel.style.borderRadius = '8px'; panel.style.padding = '15px'; panel.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)'; panel.style.display = 'none'; panel.style.fontSize = '14px'; panel.style.maxWidth = '300px'; // Download button const downloadButton = createButton('Download Chapter', null, null, panel); // Stats button const statsButton = createButton('Show Stats', null, null, panel); // Recent chapters const recentLabel = document.createElement('label'); recentLabel.innerText = 'Recent Chapters: '; const recentSelect = document.createElement('select'); recentSelect.style.margin = '5px'; recentSelect.style.width = '100%'; recentChapters.forEach(ch => { const opt = document.createElement('option'); opt.value = ch.url; opt.innerText = ch.title; recentSelect.appendChild(opt); }); if (recentChapters.length === 0) { const opt = document.createElement('option'); opt.innerText = 'No recent chapters'; recentSelect.appendChild(opt); } // Settings form const form = document.createElement('div'); // Notifications toggle const notificationLabel = document.createElement('label'); notificationLabel.innerText = 'Show Notifications: '; const notificationCheckbox = document.createElement('input'); notificationCheckbox.type = 'checkbox'; notificationCheckbox.checked = settings.showNotifications; notificationCheckbox.style.margin = '5px'; notificationLabel.appendChild(notificationCheckbox); // Notification duration const durationLabel = document.createElement('label'); durationLabel.innerText = 'Notification Duration (s): '; const durationInput = document.createElement('input'); durationInput.type = 'number'; durationInput.min = '1'; durationInput.max = '10'; durationInput.value = settings.notificationDuration; durationInput.style.width = '50px'; durationInput.style.margin = '5px'; // Text format const formatLabel = document.createElement('label'); formatLabel.innerText = 'Text Format: '; const formatSelect = document.createElement('select'); formatSelect.style.margin = '5px'; ['plain', 'numbered'].forEach(option => { const opt = document.createElement('option'); opt.value = option; opt.innerText = option.charAt(0).toUpperCase() + option.slice(1); if (option === settings.textFormat) opt.selected = true; formatSelect.appendChild(opt); }); // Save button const saveButton = createButton('Save Settings', null, null, panel); // Append elements panel.appendChild(recentLabel); panel.appendChild(recentSelect); panel.appendChild(document.createElement('br')); panel.appendChild(notificationLabel); panel.appendChild(document.createElement('br')); panel.appendChild(durationLabel); panel.appendChild(durationInput); panel.appendChild(document.createElement('br')); panel.appendChild(formatLabel); panel.appendChild(formatSelect); panel.appendChild(document.createElement('br')); // Event listeners downloadButton.addEventListener('click', () => { const { title, content, stats } = getChapterText(); if (!content) { showNotification('Could not find chapter content'); return; } const fullText = `${title}\n\n${content}`; const blob = new Blob([fullText], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${title}.txt`; a.click(); URL.revokeObjectURL(url); showNotification(`Chapter downloaded via button!\nWords: ${stats.wordCount}, Chars: ${stats.charCount}, Paras: ${stats.paragraphCount}`); }); statsButton.addEventListener('click', () => { const { title, stats } = getChapterText(); if (!stats) { showNotification('Could not find chapter content'); return; } const statsWindow = document.createElement('div'); statsWindow.style.position = 'fixed'; statsWindow.style.top = '50%'; statsWindow.style.left = '50%'; statsWindow.style.transform = 'translate(-50%, -50%)'; statsWindow.style.zIndex = '100001'; statsWindow.style.backgroundColor = '#fff'; statsWindow.style.border = '2px solid #ccc'; statsWindow.style.borderRadius = '8px'; statsWindow.style.padding = '20px'; statsWindow.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)'; statsWindow.style.fontSize = '14px'; statsWindow.style.maxWidth = '300px'; statsWindow.innerHTML = ` <h3>Statistics for ${title}</h3> <p>Words: ${stats.wordCount}</p> <p>Characters (no spaces): ${stats.charCount}</p> <p>Paragraphs: ${stats.paragraphCount}</p> `; const closeButton = document.createElement('button'); closeButton.innerText = 'Close'; closeButton.style.padding = '8px 12px'; closeButton.style.backgroundColor = '#f44336'; closeButton.style.color = 'white'; closeButton.style.border = 'none'; closeButton.style.borderRadius = '5px'; closeButton.style.cursor = 'pointer'; closeButton.style.marginTop = '10px'; closeButton.style.width = '100%'; closeButton.addEventListener('click', () => statsWindow.remove()); statsWindow.appendChild(closeButton); document.body.appendChild(statsWindow); }); recentSelect.addEventListener('change', () => { if (recentSelect.value) { window.location.href = recentSelect.value; } }); saveButton.addEventListener('click', () => { settings.showNotifications = notificationCheckbox.checked; settings.notificationDuration = Math.max(1, Math.min(10, parseInt(durationInput.value) || 3)); settings.textFormat = formatSelect.value; saveSettings(); showNotification('Settings saved!'); panel.style.display = 'none'; }); return panel; } // Create buttons const copyButton = createButton('Copy Chapter', '20px', '10px'); const settingsButton = createButton('Settings', '180px', '10px'); const nextButton = createButton('Next Chapter', '20px', '60px'); const prevButton = createButton('Prev Chapter', '180px', '60px'); // Create and append settings panel const settingsPanel = createSettingsPanel(); document.body.appendChild(settingsPanel); // Toggle settings panel settingsButton.addEventListener('click', () => { settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none'; }); // Copy chapter content function copyChapter(source) { const { title, content, stats } = getChapterText(); if (!content) { showNotification('Could not find chapter content'); return; } const fullText = `${title}\n\n${content}`; GM_setClipboard(fullText); showNotification(`Chapter copied via ${source}!\nWords: ${stats.wordCount}, Chars: ${stats.charCount}, Paras: ${stats.paragraphCount}`); } copyButton.addEventListener('click', () => copyChapter('button')); // Navigate to next chapter function goToNextChapter(source) { const nextEpisodeLink = document.querySelector('.contentMain-nextEpisode-inner'); if (!nextEpisodeLink) { showNotification('Next chapter link not found'); return; } const linkElement = nextEpisodeLink.closest('a'); if (linkElement && linkElement.href) { window.location.href = linkElement.href; } else { showNotification('Could not find the next chapter URL'); } } nextButton.addEventListener('click', () => goToNextChapter('button')); // Navigate to previous chapter function goToPrevChapter(source) { const prevEpisodeLink = document.querySelector('.contentMain-previousEpisode-inner'); if (!prevEpisodeLink) { showNotification('Previous chapter link not found'); return; } const linkElement = prevEpisodeLink.closest('a'); if (linkElement && linkElement.href) { window.location.href = linkElement.href; } else { showNotification('Could not find the previous chapter URL'); } } prevButton.addEventListener('click', () => goToPrevChapter('button')); // Hotkey support document.addEventListener('keydown', (e) => { if (e.ctrlKey) { switch (e.key.toLowerCase()) { case 'c': e.preventDefault(); copyChapter('Ctrl+C'); break; case 'd': e.preventDefault(); const { title, content, stats } = getChapterText(); if (!content) { showNotification('Could not find chapter content'); return; } const fullText = `${title}\n\n${content}`; const blob = new Blob([fullText], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${title}.txt`; a.click(); URL.revokeObjectURL(url); showNotification(`Chapter downloaded via Ctrl+D!\nWords: ${stats.wordCount}, Chars: ${stats.charCount}, Paras: ${stats.paragraphCount}`); break; case 'n': e.preventDefault(); goToNextChapter('Ctrl+N'); break; case 'p': e.preventDefault(); goToPrevChapter('Ctrl+P'); break; case 's': e.preventDefault(); settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none'; showNotification('Settings toggled via Ctrl+S!'); break; } } }); })();