Skip to content
Merged

v2.2 #11

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Grab the latest installer from the [Releases](https://github.com/LoicPandul/Imag
## Features

- Drop files anywhere in the window, or browse, to convert JPEG, PNG, WEBP, GIF, BMP and TIFF images to JPEG, WEBP or PNG.
- Optional Instant Convert mode: arm the lightning toggle next to the Convert button and images are processed the moment you drop them, no click needed. Off by default, remembered across launches.
- Metadata is always removed: EXIF, GPS coordinates, XMP, IPTC, comments. When the file is already in the target format, the cleaning is lossless, since the app rewrites the container without re-encoding a single pixel.
- The ICC color profile is deliberately kept. It contains no personal information (it is a generic file shipped with your camera or screen), and removing it would visibly shift the colors of wide-gamut images.
- To guarantee a precise weight, give a maximum size in KB: the app searches for the best quality that fits, and only downscales as a last resort. Lossy PNG relies on built-in palette quantization, so there is no external tool to install.
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "imagesconverter"
version = "2.1.0"
version = "2.2.0"
description = "Convert, compress and clean metadata from images"
authors = ["Loïc Morel"]
edition = "2021"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ImagesConverter",
"version": "2.1.0",
"version": "2.2.0",
"identifier": "org.pandul.imagesconverter",
"build": {
"frontendDist": "../ui"
Expand Down
8 changes: 7 additions & 1 deletion ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<div class="veil-frame">
<span class="bracket tl"></span><span class="bracket tr"></span>
<span class="bracket bl"></span><span class="bracket br"></span>
<p>Release to add</p>
<p id="drag-label">Release to add</p>
</div>
</div>

Expand Down Expand Up @@ -135,6 +135,12 @@ <h2 class="queue-title">Queue <span id="queue-count" class="mono"></span></h2>

<div class="action-row">
<p id="status-line" class="status-line" role="status" aria-live="polite">Drop images to get started</p>
<input type="checkbox" id="instant" class="instant-check"
aria-label="Instant convert — process images the moment they are added" />
<label for="instant" class="instant-pill" title="Convert automatically when images are added">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8.7 1.3 2 9.3h6l-.7 5.4L14 6.7H8l.7-5.4z" /></svg>
Instant
</label>
<button id="convert" class="convert-btn" type="button" disabled>Convert</button>
</div>
</footer>
Expand Down
65 changes: 58 additions & 7 deletions ui/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const els = {
clearAll: $("clear-all"),
clearDone: $("clear-done"),
convert: $("convert"),
instant: $("instant"),
dragLabel: $("drag-label"),
status: $("status-line"),
compress: $("compress"),
maxKb: $("max-kb"),
Expand Down Expand Up @@ -94,6 +96,7 @@ function saveOptions() {
maxKb: els.maxKb.value,
deleteOriginal: els.deleteOriginal.checked,
removeBg: els.removeBg.checked,
instant: els.instant.checked,
}));
} catch { /* best effort */ }
}
Expand All @@ -108,6 +111,7 @@ function restoreOptions() {
els.compress.checked = !!saved.compress;
if (/^\d*$/.test(saved.maxKb ?? "")) els.maxKb.value = saved.maxKb ?? "";
els.deleteOriginal.checked = saved.deleteOriginal !== false;
els.instant.checked = !!saved.instant;
} catch { /* corrupted storage must never brick the app */ }
syncCompressField();
}
Expand All @@ -116,6 +120,28 @@ function syncCompressField() {
els.maxKb.disabled = !els.compress.checked;
}

/* ---------- instant convert ---------- */

function instantOn() {
return els.instant.checked;
}

function syncInstantUi() {
els.dragLabel.textContent = instantOn() ? "Release to convert" : "Release to add";
}

/** Idle status line: what will happen next, given the current mode. */
function idleLine() {
const fmt = currentFormat().toUpperCase();
return instantOn()
? `Instant convert — images convert to ${fmt} as soon as you add them`
: `Mode: convert to ${fmt}`;
}

function readyLine(ready) {
return `${ready} image${ready > 1 ? "s" : ""} ready — ${currentFormat().toUpperCase()}`;
}

/* ---------- background removal ---------- */

function syncBgControl() {
Expand Down Expand Up @@ -401,6 +427,7 @@ async function addFiles(paths) {
}

let index = 0;
let armed = 0; // convertible items this add produced or re-armed
for (const info of inspected) {
const existing = items.get(info.path);
if (existing) {
Expand All @@ -412,6 +439,7 @@ async function addFiles(paths) {
existing.el?.querySelector(".thumb")?.classList.remove("alpha");
renderCard(existing);
loadThumbnail(existing);
if (existing.status === "ready") armed += 1;
}
continue;
}
Expand All @@ -429,10 +457,18 @@ async function addFiles(paths) {
items.set(item.path, item);
createCard(item, index++);
loadThumbnail(item);
if (item.status === "ready") armed += 1;
}
setView();
const ready = readyItems().length;
if (ready) setStatus(`${ready} image${ready > 1 ? "s" : ""} ready — ${currentFormat().toUpperCase()}`);
// A conversion may have started while inspect_files was in flight; calling
// convert() now would hit its cancel branch and kill that batch.
if (!ready || converting) return;
// Instant convert: adding images presses Convert for you — but only when
// this add actually brought something convertible, so arming the toggle over
// a sitting queue keeps its promise of not firing on its own.
if (instantOn() && armed) convert();
else setStatus(readyLine(ready));
}

async function browse() {
Expand Down Expand Up @@ -579,9 +615,7 @@ function wire() {
saveOptions();
if (!converting) {
const ready = readyItems().length;
setStatus(ready
? `${ready} image${ready > 1 ? "s" : ""} ready — ${currentFormat().toUpperCase()}`
: `Mode: convert to ${currentFormat().toUpperCase()}`);
setStatus(ready ? readyLine(ready) : idleLine());
}
})
);
Expand All @@ -597,6 +631,22 @@ function wire() {
});
els.deleteOriginal.addEventListener("change", saveOptions);

// Instant convert. Arming it never fires a conversion by itself: only the
// next add does — silently converting an already-sitting queue would surprise.
els.instant.addEventListener("change", () => {
saveOptions();
syncInstantUi();
if (converting) return;
const ready = readyItems().length;
if (instantOn()) {
setStatus(ready
? `Instant convert on — press Convert to run the ${ready} queued image${ready > 1 ? "s" : ""}`
: "Instant convert on — images convert as soon as you add them");
} else {
setStatus(ready ? readyLine(ready) : idleLine());
}
});

// Background removal.
els.removeBg.addEventListener("change", () => {
if (els.removeBg.checked && !bgReady) {
Expand All @@ -613,7 +663,7 @@ function wire() {
if (!converting) {
setStatus(els.removeBg.checked
? "Background removal on — output gets a transparent background"
: `Mode: convert to ${currentFormat().toUpperCase()}`);
: idleLine());
}
});
els.bgDownload.addEventListener("click", installBg);
Expand All @@ -628,7 +678,7 @@ function wire() {
items.clear();
els.list.innerHTML = "";
setView();
setStatus(`Mode: convert to ${currentFormat().toUpperCase()}`);
setStatus(idleLine());
els.browse.focus();
});
els.clearDone.addEventListener("click", () => {
Expand All @@ -649,8 +699,9 @@ restoreOptions();
wire();
moveSegmentThumb();
syncBgControl();
syncInstantUi();
setView();
initBg();
setStatus(`Mode: convert to ${currentFormat().toUpperCase()}`);
setStatus(idleLine());
// Fonts load async; the thumb depends on final label widths.
if (document.fonts?.ready) document.fonts.ready.then(moveSegmentThumb);
69 changes: 67 additions & 2 deletions ui/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -616,14 +616,14 @@ fieldset.format-control legend { padding: 0; }
.action-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
gap: 14px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--line);
}

.status-line {
flex: 1;
margin: 0;
font-size: 12px;
color: var(--text-dim);
Expand All @@ -632,6 +632,71 @@ fieldset.format-control legend { padding: 0; }
text-overflow: ellipsis;
}

/* instant convert toggle — armed = lit bolt.
The checkbox is visually hidden the screen-reader-safe way (1px, clipped):
zero-sized focusables are skipped by some assistive tech. */
.instant-check {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}

.instant-pill {
flex: none;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 14px;
border: 1px solid var(--line);
border-radius: 10px;
font-size: 12.5px;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--text-dim);
cursor: pointer;
transition: color 180ms var(--ease-snap), border-color 180ms, background 180ms, filter 150ms;
}

.instant-pill:hover {
color: var(--text);
border-color: var(--line-strong);
background: rgba(255, 255, 255, 0.03);
}

.instant-pill svg {
width: 13px;
height: 13px;
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linejoin: round;
transition: fill 180ms, filter 180ms;
}

.instant-check:checked + .instant-pill {
color: var(--lime);
border-color: var(--lime-line);
background: var(--lime-soft);
}

.instant-check:checked + .instant-pill:hover { filter: brightness(1.08); }

.instant-check:checked + .instant-pill svg {
fill: var(--lime);
stroke: var(--lime);
filter: drop-shadow(0 0 5px rgba(191, 242, 5, 0.55));
}

.instant-check:focus-visible + .instant-pill {
outline: 2px solid var(--lime);
outline-offset: 2px;
}

.status-line.good { color: var(--lime); }
.status-line.bad { color: var(--red); }

Expand Down
Loading