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