-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
137 lines (116 loc) · 5.26 KB
/
app.js
File metadata and controls
137 lines (116 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
const widthInput = document.getElementById('width');
const heightInput = document.getElementById('height');
const formatSelect = document.getElementById('format');
const qualityInput = document.getElementById('quality');
const qualityVal = document.getElementById('quality-val');
const recursiveToggle = document.getElementById('recursive');
const selectBtn = document.getElementById('select-folder');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar-fill');
const statusText = document.getElementById('status-text');
const progressPercent = document.getElementById('progress-percent');
const logContainer = document.getElementById('log-container');
qualityInput.addEventListener('input', () => {
qualityVal.textContent = `${qualityInput.value}%`;
});
function log(message, type = '') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = `> ${message}`;
logContainer.appendChild(entry);
logContainer.scrollTop = 0;
}
async function processImage(fileHandle, destFolderHandle, targetWidth, targetHeight, format, quality) {
const file = await fileHandle.getFile();
const bitmap = await createImageBitmap(file);
let finalWidth = targetWidth;
let finalHeight = targetHeight;
// Proportional logic
if (!targetWidth && targetHeight) {
finalWidth = (bitmap.width * targetHeight) / bitmap.height;
} else if (targetWidth && !targetHeight) {
finalHeight = (bitmap.height * targetWidth) / bitmap.width;
} else if (!targetWidth && !targetHeight) {
finalWidth = bitmap.width;
finalHeight = bitmap.height;
}
const canvas = document.createElement('canvas');
canvas.width = finalWidth;
canvas.height = finalHeight;
const ctx = canvas.getContext('2d');
// Clear for transparency
ctx.clearRect(0, 0, finalWidth, finalHeight);
ctx.drawImage(bitmap, 0, 0, finalWidth, finalHeight);
// Convert quality 0-100 to 0.0-1.0
const qualityValue = format === 'image/png' ? undefined : quality / 100;
const blob = await new Promise(resolve => canvas.toBlob(resolve, format, qualityValue));
// Create new file name with correct extension
const extension = format.split('/')[1];
const newName = fileHandle.name.replace(/\.[^/.]+$/, "") + "." + extension;
const newFileHandle = await destFolderHandle.getFileHandle(newName, { create: true });
const writable = await newFileHandle.createWritable();
await writable.write(blob);
await writable.close();
bitmap.close();
}
async function traverse(folderHandle, destFolderHandle, settings, stats) {
for await (const [name, handle] of folderHandle.entries()) {
if (handle.kind === 'directory') {
if (name === 'resized') continue; // Skip output folder
if (settings.recursive) {
const newDest = await destFolderHandle.getDirectoryHandle(name, { create: true });
await traverse(handle, newDest, settings, stats);
}
} else if (handle.kind === 'file') {
const ext = name.toLowerCase().split('.').pop();
if (['jpg', 'jpeg', 'png', 'bmp', 'webp'].includes(ext)) {
stats.total++;
stats.queue.push({ handle, dest: destFolderHandle });
}
}
}
}
selectBtn.addEventListener('click', async () => {
try {
const sourceFolder = await window.showDirectoryPicker();
const settings = {
width: parseInt(widthInput.value) || 0,
height: parseInt(heightInput.value) || 0,
format: formatSelect.value,
quality: parseInt(qualityInput.value),
recursive: recursiveToggle.checked
};
progressContainer.classList.remove('hidden');
log('Starting flow...');
selectBtn.disabled = true;
const resizedFolder = await sourceFolder.getDirectoryHandle('resized', { create: true });
const stats = { total: 0, current: 0, queue: [] };
statusText.textContent = "Scanning folder...";
await traverse(sourceFolder, resizedFolder, settings, stats);
log(`Found ${stats.total} images. Processing...`);
for (const item of stats.queue) {
try {
await processImage(item.handle, item.dest, settings.width, settings.height, settings.format, settings.quality);
stats.current++;
const percent = Math.round((stats.current / stats.total) * 100);
progressBar.style.width = `${percent}%`;
progressPercent.textContent = `${percent}%`;
statusText.textContent = `Processing ${stats.current}/${stats.total}`;
log(`Resized: ${item.handle.name}`, 'log-success');
} catch (err) {
log(`Failed: ${item.handle.name} - ${err.message}`, 'log-error');
}
}
log('Flow complete! ✨');
statusText.textContent = "Done!";
selectBtn.disabled = false;
} catch (err) {
if (err.name === 'AbortError') {
log('Selection cancelled.');
} else {
log(`Error: ${err.message}`, 'log-error');
console.error(err);
}
selectBtn.disabled = false;
}
});