Grajapa Downloader (Multilingual UI + Status)

Easily download all images from grajapa.shueisha.co.jp with a single click. Features an enhanced UI, real-time status, and language options (EN, JP, CN).

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Grajapa Downloader (Multilingual UI + Status)
// @namespace    http://tampermonkey.net/
// @version      0.5.1 // Incremented version for the CDN change
// @description  Easily download all images from grajapa.shueisha.co.jp with a single click. Features an enhanced UI, real-time status, and language options (EN, JP, CN).
// @author       hg542006810 (Enhanced by AI Assistant & Community)
// @match        https://www.grajapa.shueisha.co.jp/viewerV3_8/*
// @icon         https://www.google.com/s2/favicons?domain=shueisha.co.jp
// @grant        GM_addStyle
// @require      http://code.jquery.com/jquery-1.11.0.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('[GRAJAPA DOWNLOADER] Script initialized (v0.5.1)');

    // --- Translations ---
    const translations = {
        en: {
            scriptName: "Grajapa Downloader",
            btnDownload: "Download All Images",
            statusReady: "Status: Ready",
            statusInitializing: "Status: Initializing...",
            statusLocatingAlbums: "Status: Locating image albums...",
            statusProcessingAlbum: "Processing album {current} of {total}...",
            statusProcessingAlbumWait: "Processing album {current} of {total}... Please wait.",
            statusFoundImages: "Found {count} images. Preparing ZIP...",
            statusCreatingZip: "Creating ZIP... {percent}%",
            statusZipProgress: "Compressing: {file}",
            statusZipComplete: "ZIP created! ({count} images) Starting download...",
            statusNoAlbums: "No image albums found (.list-group-item)! Check console and ensure you're on the correct page.",
            statusNoImagesAfterProcessing: "No image URLs found after processing! Check console for errors.",
            statusNoValidBase64Images: "No valid Base64 images found to ZIP! Check console for link types.",
            statusErrorFrame: "Error: Could not find main image frame (.fixed-book-frame).",
            statusErrorZip: "Error creating ZIP! {message}",
            statusErrorIframe: "Error accessing iframe content. Possible cross-origin issue or content not loaded.",
            alertNoAlbums: "No image items found! (Missing '.list-group-item' class). Please check browser console and if you are on the correct page.",
            alertNoImages: "No images found! (After processing items, no image URLs were collected). Please check browser console for errors.",
            alertNoBase64: "No recognizable Base64 images were found to create the ZIP file! Check the console for details on the links found.",
            alertZipError: "Error creating ZIP file! {message}",
            alertZipComplete: "ZIP creation complete. Download starting!",
            langEnglish: "EN",
            langJapanese: "JP",
            langChinese: "CN",
            errorAddingUI: "Failed to initialize script: Could not add UI to the page."
        },
        jp: {
            scriptName: "Grajapaダウンローダー",
            btnDownload: "すべての画像をダウンロード",
            statusReady: "ステータス: 準備完了",
            statusInitializing: "ステータス: 初期化中...",
            statusLocatingAlbums: "ステータス: 画像アルバムを検索中...",
            statusProcessingAlbum: "アルバム {current}/{total} を処理中...",
            statusProcessingAlbumWait: "アルバム {current}/{total} を処理中... しばらくお待ちください。",
            statusFoundImages: "{count}枚の画像が見つかりました。ZIPを準備中...",
            statusCreatingZip: "ZIPを作成中... {percent}%",
            statusZipProgress: "圧縮中: {file}",
            statusZipComplete: "ZIP作成完了!({count}枚の画像) ダウンロードを開始します...",
            statusNoAlbums: "画像アルバムが見つかりません (.list-group-item)!コンソールを確認し、正しいページにいることを確認してください。",
            statusNoImagesAfterProcessing: "処理後に画像URLが見つかりません!コンソールでエラーを確認してください。",
            statusNoValidBase64Images: "ZIPする有効なBase64画像が見つかりません!リンクの種類をコンソールで確認してください。",
            statusErrorFrame: "エラー: メイン画像フレーム (.fixed-book-frame) が見つかりません。",
            statusErrorZip: "ZIP作成エラー!{message}",
            statusErrorIframe: "iframeコンテンツへのアクセスエラー。クロスオリジン問題またはコンテンツ未ロードの可能性があります。",
            alertNoAlbums: "画像アイテムが見つかりません!('.list-group-item'クラスがありません)。ブラウザのコンソールを確認し、正しいページにいるか確認してください。",
            alertNoImages: "画像が見つかりません!(アイテム処理後、画像URLが収集されませんでした)。ブラウザのコンソールでエラーを確認してください。",
            alertNoBase64: "ZIPファイルを作成するための認識可能なBase64画像が見つかりませんでした!見つかったリンクの詳細はコンソールを確認してください。",
            alertZipError: "ZIPファイル作成エラー!{message}",
            alertZipComplete: "ZIPの作成が完了しました。ダウンロードを開始します!",
            langEnglish: "英語",
            langJapanese: "日本語",
            langChinese: "中国語",
            errorAddingUI: "スクリプトの初期化に失敗しました:UIをページに追加できませんでした。"
        },
        cn: { // Simplified Chinese
            scriptName: "Grajapa 下载器",
            btnDownload: "下载所有图片",
            statusReady: "状态: 准备就绪",
            statusInitializing: "状态: 初始化中...",
            statusLocatingAlbums: "状态: 正在查找图片相册...",
            statusProcessingAlbum: "正在处理相册 {current}/{total}...",
            statusProcessingAlbumWait: "正在处理相册 {current}/{total}... 请稍候。",
            statusFoundImages: "找到 {count} 张图片。正在准备ZIP...",
            statusCreatingZip: "正在创建ZIP... {percent}%",
            statusZipProgress: "正在压缩: {file}",
            statusZipComplete: "ZIP创建完成!({count}张图片) 下载即将开始...",
            statusNoAlbums: "未找到图片相册 (.list-group-item)!请检查控制台并确保您在正确的页面上。",
            statusNoImagesAfterProcessing: "处理后未找到图片URL!请检查控制台中的错误。",
            statusNoValidBase64Images: "未找到可ZIP的有效Base64图片!请在控制台中检查链接类型。",
            statusErrorFrame: "错误: 未找到主图片框 (.fixed-book-frame)。",
            statusErrorZip: "创建ZIP时出错!{message}",
            statusErrorIframe: "访问iframe内容时出错。可能是跨域问题或内容未加载。",
            alertNoAlbums: "未找到图片项目!(缺少'.list-group-item'类)。请检查浏览器控制台,并确认您是否在正确的页面上。",
            alertNoImages: "未找到图片!(处理项目后,未收集到图片URL)。请检查浏览器控制台中的错误。",
            alertNoBase64: "未找到可识别的Base64图片来创建ZIP文件!请检查控制台以获取有关找到的链接的详细信息。",
            alertZipError: "创建ZIP文件时出错!{message}",
            alertZipComplete: "ZIP创建完成。下载即将开始!",
            langEnglish: "英语",
            langJapanese: "日语",
            langChinese: "中文",
            errorAddingUI: "脚本初始化失败:无法将UI添加到页面。"
        }
    };

    let currentLang = localStorage.getItem('grajapaDownloaderLang') || 'en';

    // Helper function to get translated string
    function T(key, replacements = {}) {
        let translatedString = (translations[currentLang] && translations[currentLang][key]) || translations.en[key] || `MISSING_TRANSLATION: ${key}`;
        for (const placeholder in replacements) {
            translatedString = translatedString.replace(`{${placeholder}}`, replacements[placeholder]);
        }
        return translatedString;
    }

    // --- UI Elements ---
    var uiContainer, button, statusArea, langButtonContainer;

    function createUI() {
        uiContainer = document.createElement('div');
        uiContainer.id = 'grajapa_downloader_container';

        // Language buttons
        langButtonContainer = document.createElement('div');
        langButtonContainer.id = 'grajapa_lang_selector';

        ['en', 'jp', 'cn'].forEach(langCode => {
            const langButton = document.createElement('button');
            langButton.id = `lang_btn_${langCode}`;
            langButton.textContent = translations[langCode][`lang${langCode.charAt(0).toUpperCase() + langCode.slice(1)}`];
            if (currentLang === langCode) {
                langButton.classList.add('active');
            }
            langButton.addEventListener('click', () => setLanguage(langCode));
            langButtonContainer.appendChild(langButton);
        });
        uiContainer.appendChild(langButtonContainer);

        button = document.createElement('button');
        button.id = 'grajapa_download_button';
        uiContainer.appendChild(button);

        statusArea = document.createElement('div');
        statusArea.id = 'grajapa_status_area';
        uiContainer.appendChild(statusArea);

        applyTranslationsToUI();
    }

    function applyTranslationsToUI() {
        if (!uiContainer) return;

        button.textContent = T('btnDownload');
        statusArea.textContent = T('statusReady');

        ['en', 'jp', 'cn'].forEach(langCode => {
            const langButton = document.getElementById(`lang_btn_${langCode}`);
            if (langButton) {
                langButton.textContent = translations[langCode][`lang${langCode.charAt(0).toUpperCase() + langCode.slice(1)}`];
                if (currentLang === langCode) {
                    langButton.classList.add('active');
                } else {
                    langButton.classList.remove('active');
                }
            }
        });
        if(button.disabled) {
            // Status already set by ongoing process, no need to reset to 'Ready'
        } else {
             statusArea.textContent = T('statusReady');
        }
    }

    function setLanguage(langCode) {
        currentLang = langCode;
        localStorage.setItem('grajapaDownloaderLang', langCode);
        console.log(`[GRAJAPA DOWNLOADER] Language changed to: ${langCode}`);
        applyTranslationsToUI();
        if (!button.disabled) {
            updateStatus('statusReady');
        }
    }

    GM_addStyle(`
        #grajapa_downloader_container {
            position: fixed; z-index: 10000; top: 20px; right: 20px;
            background: #f9f9f9; border: 1px solid #ccc; border-radius: 8px;
            padding: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            font-family: Arial, sans-serif; width: 280px; text-align: center;
        }
        #grajapa_lang_selector {
            margin-bottom: 10px; display: flex; justify-content: space-around;
        }
        #grajapa_lang_selector button {
            background-color: #e0e0e0; color: #333; border: 1px solid #c0c0c0;
            padding: 5px 8px; font-size: 12px; cursor: pointer; border-radius: 4px;
            transition: background-color 0.2s ease, box-shadow 0.2s ease;
        }
        #grajapa_lang_selector button:hover {
            background-color: #d0d0d0;
        }
        #grajapa_lang_selector button.active {
            background-color: #007bff; color: white; border-color: #0056b3;
            font-weight: bold; box-shadow: 0 0 5px rgba(0,123,255,0.5);
        }
        #grajapa_download_button {
            background-color: #007bff; color: white; border: none;
            padding: 10px 15px; text-align: center; display: block; width: 100%;
            font-size: 16px; margin-bottom: 10px; cursor: pointer;
            border-radius: 5px; transition: background-color 0.3s ease;
        }
        #grajapa_download_button:hover { background-color: #0056b3; }
        #grajapa_download_button:disabled { background-color: #cccccc; cursor: not-allowed; }
        #grajapa_status_area {
            font-size: 13px; color: #333; padding: 8px; border-top: 1px solid #eee;
            margin-top: 5px; min-height: 20px; background-color: #fff; border-radius: 4px;
            word-wrap: break-word;
        }
        #grajapa_status_area.error { color: #D8000C; background-color: #FFD2D2; font-weight: bold; }
        #grajapa_status_area.success { color: #2F855A; background-color: #C6F6D5; font-weight: bold; }
    `);

    function updateStatus(messageKey, type = 'info', replacements = {}) {
        const messageText = T(messageKey, replacements);
        if (statusArea) { // Ensure statusArea exists
            statusArea.textContent = messageText;
            statusArea.className = ''; // Reset class
            if (type === 'error') {
                statusArea.classList.add('error');
            } else if (type === 'success') {
                statusArea.classList.add('success');
            }
        }
        console.log(`[GRAJAPA DOWNLOADER STATUS] ${messageText}`);
    }

    async function handleDownload() {
        if (button) button.disabled = true;
        updateStatus('statusInitializing');
        console.log('[GRAJAPA DOWNLOADER] Download button clicked');

        async function getImages(index, sum, allCollectedImages) {
            updateStatus('statusProcessingAlbum', 'info', { current: index + 1, total: sum });
            console.log(`[GRAJAPA DOWNLOADER] Processing item ${index + 1} of ${sum}`);
            let currentBatchImages = [];
            const listItem = $('.list-group-item')[index];

            if (!listItem) {
                console.error('[GRAJAPA DOWNLOADER] Could not find .list-group-item for index:', index);
                updateStatus('statusNoAlbums', 'error');
                return allCollectedImages;
            }
            listItem.click();

            await new Promise(resolve => setTimeout(resolve, 1500));

            const fixedBookFrames = $('.fixed-book-frame');
            if (fixedBookFrames.length === 0 && index === 0) {
                updateStatus('statusErrorFrame', 'error');
                console.warn('[GRAJAPA DOWNLOADER] No .fixed-book-frame found');
            }

            fixedBookFrames.each(function () {
                $(this).find('iframe').each(function () {
                    try {
                        const iframeContents = $(this).contents();
                        iframeContents.find('image').each(function () {
                            const link = $(this).attr('xlink:href');
                            if (link && allCollectedImages.indexOf(link) === -1 && currentBatchImages.indexOf(link) === -1) {
                                currentBatchImages.push(link);
                            }
                        });
                        iframeContents.find('img').each(function () {
                            const srcLink = $(this).attr('src');
                            if (srcLink && allCollectedImages.indexOf(srcLink) === -1 && currentBatchImages.indexOf(srcLink) === -1) {
                                currentBatchImages.push(srcLink);
                            }
                        });
                    } catch (e) {
                        console.error('[GRAJAPA DOWNLOADER] Error accessing iframe content:', e.message);
                    }
                });
            });

            allCollectedImages = allCollectedImages.concat(currentBatchImages);
            console.log(`[GRAJAPA DOWNLOADER] Images found this round: ${currentBatchImages.length}. Total collected: ${allCollectedImages.length}`);

            if (index + 1 < sum) {
                updateStatus('statusProcessingAlbumWait', 'info', { current: index + 1, total: sum });
                await new Promise((resolve) => setTimeout(resolve, 1000));
                return getImages(index + 1, sum, allCollectedImages);
            }
            return allCollectedImages;
        }

        updateStatus('statusLocatingAlbums');
        const itemCount = $('.list-group-item').length;
        console.log(`[GRAJAPA DOWNLOADER] Found items (.list-group-item): ${itemCount}`);

        if (itemCount === 0) {
            updateStatus('statusNoAlbums', 'error');
            alert(T('alertNoAlbums'));
            if (button) button.disabled = false;
            return;
        }

        const images = await getImages(0, itemCount, []);
        console.log(`[GRAJAPA DOWNLOADER] Total images collected after recursion: ${images.length}`);

        if (images.length === 0) {
            updateStatus('statusNoImagesAfterProcessing', 'error');
            alert(T('alertNoImages'));
            if (button) button.disabled = false;
            return;
        }

        updateStatus('statusFoundImages', 'info', { count: images.length });
        const zip = new JSZip();
        const imgFolder = zip.folder('images');
        let validImageCount = 0;

        images.forEach((item) => {
            if (typeof item !== 'string') return;
            let base64Data = '';
            let extension = '';

            if (item.startsWith('data:image/jpeg;base64,')) {
                base64Data = item.replace('data:image/jpeg;base64,', '');
                extension = '.jpeg';
            } else if (item.startsWith('data:image/jpg;base64,')) {
                base64Data = item.replace('data:image/jpg;base64,', '');
                extension = '.jpg';
            } else if (item.startsWith('data:image/png;base64,')) {
                base64Data = item.replace('data:image/png;base64,', '');
                extension = '.png';
            } else {
                console.warn(`[GRAJAPA DOWNLOADER] Unsupported link format or not a known Base64 data URI: ${item}`);
                return;
            }

            if (base64Data && extension) {
                imgFolder.file((validImageCount + 1) + extension, base64Data, { base64: true });
                validImageCount++;
            }
        });

        if (validImageCount === 0) {
            updateStatus('statusNoValidBase64Images', 'error');
            alert(T('alertNoBase64'));
            if (button) button.disabled = false;
            return;
        }

        console.log(`[GRAJAPA DOWNLOADER] Creating ZIP file for ${validImageCount} images.`);
        zip.generateAsync({ type: 'blob' }, (metadata) => {
            updateStatus('statusCreatingZip', 'info', { percent: metadata.percent.toFixed(0) });
            if (metadata.currentFile) {
                console.log(T('statusZipProgress', { file: metadata.currentFile }));
            }
        })
        .then((content) => {
            const a = document.createElement('a');
            a.href = URL.createObjectURL(content);
            let pageTitle = document.title.replace(/[<>:"/\\|?*]+/g, '_').trim() || 'grajapa_images';
            a.download = `${pageTitle}_${Date.now()}.zip`;
            a.click();
            URL.revokeObjectURL(a.href);
            console.log(`[GRAJAPA DOWNLOADER] ZIP file created and download initiated: ${a.download}`);
            updateStatus('statusZipComplete', 'success', { count: validImageCount });
            alert(T('alertZipComplete'));
            setTimeout(() => {
                if (button && !button.disabled) updateStatus('statusReady');
            }, 7000);
        })
        .catch((err) => {
            console.error('[GRAJAPA DOWNLOADER] Error creating ZIP:', err);
            updateStatus('statusErrorZip', 'error', { message: err.message });
            alert(T('alertZipError', { message: err.message }));
        })
        .finally(() => {
            if (button) button.disabled = false;
            // Check current status class to avoid resetting a success/error message immediately to "Ready"
             if (statusArea && !(statusArea.classList.contains('success') || statusArea.classList.contains('error'))) {
                 updateStatus('statusReady');
            }
        });
    }

    function init() {
        createUI();
        if (button) { // Ensure button is created before assigning onclick
             button.onclick = handleDownload;
        }


        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                if (uiContainer) document.body.appendChild(uiContainer);
                console.log('[GRAJAPA DOWNLOADER] UI added after DOMContentLoaded.');
            });
        } else {
            try {
                if (uiContainer) document.body.appendChild(uiContainer);
                console.log('[GRAJAPA DOWNLOADER] UI added to body.');
            } catch (e) {
                console.error('[GRAJAPA DOWNLOADER] Could not append UI to body:', e);
                alert(T('errorAddingUI'));
            }
        }
    }

    init();

})();