Camera chooser: back/front toggle (header + live-scan flip), persisted, drives scanner + all capture inputs; fix getLS->LS init crash
Files touched
M data/build.jsonM public/index.html
Diff
commit 3355c70893250547615893dd24fb8cad074e313f
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jun 25 10:33:14 2026 -0700
Camera chooser: back/front toggle (header + live-scan flip), persisted, drives scanner + all capture inputs; fix getLS->LS init crash
---
data/build.json | 6 ++++--
public/index.html | 30 +++++++++++++++++++++++++++---
2 files changed, 31 insertions(+), 5 deletions(-)
diff --git a/data/build.json b/data/build.json
index e343f1f..c966201 100644
--- a/data/build.json
+++ b/data/build.json
@@ -1,5 +1,5 @@
{
- "next": 37,
+ "next": 39,
"map": {
"63863152": 27,
"578af86f": 2,
@@ -35,6 +35,8 @@
"224e5276": 33,
"3ee9018c": 34,
"008cadb1": 35,
- "237a0e5e": 36
+ "237a0e5e": 36,
+ "57975a47": 37,
+ "331a91f4": 38
}
}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index ed9089a..e98046c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -80,6 +80,7 @@
@keyframes scan{0%{top:8%}50%{top:90%}100%{top:8%}}
.scan-status{position:absolute;bottom:max(90px,22%);left:50%;transform:translateX(-50%);background:rgba(8,7,6,.8);color:#fff;font:700 18px/1 ui-monospace,Menlo,monospace;letter-spacing:.04em;padding:11px 18px;border-radius:99px;border:1px solid rgba(255,255,255,.2)}
.scan-cancel{position:absolute;bottom:max(28px,env(safe-area-inset-bottom));left:50%;transform:translateX(-50%);background:#16140f;border:1px solid var(--line);color:var(--ink);border-radius:12px;padding:13px 26px;font-size:15px;font-weight:600;cursor:pointer}
+ .scan-flip{position:absolute;bottom:max(28px,env(safe-area-inset-bottom));right:18px;background:#16140f;border:1px solid var(--line);color:var(--ink);border-radius:12px;padding:13px 18px;font-size:15px;font-weight:600;cursor:pointer}
/* photo gallery */
.gallery{position:fixed;inset:0;z-index:90;background:rgba(10,9,7,.98);display:flex;flex-direction:column}
.gallery[hidden]{display:none}
@@ -155,6 +156,7 @@
<div class="controls">
<input type="search" id="q" placeholder="🔎 Search name or model #…" autocapitalize="characters">
<button class="scanbtn" id="scanBtn" title="Scan a printed SKU/model #">🔢 Scan</button>
+ <button class="scanbtn" id="camBtn" title="Choose camera (back / front)">📷 Back</button>
<input type="file" accept="image/*" capture="environment" id="scanInput" hidden>
<select id="sort">
<option value="ready">Priced first</option>
@@ -186,6 +188,7 @@
<video id="scanvideo" playsinline muted></video>
<div class="scan-frame"><div class="scan-line"></div></div>
<div class="scan-status" id="scanStatus">Point at the printed SKU…</div>
+ <button class="scan-flip" id="scanFlip">🔄 Flip camera</button>
<button class="scan-cancel" id="scanCancel">✕ Cancel</button>
</div>
@@ -414,7 +417,7 @@ function render(){
<div class="actions">
${adjustable?'<button class="enh">✨ Enhance</button>':''}
${x.product_id?'<button class="gal">🖼 Photos</button>':''}
- <label class="shoot">${isNew?'📷 Create + Go Live':photographed?'📷 Update Photo':'📷 Add Photo'}<input type="file" accept="image/*" capture="environment"></label>
+ <label class="shoot">${isNew?'📷 Create + Go Live':photographed?'📷 Update Photo':'📷 Add Photo'}<input type="file" accept="image/*" capture="${camFacing==='user'?'user':'environment'}"></label>
${(x.done||x.live||x.image||isNew)?'':'<button class="skip">Skip</button>'}
</div>
</div>`;
@@ -829,14 +832,31 @@ document.querySelectorAll('.chip').forEach(c=>c.addEventListener('click',()=>{
// 🔢 Scan a printed mfr#/SKU with the camera → OCR (server Vision) → jump to that product.
/* ===== LIVE scanner: stream the camera, OCR each frame, LOCK IN when a SKU repeats ===== */
let scanStream=null,scanTimer=null,scanBusy=false,_lastTop=null,_lockHits=0;
+// shared camera choice (back/front) — drives BOTH the live scanner and every "Add Photo" capture input
+let camFacing=LS('camFacing')||'environment';
+function applyCamCapture(){ // point all <input capture> pickers at the chosen lens
+ const v=(camFacing==='user')?'user':'environment';
+ document.querySelectorAll('input[type=file][accept^="image"]').forEach(i=>{ if(i.hasAttribute('capture')) i.setAttribute('capture',v); });
+}
+async function startScanStream(){ // (re)acquire the live stream on the current facing
+ if(scanStream){ scanStream.getTracks().forEach(t=>t.stop()); scanStream=null; }
+ scanStream=await navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:camFacing},width:{ideal:1280}}});
+ const v=$('#scanvideo'); v.srcObject=scanStream; try{await v.play();}catch(e){}
+}
async function openLiveScan(){
- try{ scanStream=await navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:'environment'},width:{ideal:1280}}}); }
+ try{ await startScanStream(); }
catch(e){ toast('Camera blocked — using snap'); return $('#scanInput').click(); }
- const v=$('#scanvideo'); v.srcObject=scanStream; try{await v.play();}catch(e){}
_lastTop=null;_lockHits=0; $('#scanStatus').textContent='Point at the printed SKU…';
+ $('#scanFlip').textContent=(camFacing==='user'?'🔄 Back camera':'🔄 Front camera');
$('#scanlive').hidden=false; document.body.style.overflow='hidden';
scanTimer=setInterval(scanTick,600);
}
+async function flipCam(){ // toggle back<->front, persist, re-acquire live + re-point capture inputs
+ camFacing=(camFacing==='user')?'environment':'user'; setLS('camFacing',camFacing); applyCamCapture(); syncCamBtn();
+ $('#scanFlip').textContent=(camFacing==='user'?'🔄 Back camera':'🔄 Front camera');
+ if(!$('#scanlive').hidden){ try{ await startScanStream(); }catch(e){ toast('Could not switch camera'); } }
+ else toast(camFacing==='user'?'📷 Front camera':'📷 Back camera');
+}
function closeLiveScan(){ if(scanTimer)clearInterval(scanTimer); scanTimer=null; if(scanStream){scanStream.getTracks().forEach(t=>t.stop());scanStream=null;} $('#scanlive').hidden=true; document.body.style.overflow=''; }
async function scanTick(){
if(scanBusy)return; scanBusy=true;
@@ -889,6 +909,9 @@ async function applyScan(d){
else { chipTo('shop'); toast('✓ '+hit.q+' — '+hit.n+' in Shopify'); doShopSearch(); }
}
$('#scanCancel').addEventListener('click',closeLiveScan);
+$('#scanFlip').addEventListener('click',flipCam);
+function syncCamBtn(){ $('#camBtn').textContent=(camFacing==='user'?'🤳 Front':'📷 Back'); }
+$('#camBtn').addEventListener('click',flipCam);
$('#scanBtn').addEventListener('click',()=>{
if(window.isSecureContext && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) openLiveScan();
else $('#scanInput').click(); // HTTP (no secure context) → fall back to single-snap
@@ -906,6 +929,7 @@ $('#scanInput').addEventListener('change',async e=>{
await applyScan(d);
}catch(err){ toast('Scan failed — check connection'); }
});
+applyCamCapture(); syncCamBtn(); // apply saved camera choice to capture inputs + header toggle
load(); loadNew(); loadTwil(); loadFav(); loadRecent();
setInterval(()=>{ if(filter!=='any'&&filter!=='shop'&&filter!=='fav'&&filter!=='recent') load(); }, 60000);
</script>