Compare commits
10 Commits
86d3aac771
...
0de8e80019
| Author | SHA1 | Date | |
|---|---|---|---|
| 0de8e80019 | |||
| ccb27f2f09 | |||
| c1191c90a9 | |||
| e4259d27f3 | |||
| 05278e9ae3 | |||
| 4f71779a12 | |||
| c1fb21d165 | |||
| 3515edbc46 | |||
| b3ba13ad14 | |||
| 046416d62c |
+19
-1
@@ -168,6 +168,24 @@ $("#logout").onclick=logout;
|
||||
$("#themeSelect").onchange=e=>{try{localStorage.setItem("vboxstock-theme",e.target.value)}catch{}applyTheme(e.target.value);};
|
||||
systemTheme.addEventListener?.("change",()=>{if(savedTheme()==="system")applyTheme("system")});
|
||||
$("#date").textContent=new Date().toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"});
|
||||
if(!("BarcodeDetector" in window))document.addEventListener("click",e=>{const button=e.target.closest(".scan-camera");if(button){e.stopPropagation();scanLabelCompat(button.closest("form"),button.closest("form").querySelector(".scan-help"))}},true);
|
||||
async function scanLabelWithLibrary(form,help){if(!navigator.mediaDevices?.getUserMedia){help.textContent="Camera access is unavailable. Use manual entry or enable camera access for this site.";return}let stream,overlay,controls;try{const {BrowserMultiFormatReader}=await import("https://cdn.jsdelivr.net/npm/@zxing/browser@0.1.5/+esm"),reader=new BrowserMultiFormatReader();stream=await navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:"environment"}}});const video=document.createElement("video");video.autoplay=true;video.playsInline=true;video.srcObject=stream;overlay=document.createElement("div");overlay.className="scanner-overlay";overlay.innerHTML='<div><strong>Point at one barcode at a time</strong><small>Scan UID, serial number, and MAC on the same label.</small><button type="button">Close camera</button></div>';overlay.prepend(video);document.body.append(overlay);const finish=()=>{controls?.stop();stream.getTracks().forEach(track=>track.stop());overlay.remove()};overlay.querySelector("button").onclick=finish;await video.play();controls=await reader.decodeFromVideoElement(video,(result,error)=>{if(!result)return;const clean=String(result.getText()||"").trim().replace(/^\s*(UID|SN|MAC)\s*[:#]?\s*/i,"");if(!clean||[form.elements.uid.value,form.elements.sn.value,form.elements.mac.value].includes(clean))return;if(/^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(clean)||/^[0-9a-f]{12}$/i.test(clean))form.elements.mac.value=clean;else if(/^V\w/i.test(clean))form.elements.sn.value=clean;else if(!form.elements.uid.value)form.elements.uid.value=clean;else if(!form.elements.sn.value)form.elements.sn.value=clean;else if(!form.elements.mac.value)form.elements.mac.value=clean;help.textContent=form.elements.uid.value&&form.elements.sn.value&&form.elements.mac.value?"All three identifiers scanned. Close the camera and add this device.":"Barcode captured. Aim at the next code."})}catch(error){if(controls)controls.stop();if(stream)stream.getTracks().forEach(track=>track.stop());if(overlay)overlay.remove();help.textContent="Automatic scanning could not load. Enter the three identifiers manually or try Chrome."}}
|
||||
async function scanLabelPhoto(form,help,file){let url;try{const {BrowserMultiFormatReader}=await import("https://cdn.jsdelivr.net/npm/@zxing/browser@0.1.5/+esm"),reader=new BrowserMultiFormatReader();url=URL.createObjectURL(file);const image=new Image();image.src=url;await image.decode();const canvas=document.createElement("canvas"),ctx=canvas.getContext("2d"),found=[];canvas.width=image.naturalWidth;canvas.height=Math.floor(image.naturalHeight/3);for(let i=0;i<3;i++){ctx.clearRect(0,0,canvas.width,canvas.height);ctx.drawImage(image,0,i*canvas.height,canvas.width,canvas.height,0,0,canvas.width,canvas.height);try{const result=reader.decodeFromCanvas(canvas),value=String(result.getText()||"").trim().replace(/^\\s*(UID|SN|MAC)\\s*[:#]?\\s*/i,"");if(value&&!found.includes(value))found.push(value)}catch{}}for(const clean of found){if(/^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(clean)||/^[0-9a-f]{12}$/i.test(clean))form.elements.mac.value=clean;else if(/^V\\w/i.test(clean))form.elements.sn.value=clean;else if(!form.elements.uid.value)form.elements.uid.value=clean;else if(!form.elements.sn.value)form.elements.sn.value=clean;else if(!form.elements.mac.value)form.elements.mac.value=clean}help.textContent=found.length?found.length+" barcode"+(found.length===1?"":"s")+" read from the photo. Review the fields before adding the device.":"No barcodes were detected. Retake the photo with the full label in focus."}catch(error){help.textContent="The photo could not be processed. Retake it in brighter light or enter the values manually."}finally{if(url)URL.revokeObjectURL(url);file=null}}
|
||||
const receiveObserver=new MutationObserver(()=>{const form=$("#receiveForm");if(!form||form.querySelector(".photo-scan"))return;const button=document.createElement("button");button.type="button";button.className="secondary photo-scan";button.textContent="Take photo of label";const input=document.createElement("input");input.type="file";input.accept="image/*";input.capture="environment";input.className="photo-input";input.style.position="absolute";input.style.width="1px";input.style.height="1px";input.style.opacity="0";input.style.pointerEvents="none";form.querySelector(".scan-panel .scan-camera").after(button,input);button.onclick=()=>input.click();input.onchange=()=>{if(input.files?.[0])scanLabelPhoto(form,form.querySelector(".scan-help"),input.files[0]);input.value=""}});receiveObserver.observe($("#modalBody"),{childList:true});
|
||||
async function scanLabelPhotoFixed(form,help,file){let url;try{const {BrowserMultiFormatReader}=await import("https://cdn.jsdelivr.net/npm/@zxing/browser@0.1.5/+esm"),reader=new BrowserMultiFormatReader();url=URL.createObjectURL(file);const image=new Image();image.src=url;await image.decode();const canvas=document.createElement("canvas"),ctx=canvas.getContext("2d"),found=[];canvas.width=image.naturalWidth;canvas.height=Math.floor(image.naturalHeight/3);for(let i=0;i<3;i++){ctx.clearRect(0,0,canvas.width,canvas.height);ctx.drawImage(image,0,i*canvas.height,canvas.width,canvas.height,0,0,canvas.width,canvas.height);try{const result=reader.decodeFromCanvas(canvas),value=String(result.getText()||"").trim().replace(/^\s*(UID|SN|MAC)\s*[:#]?\s*/i,"");if(value&&!found.includes(value))found.push(value)}catch{}}if(found.length===3){form.elements.uid.value=found[0];form.elements.sn.value=found[1];form.elements.mac.value=found[2]}else for(const value of found){const clean=value.replace(/^\s*(UID|SN|MAC)\s*[:#]?\s*/i,"");if(/^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(clean)||/^[0-9a-f]{12}$/i.test(clean))form.elements.mac.value=clean;else if(/^V[0-9A-Z]/i.test(clean))form.elements.sn.value=clean;else if(!form.elements.uid.value)form.elements.uid.value=clean;else if(!form.elements.sn.value)form.elements.sn.value=clean;else if(!form.elements.mac.value)form.elements.mac.value=clean}help.textContent=found.length?found.length+" barcode"+(found.length===1?"":"s")+" read from the photo. Review the fields before adding the device.":"No barcodes were detected. Retake the photo with the full label in focus."}catch(error){help.textContent="The photo could not be processed. Retake it in brighter light or enter the values manually."}finally{if(url)URL.revokeObjectURL(url)}}
|
||||
const photoFixObserver=new MutationObserver(()=>{const input=$("#receiveForm .photo-input");if(input&&!input.dataset.fixed){input.dataset.fixed="true";input.onchange=()=>{if(input.files?.[0])scanLabelPhotoFixed($("#receiveForm"),$("#receiveForm .scan-help"),input.files[0]);input.value=""}}});photoFixObserver.observe($("#modalBody"),{childList:true});
|
||||
async function scanLabelPhotoBetter(form,help,file){let url;try{const {BrowserMultiFormatReader}=await import("https://cdn.jsdelivr.net/npm/@zxing/browser@0.1.5/+esm"),reader=new BrowserMultiFormatReader();url=URL.createObjectURL(file);const image=new Image();image.src=url;await image.decode();const canvas=document.createElement("canvas"),ctx=canvas.getContext("2d"),found=[];canvas.width=image.naturalWidth;canvas.height=image.naturalHeight;const regions=[{y:0,h:1},{y:.2,h:.65},{y:.33,h:.22},{y:.52,h:.22},{y:.7,h:.22}];for(const region of regions){const sy=Math.floor(image.naturalHeight*region.y),sh=Math.floor(image.naturalHeight*region.h);canvas.width=image.naturalWidth;canvas.height=sh;ctx.drawImage(image,0,sy,image.naturalWidth,sh,0,0,canvas.width,canvas.height);try{const result=await reader.decodeFromImageUrl(canvas.toDataURL("image/jpeg",.95)),value=String(result.getText()||"").trim().replace(/^\s*(UID|SN|MAC)\s*[:#]?\s*/i,"");if(value&&!found.includes(value))found.push(value)}catch{}}if(found.length===3){form.elements.uid.value=found[0];form.elements.sn.value=found[1];form.elements.mac.value=found[2]}else for(const value of found){if(/^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(value)||/^[0-9a-f]{12}$/i.test(value))form.elements.mac.value=value;else if(/^V[0-9A-Z]/i.test(value))form.elements.sn.value=value;else if(!form.elements.uid.value)form.elements.uid.value=value;else if(!form.elements.sn.value)form.elements.sn.value=value;else if(!form.elements.mac.value)form.elements.mac.value=value}help.textContent=found.length?found.length+" barcode"+(found.length===1?"":"s")+" read from the photo. Review the fields before adding the device.":"No barcodes were detected. Retake the photo closer and in brighter light."}catch(error){help.textContent="The photo could not be decoded. Retake it with the label filling most of the frame, or enter the values manually."}finally{if(url)URL.revokeObjectURL(url)}}
|
||||
const photoBetterObserver=new MutationObserver(()=>{const input=$("#receiveForm .photo-input");if(input&&!input.dataset.better){input.dataset.better="true";input.onchange=()=>{if(input.files?.[0])scanLabelPhotoBetter($("#receiveForm"),$("#receiveForm .scan-help"),input.files[0]);input.value=""}}});photoBetterObserver.observe($("#modalBody"),{childList:true});
|
||||
async function scanLabelPhotoFinal(form,help,file){let url;try{const {BrowserMultiFormatReader}=await import("https://cdn.jsdelivr.net/npm/@zxing/browser@0.1.5/+esm"),reader=new BrowserMultiFormatReader();url=URL.createObjectURL(file);const image=new Image();image.src=url;await image.decode();const canvas=document.createElement("canvas"),ctx=canvas.getContext("2d"),found=[];const regions=[{y:0,h:1},{y:.18,h:.68},{y:.32,h:.2},{y:.52,h:.2},{y:.7,h:.2}];for(const region of regions){const sy=Math.floor(image.naturalHeight*region.y),sh=Math.floor(image.naturalHeight*region.h);canvas.width=image.naturalWidth;canvas.height=sh;ctx.drawImage(image,0,sy,image.naturalWidth,sh,0,0,canvas.width,canvas.height);const crop=new Image();crop.src=canvas.toDataURL("image/jpeg",.98);await crop.decode();try{const result=await reader.decodeFromImageElement(crop),value=String(result.getText()||"").trim().replace(/^\s*(UID|SN|MAC)\s*[:#]?\s*/i,"");if(value&&!found.includes(value))found.push(value)}catch{}}if(found.length===3){form.elements.uid.value=found[0];form.elements.sn.value=found[1];form.elements.mac.value=found[2]}else for(const value of found){if(/^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(value)||/^[0-9a-f]{12}$/i.test(value))form.elements.mac.value=value;else if(/^V[0-9A-Z]/i.test(value))form.elements.sn.value=value;else if(!form.elements.uid.value)form.elements.uid.value=value;else if(!form.elements.sn.value)form.elements.sn.value=value;else if(!form.elements.mac.value)form.elements.mac.value=value}help.textContent=found.length?found.length+" barcode"+(found.length===1?"":"s")+" read. Review the fields before adding the device.":"No barcodes detected. Retake the photo with the label filling most of the frame."}catch(error){help.textContent="The photo could not be decoded. Retake it closer and in brighter light, or enter the values manually."}finally{if(url)URL.revokeObjectURL(url)}}
|
||||
const photoFinalObserver=new MutationObserver(()=>{const input=$("#receiveForm .photo-input");if(input&&!input.dataset.final){input.dataset.final="true";input.onchange=()=>{if(input.files?.[0])scanLabelPhotoFinal($("#receiveForm"),$("#receiveForm .scan-help"),input.files[0]);input.value=""}}});photoFinalObserver.observe($("#modalBody"),{childList:true});
|
||||
async function scanLabelPhotoPreprocessed(form,help,file){let url;try{const {BrowserMultiFormatReader}=await import("https://cdn.jsdelivr.net/npm/@zxing/browser@0.1.5/+esm"),reader=new BrowserMultiFormatReader();url=URL.createObjectURL(file);const image=new Image();image.src=url;await image.decode();const source=document.createElement("canvas"),sctx=source.getContext("2d");source.width=image.naturalWidth;source.height=image.naturalHeight;sctx.drawImage(image,0,0);const found=[],regions=[{y:0,h:1},{y:.15,h:.7},{y:.28,h:.24},{y:.48,h:.24},{y:.68,h:.24}];for(const region of regions){const sy=Math.floor(source.height*region.y),sh=Math.floor(source.height*region.h),canvas=document.createElement("canvas"),ctx=canvas.getContext("2d"),scale=2;canvas.width=source.width*scale;canvas.height=sh*scale;ctx.drawImage(source,0,sy,source.width,sh,0,0,canvas.width,canvas.height);const pixels=ctx.getImageData(0,0,canvas.width,canvas.height);for(let i=0;i<pixels.data.length;i+=4){const gray=(pixels.data[i]*.299+pixels.data[i+1]*.587+pixels.data[i+2]*.114);const value=gray>175?255:gray<80?0:gray;pixels.data[i]=pixels.data[i+1]=pixels.data[i+2]=value}ctx.putImageData(pixels,0,0);const crop=new Image();crop.src=canvas.toDataURL("image/png");await crop.decode();try{const result=await reader.decodeFromImageElement(crop),value=String(result.getText()||"").trim().replace(/^\s*(UID|SN|MAC)\s*[:#]?\s*/i,"");if(value&&!found.includes(value))found.push(value)}catch{}}if(found.length===3){form.elements.uid.value=found[0];form.elements.sn.value=found[1];form.elements.mac.value=found[2]}else for(const value of found){if(/^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(value)||/^[0-9a-f]{12}$/i.test(value))form.elements.mac.value=value;else if(/^V[0-9A-Z]/i.test(value))form.elements.sn.value=value;else if(!form.elements.uid.value)form.elements.uid.value=value;else if(!form.elements.sn.value)form.elements.sn.value=value;else if(!form.elements.mac.value)form.elements.mac.value=value}help.textContent=found.length?found.length+" barcode"+(found.length===1?"":"s")+" read after image enhancement. Review the fields before adding the device.":"No barcodes detected after image enhancement. Try a closer, brighter photo."}catch(error){help.textContent="The enhanced photo could not be decoded. Enter the identifiers manually."}finally{if(url)URL.revokeObjectURL(url)}}
|
||||
const photoPreprocessObserver=new MutationObserver(()=>{const input=$("#receiveForm .photo-input");if(input&&!input.dataset.preprocessed){input.dataset.preprocessed="true";input.onchange=()=>{if(input.files?.[0])scanLabelPhotoPreprocessed($("#receiveForm"),$("#receiveForm .scan-help"),input.files[0]);input.value=""}}});photoPreprocessObserver.observe($("#modalBody"),{childList:true});
|
||||
async function scanLabelPhotoOcr(form,help,file){let url,worker;try{const Tesseract=await import("https://cdn.jsdelivr.net/npm/tesseract.js@5/+esm");url=URL.createObjectURL(file);worker=await Tesseract.createWorker("eng");const result=await worker.recognize(url),text=String(result.data.text||"").replace(/\r/g," ");const uid=text.match(/UID\s*[:#]?\s*([A-Z0-9]+)/i)?.[1],sn=text.match(/SN\s*[:#]?\s*([A-Z0-9]+)/i)?.[1],mac=text.match(/MAC\s*[:#]?\s*((?:[A-F0-9]{2}[:\-]?){6})/i)?.[1];if(uid)form.elements.uid.value=uid;if(sn)form.elements.sn.value=sn;if(mac)form.elements.mac.value=mac.replace(/-/g,":").match(/.{2}/g)?.join(":")||mac;const count=[uid,sn,mac].filter(Boolean).length;help.textContent=count?count+" labeled value"+(count===1?"":"s")+" read from the photo. Review the fields before adding the device.":"No UID, SN, or MAC text was recognized. Retake the photo with the label filling most of the frame."}catch(error){help.textContent="The printed label text could not be recognized. Retake it closer and in brighter light, or enter the values manually."}finally{await worker?.terminate().catch(()=>{});if(url)URL.revokeObjectURL(url)}}
|
||||
const photoOcrObserver=new MutationObserver(()=>{const input=$("#receiveForm .photo-input");if(input&&!input.dataset.ocr){input.dataset.ocr="true";input.onchange=()=>{if(input.files?.[0])scanLabelPhotoOcr($("#receiveForm"),$("#receiveForm .scan-help"),input.files[0]);input.value=""}}});photoOcrObserver.observe($("#modalBody"),{childList:true});
|
||||
const normalizeMacValue=value=>{const hex=String(value||"").replace(/[^0-9a-f]/gi,"").slice(0,12);return hex.length===12?(hex.match(/.{2}/g)||[]).join(":"):value};
|
||||
const macFormatObserver=new MutationObserver(()=>{const input=$("#receiveForm .photo-input");if(input&&!input.dataset.macfix){input.dataset.macfix="true";input.onchange=async()=>{if(input.files?.[0]){await scanLabelPhotoOcr($("#receiveForm"),$("#receiveForm .scan-help"),input.files[0]);$("#receiveForm").elements.mac.value=normalizeMacValue($("#receiveForm").elements.mac.value)}input.value=""}}});macFormatObserver.observe($("#modalBody"),{childList:true});
|
||||
async function scanLabelPhotoTargeted(form,help,file){let url,worker;try{const Tesseract=await import("https://cdn.jsdelivr.net/npm/tesseract.js@5/+esm");url=URL.createObjectURL(file);const image=new Image();image.src=url;await image.decode();const source=document.createElement("canvas"),sctx=source.getContext("2d");source.width=image.naturalWidth;source.height=image.naturalHeight;sctx.drawImage(image,0,0);const pixels=sctx.getImageData(0,0,source.width,source.height).data;let minX=source.width,minY=source.height,maxX=0,maxY=0;for(let y=0;y<source.height;y+=4)for(let x=0;x<source.width;x+=4){const i=(y*source.width+x)*4;if(pixels[i]>170&&pixels[i+1]>170&&pixels[i+2]>170){minX=Math.min(minX,x);minY=Math.min(minY,y);maxX=Math.max(maxX,x);maxY=Math.max(maxY,y)}}if(maxX<=minX||maxY<=minY)throw new Error("label not found");const pad=24,crop=document.createElement("canvas"),ctx=crop.getContext("2d"),x=Math.max(0,minX-pad),y=Math.max(0,minY-pad),w=Math.min(source.width-x,maxX-minX+pad*2),h=Math.min(source.height-y,maxY-minY+pad*2),scale=2;crop.width=w*scale;crop.height=h*scale;ctx.drawImage(source,x,y,w,h,0,0,crop.width,crop.height);const data=ctx.getImageData(0,0,crop.width,crop.height);for(let i=0;i<data.data.length;i+=4){const gray=data.data[i]*.299+data.data[i+1]*.587+data.data[i+2]*.114;const v=gray>185?255:gray<100?0:gray;data.data[i]=data.data[i+1]=data.data[i+2]=v}ctx.putImageData(data,0,0);worker=await Tesseract.createWorker("eng");await worker.setParameters({tessedit_pageseg_mode:"6",tessedit_char_whitelist:"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:- "});const result=await worker.recognize(crop.toDataURL("image/png")),text=String(result.data.text||"").replace(/\r/g," ");const uid=text.match(/UID\s*[:#]?\s*([A-Z0-9]+)/i)?.[1],sn=text.match(/SN\s*[:#]?\s*([A-Z0-9]+)/i)?.[1],raw=text.match(/MAC\s*[:#]?\s*([A-F0-9: -]{12,})/i)?.[1],mac=raw?normalizeMacValue(raw):null;if(uid)form.elements.uid.value=uid;if(sn)form.elements.sn.value=sn;if(mac)form.elements.mac.value=mac;const count=[uid,sn,mac].filter(Boolean).length;help.textContent=count?count+" labeled value"+(count===1?"":"s")+" read from the cropped label. Review before adding.":"No labeled values detected. Retake the photo closer and keep the white label fully visible."}catch(error){help.textContent="The label could not be read. Retake a close, well-lit photo with the white label filling the frame."}finally{await worker?.terminate().catch(()=>{});if(url)URL.revokeObjectURL(url)}}
|
||||
const targetedOcrObserver=new MutationObserver(()=>{const input=$("#receiveForm .photo-input");if(input&&!input.dataset.targeted){input.dataset.targeted="true";input.onchange=()=>{if(input.files?.[0])scanLabelPhotoTargeted($("#receiveForm"),$("#receiveForm .scan-help"),input.files[0]);input.value=""}}});targetedOcrObserver.observe($("#modalBody"),{childList:true});
|
||||
const autoAddScanObserver=new MutationObserver(()=>{const form=$("#receiveForm"),help=form?.querySelector(".scan-help"),marker=form?.querySelector("[data-scan-added]");if(form&&marker&&!form.elements.uid.value&&!form.elements.sn.value&&!form.elements.mac.value)marker.remove();if(form&&help?.textContent.includes("read")&&form.elements.uid.value&&form.elements.sn.value&&form.elements.mac.value&&!marker){const added=document.createElement("i");added.dataset.scanAdded="true";added.hidden=true;form.append(added);form.querySelector(".add-device").click()}});autoAddScanObserver.observe($("#modalBody"),{childList:true,subtree:true,characterData:true});
|
||||
if(!("BarcodeDetector" in window))document.addEventListener("click",e=>{const button=e.target.closest(".scan-camera");if(button){e.stopPropagation();scanLabelWithLibrary(button.closest("form"),button.closest("form").querySelector(".scan-help"))}},true);
|
||||
applyTheme();
|
||||
initialize();
|
||||
|
||||
Reference in New Issue
Block a user