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