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:
parent
776bd6366d
commit
e86aa5bae2
|
|
@ -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)`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}">×</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}">×</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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue