tailwind.config = { theme: { extend: { fontFamily: { sans: ['Roboto', 'sans-serif'], }, colors: { theme: { 500: 'rgb(var(--theme-500) / )', // Main accent 200: 'rgb(var(--theme-200) / )', // Light accent 100: 'rgb(var(--theme-100) / )', // Lighter accent 900: 'rgb(var(--theme-900) / )', // Dark/Shadow }, surface: '#171717', background: '#000000', } } } } function triggerSadness(title = "Processing Failed", msg = "Something went wrong.") { const overlay = document.getElementById('sadOverlay'); const card = document.getElementById('sadCard'); const container = document.getElementById('sadParticles'); const icon = document.getElementById('sadIcon'); document.getElementById('sadTitle').textContent = title; document.getElementById('sadMessage').textContent = msg; container.innerHTML = ''; // Clear old particles if (currentTheme === 'fire') { icon.textContent = 'local_fire_department'; // Broken fire vibe icon.classList.add('grayscale'); // Make fire look "burnt out" spawnAsh(container); } else { icon.textContent = 'cloud_off'; // No cloud/rain vibe icon.classList.remove('grayscale'); spawnRain(container); } overlay.classList.remove('hidden'); setTimeout(() => { overlay.classList.remove('opacity-0'); card.classList.remove('translate-y-10', 'opacity-0'); card.classList.add('shake-gentle'); // Add sad shake }, 50); if (navigator.vibrate) navigator.vibrate([50, 50, 50]); } function closeSadness() { const overlay = document.getElementById('sadOverlay'); const card = document.getElementById('sadCard'); overlay.classList.add('opacity-0'); card.classList.add('translate-y-10', 'opacity-0'); setTimeout(() => { overlay.classList.add('hidden'); document.getElementById('sadParticles').innerHTML = ''; // Stop animation document.getElementById('sadIcon').classList.remove('grayscale'); }, 500); } function retrySadness() { closeSadness(); setTimeout(() => { document.getElementById('compressBtn').click(); }, 600); } function spawnAsh(container) { for(let i=0; i<30; i++) { const ash = document.createElement('div'); ash.classList.add('sad-particle', 'ash'); ash.style.left = Math.random() * 100 + '%'; ash.style.width = (Math.random() * 4 + 2) + 'px'; ash.style.height = ash.style.width; ash.style.animationDuration = (Math.random() * 3 + 2) + 's'; ash.style.animationDelay = (Math.random() * 2) + 's'; container.appendChild(ash); } } function spawnRain(container) { for(let i=0; i<50; i++) { const drop = document.createElement('div'); drop.classList.add('sad-particle', 'rain'); drop.style.left = Math.random() * 100 + '%'; drop.style.height = (Math.random() * 15 + 10) + 'px'; drop.style.animationDuration = (Math.random() * 1 + 0.5) + 's'; // Fast rain drop.style.animationDelay = (Math.random() * 2) + 's'; container.appendChild(drop); } } const sleep = (ms) => new Promise(r => setTimeout(r, ms)); let currentTheme = 'fire'; // 'fire' or 'ice' function setAppTheme(themeName) { currentTheme = themeName; document.documentElement.setAttribute('data-theme', themeName); localStorage.setItem('settings_theme', themeName); const btnFire = document.getElementById('btnThemeFire'); const btnIce = document.getElementById('btnThemeIce'); if (themeName === 'fire') { btnFire.classList.add('active'); btnIce.classList.remove('active'); updateSidebarText("Fire Edition", "local_fire_department"); updateButtonIcon("local_fire_department"); } else { btnIce.classList.add('active'); btnFire.classList.remove('active'); updateSidebarText("Ice Edition", "ac_unit"); updateButtonIcon("ac_unit"); } renderHistory(); } function updateSidebarText(edition, icon) { document.getElementById('sidebarEditionText').textContent = `Version 2.4.0 • ${edition}`; document.getElementById('sidebarIcon').textContent = icon; } function updateButtonIcon(icon) { const btn = document.getElementById('compressBtn'); const span = btn.querySelector('.material-icons-round'); if(span) span.textContent = icon; } const celebrationContainer = document.getElementById('celebrationContainer'); const congratsText = document.getElementById('congratsText'); function triggerFullCelebration() { celebrationContainer.classList.remove('hidden'); congratsText.classList.remove('hidden'); congratsText.classList.remove('fade-out'); spawnBalloons(20); fireConfettiBurst(); setTimeout(() => { congratsText.classList.add('fade-out'); const balloons = document.querySelectorAll('.balloon'); balloons.forEach(b => b.remove()); setTimeout(() => { celebrationContainer.classList.add('hidden'); congratsText.classList.add('hidden'); congratsText.classList.remove('fade-out'); }, 500); }, 3000); } function fireConfettiBurst() { const fireColors = ['#FF4500', '#F59E0B', '#B91C1C', '#FFFFFF']; const iceColors = ['#06B6D4', '#2563EB', '#A5F3FC', '#FFFFFF']; const colors = currentTheme === 'fire' ? fireColors : iceColors; confetti({ particleCount: 150, spread: 100, origin: { y: 0.6 }, zIndex: 150, colors: colors }); setTimeout(() => { const end = Date.now() + 1000; ( function frame() { confetti({ particleCount: 5, angle: 270, spread: 180, origin: { y: -0.1, x: Math.random() }, zIndex: 150, colors: colors }); if (Date.now() < end) requestAnimationFrame(frame); }()); }, 200); } function spawnBalloons(count) { const fireBalloons = ['#FF4500', '#FFD700', '#DC2626', '#F97316']; const iceBalloons = ['#06B6D4', '#3B82F6', '#67E8F9', '#1E40AF']; const colors = currentTheme === 'fire' ? fireBalloons : iceBalloons; for (let i = 0; i < count; i++) { const b = document.createElement('div'); b.className = 'balloon'; const color = colors[Math.floor(Math.random() * colors.length)]; const left = Math.random() * 90 + 5; const delay = Math.random() * 0.5; const duration = 2 + Math.random() * 1.5; const scale = 0.8 + Math.random() * 0.4; b.style.backgroundColor = color; b.style.color = color; b.style.left = `${left}%`; b.style.animationDelay = `${delay}s`; b.style.animationDuration = `${duration}s`; b.style.transform = `scale(${scale})`; celebrationContainer.appendChild(b); } } const DB_NAME = 'CompressorDB'; const DB_VERSION = 1; const STORE_NAME = 'files'; let db; function initDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = (e) => { const db = e.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'id' }); } }; request.onsuccess = (e) => { db = e.target.result; resolve(db); }; request.onerror = (e) => reject(e); }); } async function saveFile(id, blob) { if (!db) await initDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); store.put({ id, blob }); tx.oncomplete = () => resolve(); tx.onerror = (e) => reject(e); }); } async function getFile(id) { if (!db) await initDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const req = store.get(id); req.onsuccess = () => resolve(req.result ? req.result.blob : null); req.onerror = () => reject(null); }); } async function deleteFile(id) { if (!db) await initDB(); const tx = db.transaction(STORE_NAME, 'readwrite'); tx.objectStore(STORE_NAME).delete(id); } async function clearFiles() { if (!db) await initDB(); const tx = db.transaction(STORE_NAME, 'readwrite'); tx.objectStore(STORE_NAME).clear(); } const HISTORY_KEY = 'compressor_history_v1'; let historySelectionMode = false; let selectedHistoryIds = new Set(); let longPressTimer; let isHistoryEnabled = true; function formatBytes(bytes, decimals = 1) { 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]; } function getHistory() { try { const h = localStorage.getItem(HISTORY_KEY); return h ? JSON.parse(h) : []; } catch(e) { return []; } } async function addToHistory(originalFile, compressedBlob) { if (!isHistoryEnabled) return; const id = Date.now(); try { await saveFile(id, compressedBlob); } catch (e) { console.error("Storage error", e); } const history = getHistory(); const savings = Math.max(0, originalFile.size - compressedBlob.size); const percent = ((savings / originalFile.size) * 100).toFixed(0); const newItem = { id: id, name: originalFile.name, type: originalFile.type, originalSize: originalFile.size, compressedSize: compressedBlob.size, percentSaved: percent, timestamp: new Date().toLocaleDateString() }; history.unshift(newItem); localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); } function startLongPress(e, id) { if (historySelectionMode) return; longPressTimer = setTimeout(() => { historySelectionMode = true; selectedHistoryIds.add(id); if (navigator.vibrate) navigator.vibrate(50); renderHistory(); }, 350); } function cancelLongPress() { clearTimeout(longPressTimer); } function handleItemClick(id) { if (!historySelectionMode) return; if (selectedHistoryIds.has(id)) { selectedHistoryIds.delete(id); if (selectedHistoryIds.size === 0) historySelectionMode = false; } else { selectedHistoryIds.add(id); } renderHistory(); } function exitSelectionMode() { historySelectionMode = false; selectedHistoryIds.clear(); renderHistory(); } async function deleteSelected() { if (selectedHistoryIds.size === 0) return; let history = getHistory(); const idsToDelete = Array.from(selectedHistoryIds); for (const id of idsToDelete) { await deleteFile(id); } history = history.filter(item => !selectedHistoryIds.has(item.id)); localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); exitSelectionMode(); showSnackbar(`${idsToDelete.length} items deleted`, 'delete'); } function renderHistory() { const list = document.getElementById('historyListContainer'); const footer = document.getElementById('historyFooter'); const items = getHistory(); if (items.length === 0) { list.innerHTML = `
folder_off No history yet
`; footer.innerHTML = ``; return; } list.innerHTML = items.map(item => { const isSelected = selectedHistoryIds.has(item.id); const selectClass = isSelected ? 'history-item-selected ring-1 ring-theme-500' : 'bg-neutral-800/40 hover:bg-neutral-800/60 border-neutral-800'; const icon = isSelected ? 'check_circle' : (item.type.includes('pdf') ? 'picture_as_pdf' : (item.type.includes('image') ? 'image' : 'description')); const iconColor = isSelected ? 'text-theme-500' : 'text-theme-500'; return `
${icon}

${item.name}

${formatBytes(item.originalSize)} ${formatBytes(item.compressedSize)} ${item.timestamp}
${!historySelectionMode ? ` -${item.percentSaved}% ` : `
${isSelected ? 'check' : ''}
`}
`}).join(''); if (historySelectionMode) { const count = selectedHistoryIds.size; footer.innerHTML = `
`; } else { footer.innerHTML = ` `; } } async function downloadHistoryItem(id, originalName) { const blob = await getFile(id); if (blob) { const prefix = "compressed_"; const filename = originalName.startsWith(prefix) ? originalName : prefix + originalName; showResult(blob, filename); } else { showSnackbar("File no longer available", "error_outline"); } } async function clearHistory() { showConfirmation("This will permanently remove all your saved files and history logs.", async () => { localStorage.removeItem(HISTORY_KEY); await clearFiles(); renderHistory(); showSnackbar('History & files cleared', 'delete'); }); } let pendingAction = null; function showConfirmation(msg, action) { const overlay = document.getElementById('confirmationModalOverlay'); const msgEl = document.getElementById('confirmationMessage'); const confirmBtn = document.getElementById('confirmActionBtn'); const modalBody = overlay.querySelector('div'); // select the inner card if(msg) msgEl.textContent = msg; pendingAction = action; overlay.classList.remove('hidden'); setTimeout(() => { overlay.classList.remove('opacity-0'); modalBody.classList.remove('scale-95'); modalBody.classList.add('scale-100'); }, 10); confirmBtn.onclick = () => { if (pendingAction) pendingAction(); closeConfirmation(); }; } function closeConfirmation() { const overlay = document.getElementById('confirmationModalOverlay'); const modalBody = overlay.querySelector('div'); overlay.classList.add('opacity-0'); modalBody.classList.remove('scale-100'); modalBody.classList.add('scale-95'); setTimeout(() => { overlay.classList.add('hidden'); pendingAction = null; }, 300); } function toggleHistory() { if (!isHistoryEnabled) { showSnackbar("Save history is disabled", "history_toggle_off"); return; } const modal = document.getElementById('historyModal'); const overlay = document.getElementById('historyModalOverlay'); if (!document.getElementById('sidebar').classList.contains('-translate-x-full')) toggleSidebar(); if (overlay.classList.contains('hidden')) { renderHistory(); overlay.classList.remove('hidden'); setTimeout(() => { overlay.classList.remove('opacity-0'); modal.classList.remove('scale-95'); modal.classList.add('scale-100'); }, 10); } else { exitSelectionMode(); overlay.classList.add('opacity-0'); modal.classList.remove('scale-100'); modal.classList.add('scale-95'); setTimeout(() => { overlay.classList.add('hidden'); }, 300); } } function toggleSettings() { const modal = document.getElementById('settingsModal'); const overlay = document.getElementById('settingsModalOverlay'); if (!document.getElementById('sidebar').classList.contains('-translate-x-full')) toggleSidebar(); if (overlay.classList.contains('hidden')) { overlay.classList.remove('hidden'); setTimeout(() => { overlay.classList.remove('opacity-0'); modal.classList.remove('scale-95'); modal.classList.add('scale-100'); }, 10); } else { overlay.classList.add('opacity-0'); modal.classList.remove('scale-100'); modal.classList.add('scale-95'); setTimeout(() => { overlay.classList.add('hidden'); }, 300); } } function toggleSaveHistory(el) { isHistoryEnabled = el.checked; localStorage.setItem('settings_save_history', isHistoryEnabled); updateHistoryButtonState(); showSnackbar(isHistoryEnabled ? "History saving enabled" : "History saving disabled", "settings"); } function updateHistoryButtonState() { const btn = document.getElementById('sidebarHistoryBtn'); if (!isHistoryEnabled) { btn.classList.add('btn-disabled'); btn.querySelector('span.material-icons-round').classList.remove('text-theme-500'); btn.querySelector('span.material-icons-round').classList.add('text-gray-500'); } else { btn.classList.remove('btn-disabled'); btn.querySelector('span.material-icons-round').classList.remove('text-gray-500'); btn.querySelector('span.material-icons-round').classList.add('text-theme-500'); } } function toggleSidebar() { const sidebar = document.getElementById('sidebar'); const overlay = document.getElementById('sidebarOverlay'); if (sidebar.classList.contains('-translate-x-full')) { sidebar.classList.remove('-translate-x-full'); overlay.classList.remove('hidden'); setTimeout(() => overlay.classList.remove('opacity-0'), 10); } else { sidebar.classList.add('-translate-x-full'); overlay.classList.add('opacity-0'); setTimeout(() => overlay.classList.add('hidden'), 300); } } function toggleMoreMenu(e) { if(e) e.stopPropagation(); const menu = document.getElementById('moreMenu'); if (menu.classList.contains('scale-0')) menu.classList.remove('scale-0', 'opacity-0'); else menu.classList.add('scale-0', 'opacity-0'); } function resetApp() { fileInput.value = ""; fileNameDisplay.textContent = "Tap to select file"; fileNameDisplay.classList.add("text-gray-300"); fileNameDisplay.classList.remove("text-white"); fileIconContainer.innerHTML = `cloud_upload`; desiredSizeInput.value = ""; selectPreset(medBtn, 0.6, 5); toggleMoreMenu(); showSnackbar("App reset successfully", "refresh"); } function showHelp() { toggleMoreMenu(); showSnackbar("Select a file and choose a compression level.", "help"); } document.addEventListener('click', (e) => { const unitDropdown = document.getElementById('customUnitDropdown'); const unitOpts = document.getElementById('unitOptions'); const unitArrow = document.getElementById('unitArrow'); if (unitDropdown && !unitDropdown.contains(e.target) && !unitOpts.classList.contains('hidden')) { unitOpts.classList.add('hidden'); unitArrow.classList.remove('rotate-180'); } const moreMenu = document.getElementById('moreMenu'); const btnMore = document.getElementById('btnMore'); if (moreMenu && !moreMenu.contains(e.target) && e.target !== btnMore && !btnMore.contains(e.target) && !moreMenu.classList.contains('scale-0')) { moreMenu.classList.add('scale-0', 'opacity-0'); } }); const activeChipClass = 'bg-theme-500/10 border-theme-500 text-theme-500 ring-1 ring-theme-500/20'; const inactiveChipClass = 'border-neutral-700 text-gray-400'; let currentMode = 'preset'; const fileInput = document.getElementById("fileInput"); const fileNameDisplay = document.getElementById("fileNameDisplay"); const fileIconContainer = document.getElementById("fileIconContainer"); const tabPreset = document.getElementById("tabPreset"); const tabCustom = document.getElementById("tabCustom"); const presetSection = document.getElementById("presetSection"); const customSection = document.getElementById("customSection"); const desiredSizeInput = document.getElementById("desiredSize"); let currentUnit = 'KB'; const compressBtn = document.getElementById("compressBtn"); const cancelBtn = document.getElementById("cancelBtn"); const loadingOverlay = document.getElementById("loadingOverlay"); const progressBar = document.getElementById("progressBar"); const progressStatus = document.getElementById("progressStatus"); const progressDetails = document.getElementById("progressDetails"); const timerText = document.getElementById("timerText"); const snackbar = document.getElementById("snackbar"); const snackMsg = document.getElementById("snackMsg"); const snackIcon = document.getElementById("snackIcon"); const maxBtn = document.getElementById("maxBtn"); const medBtn = document.getElementById("medBtn"); const minBtn = document.getElementById("minBtn"); const presetButtons = [maxBtn, medBtn, minBtn]; let presetQuality = null; let presetLevel = null; let isCancelled = false; let startTime = 0; function toggleUnitDropdown() { const opts = document.getElementById('unitOptions'); const arrow = document.getElementById('unitArrow'); const isHidden = opts.classList.contains('hidden'); if (isHidden) { opts.classList.remove('hidden'); arrow.classList.add('rotate-180'); } else { opts.classList.add('hidden'); arrow.classList.remove('rotate-180'); } } function selectUnit(unit) { currentUnit = unit; document.getElementById('selectedUnitText').textContent = unit; const opts = document.getElementById('unitOptions'); const arrow = document.getElementById('unitArrow'); opts.classList.add('hidden'); arrow.classList.remove('rotate-180'); } const notificationWrapper = document.getElementById('notificationWrapper'); const topNotification = document.getElementById('topNotification'); let notificationTimeout; function showTopNotification() { notificationWrapper.classList.remove('-translate-y-[200%]'); notificationWrapper.classList.add('translate-y-0'); resetTimer(); } function hideTopNotification() { notificationWrapper.classList.remove('translate-y-0'); notificationWrapper.classList.add('-translate-y-[200%]'); } function resetTimer() { clearTimeout(notificationTimeout); notificationTimeout = setTimeout(hideTopNotification, 3000); } topNotification.addEventListener('click', () => { hideTopNotification(); }); async function init() { selectPreset(medBtn, 0.6, 5); await initDB(); const savedSetting = localStorage.getItem('settings_save_history'); if(savedSetting !== null) { isHistoryEnabled = savedSetting === 'true'; } document.getElementById('historyToggle').checked = isHistoryEnabled; updateHistoryButtonState(); const savedTheme = localStorage.getItem('settings_theme'); if (savedTheme) { setAppTheme(savedTheme); } else { setAppTheme('fire'); } const splash = document.getElementById('nativeSplash'); setTimeout(() => { if(splash) { splash.classList.add('opacity-0', 'pointer-events-none'); setTimeout(() => { splash.style.display = 'none'; }, 500); } }, 1000); } async function init() { updateHistoryButtonState(); const savedTheme = localStorage.getItem('settings_theme'); if (savedTheme) setAppTheme(savedTheme); else setAppTheme('fire'); startOnboardingTour(); const splash = document.getElementById('nativeSplash'); } function switchMode(mode) { currentMode = mode; if (mode === 'preset') { tabPreset.classList.add('bg-neutral-700', 'text-white'); tabPreset.classList.remove('text-gray-400', 'hover:bg-neutral-700/50'); tabCustom.classList.remove('bg-neutral-700', 'text-white'); tabCustom.classList.add('text-gray-400', 'hover:bg-neutral-700/50'); presetSection.classList.remove('hidden'); customSection.classList.add('hidden'); desiredSizeInput.value = ""; } else { tabCustom.classList.add('bg-neutral-700', 'text-white'); tabCustom.classList.remove('text-gray-400', 'hover:bg-neutral-700/50'); tabPreset.classList.remove('bg-neutral-700', 'text-white'); tabPreset.classList.add('text-gray-400', 'hover:bg-neutral-700/50'); customSection.classList.remove('hidden'); presetSection.classList.add('hidden'); presetQuality = null; presetLevel = null; presetButtons.forEach(btn => { btn.classList.remove(...activeChipClass.split(' ')); btn.classList.add(...inactiveChipClass.split(' ')); }); } } function selectPreset(btn, quality, level) { if(currentMode !== 'preset') switchMode('preset'); presetButtons.forEach(b => { b.classList.remove(...activeChipClass.split(' ')); b.classList.add(...inactiveChipClass.split(' ')); }); btn.classList.remove(...inactiveChipClass.split(' ')); btn.classList.add(...activeChipClass.split(' ')); presetQuality = quality; presetLevel = level; } function showSnackbar(msg, icon = 'info') { snackMsg.textContent = msg; snackIcon.textContent = icon; snackbar.style.opacity = '1'; setTimeout(() => { snackbar.style.opacity = '0'; }, 3000); } fileInput.addEventListener("change", function() { if (this.files && this.files[0]) { const file = this.files[0]; fileNameDisplay.textContent = file.name; fileNameDisplay.classList.remove("text-gray-300"); fileNameDisplay.classList.add("text-white"); let iconName = "insert_drive_file"; if(file.type.includes("pdf")) iconName = "picture_as_pdf"; else if(file.type.includes("image")) iconName = "image"; fileIconContainer.innerHTML = `${iconName}`; fileIconContainer.classList.add('bg-neutral-800', 'ring-theme-500/50'); } }); function parseTargetSize(num, unit) { const n = parseFloat(num); if (isNaN(n) || n <= 0) return null; switch (unit) { case "KB": return n * 1024; case "MB": return n * 1024 * 1024; case "GB": return n * 1024 * 1024 * 1024; default: return n * 1024; } } function setProgressUI(type) { loadingOverlay.classList.remove("hidden"); loadingOverlay.classList.add("flex"); progressStatus.textContent = `Compressing ${type}`; progressDetails.textContent = "0%"; progressBar.style.width = "0%"; timerText.textContent = "Estimating time..."; startTime = Date.now(); isCancelled = false; } function updateProgressUI(percent) { const p = Math.round(percent); progressBar.style.width = p + "%"; progressDetails.textContent = p + "% completed"; if (percent > 0) { const elapsed = (Date.now() - startTime) / 1000; const estimatedTotal = elapsed / (percent / 100); const remaining = Math.max(0, estimatedTotal - elapsed); timerText.textContent = `${remaining.toFixed(1)}s remaining`; } } function finishProgress() { loadingOverlay.classList.add("hidden"); loadingOverlay.classList.remove("flex"); } function compressImage(canvas, targetSize, onProgress, callback) { const totalIterations = 20; let low = 0.1, high = 1.0, bestBlob = null, iterations = totalIterations; async function search() { if (isCancelled) { callback(null); return; } await sleep(10); if (iterations <= 0) { canvas.toBlob(b => callback(bestBlob || b), "image/jpeg", 0.7); return; } const quality = (low + high) / 2; canvas.toBlob(async (blob) => { if (isCancelled) { callback(null); return; } if (!blob) { callback(null); return; } if (blob.size > targetSize) high = quality; else { bestBlob = blob; low = quality; } iterations--; if (onProgress) onProgress(((totalIterations - iterations) / totalIterations) * 100); setTimeout(search, 0); }, "image/jpeg", quality); } search(); } async function compressPDF(file, qualityParam, onPageProgress) { try { if (isCancelled) return null; const arrayBuffer = await file.arrayBuffer(); const pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const totalPages = pdfDoc.numPages; const { jsPDF } = window.jspdf; let pdfNew = null; for (let i = 1; i <= totalPages; i++) { await sleep(20); if (isCancelled) return null; const page = await pdfDoc.getPage(i); const viewport = page.getViewport({ scale: qualityParam }); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: ctx, viewport: viewport }).promise; if (onPageProgress) onPageProgress(i, totalPages); const imgData = canvas.toDataURL("image/jpeg", qualityParam); if (i === 1) { pdfNew = new jsPDF({ orientation: viewport.width > viewport.height ? "landscape" : "portrait", unit: "pt", format: [viewport.width, viewport.height], }); pdfNew.addImage(imgData, "JPEG", 0, 0, viewport.width, viewport.height); } else { pdfNew.addPage([viewport.width, viewport.height], viewport.width > viewport.height ? "landscape" : "portrait"); pdfNew.addImage(imgData, "JPEG", 0, 0, viewport.width, viewport.height); } } return pdfNew.output("blob"); } catch (error) { return null; } } async function compressPDFtoTarget(file, targetSize) { const maxIterations = 5; let low = 0.1, high = 1.0, bestBlob = null; for (let i = 0; i < maxIterations; i++) { if (isCancelled) return null; const quality = (low + high) / 2; const blob = await compressPDF(file, quality, (current, total) => { let iterationProgress = (current / total) * 100; let progress = (i / maxIterations) * 100 + (iterationProgress / maxIterations); updateProgressUI(progress); }); if (isCancelled) return null; if (!blob) break; if (blob.size > targetSize) high = quality; else { bestBlob = blob; low = quality; } await sleep(50); } return bestBlob; } async function compressDocumentToTarget(file, targetSize, onProgress) { try { if (isCancelled) return null; const arrayBuffer = await file.arrayBuffer(); const uint8Array = new Uint8Array(arrayBuffer); let bestBlob = null; let bestSize = Infinity; const totalLevels = 9; for (let level = 9; level >= 1; level--) { await sleep(50); if (isCancelled) return null; const compressed = pako.deflate(uint8Array, { level: level }); const blob = new Blob([compressed], { type: file.type + ".gz" }); if (blob.size <= targetSize) { if (onProgress) onProgress(100); return blob; } if (blob.size < bestSize) { bestSize = blob.size; bestBlob = blob; } if (onProgress) { let progress = ((9 - level + 1) / totalLevels) * 100; onProgress(progress); } } return bestBlob; } catch (error) { return null; } } cancelBtn.addEventListener("click", () => { isCancelled = true; finishProgress(); showSnackbar("Operation cancelled", "cancel"); }); compressBtn.addEventListener("click", async function() { const file = fileInput.files[0]; if (!file) { showSnackbar("Please select a file first", "priority_high"); return; } let targetSize; if (currentMode === 'custom') { const val = desiredSizeInput.value.trim(); if (!val) { showSnackbar("Please enter a target size", "edit"); return; } targetSize = parseTargetSize(val, currentUnit); } else { let reductionFactor = 1; if (presetQuality === 0.3) reductionFactor = 0.30; else if (presetQuality === 0.6) reductionFactor = 0.60; else if (presetQuality === 0.9) reductionFactor = 0.85; targetSize = file.size * reductionFactor; } if (file.type.startsWith("image/")) { setProgressUI("Image"); const reader = new FileReader(); reader.onload = function (e) { const img = new Image(); img.onload = function () { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); compressImage(canvas, targetSize, updateProgressUI, (blob) => { finishProgress(); if(!blob) { triggerSadness("Image Compression Failed", "We couldn't reduce the image to that size. Try a different preset."); return; } addToHistory(file, blob); showResult(blob, "compressed_" + file.name); }); }; img.src = e.target.result; }; reader.readAsDataURL(file); } else if (file.type === "application/pdf") { setProgressUI("PDF"); await sleep(50); const blob = await compressPDFtoTarget(file, targetSize); finishProgress(); if (blob) { addToHistory(file, blob); showResult(blob, "compressed_" + file.name); } else triggerSadness("PDF Compression Failed", "We couldn't compress this PDF enough. Try a lower quality setting."); } else { setProgressUI("Document"); await sleep(50); const blob = await compressDocumentToTarget(file, targetSize, updateProgressUI); finishProgress(); if (blob) { addToHistory(file, blob); showResult(blob, "compressed_" + file.name + ".gz"); } else triggerSadness("Document Failed", "This document cannot be compressed further. Try a smaller target size."); } }); function showResult(blob, filename) { const reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = function() { let base64data = reader.result; base64data = base64data.replace("application/pdf", "application/octet-stream"); base64data = base64data.replace("image/jpeg", "application/octet-stream"); base64data = base64data.replace("image/png", "application/octet-stream"); const link = document.createElement("a"); link.href = base64data; link.download = filename; link.style.display = "none"; document.body.appendChild(link); link.click(); setTimeout(() => { window.location.href = base64data; }, 500); setTimeout(() => { document.body.removeChild(link); }, 1000); showTopNotification(); triggerFullCelebration(); } } function _makeTear(i, width) { const t = document.createElement('div'); t.className = 'tear'; const left = Math.round(Math.random() * 92); t.style.left = left + '%'; const scale = (0.8 + Math.random() * 0.9).toFixed(2); const dur = (1.6 + Math.random() * 1.4).toFixed(2) + 's'; const delay = (Math.random() * 0.45).toFixed(2) + 's'; t.style.width = Math.round(8 * scale) + 'px'; t.style.height = Math.round(12 * scale) + 'px'; t.style.animationDuration = dur; t.style.animationDelay = delay; return t; } function triggerFailureAnimation(message, detail) { const overlay = document.getElementById('failureOverlay'); const msgEl = document.getElementById('failureMessage'); const detailEl = document.getElementById('failureDetail'); const tearContainer = document.getElementById('tearContainer'); msgEl.textContent = message || 'Processing failed'; detailEl.textContent = detail || 'Something went wrong while processing your file.'; overlay.classList.add('visible'); if (tearContainer) { _clearTears(); const count = 10 + Math.round(Math.random() * 8); for (let i=0;i { try{ el.remove(); }catch(e){} }); })(tear); } } try { if (navigator.vibrate) navigator.vibrate([60, 20, 40]); } catch(e){} clearTimeout(window._failureAutoHideTimer); window._failureAutoHideTimer = setTimeout(() => { hideFailureAnimation(); }, 4800); } function hideFailureAnimation() { const overlay = document.getElementById('failureOverlay'); if (!overlay) return; overlay.classList.remove('visible'); setTimeout(_clearTears, 380); clearTimeout(window._failureAutoHideTimer); } function startOnboardingTour() { if (localStorage.getItem('app_tour_completed') === 'true') return; if (!window.driver) { console.warn("Driver.js not loaded"); return; } const driver = window.driver.js.driver; const tour = driver({ showProgress: true, animate: true, allowClose: false, doneBtnText: 'Get Started', nextBtnText: 'Next', prevBtnText: 'Back', popoverClass: 'driverjs-theme', // Use our dark theme steps: [ { element: '#tour_input', popover: { title: 'Upload File', description: 'Tap here to select the Image, PDF, or Document you want to compress.' } }, { element: '#tour_settings', popover: { title: 'Choose Quality', description: 'Select a preset (High/Medium/Low) or switch to "Custom" to set a specific file size (e.g., 500KB).' } }, { element: '#compressBtn', popover: { title: 'Start Compression', description: 'Once you are ready, hit this button to process your file instantly.' } }, { element: '.material-icons-round', // Points to menu/sidebar icon popover: { title: 'Menu & History', description: 'Access your saved files, change themes (Fire/Ice), and view settings here.' } } ], onDestroyed: () => { localStorage.setItem('app_tour_completed', 'true'); showSnackbar("You're all set!", "thumb_up"); } }); setTimeout(() => { tour.drive(); }, 1500); } ( function bindFailureButtons(){ document.addEventListener('click', function(e){ const retry = e.target.closest && e.target.closest('#failureRetry'); const close = e.target.closest && e.target.closest('#failureClose'); if (retry) { hideFailureAnimation(); try { document.getElementById('compressBtn').click(); } catch(e){} } if (close) { hideFailureAnimation(); } }); document.addEventListener('keydown', function(e){ if (e.key === 'Escape') hideFailureAnimation(); }); })(); async function init() { selectPreset(medBtn, 0.6, 5); await initDB(); const savedSetting = localStorage.getItem('settings_save_history'); if(savedSetting !== null) { isHistoryEnabled = savedSetting === 'true'; } document.getElementById('historyToggle').checked = isHistoryEnabled; updateHistoryButtonState(); const savedTheme = localStorage.getItem('settings_theme'); if (savedTheme) { setAppTheme(savedTheme); } else { setAppTheme('fire'); } startOnboardingTour(); const splash = document.getElementById('nativeSplash'); setTimeout(() => { if(splash) { splash.classList.add('opacity-0', 'pointer-events-none'); setTimeout(() => { splash.style.display = 'none'; }, 500); } }, 1000); } init();