';
} else if(item.status === 'error'){
sizeRowHTML = '
' + (item.error || 'Could not compress this image.') + '
';
} else {
sizeRowHTML = '
' + formatBytes(item.originalSize) + '
';
}
var downloadDisabled = item.status !== 'done';
card.innerHTML =
'
' +
'
' +
'
' +
badgeHTML +
'
' +
'
' + item.file.name + '
' +
sizeRowHTML +
'
';
card.querySelector('[data-action="remove"]').addEventListener('click', function(){
removeItem(item.id);
});
var dlBtn = card.querySelector('[data-action="download"]');
if(dlBtn && !downloadDisabled){
dlBtn.addEventListener('click', function(){
downloadSingle(item);
});
}
return card;
}
function updateCardInDom(item){
var existing = resultsGrid.querySelector('[data-id="' + item.id + '"]');
var fresh = buildCard(item);
if(existing){
resultsGrid.replaceChild(fresh, existing);
} else {
resultsGrid.appendChild(fresh);
}
}
function removeItem(id){
var idx = items.findIndex(function(i){ return i.id === id; });
if(idx === -1) return;
var item = items[idx];
if(item.originalURL) URL.revokeObjectURL(item.originalURL);
if(item.compressedURL) URL.revokeObjectURL(item.compressedURL);
items.splice(idx, 1);
var node = resultsGrid.querySelector('[data-id="' + id + '"]');
if(node) node.remove();
if(items.length === 0){
settingsPanel.classList.remove('visible');
statsBar.classList.remove('visible');
}
updateStats();
}
/* ---------- Stats ---------- */ function updateStats(){
var totalFiles = items.length;
var doneItems = items.filter(function(i){ return i.status === 'done'; });
var originalTotal = items.reduce(function(sum,i){ return sum + i.originalSize; }, 0);
// Pending/error items count toward "compressed" total as their original size (no savings yet)
var displayedCompressed = items.reduce(function(sum,i){
return sum + (i.status === 'done' ? i.compressedSize : i.originalSize);
}, 0);
var saved = Math.max(0, originalTotal - displayedCompressed);
var pct = originalTotal > 0 ? Math.round((saved / originalTotal) * 100) : 0;
statFiles.textContent = totalFiles;
statOriginal.textContent = formatBytes(originalTotal);
statCompressed.textContent = formatBytes(displayedCompressed);
statSaved.textContent = formatBytes(saved);
var offset = RING_CIRCUMFERENCE - (RING_CIRCUMFERENCE * pct / 100);
ringFg.style.strokeDashoffset = offset;
ringLabel.textContent = pct + '%';
if(totalFiles > 0){
statsBar.classList.add('visible');
}
downloadAllBtn.disabled = doneItems.length === 0;
}
/* ---------- Compression core ---------- */ function loadImage(file){
return new Promise(function(resolve, reject){
var url = URL.createObjectURL(file);
var img = new Image();
img.onload = function(){ resolve({ img: img, url: url }); };
img.onerror = function(){ URL.revokeObjectURL(url); reject(new Error('Could not read this image.')); };
img.src = url;
});
}
function compressOne(item){
var quality = parseInt(qualityRange.value, 10) / 100;
var formatChoice = formatSelect.value;
var maxWidth = resizeToggle.checked ? parseInt(maxWidthInput.value, 10) : 0;
return loadImage(item.file).then(function(res){
var img = res.img;
var width = img.naturalWidth;
var height = img.naturalHeight;
if(maxWidth && maxWidth > 0 && width > maxWidth){
height = Math.round(height * (maxWidth / width));
width = maxWidth;
}
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
var mime = formatChoice === 'keep' ? (item.file.type || 'image/jpeg') : formatChoice;
if(mime === 'image/jpeg'){
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, width, height);
}
ctx.drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(res.url);
var qParam = (mime === 'image/png') ? undefined : quality;
return new Promise(function(resolve, reject){
canvas.toBlob(function(blob){
if(!blob){ reject(new Error('Compression failed for this image.')); return; }
resolve({ blob: blob, mime: mime, width: width, height: height });
}, mime, qParam);
});
});
}
function runCompression(){
if(items.length === 0) return;
compressBtn.disabled = true;
var queue = items.slice();
var i = 0;
function next(){
if(i >= queue.length){
compressBtn.disabled = false;
announce('Compression complete.');
return;
}
var item = queue[i];
item.status = 'processing';
item.error = null;
updateCardInDom(item);
compressOne(item).then(function(result){
if(item.compressedURL) URL.revokeObjectURL(item.compressedURL);
item.compressedBlob = result.blob;
item.compressedSize = result.blob.size;
item.compressedURL = URL.createObjectURL(result.blob);
item.outWidth = result.width;
item.outHeight = result.height;
item.status = 'done';
updateCardInDom(item);
updateStats();
i++;
next();
}).catch(function(err){
item.status = 'error';
item.error = err && err.message ? err.message : 'Compression failed.';
updateCardInDom(item);
updateStats();
i++;
next();
});
}
next();
}
compressBtn.addEventListener('click', runCompression);
/* ---------- Downloads ---------- */ function downloadFileName(item){
var mime = item.compressedBlob ? item.compressedBlob.type : item.file.type;
var ext = extFromMime(mime) || ('.' + (item.file.name.split('.').pop() || 'img'));
return baseName(item.file.name) + '-compressed' + ext;
}
function downloadSingle(item){
if(!item.compressedBlob) return;
var a = document.createElement('a');
a.href = item.compressedURL;
a.download = downloadFileName(item);
document.body.appendChild(a);
a.click();
a.remove();
}
downloadAllBtn.addEventListener('click', function(){
var doneItems = items.filter(function(i){ return i.status === 'done'; });
if(doneItems.length === 0) return;
if(typeof JSZip === 'undefined'){
announce('Could not load ZIP library — downloading files individually instead.');
doneItems.forEach(function(item){ downloadSingle(item); });
return;
}
downloadAllBtn.disabled = true;
var originalLabel = downloadAllBtn.innerHTML;
downloadAllBtn.innerHTML = '
Zipping...';
var zip = new JSZip();
var usedNames = {};
doneItems.forEach(function(item){
var name = downloadFileName(item);
if(usedNames[name]){
usedNames[name]++;
var dot = name.lastIndexOf('.');
name = name.slice(0, dot) + '-' + usedNames[name] + name.slice(dot);
} else {
usedNames[name] = 1;
}
zip.file(name, item.compressedBlob);
});
zip.generateAsync({ type: 'blob' }).then(function(content){
var url = URL.createObjectURL(content);
var a = document.createElement('a');
a.href = url;
a.download = 'compressed-images.zip';
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function(){ URL.revokeObjectURL(url); }, 4000);
downloadAllBtn.disabled = false;
downloadAllBtn.innerHTML = originalLabel;
announce('ZIP file downloaded.');
}).catch(function(){
downloadAllBtn.disabled = false;
downloadAllBtn.innerHTML = originalLabel;
announce('Could not create ZIP file. Please try downloading images individually.');
});
});
/* ---------- Clear all ---------- */ clearAllBtn.addEventListener('click', function(){
items.forEach(function(item){
if(item.originalURL) URL.revokeObjectURL(item.originalURL);
if(item.compressedURL) URL.revokeObjectURL(item.compressedURL);
});
items = [];
resultsGrid.innerHTML = '';
settingsPanel.classList.remove('visible');
statsBar.classList.remove('visible');
downloadAllBtn.disabled = true;
ringFg.style.strokeDashoffset = RING_CIRCUMFERENCE;
ringLabel.textContent = '0%';
announce('All images cleared.');
});
})();