wa-dev-tools/public/compress.html
2026-03-21 22:06:15 +03:00

516 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Сжать картинки</title>
<script src="/vendor/tailwind.js"></script>
<link href="/vendor/fonts.css" rel="stylesheet">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
mono: ['JetBrains Mono', 'monospace'],
sans: ['Manrope', 'sans-serif'],
},
colors: {
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
warn: '#ffd600',
danger: '#ff5252',
}
}
}
}
</script>
<style>
/* Sidebar */
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
.main-content{margin-left:56px}
/* Mobile bottom bar */
@media(max-width:640px){
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
.sidebar-logo{display:none}
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
.main-content{margin-left:0;padding-bottom:60px}
}
/* Theme variables */
:root {
--bg: #f5f5f7;
--bg-grad1: rgba(0, 67, 184, 0.04);
--bg-grad2: rgba(0, 50, 140, 0.03);
--surface-800: rgba(255,255,255,0.85);
--surface-700: #f0f0f3;
--surface-600: #dde0e6;
--text-primary: #1a1a2e;
--text-secondary: #555;
--text-muted: #888;
--bar-bg: #e0e0e0;
--select-bg: #f0f0f3;
--select-color: #1a1a2e;
}
.dark {
--bg: #060c18;
--bg-grad1: rgba(0, 84, 230, 0.03);
--bg-grad2: rgba(0, 67, 184, 0.02);
--surface-800: rgba(9,16,32,0.8);
--surface-700: #0d1828;
--surface-600: #162040;
--text-primary: #fff;
--text-secondary: #c0c0c0;
--text-muted: #666;
--bar-bg: #0d1828;
--select-bg: #091020;
--select-color: #e0e0e0;
}
body {
background: var(--bg);
background-image:
radial-gradient(ellipse at 20% 50%, var(--bg-grad1) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, var(--bg-grad2) 0%, transparent 50%);
min-height: 100vh;
transition: background 0.3s;
}
.dark .noise::before {
content: '';
position: fixed;
inset: 0;
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
}
.dropzone-active { border-color: #0054e6 !important; background: rgba(0, 84, 230, 0.05) !important; }
.dropzone-active .drop-icon { transform: scale(1.1) translateY(-4px); }
.drop-icon { transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
.savings-bar { height: 6px; border-radius: 3px; background: var(--bar-bg); overflow: hidden; }
.savings-bar-fill { height: 100%; border-radius: 3px; transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1); }
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
.fade-up { animation: fadeUp 0.4s ease-out forwards; }
.fade-up-delay { animation: fadeUp 0.4s ease-out 0.1s forwards; opacity: 0; }
@keyframes pulse-border { 0%, 100% { border-color: rgba(0, 84, 230, 0.2); } 50% { border-color: rgba(0, 84, 230, 0.5); } }
.processing { animation: pulse-border 1.5s ease-in-out infinite; }
.preview-grid img { transition: transform 0.2s ease; }
.preview-grid img:hover { transform: scale(1.05); }
progress { appearance: none; height: 4px; border-radius: 2px; overflow: hidden; }
progress::-webkit-progress-bar { background: var(--bar-bg); border-radius: 2px; }
progress::-webkit-progress-value { background: linear-gradient(90deg, #0043b8, #0054e6); border-radius: 2px; transition: width 0.2s; }
select, select option { background: var(--select-bg); color: var(--select-color); }
/* Theme toggle */
.theme-toggle { position: relative; width: 44px; height: 24px; border-radius: 12px; cursor: pointer; transition: background 0.3s; }
.dark .theme-toggle { background: #162040; }
.theme-toggle { background: #dde0e6; }
.theme-toggle-knob { position: absolute; top: 2px; width: 20px; height: 20px; border-radius: 50%; transition: left 0.3s, background 0.3s; display: flex; align-items: center; justify-content: center; }
.dark .theme-toggle-knob { left: 22px; background: #0d1828; }
.theme-toggle-knob { left: 2px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
</style>
</head>
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-logo">WA</div>
<div class="sidebar-nav">
<a href="/" class="sidebar-link" title="Главная">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
</a>
<a href="/compress" class="sidebar-link active" title="Картинки">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
</a>
<a href="/md" class="sidebar-link" title="Markdown">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
</a>
<a href="/placeholder" class="sidebar-link" title="Placeholder">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
</a>
<a href="/parser" class="sidebar-link" title="Parser">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
</a>
<a href="/password" class="sidebar-link" title="Password">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
</a>
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
</a>
<a href="/converter" class="sidebar-link" title="Converter">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
</a>
<a href="/formatter" class="sidebar-link" title="Formatter">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
</a>
<a href="/editor" class="sidebar-link" title="Editor">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
</a>
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
</a>
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
</a>
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
</a>
</div>
<div class="sidebar-bottom">
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
<div class="theme-toggle-knob">
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
</div>
</div>
</div>
</nav>
<div class="main-content">
<div class="relative z-10 max-w-2xl mx-auto px-4 py-8 sm:py-12">
<!-- Header -->
<div class="text-center mb-8 fade-up">
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
JPEG &middot; PNG &middot; WebP
</div>
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
Сжать <span class="text-accent">картинки</span>
</h1>
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">до 50 файлов &middot; до 20 МБ каждый</p>
</div>
<!-- Main Card -->
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
<!-- Dropzone -->
<div id="dropzone" class="relative border-2 border-dashed dark:border-surface-600 border-gray-300 rounded-xl p-8 sm:p-10 text-center cursor-pointer dark:hover:border-gray-500 hover:border-gray-400 transition-all duration-300 group">
<div class="drop-icon mb-3">
<svg class="mx-auto w-10 h-10 dark:text-gray-500 text-gray-400 group-hover:text-accent transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
</div>
<p class="text-sm dark:text-gray-400 text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">
Перетащите файлы сюда или <span class="text-accent font-medium">выберите</span>
</p>
<input type="file" multiple accept="image/jpeg,image/png,image/webp" id="fileInput" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
</div>
<!-- Preview -->
<div id="preview" class="preview-grid grid grid-cols-3 sm:grid-cols-4 gap-2 mt-4 hidden"></div>
<!-- File count badge -->
<div id="fileCount" class="hidden mt-3 text-center">
<span class="inline-flex items-center gap-1.5 text-xs font-mono dark:text-gray-400 text-gray-500 dark:bg-surface-700 bg-gray-100 px-3 py-1 rounded-full">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z" />
</svg>
<span id="fileCountText">0 файлов</span>
</span>
</div>
<!-- Controls -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-5">
<div>
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Формат</label>
<select id="format" class="w-full px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm focus:outline-none focus:border-accent/50 transition-colors">
<option value="original">Оригинал</option>
<option value="webp">WebP</option>
<option value="jpeg">JPEG</option>
<option value="png">PNG</option>
</select>
</div>
<div>
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Размер</label>
<select id="resize" class="w-full px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm focus:outline-none focus:border-accent/50 transition-colors">
<option value="0">Не изменять</option>
<option value="4600">4600px</option>
<option value="3600">3600px</option>
<option value="2600">2600px</option>
<option value="1600">1600px</option>
<option value="1200">1200px</option>
<option value="800">800px</option>
</select>
</div>
</div>
<!-- Progress -->
<progress id="progressBar" class="w-full mt-4 hidden" value="0" max="100"></progress>
<!-- Compress button -->
<button id="compressBtn" disabled
class="mt-5 w-full py-3 rounded-xl font-semibold text-sm tracking-wide transition-all duration-300
bg-accent/10 text-accent/40 border border-accent/10 cursor-not-allowed
enabled:bg-accent enabled:text-white enabled:border-accent enabled:cursor-pointer
enabled:hover:bg-accent-bright enabled:hover:shadow-lg enabled:hover:shadow-accent/20
enabled:active:scale-[0.98]">
Сжать
</button>
</div>
<!-- Results -->
<div id="results" class="hidden mt-5 fade-up">
<!-- Stats card -->
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl overflow-hidden shadow-sm dark:shadow-none">
<!-- Summary header -->
<div id="summary" class="flex items-center justify-between px-5 sm:px-7 py-4 border-b dark:border-surface-600 border-gray-200 dark:bg-surface-700/30 bg-gray-50/50">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-accent/10 flex items-center justify-center">
<svg class="w-4 h-4 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
</div>
<div>
<div class="text-sm font-semibold dark:text-white text-gray-900" id="summaryTitle">Готово</div>
<div class="text-xs dark:text-gray-500 text-gray-400 font-mono" id="summaryDetail"></div>
</div>
</div>
<div class="text-right">
<div class="text-lg font-bold font-mono" id="summaryPercent"></div>
<div class="text-xs dark:text-gray-500 text-gray-400">экономия</div>
</div>
</div>
<!-- Stats table -->
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-xs font-mono dark:text-gray-500 text-gray-400 uppercase tracking-wider border-b dark:border-surface-600 border-gray-200">
<th class="text-left px-5 py-3">Файл</th>
<th class="text-right px-3 py-3">Было</th>
<th class="text-right px-3 py-3">Стало</th>
<th class="text-right px-5 py-3 w-32">Экономия</th>
</tr>
</thead>
<tbody id="statsBody"></tbody>
</table>
</div>
</div>
<!-- Download button -->
<a id="downloadBtn" href="#" download
class="mt-4 flex items-center justify-center gap-2 w-full py-3.5 rounded-xl font-semibold text-sm tracking-wide
bg-accent text-white hover:bg-accent-bright transition-all duration-300
hover:shadow-lg hover:shadow-accent/20 active:scale-[0.98]">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
Скачать архив
</a>
</div>
<!-- Error -->
<div id="errorBlock" class="hidden mt-5 bg-danger/10 border border-danger/20 rounded-xl px-5 py-4 text-sm text-danger font-medium fade-up"></div>
</div>
</div><!-- /main-content -->
<script>
// Theme toggle
const html = document.documentElement;
const savedTheme = localStorage.getItem('theme');
if (savedTheme) { html.className = savedTheme; }
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
document.getElementById('themeToggle').addEventListener('click', () => {
const isDark = html.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : '');
});
const $ = id => document.getElementById(id);
const dropzone = $('dropzone');
const fileInput = $('fileInput');
const progressBar = $('progressBar');
const compressBtn = $('compressBtn');
const results = $('results');
const errorBlock = $('errorBlock');
const preview = $('preview');
let selectedFiles = null;
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' Б';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' КБ';
return (bytes / (1024 * 1024)).toFixed(2) + ' МБ';
}
function pluralFiles(n) {
const mod = n % 10;
const mod100 = n % 100;
if (mod === 1 && mod100 !== 11) return n + ' файл';
if (mod >= 2 && mod <= 4 && (mod100 < 12 || mod100 > 14)) return n + ' файла';
return n + ' файлов';
}
// Drag & drop
dropzone.addEventListener('click', () => fileInput.click());
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('dropzone-active'); });
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dropzone-active'));
dropzone.addEventListener('drop', e => { e.preventDefault(); dropzone.classList.remove('dropzone-active'); processFiles(e.dataTransfer.files); });
fileInput.addEventListener('change', e => processFiles(e.target.files));
function processFiles(files) {
results.classList.add('hidden');
errorBlock.classList.add('hidden');
preview.innerHTML = '';
preview.classList.add('hidden');
compressBtn.disabled = true;
if (!files.length) return;
const validTypes = ['image/jpeg', 'image/png', 'image/webp'];
const valid = Array.from(files).filter(f => validTypes.includes(f.type) && f.size <= 20 * 1024 * 1024);
if (!valid.length) {
showError('Нет подходящих файлов. Допустимы JPEG, PNG, WebP до 20 МБ.');
return;
}
selectedFiles = valid;
$('fileCount').classList.remove('hidden');
$('fileCountText').textContent = pluralFiles(valid.length);
// Show previews (max 8)
const shown = valid.slice(0, 8);
shown.forEach(file => {
const reader = new FileReader();
reader.onload = e => {
const img = document.createElement('img');
img.src = e.target.result;
img.className = 'w-full aspect-square object-cover rounded-lg border dark:border-surface-600 border-gray-200';
preview.appendChild(img);
preview.classList.remove('hidden');
};
reader.readAsDataURL(file);
});
if (valid.length > 8) {
const more = document.createElement('div');
more.className = 'w-full aspect-square rounded-lg border dark:border-surface-600 border-gray-200 dark:bg-surface-700 bg-gray-100 flex items-center justify-center text-xs dark:text-gray-500 text-gray-400 font-mono';
more.textContent = '+' + (valid.length - 8);
preview.appendChild(more);
preview.classList.remove('hidden');
}
compressBtn.disabled = false;
compressBtn.textContent = 'Сжать';
}
function showError(msg) {
errorBlock.textContent = msg;
errorBlock.classList.remove('hidden');
}
compressBtn.addEventListener('click', () => {
if (!selectedFiles || !selectedFiles.length) return;
compressBtn.disabled = true;
compressBtn.textContent = 'Сжимаю...';
results.classList.add('hidden');
errorBlock.classList.add('hidden');
progressBar.hidden = false;
progressBar.value = 0;
const formData = new FormData();
selectedFiles.forEach(f => formData.append('images', f));
formData.append('resize', $('resize').value);
formData.append('format', $('format').value);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/compress');
xhr.responseType = 'json';
xhr.upload.onprogress = e => {
if (e.lengthComputable) progressBar.value = (e.loaded / e.total) * 100;
};
xhr.onload = () => {
progressBar.hidden = true;
compressBtn.textContent = 'Сжать';
compressBtn.disabled = false;
if (xhr.status === 200 && xhr.response && xhr.response.success) {
renderResults(xhr.response);
} else if (xhr.status === 429) {
showError('Слишком много запросов. Подождите минуту.');
} else {
const msg = xhr.response && xhr.response.error ? xhr.response.error : 'Ошибка при сжатии.';
showError(msg);
}
};
xhr.onerror = () => {
progressBar.hidden = true;
compressBtn.textContent = 'Сжать';
compressBtn.disabled = false;
showError('Ошибка сети. Повторите позже.');
};
xhr.send(formData);
});
function renderResults(data) {
const { stats, downloadUrl } = data;
// Summary
const totalOrig = stats.reduce((s, f) => s + f.originalSize, 0);
const totalComp = stats.reduce((s, f) => s + f.compressedSize, 0);
const totalSavings = totalOrig > 0 ? Math.round((1 - totalComp / totalOrig) * 100) : 0;
$('summaryDetail').textContent = `${formatSize(totalOrig)}${formatSize(totalComp)}`;
const pctEl = $('summaryPercent');
pctEl.textContent = (totalSavings > 0 ? '-' : '') + totalSavings + '%';
pctEl.className = 'text-lg font-bold font-mono ' + savingsColor(totalSavings);
// Table
const tbody = $('statsBody');
tbody.innerHTML = '';
stats.forEach((f, i) => {
const row = document.createElement('tr');
row.className = 'border-b dark:border-surface-600/50 border-gray-100 last:border-0' + (i % 2 === 0 ? ' dark:bg-surface-700/20 bg-gray-50/50' : '');
const sColor = savingsColor(f.savings);
row.innerHTML = `
<td class="px-5 py-2.5 font-mono text-xs dark:text-gray-300 text-gray-600 truncate max-w-[180px]" title="${f.outputFilename}">${f.outputFilename}</td>
<td class="text-right px-3 py-2.5 font-mono text-xs dark:text-gray-500 text-gray-400">${formatSize(f.originalSize)}</td>
<td class="text-right px-3 py-2.5 font-mono text-xs dark:text-gray-300 text-gray-600">${formatSize(f.compressedSize)}</td>
<td class="px-5 py-2.5">
<div class="flex items-center gap-2 justify-end">
<div class="savings-bar flex-1 max-w-[60px]">
<div class="savings-bar-fill ${barColor(f.savings)}" style="width: ${Math.max(0, Math.min(100, f.savings))}%"></div>
</div>
<span class="font-mono text-xs font-medium ${sColor} w-10 text-right">${f.savings > 0 ? '-' : ''}${f.savings}%</span>
</div>
</td>`;
tbody.appendChild(row);
});
// Download
$('downloadBtn').href = downloadUrl;
results.classList.remove('hidden');
}
function savingsColor(pct) {
if (pct >= 40) return 'text-accent';
if (pct >= 15) return 'text-accent/70';
if (pct >= 0) return 'text-warn';
return 'text-danger';
}
function barColor(pct) {
if (pct >= 40) return 'bg-accent';
if (pct >= 15) return 'bg-accent/60';
if (pct >= 0) return 'bg-warn';
return 'bg-danger';
}
</script>
</body>
</html>