ShinyySwapper / compressor.html
ShinyyMineyyON's picture
Upload 112 files
2b9f9c9 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shinyy Studio | Asset Compressor</title>
<!-- Modern typography -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: 'Inter', sans-serif;
background: radial-gradient(circle at top left, #1a1c2c 0%, #0d0e14 100%);
color: #e2e8f0;
min-height: 100vh;
overflow-x: hidden;
}
.glass {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
.glass-card {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-card:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(99, 102, 241, 0.3);
}
.glass-card.active {
background: rgba(99, 102, 241, 0.15);
border-color: rgba(99, 102, 241, 0.5);
box-shadow: 0 0 20px rgba(99, 102, 241, 0.2);
}
/* Tabs */
.tab-btn { transition: all 0.3s ease; }
.tab-btn.active {
background: rgba(99, 102, 241, 0.2);
color: #fff;
border-bottom: 2px solid #6366f1;
}
.format-btn { transition: all 0.2s ease; }
.format-btn.active {
background: #6366f1;
color: #fff;
font-weight: 600;
}
/* Custom Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 10px; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); }
/* Modern Range Sliders */
input[type=range] { -webkit-appearance: none; background: transparent; width: 100%; margin: 10px 0; }
input[type=range]:focus { outline: none; }
input[type=range]::-webkit-slider-runnable-track {
width: 100%; height: 6px; border-radius: 3px; background: rgba(255,255,255,0.1);
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; height: 16px; width: 16px; border-radius: 50%;
background: #6366f1; cursor: pointer; margin-top: -5px; transition: transform 0.1s;
box-shadow: 0 0 10px rgba(99, 102, 241, 0.5);
}
input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.2); }
input[type=range]:disabled::-webkit-slider-thumb { background: #555; box-shadow: none; }
/* Drag & Drop Global Overlay */
#drop-overlay { display: none; }
body.dragging #drop-overlay { display: flex; }
.checker-bg {
background-image:
linear-gradient(45deg, rgba(255,255,255,0.03) 25%, transparent 25%),
linear-gradient(-45deg, rgba(255,255,255,0.03) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.03) 75%),
linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.03) 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
.fade-in { animation: fadeIn 0.4s ease-out forwards; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.pulse-border {
animation: pulse-border 2s infinite;
}
@keyframes pulse-border {
0%, 100% { border-color: rgba(99, 102, 241, 0.2); }
50% { border-color: rgba(99, 102, 241, 0.8); box-shadow: 0 0 20px rgba(99,102,241,0.3); }
}
</style>
</head>
<body class="min-h-screen overflow-x-hidden p-2 md:p-6 flex flex-col">
<!-- Drag & Drop Global Overlay -->
<div id="drop-overlay" class="fixed inset-0 bg-indigo-900/40 backdrop-blur-sm z-[100] items-center justify-center text-3xl font-bold text-white pointer-events-none">
<div class="glass p-12 rounded-[2rem] text-center border-indigo-500 shadow-2xl pulse-border">
<svg class="w-16 h-16 mx-auto mb-4 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
Drop assets to import into Studio
</div>
</div>
<!-- VIEW 1: Empty State -->
<div id="empty-state" class="flex-1 flex flex-col items-center justify-center relative z-10 fade-in">
<div class="text-center mb-10">
<h1 class="text-4xl font-bold tracking-tight text-white mb-2">Asset Compressor <span class="text-indigo-500 text-lg font-medium ml-2 px-3 py-1 glass rounded-full">Module</span></h1>
<p class="text-slate-400 font-light">Optimize, resize, and fine-tune your generations.</p>
</div>
<div id="main-dropzone" class="glass max-w-2xl w-full p-16 rounded-[2.5rem] cursor-pointer group hover:bg-white/5 transition-all border-dashed border-2 border-white/20 hover:border-indigo-500/50">
<input type="file" accept="image/*,video/*" multiple class="hidden" id="file-upload-main">
<label for="file-upload-main" class="cursor-pointer flex flex-col items-center gap-6 w-full h-full">
<div class="w-20 h-20 rounded-full bg-indigo-500/10 flex items-center justify-center group-hover:scale-110 transition-transform group-hover:bg-indigo-500/20">
<svg class="w-10 h-10 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4v16m8-8H4"></path></svg>
</div>
<div class="text-center">
<h3 class="text-2xl font-bold text-white mb-2">Import Media</h3>
<p class="text-slate-400 text-sm">Drag and drop images or videos here, or click to browse.</p>
</div>
</label>
</div>
<div id="importLoadingStatus" class="hidden mt-6 text-indigo-400 font-medium animate-pulse">Checking for transferred assets...</div>
</div>
<!-- VIEW 2: Editor State -->
<div id="editor-state" class="mx-auto w-full max-w-[1600px] flex flex-col overflow-hidden relative z-10 hidden fade-in h-[calc(100vh-3rem)]">
<!-- Header & Tabs -->
<div class="flex flex-col md:flex-row justify-between items-center mb-6 gap-4">
<h1 class="text-2xl font-bold tracking-tight text-white">Asset <span class="text-indigo-500">Compressor</span></h1>
<div class="flex gap-2 p-1 glass rounded-2xl">
<button class="tab-btn active px-6 py-2 rounded-xl text-sm font-medium text-slate-400 hover:text-white" data-tab="image" onclick="switchTab('image')">Image Tuning</button>
<button class="tab-btn px-6 py-2 rounded-xl text-sm font-medium text-slate-400 hover:text-white" data-tab="video" onclick="switchTab('video')">Video Tuning</button>
</div>
</div>
<!-- SHARED LAYOUT CONTAINER -->
<div class="glass rounded-[2rem] p-2 flex flex-col md:flex-row flex-1 overflow-hidden w-full gap-2">
<!-- LEFT SIDEBAR: Inventory -->
<div class="w-full md:w-80 flex flex-col shrink-0 bg-black/20 rounded-3xl overflow-hidden border border-white/5">
<div class="p-5 border-b border-white/10 flex items-center justify-between">
<h2 class="font-bold text-white text-lg flex items-center gap-2">
Inventory <span id="gallery-count" class="bg-indigo-500/20 text-indigo-300 text-xs px-2 py-0.5 rounded-full">0</span>
</h2>
<label for="add-more" class="cursor-pointer text-indigo-400 hover:text-indigo-300 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
<input id="add-more" type="file" multiple accept="image/*,video/*" class="hidden">
</label>
</div>
<div id="gallery-list" class="flex-1 overflow-y-auto p-4 space-y-3">
<!-- Gallery items injected here via JS -->
</div>
<div class="p-4 border-t border-white/10 bg-black/20">
<button id="btn-download-all" class="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-emerald-600/20 text-sm">
Download All Processed
</button>
</div>
</div>
<!-- RIGHT MAIN: Crafting Table -->
<div class="flex-1 flex flex-col overflow-y-auto relative rounded-3xl">
<!-- Empty Editor Placeholder -->
<div id="editor-empty" class="flex-1 flex flex-col items-center justify-center text-slate-500 hidden">
<svg class="w-16 h-16 mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
Select an item from the inventory
</div>
<div id="editor-content" class="p-6 w-full space-y-6 hidden h-full">
<!-- Header for Active Item -->
<div class="glass p-5 rounded-2xl flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div class="min-w-0 flex-1">
<h1 id="active-filename" class="text-xl font-bold text-white truncate">filename.ext</h1>
<div class="text-sm text-slate-400 mt-1 flex gap-4 font-medium items-center">
<span id="active-dimensions" class="flex items-center gap-1"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path></svg> 0 x 0</span>
<span id="active-savings" class="text-emerald-400 bg-emerald-400/10 px-2 py-0.5 rounded-md">Saved 0%</span>
</div>
</div>
<button id="btn-apply-all" class="bg-indigo-600 hover:bg-indigo-500 text-white px-6 py-2.5 rounded-xl font-medium transition-all text-sm shadow-lg shadow-indigo-500/20 whitespace-nowrap">
Apply Settings to All
</button>
</div>
<!-- Controls & Preview Grid -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start h-[calc(100%-100px)]">
<!-- Controls Column -->
<div class="lg:col-span-4 space-y-6 overflow-y-auto pr-2 pb-10 max-h-full">
<!-- SECTION 1: Compression -->
<div class="glass-card p-6 rounded-2xl">
<h2 class="text-sm font-bold text-indigo-400 uppercase tracking-wider mb-5">Output Format</h2>
<div class="space-y-6">
<!-- Image Formats -->
<div id="image-format-section">
<div class="flex gap-2 p-1 bg-black/30 rounded-xl" id="format-selectors">
<button data-format="image/jpeg" class="format-btn active flex-1 py-2 rounded-lg text-sm text-slate-400">JPG</button>
<button data-format="image/webp" class="format-btn flex-1 py-2 rounded-lg text-sm text-slate-400">WEBP</button>
<button data-format="image/png" class="format-btn flex-1 py-2 rounded-lg text-sm text-slate-400">PNG</button>
</div>
</div>
<!-- Video Formats -->
<div id="video-format-section" class="hidden">
<div class="flex gap-2 p-1 bg-black/30 rounded-xl">
<button class="format-btn active flex-1 py-2 rounded-lg text-sm cursor-default">WEBM (VP8 Native)</button>
</div>
</div>
<div>
<div class="flex justify-between mb-1">
<label class="text-sm font-medium text-slate-300" id="quality-label">Compression Quality</label>
<span id="val-quality" class="text-sm font-bold text-indigo-400">80%</span>
</div>
<input type="range" id="slider-quality" min="0.1" max="1.0" step="0.01" value="0.8">
</div>
<div>
<div class="flex justify-between mb-1">
<label class="text-sm font-medium text-slate-300">Resolution Scale</label>
<span id="val-scale" class="text-sm font-bold text-indigo-400">100%</span>
</div>
<input type="range" id="slider-scale" min="0.1" max="1.0" step="0.05" value="1.0">
</div>
</div>
</div>
<!-- SECTION 2: Adjustments -->
<div class="glass-card p-6 rounded-2xl">
<div class="flex items-center justify-between mb-5">
<h2 class="text-sm font-bold text-indigo-400 uppercase tracking-wider">Color Tuning</h2>
<button id="btn-reset-edits" class="text-xs text-slate-500 hover:text-white transition-colors">Reset</button>
</div>
<div class="space-y-4">
<div>
<div class="flex justify-between text-sm">
<label class="font-medium text-slate-300">Brightness</label>
<span id="val-brightness" class="text-slate-400">100%</span>
</div>
<input type="range" id="slider-brightness" min="0" max="200" value="100">
</div>
<div>
<div class="flex justify-between text-sm">
<label class="font-medium text-slate-300">Contrast</label>
<span id="val-contrast" class="text-slate-400">100%</span>
</div>
<input type="range" id="slider-contrast" min="0" max="200" value="100">
</div>
<div>
<div class="flex justify-between text-sm">
<label class="font-medium text-slate-300">Saturation</label>
<span id="val-saturation" class="text-slate-400">100%</span>
</div>
<input type="range" id="slider-saturation" min="0" max="200" value="100">
</div>
<div>
<div class="flex justify-between text-sm">
<label class="font-medium text-slate-300">Hue Rotation</label>
<span id="val-hue" class="text-slate-400"></span>
</div>
<input type="range" id="slider-hue" min="0" max="360" value="0">
</div>
</div>
</div>
<!-- Info & Action -->
<div class="glass-card p-6 rounded-2xl">
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-black/30 rounded-xl p-3 text-center border border-white/5">
<div class="text-xs text-slate-400 mb-1">Original Size</div>
<div id="stat-original" class="text-lg font-bold text-white">0 MB</div>
</div>
<div class="bg-emerald-500/10 rounded-xl p-3 text-center border border-emerald-500/20">
<div class="text-xs text-emerald-400 mb-1">New Size</div>
<div id="stat-result" class="text-lg font-bold text-white">0 MB</div>
</div>
</div>
<button id="btn-process-video" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3.5 rounded-xl mb-4 hidden transition-all shadow-lg shadow-indigo-600/20">
Process & Encode Video
</button>
<button id="btn-download-single" class="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-3.5 rounded-xl transition-all shadow-lg shadow-emerald-600/20">
Download Output
</button>
</div>
</div>
<!-- Preview Column -->
<div class="lg:col-span-8 bg-black/40 border border-white/10 rounded-2xl flex flex-col items-center justify-center relative h-full min-h-[400px] overflow-hidden checker-bg shadow-inner">
<!-- Processing Overlay -->
<div id="preview-loading" class="absolute inset-0 flex flex-col items-center justify-center z-30 bg-black/60 backdrop-blur-sm hidden">
<div class="w-12 h-12 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mb-4"></div>
<div id="loading-text" class="text-xl font-bold text-white mb-4">Encoding...</div>
<div class="w-64 h-2 bg-slate-800 rounded-full overflow-hidden">
<div id="loading-bar-progress" class="h-full bg-indigo-500 transition-all duration-100" style="width:0%"></div>
</div>
</div>
<!-- Image Preview -->
<img id="preview-image" src="" alt="Preview" class="max-w-full max-h-full object-contain relative z-10 transition-opacity duration-300">
<!-- Video Preview -->
<video id="preview-video" src="" controls class="max-w-full max-h-full object-contain relative z-10 hidden w-full h-full"></video>
<div class="absolute top-4 left-4 z-20 bg-black/50 backdrop-blur-md text-white border border-white/10 text-xs font-bold px-3 py-1.5 rounded-lg flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-emerald-400"></span> Live Preview
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Application Logic -->
<script>
// --- State ---
const state = {
currentTab: 'image', // 'image' or 'video'
images: [],
videos: [],
activeImageId: null,
activeVideoId: null,
imageSettings: { format: 'image/jpeg', quality: 0.8, scale: 1.0 },
videoSettings: { format: 'video/webm', quality: 2.5, scale: 1.0 }, // quality = bitrate in Mbps
imageEdits: { brightness: 100, contrast: 100, saturation: 100, hue: 0 },
videoEdits: { brightness: 100, contrast: 100, saturation: 100, hue: 0 }
};
// --- DOM Elements ---
const els = {
body: document.body,
emptyState: document.getElementById('empty-state'),
editorState: document.getElementById('editor-state'),
fileMain: document.getElementById('file-upload-main'),
fileAdd: document.getElementById('add-more'),
galleryList: document.getElementById('gallery-list'),
galleryCount: document.getElementById('gallery-count'),
editorEmpty: document.getElementById('editor-empty'),
editorContent: document.getElementById('editor-content'),
activeFilename: document.getElementById('active-filename'),
activeDimensions: document.getElementById('active-dimensions'),
activeSavings: document.getElementById('active-savings'),
previewImage: document.getElementById('preview-image'),
previewVideo: document.getElementById('preview-video'),
previewLoading: document.getElementById('preview-loading'),
loadingText: document.getElementById('loading-text'),
loadingProgress: document.getElementById('loading-bar-progress'),
statOriginal: document.getElementById('stat-original'),
statResult: document.getElementById('stat-result'),
formatBtns: document.querySelectorAll('.format-btn'),
imgFormatSection: document.getElementById('image-format-section'),
vidFormatSection: document.getElementById('video-format-section'),
sQuality: document.getElementById('slider-quality'),
sScale: document.getElementById('slider-scale'),
sBrightness: document.getElementById('slider-brightness'),
sContrast: document.getElementById('slider-contrast'),
sSaturation: document.getElementById('slider-saturation'),
sHue: document.getElementById('slider-hue'),
lQuality: document.getElementById('quality-label'),
vQuality: document.getElementById('val-quality'),
vScale: document.getElementById('val-scale'),
vBrightness: document.getElementById('val-brightness'),
vContrast: document.getElementById('val-contrast'),
vSaturation: document.getElementById('val-saturation'),
vHue: document.getElementById('val-hue'),
btnResetEdits: document.getElementById('btn-reset-edits'),
btnApplyAll: document.getElementById('btn-apply-all'),
btnDownloadSingle: document.getElementById('btn-download-single'),
btnDownloadAll: document.getElementById('btn-download-all'),
btnProcessVideo: document.getElementById('btn-process-video'),
importLoadingStatus: document.getElementById('importLoadingStatus')
};
let debounceTimer = null;
const generateId = () => Math.random().toString(36).substr(2, 9);
const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 B';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
// --- Iframe Integration Bridge ---
async function autoLoadTransferredAssets() {
let urlsToLoad = [];
// Try extracting from window.parent (the Swapper)
try {
if (window.parent && window.parent !== window) {
// Method A: Swapper sets currentBatchUrls for multi-swap
if (window.parent.currentBatchUrls && window.parent.currentBatchUrls.length > 0) {
urlsToLoad = [...window.parent.currentBatchUrls];
} else {
// Method B: Swapper History Single Image Modal
const histImg = window.parent.document.getElementById('histModalResult');
if (histImg && histImg.src) urlsToLoad.push(histImg.src);
// Method C: Swapper Immediate Single Output Slider
const sliderRes = window.parent.document.getElementById('sliderAfter');
if (sliderRes) {
const img = sliderRes.querySelector('img');
if (img && img.src) urlsToLoad.push(img.src);
}
}
}
} catch(e) {
console.log("Not in iframe or cross-origin isolated.");
}
// Method D: Fallback to localStorage queue
try {
const ls = localStorage.getItem('compressor_queue');
if (ls) {
const parsed = JSON.parse(ls);
if (Array.isArray(parsed)) urlsToLoad = [...new Set([...urlsToLoad, ...parsed])];
localStorage.removeItem('compressor_queue'); // consume it
}
} catch(e) {}
urlsToLoad = [...new Set(urlsToLoad.filter(url => url.length > 0))];
if (urlsToLoad.length > 0) {
els.importLoadingStatus.classList.remove('hidden');
els.importLoadingStatus.innerText = `Importing ${urlsToLoad.length} assets from Studio...`;
const filePromises = urlsToLoad.map(async (url, idx) => {
try {
const res = await fetch(url);
const blob = await res.blob();
const ext = blob.type.split('/')[1] || 'png';
return new File([blob], `ShinyyOutput_${idx+1}.${ext}`, { type: blob.type });
} catch(e) { return null; }
});
const files = (await Promise.all(filePromises)).filter(Boolean);
if(files.length > 0) {
handleFiles(files);
}
els.importLoadingStatus.classList.add('hidden');
}
}
// --- Tab Switching ---
window.switchTab = (tabMode) => {
state.currentTab = tabMode;
// Update Tab Buttons UI
document.querySelectorAll('.tab-btn').forEach(btn => {
if(btn.dataset.tab === tabMode) btn.classList.add('active');
else btn.classList.remove('active');
});
// Update UI blocks based on mode
if (tabMode === 'image') {
els.imgFormatSection.classList.remove('hidden');
els.vidFormatSection.classList.add('hidden');
els.lQuality.innerText = "Compression Quality";
els.previewImage.classList.remove('hidden');
els.previewVideo.classList.add('hidden');
els.btnProcessVideo.classList.add('hidden');
els.btnApplyAll.classList.remove('hidden');
// Set Sliders to Image Settings
els.sQuality.min = "0.1"; els.sQuality.max = "1.0"; els.sQuality.step = "0.01";
} else {
els.imgFormatSection.classList.add('hidden');
els.vidFormatSection.classList.remove('hidden');
els.lQuality.innerText = "Target Bitrate (Mbps)";
els.previewImage.classList.add('hidden');
els.previewVideo.classList.remove('hidden');
els.btnProcessVideo.classList.remove('hidden');
els.btnApplyAll.classList.add('hidden');
// Set Sliders to Video Settings
els.sQuality.min = "0.5"; els.sQuality.max = "10.0"; els.sQuality.step = "0.5";
}
updateUI();
};
// --- Core Engines ---
// Image Engine
const processImageCanvas = (item, settings, edits) => {
return new Promise((resolve) => {
const img = new Image();
img.src = item.originalPreview;
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const newWidth = Math.max(1, Math.floor(item.originalDimensions.width * settings.scale));
const newHeight = Math.max(1, Math.floor(item.originalDimensions.height * settings.scale));
canvas.width = newWidth;
canvas.height = newHeight;
ctx.filter = `brightness(${edits.brightness}%) contrast(${edits.contrast}%) saturate(${edits.saturation}%) hue-rotate(${edits.hue}deg)`;
ctx.drawImage(img, 0, 0, newWidth, newHeight);
canvas.toBlob((blob) => {
if(!blob) return resolve(null);
if (item.compressedUrl) URL.revokeObjectURL(item.compressedUrl);
resolve({
compressedUrl: URL.createObjectURL(blob),
compressedSize: blob.size,
isProcessing: false,
status: 'done'
});
}, settings.format, parseFloat(settings.quality));
};
img.onerror = () => resolve({ isProcessing: false, status: 'error' });
});
};
// Video Engine (MediaRecorder)
const processVideoCanvas = (item, settings, edits) => {
return new Promise((resolve) => {
const video = document.createElement('video');
video.src = item.originalPreview;
video.muted = true;
video.crossOrigin = 'anonymous';
video.onloadedmetadata = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = Math.max(1, Math.floor(item.originalDimensions.width * settings.scale));
canvas.height = Math.max(1, Math.floor(item.originalDimensions.height * settings.scale));
const stream = canvas.captureStream(30);
const bitrate = parseFloat(settings.quality) * 1000000;
const options = { mimeType: 'video/webm', videoBitsPerSecond: bitrate };
let recorder;
try { recorder = new MediaRecorder(stream, options); }
catch (e) { recorder = new MediaRecorder(stream); }
const chunks = [];
recorder.ondataavailable = e => { if (e.data && e.data.size > 0) chunks.push(e.data); };
recorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
if (item.compressedUrl) URL.revokeObjectURL(item.compressedUrl);
resolve({
compressedUrl: URL.createObjectURL(blob),
compressedSize: blob.size,
isProcessing: false,
status: 'done'
});
};
video.play();
recorder.start();
const drawFrame = () => {
if (video.paused || video.ended) return;
ctx.filter = `brightness(${edits.brightness}%) contrast(${edits.contrast}%) saturate(${edits.saturation}%) hue-rotate(${edits.hue}deg)`;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const percent = (video.currentTime / video.duration) * 100;
els.loadingProgress.style.width = `${percent}%`;
els.loadingText.innerText = `ENCODING: ${Math.floor(percent)}%`;
requestAnimationFrame(drawFrame);
};
video.addEventListener('play', () => drawFrame());
video.addEventListener('ended', () => recorder.stop());
};
});
};
// --- Processing Triggers ---
const recompressActiveImage = () => {
if (state.currentTab !== 'image') return;
if (!state.activeImageId) return;
const item = state.images.find(i => i.id === state.activeImageId);
if (!item) return;
item.isProcessing = true;
updateUI();
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const result = await processImageCanvas(item, state.imageSettings, state.imageEdits);
if (result) Object.assign(item, result);
updateUI();
}, 150);
};
els.btnProcessVideo.addEventListener('click', async () => {
if (state.currentTab !== 'video' || !state.activeVideoId) return;
const item = state.videos.find(i => i.id === state.activeVideoId);
if (!item) return;
item.isProcessing = true;
els.previewVideo.pause();
updateUI();
const result = await processVideoCanvas(item, state.videoSettings, state.videoEdits);
if (result) Object.assign(item, result);
updateUI();
});
// --- File Handling ---
const handleFiles = (files) => {
if (!files || files.length === 0) return;
let switchedTab = false;
Array.from(files).forEach(file => {
const isImage = file.type.match('image.*');
const isVideo = file.type.match('video.*');
if (!isImage && !isVideo) return;
const id = generateId();
const objectUrl = URL.createObjectURL(file);
const processFileDimensions = (width, height) => {
const newItem = {
id, file, originalPreview: objectUrl,
originalSize: file.size,
originalDimensions: { width, height },
compressedUrl: null, compressedSize: 0,
isProcessing: isImage, // Images auto-process
status: 'pending'
};
if (isImage) {
if (state.images.length === 0) state.activeImageId = id;
state.images.push(newItem);
if (!switchedTab && state.currentTab !== 'image') {
switchTab('image'); switchedTab = true;
}
processImageCanvas(newItem, state.imageSettings, state.imageEdits).then(res => {
Object.assign(newItem, res);
updateUI();
});
} else if (isVideo) {
if (state.videos.length === 0) state.activeVideoId = id;
state.videos.push(newItem);
if (!switchedTab && state.currentTab !== 'video') {
switchTab('video'); switchedTab = true;
}
}
updateUI();
};
if (isImage) {
const img = new Image();
img.onload = () => processFileDimensions(img.width, img.height);
img.src = objectUrl;
} else if (isVideo) {
const vid = document.createElement('video');
vid.onloadedmetadata = () => processFileDimensions(vid.videoWidth, vid.videoHeight);
vid.src = objectUrl;
}
});
};
// --- UI Updates ---
const updateUI = () => {
const isImageTab = state.currentTab === 'image';
const itemsList = isImageTab ? state.images : state.videos;
const activeId = isImageTab ? state.activeImageId : state.activeVideoId;
const settings = isImageTab ? state.imageSettings : state.videoSettings;
const edits = isImageTab ? state.imageEdits : state.videoEdits;
// Global visibility
if (state.images.length === 0 && state.videos.length === 0) {
els.emptyState.classList.remove('hidden');
els.editorState.classList.add('hidden');
return;
} else {
els.emptyState.classList.add('hidden');
els.editorState.classList.remove('hidden');
}
// Sync Sliders
els.sQuality.value = settings.quality;
els.sScale.value = settings.scale;
els.sBrightness.value = edits.brightness;
els.sContrast.value = edits.contrast;
els.sSaturation.value = edits.saturation;
els.sHue.value = edits.hue;
// Sync Values
els.vQuality.innerText = isImageTab ? `${Math.round(settings.quality * 100)}%` : `${settings.quality} Mbps`;
els.vScale.innerText = `${Math.round(settings.scale * 100)}%`;
els.vBrightness.innerText = `${edits.brightness}%`;
els.vContrast.innerText = `${edits.contrast}%`;
els.vSaturation.innerText = `${edits.saturation}%`;
els.vHue.innerText = `${edits.hue}°`;
els.galleryCount.innerText = itemsList.length;
// Render Sidebar
els.galleryList.innerHTML = '';
itemsList.forEach(item => {
const isActive = item.id === activeId;
const div = document.createElement('div');
div.className = `glass-card p-2 rounded-xl cursor-pointer flex gap-3 items-center group ${isActive ? 'active' : ''}`;
div.onclick = () => {
if (isImageTab) state.activeImageId = item.id; else state.activeVideoId = item.id;
updateUI();
};
let sizeStr = `<span class="text-slate-400">${formatBytes(item.originalSize)}</span>`;
if (item.status === 'done') {
sizeStr += ` <svg class="w-3 h-3 text-slate-500 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> <span class="text-emerald-400 font-bold">${formatBytes(item.compressedSize)}</span>`;
}
const thumbHtml = isImageTab
? `<img src="${item.originalPreview}" class="w-full h-full object-cover">`
: `<video src="${item.originalPreview}#t=0.1" class="w-full h-full object-cover"></video>`;
div.innerHTML = `
<div class="w-12 h-12 rounded-lg bg-black/50 overflow-hidden relative shrink-0">
${thumbHtml}
${!isImageTab ? `<div class="absolute inset-0 flex justify-center items-center text-white bg-black/30"><svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M4 4l12 6-12 6z"/></svg></div>` : ''}
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold text-white truncate group-hover:text-indigo-300 transition-colors">${item.file.name}</div>
<div class="text-xs mt-0.5 flex items-center gap-1">${sizeStr}</div>
</div>
<div class="flex items-center gap-1 flex-col">
${item.isProcessing ? '<div class="w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>' : ''}
<button class="btn-rm-single text-slate-500 hover:text-rose-400 p-1 opacity-0 group-hover:opacity-100 transition-all"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg></button>
</div>
`;
div.querySelector('.btn-rm-single').onclick = (e) => {
e.stopPropagation();
if (isImageTab) {
state.images = state.images.filter(i => i.id !== item.id);
if (state.activeImageId === item.id) state.activeImageId = state.images.length > 0 ? state.images[0].id : null;
} else {
state.videos = state.videos.filter(i => i.id !== item.id);
if (state.activeVideoId === item.id) state.activeVideoId = state.videos.length > 0 ? state.videos[0].id : null;
}
updateUI();
};
els.galleryList.appendChild(div);
});
// Update Editor Workspace
const activeItem = itemsList.find(i => i.id === activeId);
if (!activeItem) {
els.editorEmpty.classList.remove('hidden');
els.editorContent.classList.add('hidden');
return;
}
els.editorEmpty.classList.add('hidden');
els.editorContent.classList.remove('hidden');
els.activeFilename.innerText = activeItem.file.name;
els.activeDimensions.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path></svg> ${Math.floor(activeItem.originalDimensions.width * settings.scale)} x ${Math.floor(activeItem.originalDimensions.height * settings.scale)}`;
let savings = 0;
if (activeItem.compressedSize > 0) savings = (100 - (activeItem.compressedSize / activeItem.originalSize * 100)).toFixed(1);
els.activeSavings.innerText = `Saved ${savings}%`;
if(savings < 0) { els.activeSavings.className = "text-amber-400 bg-amber-400/10 px-2 py-0.5 rounded-md"; els.activeSavings.innerText = `Larger ${Math.abs(savings)}%`; }
else { els.activeSavings.className = "text-emerald-400 bg-emerald-400/10 px-2 py-0.5 rounded-md"; }
// Setup Preview
if (isImageTab) {
els.previewImage.src = activeItem.compressedUrl || activeItem.originalPreview;
els.previewImage.style.filter = '';
if (activeItem.isProcessing) {
els.previewLoading.classList.remove('hidden');
els.loadingProgress.style.width = `100%`;
els.loadingText.innerText = "Applying Filter...";
els.previewImage.classList.add('opacity-30');
els.btnDownloadSingle.disabled = true;
els.btnDownloadSingle.innerText = "Processing...";
els.btnDownloadSingle.classList.add('opacity-50', 'cursor-not-allowed');
} else {
els.previewLoading.classList.add('hidden');
els.previewImage.classList.remove('opacity-30');
els.btnDownloadSingle.disabled = false;
els.btnDownloadSingle.innerText = "Download Output";
els.btnDownloadSingle.classList.remove('opacity-50', 'cursor-not-allowed');
}
} else {
const videoTarget = activeItem.compressedUrl || activeItem.originalPreview;
if(els.previewVideo.getAttribute('data-src') !== videoTarget) {
els.previewVideo.src = videoTarget;
els.previewVideo.setAttribute('data-src', videoTarget);
}
if (!activeItem.compressedUrl) {
els.previewVideo.style.filter = `brightness(${edits.brightness}%) contrast(${edits.contrast}%) saturate(${edits.saturation}%) hue-rotate(${edits.hue}deg)`;
} else {
els.previewVideo.style.filter = '';
}
if (activeItem.isProcessing) {
els.previewLoading.classList.remove('hidden');
els.previewVideo.classList.add('opacity-30');
els.btnProcessVideo.disabled = true;
els.btnDownloadSingle.disabled = true;
els.btnProcessVideo.innerText = "Encoding...";
els.btnProcessVideo.classList.add('opacity-50', 'cursor-not-allowed');
} else {
els.previewLoading.classList.add('hidden');
els.previewVideo.classList.remove('opacity-30');
els.btnProcessVideo.disabled = false;
els.btnProcessVideo.innerText = "Process & Encode Video";
els.btnProcessVideo.classList.remove('opacity-50', 'cursor-not-allowed');
els.btnDownloadSingle.disabled = !activeItem.compressedUrl;
els.btnDownloadSingle.innerText = activeItem.compressedUrl ? "Download Video" : "Requires Encoding";
if(!activeItem.compressedUrl) els.btnDownloadSingle.classList.add('opacity-50', 'cursor-not-allowed');
else els.btnDownloadSingle.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
els.statOriginal.innerText = formatBytes(activeItem.originalSize);
els.statResult.innerText = activeItem.compressedSize > 0 ? formatBytes(activeItem.compressedSize) : 'Pending';
if (isImageTab && settings.format === 'image/png') els.sQuality.disabled = true;
else els.sQuality.disabled = false;
};
// --- Event Listeners ---
['dragenter', 'dragover'].forEach(evt => {
window.addEventListener(evt, e => { e.preventDefault(); els.body.classList.add('dragging'); });
});
['dragleave', 'drop'].forEach(evt => {
window.addEventListener(evt, e => {
e.preventDefault();
if(evt === 'dragleave' && e.clientX > 0 && e.clientY > 0 && e.clientX < window.innerWidth && e.clientY < window.innerHeight) return;
els.body.classList.remove('dragging');
});
});
window.addEventListener('drop', e => handleFiles(e.dataTransfer.files));
els.fileMain.addEventListener('change', e => handleFiles(e.target.files));
els.fileAdd.addEventListener('change', e => handleFiles(e.target.files));
els.formatBtns.forEach(btn => {
btn.addEventListener('click', () => {
if (state.currentTab !== 'image') return;
els.formatBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.imageSettings.format = btn.dataset.format;
recompressActiveImage();
});
});
const bindSlider = (sliderEl, stateKey) => {
sliderEl.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
const isImg = state.currentTab === 'image';
if (stateKey === 'quality' || stateKey === 'scale') {
if (isImg) state.imageSettings[stateKey] = val;
else state.videoSettings[stateKey] = val;
} else {
if (isImg) state.imageEdits[stateKey] = val;
else state.videoEdits[stateKey] = val;
}
if (isImg) recompressActiveImage();
else updateUI();
});
};
bindSlider(els.sQuality, 'quality');
bindSlider(els.sScale, 'scale');
bindSlider(els.sBrightness, 'brightness');
bindSlider(els.sContrast, 'contrast');
bindSlider(els.sSaturation, 'saturation');
bindSlider(els.sHue, 'hue');
els.btnResetEdits.addEventListener('click', () => {
const defaultEdits = { brightness: 100, contrast: 100, saturation: 100, hue: 0 };
if (state.currentTab === 'image') {
state.imageEdits = { ...defaultEdits };
recompressActiveImage();
} else {
state.videoEdits = { ...defaultEdits };
updateUI();
}
});
// Apply All (Image Only)
els.btnApplyAll.addEventListener('click', async () => {
if (state.currentTab !== 'image') return;
state.images.forEach(i => {
if (i.id !== state.activeImageId) i.isProcessing = true;
});
updateUI();
for (let i = 0; i < state.images.length; i++) {
const item = state.images[i];
if (item.id === state.activeImageId) continue;
const result = await processImageCanvas(item, state.imageSettings, state.imageEdits);
if (result) Object.assign(item, result);
}
updateUI();
});
const downloadFile = (item, isImage) => {
if (!item.compressedUrl) return;
const link = document.createElement('a');
link.href = item.compressedUrl;
let ext = 'webm';
if (isImage) {
const mime = state.imageSettings.format;
if(mime === 'image/jpeg') ext = 'jpg';
else if(mime === 'image/png') ext = 'png';
else if(mime === 'image/webp') ext = 'webp';
}
link.download = `ShinyyOptimized_${item.file.name.split('.')[0]}.${ext}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
els.btnDownloadSingle.addEventListener('click', () => {
const isImg = state.currentTab === 'image';
const list = isImg ? state.images : state.videos;
const id = isImg ? state.activeImageId : state.activeVideoId;
const activeItem = list.find(i => i.id === id);
if(activeItem && !activeItem.isProcessing && activeItem.compressedUrl) downloadFile(activeItem, isImg);
});
els.btnDownloadAll.addEventListener('click', async () => {
const isImg = state.currentTab === 'image';
const itemsList = isImg ? state.images : state.videos;
const itemsToDownload = itemsList.filter(item => !item.isProcessing && item.compressedUrl);
for (let i = 0; i < itemsToDownload.length; i++) {
downloadFile(itemsToDownload[i], isImg);
await new Promise(resolve => setTimeout(resolve, 300));
}
});
// Boot process
window.addEventListener('DOMContentLoaded', () => {
switchTab('image');
autoLoadTransferredAssets();
});
</script>
</body>
</html>