[object Object]

← back to Dw Photo Capture

Camera chooser: back/front toggle (header + live-scan flip), persisted, drives scanner + all capture inputs; fix getLS->LS init crash

3355c70893250547615893dd24fb8cad074e313f · 2026-06-25 10:33:14 -0700 · Steve

Files touched

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>

← f482ee7 Multi-pass OCR scan: try every candidate (DW SKU + mfr# + na  ·  back to Dw Photo Capture  ·  Fix front-camera scan: mirror-retry (ocrSmart) recovers back 4ba1fb1 →