apps/shrink/templates/index.html 5.7 K raw
1
{{define "index.html"}}{{template "base.html" .}}{{end}}
2
{{define "title"}}SHRINK{{end}}
3
{{define "content"}}
4
5
<div id="drop-zone">
6
    <p>DROP IMAGE HERE OR CLICK TO SELECT</p>
7
    <input type="file" id="file-input" accept="image/*" hidden>
8
</div>
9
10
<div id="preview-section">
11
    <img id="preview-img" alt="Preview">
12
    <p id="original-size"></p>
13
</div>
14
15
<div id="controls">
16
    <div class="control-row">
17
        <label for="quality-slider">QUALITY</label>
18
        <input type="range" id="quality-slider" min="1" max="100" value="80">
19
        <span id="quality-value">80</span>
20
    </div>
21
    <div class="control-row">
22
        <label>RESIZE</label>
23
        <input type="number" id="width-input" placeholder="WIDTH" min="1">
24
        <span class="dimension-sep">x</span>
25
        <span id="height-display">—</span>
26
    </div>
27
    <button id="compress-btn">COMPRESS</button>
28
</div>
29
30
<div id="result-section">
31
    <div class="size-comparison">
32
        <div>
33
            <label>ORIGINAL</label>
34
            <p id="result-original-size"></p>
35
        </div>
36
        <div>
37
            <label>COMPRESSED</label>
38
            <p id="result-compressed-size"></p>
39
        </div>
40
        <div>
41
            <label>REDUCTION</label>
42
            <p id="result-reduction"></p>
43
        </div>
44
    </div>
45
    <a id="download-link">
46
        <button>DOWNLOAD</button>
47
    </a>
48
</div>
49
50
<script>
51
const dropZone = document.getElementById('drop-zone');
52
const fileInput = document.getElementById('file-input');
53
const previewSection = document.getElementById('preview-section');
54
const previewImg = document.getElementById('preview-img');
55
const originalSize = document.getElementById('original-size');
56
const controls = document.getElementById('controls');
57
const qualitySlider = document.getElementById('quality-slider');
58
const qualityValue = document.getElementById('quality-value');
59
const widthInput = document.getElementById('width-input');
60
const heightDisplay = document.getElementById('height-display');
61
const compressBtn = document.getElementById('compress-btn');
62
const resultSection = document.getElementById('result-section');
63
const resultOriginalSize = document.getElementById('result-original-size');
64
const resultCompressedSize = document.getElementById('result-compressed-size');
65
const resultReduction = document.getElementById('result-reduction');
66
const downloadLink = document.getElementById('download-link');
67
68
let selectedFile = null;
69
let naturalWidth = 0;
70
let naturalHeight = 0;
71
72
function formatBytes(bytes) {
73
    if (bytes < 1024) return bytes + ' B';
74
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
75
    return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
76
}
77
78
function handleFile(file) {
79
    if (!file || !file.type.startsWith('image/')) return;
80
    selectedFile = file;
81
    const url = URL.createObjectURL(file);
82
    previewImg.src = url;
83
    originalSize.textContent = formatBytes(file.size);
84
    previewSection.classList.add('visible');
85
    controls.classList.add('visible');
86
    resultSection.classList.remove('visible');
87
88
    const tmp = new Image();
89
    tmp.onload = () => {
90
        naturalWidth = tmp.naturalWidth;
91
        naturalHeight = tmp.naturalHeight;
92
        widthInput.value = naturalWidth;
93
        heightDisplay.textContent = naturalHeight;
94
    };
95
    tmp.src = url;
96
}
97
98
dropZone.addEventListener('click', () => fileInput.click());
99
fileInput.addEventListener('change', () => {
100
    if (fileInput.files.length) handleFile(fileInput.files[0]);
101
});
102
103
dropZone.addEventListener('dragover', (e) => {
104
    e.preventDefault();
105
    dropZone.classList.add('drag-over');
106
});
107
dropZone.addEventListener('dragleave', () => {
108
    dropZone.classList.remove('drag-over');
109
});
110
dropZone.addEventListener('drop', (e) => {
111
    e.preventDefault();
112
    dropZone.classList.remove('drag-over');
113
    if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
114
});
115
116
qualitySlider.addEventListener('input', () => {
117
    qualityValue.textContent = qualitySlider.value;
118
});
119
120
widthInput.addEventListener('input', () => {
121
    if (naturalWidth > 0) {
122
        const w = parseInt(widthInput.value) || 0;
123
        heightDisplay.textContent = w > 0 ? Math.round(w * naturalHeight / naturalWidth) : '—';
124
    }
125
});
126
127
compressBtn.addEventListener('click', async () => {
128
    if (!selectedFile) return;
129
    compressBtn.disabled = true;
130
    compressBtn.textContent = 'COMPRESSING...';
131
132
    const formData = new FormData();
133
    formData.append('file', selectedFile);
134
    formData.append('quality', qualitySlider.value);
135
    const w = parseInt(widthInput.value) || 0;
136
    if (w > 0) {
137
        formData.append('width', w.toString());
138
    }
139
140
    try {
141
        const res = await fetch('/compress', { method: 'POST', body: formData });
142
        if (!res.ok) {
143
            const text = await res.text();
144
            alert('Compression failed: ' + text);
145
            return;
146
        }
147
        const blob = await res.blob();
148
        const originalBytes = selectedFile.size;
149
        const compressedBytes = blob.size;
150
        const reduction = ((1 - compressedBytes / originalBytes) * 100).toFixed(1);
151
152
        resultOriginalSize.textContent = formatBytes(originalBytes);
153
        resultCompressedSize.textContent = formatBytes(compressedBytes);
154
        resultReduction.textContent = reduction + '%';
155
156
        if (downloadLink.href && downloadLink.href.startsWith('blob:')) {
157
            URL.revokeObjectURL(downloadLink.href);
158
        }
159
        downloadLink.href = URL.createObjectURL(blob);
160
        downloadLink.download = selectedFile.name.replace(/\.[^.]+$/, '') + '_compressed.jpg';
161
        resultSection.classList.add('visible');
162
    } catch (err) {
163
        alert('Error: ' + err.message);
164
    } finally {
165
        compressBtn.disabled = false;
166
        compressBtn.textContent = 'COMPRESS';
167
    }
168
});
169
</script>
170
171
{{end}}