Store photos as native Blobs instead of base64 data URLs

IndexedDB stores Blobs via structured clone, eliminating the ~33% base64
overhead and avoiding a round-trip through FileReader on every capture.

- photoService.js: capture pushes { blob: file } directly; render sets img.src
  via URL.createObjectURL(p.blob); openLightbox/closeLightbox revoke blob URLs.
- db.js: drop JSON.parse(JSON.stringify(...)) clone which stripped Blobs;
  IndexedDB put() performs structured clone internally.
- exportService.js: download p.blob directly.
- utils.js: remove readFileAsDataUrl and dataUrlToBlob (both unused).
This commit is contained in:
Randy Fischer 2026-04-15 15:40:09 +02:00
parent 776bd6366d
commit e86aa5bae2
4 changed files with 37 additions and 28 deletions

View File

@ -24,10 +24,16 @@
`${fd.orderNr.replace('/', '-')}_inspectie_${new Date().toISOString().slice(0, 10)}.xml`);
let pc = 0;
for (const [nr, photos] of Object.entries(fd.photos)) {
photos.forEach((p, i) => { pc++; I.downloadBlob(I.dataUrlToBlob(p.dataUrl), `${fd.orderNr.replace('/', '-')}_loc${nr}_foto${i + 1}.jpg`); });
photos.forEach((p, i) => {
pc++;
I.downloadBlob(p.blob, `${fd.orderNr.replace('/', '-')}_loc${nr}_foto${i + 1}.jpg`);
});
}
for (const [pos, photos] of Object.entries(fd.overviewPhotos)) {
photos.forEach((p, i) => { pc++; I.downloadBlob(I.dataUrlToBlob(p.dataUrl), `${fd.orderNr.replace('/', '-')}_${pos}_foto${i + 1}.jpg`); });
photos.forEach((p, i) => {
pc++;
I.downloadBlob(p.blob, `${fd.orderNr.replace('/', '-')}_${pos}_foto${i + 1}.jpg`);
});
}
alert(`Opgeslagen!\n- 1 XML-bestand\n- ${pc} foto('s)`);
};

View File

@ -1,4 +1,8 @@
(function (A, D, I) {
function photoSrc(p) {
return URL.createObjectURL(p.blob);
}
A.openPhotoModal = function (nr, loc) {
A.state.currentPhotoRow = nr;
document.getElementById('photoModalTitle').textContent = `Foto's - Nr ${nr}: ${loc}`;
@ -17,7 +21,7 @@
for (const file of e.target.files) {
if (!A.state.formData.photos[row]) A.state.formData.photos[row] = [];
A.state.formData.photos[row].push({
dataUrl: await I.readFileAsDataUrl(file),
blob: file,
timestamp: new Date().toISOString(),
gps: await I.getGPS(),
filename: file.name
@ -39,14 +43,19 @@
}
grid.innerHTML = photos.map((p, i) => `
<div class="photo-thumb">
<img src="${p.dataUrl}" data-r="${row}" data-i="${i}">
<img data-r="${row}" data-i="${i}">
<button class="delete-btn" data-action="delete-photo" data-r="${row}" data-i="${i}">&times;</button>
<div class="photo-time">${new Date(p.timestamp).toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' })}${p.gps ? ' \u2316' : ''}</div>
</div>`).join('');
photos.forEach((p, i) => {
const img = grid.querySelector(`img[data-r="${row}"][data-i="${i}"]`);
if (img) img.src = photoSrc(p);
});
grid.querySelectorAll('img[data-r]').forEach(img => img.addEventListener('click', () => {
const ph = A.state.formData.photos[img.dataset.r]?.[+img.dataset.i];
if (ph) A.openLightbox(ph.dataUrl);
if (ph) A.openLightbox(photoSrc(ph));
}));
grid.querySelectorAll('button[data-action="delete-photo"]').forEach(btn => btn.addEventListener('click', (ev) => {
ev.stopPropagation();
@ -84,7 +93,7 @@
if (!A.state.formData.overviewPhotos[pos]) A.state.formData.overviewPhotos[pos] = [];
for (const file of e.target.files) {
A.state.formData.overviewPhotos[pos].push({
dataUrl: await I.readFileAsDataUrl(file),
blob: file,
timestamp: new Date().toISOString(),
gps: await I.getGPS(),
filename: file.name
@ -121,15 +130,20 @@
}
grid.innerHTML = all.map(it => `
<div class="thumb-item">
<img src="${it.photo.dataUrl}" data-pos="${it.pos}" data-idx="${it.index}">
<img data-pos="${it.pos}" data-idx="${it.index}">
<button class="thumb-del" data-action="delete-ov-photo" data-pos="${it.pos}" data-idx="${it.index}">&times;</button>
<div class="thumb-label">${it.label}</div>
</div>`).join('');
all.forEach(it => {
const img = grid.querySelector(`img[data-pos="${it.pos}"][data-idx="${it.index}"]`);
if (img) img.src = photoSrc(it.photo);
});
grid.querySelectorAll('img[data-pos]').forEach(img => img.addEventListener('click', ev => {
ev.stopPropagation();
const ph = A.state.formData.overviewPhotos[img.dataset.pos]?.[+img.dataset.idx];
if (ph) A.openLightbox(ph.dataUrl);
if (ph) A.openLightbox(photoSrc(ph));
}));
grid.querySelectorAll('button[data-action="delete-ov-photo"]').forEach(btn => btn.addEventListener('click', ev => {
ev.stopPropagation();
@ -152,12 +166,18 @@
};
A.openLightbox = function (src) {
document.getElementById('lightboxImg').src = src;
const img = document.getElementById('lightboxImg');
const prev = img.src;
img.src = src;
document.getElementById('lightbox').classList.add('active');
if (prev && prev.startsWith('blob:')) URL.revokeObjectURL(prev);
};
A.closeLightbox = function () {
const img = document.getElementById('lightboxImg');
const src = img.src;
document.getElementById('lightbox').classList.remove('active');
document.getElementById('lightboxImg').src = '';
img.src = '';
if (src && src.startsWith('blob:')) URL.revokeObjectURL(src);
};
})(window.App.Application, window.App.Domain, window.App.Infrastructure);

View File

@ -27,7 +27,7 @@
const d = await ensureDb();
return new Promise((resolve, reject) => {
const tx = d.transaction('inspections', 'readwrite');
tx.objectStore('inspections').put(JSON.parse(JSON.stringify(formData)));
tx.objectStore('inspections').put(formData);
tx.oncomplete = () => resolve();
tx.onerror = reject;
});

View File

@ -11,23 +11,6 @@
URL.revokeObjectURL(a.href);
};
I.dataUrlToBlob = function (dataUrl) {
const parts = dataUrl.split(',');
const mime = parts[0].match(/:(.*?);/)[1];
const bin = atob(parts[1]);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
return new Blob([arr], { type: mime });
};
I.readFileAsDataUrl = function (file) {
return new Promise(resolve => {
const fr = new FileReader();
fr.onload = () => resolve(fr.result);
fr.readAsDataURL(file);
});
};
I.formatDate = function (d) {
if (!d || d.length !== 8) return d || '-';
return d.substring(6, 8) + '.' + d.substring(4, 6) + '.' + d.substring(0, 4);