Kakuyomu Chapter Copier, Navigator, Downloader with Settings and Bookmarks

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;
            }
        }
    });
})();