Show previews, organize image types, and allow ZIP download with proper naming
当前为
// ==UserScript==
// @name IMVU _contents.json Viewer with Zip Download
// @namespace http://tampermonkey.net/
// @version 1.2.9
// @auther heapsofjoy
// @description Show previews, organize image types, and allow ZIP download with proper naming
// @match https://userimages-akm.imvu.com/productdata/*/*/_contents.json
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// ==/UserScript==
(function () {
'use strict';
let files;
try {
files = JSON.parse(document.body.innerText);
} catch (e) {
console.error("Failed to parse JSON:", e);
return;
}
const baseUrl = window.location.href.replace(/\/_contents\.json$/, '');
const parts = window.location.pathname.split('/');
const prodId = parts[2];
const rev = parts[3];
const zipFilename = `${prodId}_rev${rev}_files.zip`;
// Basic styling
document.body.innerHTML = '';
document.body.style.background = '#111';
document.body.style.color = '#eee';
document.body.style.fontFamily = 'Arial, sans-serif';
document.body.style.padding = '20px';
const heading = document.createElement('h1');
heading.textContent = '📦 Product Files';
heading.style.marginBottom = '10px';
document.body.appendChild(heading);
// Download ZIP button
const downloadAllBtn = document.createElement('button');
downloadAllBtn.textContent = '⬇ Download All as ZIP';
downloadAllBtn.style.marginBottom = '25px';
downloadAllBtn.style.padding = '10px 20px';
downloadAllBtn.style.fontSize = '16px';
downloadAllBtn.style.backgroundColor = '#ff69b4';
downloadAllBtn.style.color = 'white';
downloadAllBtn.style.border = 'none';
downloadAllBtn.style.borderRadius = '8px';
downloadAllBtn.style.cursor = 'pointer';
downloadAllBtn.onclick = async () => {
downloadAllBtn.disabled = true;
downloadAllBtn.textContent = 'Zipping...';
const zip = new JSZip();
for (const file of files) {
const originalName = file.name || file.url;
if (isExcluded(originalName)) continue;
const url = `${baseUrl}/${file.url || file.name}`;
try {
const res = await fetch(url);
const blob = await res.blob();
let adjustedName = originalName;
// Check MIME type and adjust the file extension accordingly
const mime = blob.type;
// If it's an image, we will try to adjust the extension based on the mime type
if (mime.startsWith('image/')) {
// Handle PNG and JPEG files
if (mime === 'image/png') {
adjustedName = originalName.replace(/\.(jpe?g|gif|bmp|tga)$/i, '.png');
} else if (mime === 'image/jpeg' || mime === 'image/jpg') {
adjustedName = originalName.replace(/\.(png|gif|bmp|tga)$/i, '.jpg');
} else if (mime === 'image/gif') {
adjustedName = originalName.replace(/\.(png|jpeg|bmp|tga)$/i, '.gif');
} else if (mime === 'image/bmp') {
adjustedName = originalName.replace(/\.(png|jpeg|gif|tga)$/i, '.bmp');
} else if (mime === 'image/tga') {
// Handle .tga files
adjustedName = originalName.replace(/\.tga$/i, '.png'); // You can decide on a default format like PNG
}
// Add other image formats here if necessary
} else {
// Non-image files should be left as-is
// This handles non-image files or files with unrecognized mime types
adjustedName = originalName;
}
// Add the file to the zip with the adjusted name
zip.file(adjustedName, blob);
} catch (e) {
console.error('Failed to fetch:', originalName, e);
}
}
zip.generateAsync({ type: 'blob' }).then(blob => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = zipFilename;
document.body.appendChild(a);
a.click();
a.remove();
downloadAllBtn.textContent = '⬇ Download All as ZIP';
downloadAllBtn.disabled = false;
});
};
document.body.appendChild(downloadAllBtn);
// Layout containers
const imageGrid = document.createElement('div');
imageGrid.style.display = 'grid';
imageGrid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(250px, 1fr))';
imageGrid.style.gap = '15px';
imageGrid.style.marginBottom = '50px';
document.body.appendChild(imageGrid);
const otherImagesSection = document.createElement('div');
otherImagesSection.style.marginTop = '30px';
document.body.appendChild(otherImagesSection);
const fileList = document.createElement('div');
fileList.style.paddingTop = '20px';
fileList.style.borderTop = '1px solid #444';
fileList.style.marginTop = '30px';
document.body.appendChild(fileList);
const imageBoxColor = [
//String.fromCharCode(120, 109, 102),
//String.fromCharCode(120, 115, 102)
];
const isExcluded = (filename) => {
return imageBoxColor.some(ext => filename.toLowerCase().endsWith(ext));
};
const isPreviewImage = (filename) => {
return (
/\.(png|jpe?g|gif|webp|bmp|tga)$/i.test(filename) ||
!/\.[^/.]+$/.test(filename) // no extension, treat as image
);
};
const isOtherImageType = (filename) => /\.(dds)$/i.test(filename);
files.forEach(async (file) => {
const name = file.name || file.url || 'Unnamed';
const urlPart = file.url || file.name;
if (isExcluded(name)) return;
const fileUrl = `${baseUrl}/${urlPart}`;
try {
const res = await fetch(fileUrl);
const blob = await res.blob();
const mime = blob.type;
const isImage = mime.startsWith('image/');
const lowerName = name.toLowerCase();
const isCommonImage = mime.startsWith('image/') &&
['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'].includes(mime);
const isRealTGA = lowerName.endsWith('.tga') && (mime === 'application/octet-stream' || mime === 'image/x-icon');
// If it's a real TGA file (not displayable), show in "other" section
if (isRealTGA) {
const otherImageEntry = document.createElement('div');
otherImageEntry.style.marginBottom = '10px';
const link = document.createElement('a');
link.href = fileUrl;
link.download = name;
link.textContent = name;
link.style.color = '#ff69b4';
link.style.textDecoration = 'none';
link.style.wordBreak = 'break-all';
otherImageEntry.appendChild(link);
otherImagesSection.appendChild(otherImageEntry);
} else if (isImage && isPreviewImage(name)) {
const container = document.createElement('div');
container.style.background = '#222';
container.style.padding = '10px';
container.style.borderRadius = '10px';
container.style.boxShadow = '0 0 10px rgba(0,0,0,0.3)';
container.style.textAlign = 'center';
const img = document.createElement('img');
img.src = URL.createObjectURL(blob);
img.alt = name;
img.style.maxWidth = '100%';
img.style.maxHeight = '200px';
img.style.width = 'auto'; // Ensures the image maintains its aspect ratio
img.style.height = 'auto'; // Keeps the height flexible based on the width
img.style.minWidth = '150px'; // Sets a minimum width for the image box
img.style.display = 'block';
img.style.margin = '0 auto 10px';
container.appendChild(img);
const link = document.createElement('a');
link.href = fileUrl;
link.download = name;
link.textContent = name;
link.style.color = '#ff69b4';
link.style.textDecoration = 'none';
link.style.wordBreak = 'break-all';
container.appendChild(link);
const mimeInfo = document.createElement('div');
mimeInfo.textContent = `(${mime})`;
mimeInfo.style.color = '#999';
mimeInfo.style.fontSize = '12px';
mimeInfo.style.marginTop = '5px';
container.appendChild(mimeInfo);
imageGrid.appendChild(container);
} else if (/\.xml$/i.test(name)) {
// If it's an XML file, add preview functionality
const fileEntry = document.createElement('div');
fileEntry.style.marginBottom = '10px';
const link = document.createElement('a');
link.href = fileUrl;
link.download = name;
link.textContent = name;
link.style.color = '#ff69b4';
link.style.textDecoration = 'none';
link.style.wordBreak = 'break-all';
fileEntry.appendChild(link);
// Create a preview button for XML files
const previewBtn = document.createElement('button');
previewBtn.textContent = '↓';
previewBtn.style.marginLeft = '10px';
previewBtn.style.padding = '2px 8px';
previewBtn.style.fontSize = '12px';
previewBtn.style.background = '#333';
previewBtn.style.color = '#fff';
previewBtn.style.border = '1px solid #666';
previewBtn.style.borderRadius = '4px';
previewBtn.style.cursor = 'pointer';
// Create a preview box for XML file content
const previewBox = document.createElement('pre');
previewBox.style.display = 'none';
previewBox.style.background = '#1e1e1e';
previewBox.style.color = '#ccc';
previewBox.style.padding = '10px';
previewBox.style.marginTop = '5px';
previewBox.style.border = '1px solid #333';
previewBox.style.borderRadius = '6px';
previewBox.style.maxHeight = '300px';
previewBox.style.overflowY = 'auto';
previewBox.style.whiteSpace = 'pre-wrap';
// Toggle preview box visibility when clicking the preview button
previewBtn.onclick = async () => {
if (previewBox.style.display === 'none') {
previewBtn.textContent = '↑';
if (!previewBox.textContent) {
try {
const res = await fetch(fileUrl);
const text = await res.text();
previewBox.textContent = text;
} catch (err) {
previewBox.textContent = '[Error loading XML]';
}
}
previewBox.style.display = 'block';
} else {
previewBtn.textContent = '↓';
previewBox.style.display = 'none';
}
};
fileEntry.appendChild(previewBtn);
fileEntry.appendChild(previewBox);
fileList.appendChild(fileEntry);
} else {
// For non-image files, no preview
const fileEntry = document.createElement('div');
fileEntry.style.marginBottom = '10px';
const link = document.createElement('a');
link.href = fileUrl;
link.download = name;
link.textContent = name;
link.style.color = '#ff69b4';
link.style.textDecoration = 'none';
link.style.wordBreak = 'break-all';
fileEntry.appendChild(link);
fileList.appendChild(fileEntry);
}
} catch (e) {
console.error('Failed to fetch or analyze file:', name, e);
}
});
})();