Spaces:
Running
Running
| {# Revision Notes Modal - v5 Ultra Fast #} | |
| <style> | |
| #notesModal .modal-body{padding:0;overflow:hidden} | |
| #ncw{width:100%;height:100%;background:#fff;position:relative;touch-action:none} | |
| #nc{position:absolute;top:0;left:0;background:#fff;background-image:radial-gradient(#ddd 1px,transparent 1px);background-size:20px 20px} | |
| .ntb{position:absolute;top:12px;left:50%;transform:translateX(-50%);display:flex;gap:4px;padding:8px 12px;border-radius:50px;background:rgba(0,0,0,.9);z-index:99} | |
| .tb{width:36px;height:36px;border:0;border-radius:50%;background:0 0;color:rgba(255,255,255,.7);font-size:1rem;cursor:pointer;transition:.1s} | |
| .tb:hover{background:rgba(255,255,255,.15);color:#fff} | |
| .tb.on{background:#0d6efd;color:#fff} | |
| .sp{width:1px;height:24px;background:rgba(255,255,255,.2);margin:0 4px;align-self:center} | |
| .cd{width:22px;height:22px;border-radius:50%;border:2px solid transparent;cursor:pointer}.cd.on{border-color:#fff;transform:scale(1.15)} | |
| .cg{display:flex;gap:4px;align-items:center} | |
| #rpnl{position:absolute;bottom:12px;right:12px;width:180px;background:#222;border-radius:8px;padding:6px;z-index:90} | |
| #rpnl img{width:100%;border-radius:4px} | |
| #rpnl.hide{display:none} | |
| .sz{width:50px;height:28px;background:#333;border:0;border-radius:4px;color:#fff;font-size:.75rem;text-align:center} | |
| </style> | |
| <div class="modal fade" id="notesModal" tabindex="-1" data-bs-backdrop="static"> | |
| <div class="modal-dialog modal-fullscreen"><div class="modal-content bg-dark"><div class="modal-body"> | |
| <div class="ntb"> | |
| <button class="tb on" data-tool="pen" title="Pen"><i class="fas fa-pencil-alt"></i></button> | |
| <button class="tb" data-tool="marker" title="Marker"><i class="fas fa-marker"></i></button> | |
| <button class="tb" data-tool="eraser" title="Eraser"><i class="fas fa-eraser"></i></button> | |
| <div class="sp"></div> | |
| <div class="cg"> | |
| <div class="cd on" data-c="#222" style="background:#222"></div> | |
| <div class="cd" data-c="#e63946" style="background:#e63946"></div> | |
| <div class="cd" data-c="#0d6efd" style="background:#0d6efd"></div> | |
| <div class="cd" data-c="#2a9d8f" style="background:#2a9d8f"></div> | |
| </div> | |
| <div class="sp"></div> | |
| <input type="number" class="sz" id="bsize" value="2" min="1" max="30"> | |
| <div class="sp"></div> | |
| <button class="tb" id="bundo" title="Undo"><i class="fas fa-undo"></i></button> | |
| <button class="tb" id="bclear" title="Clear"><i class="fas fa-trash"></i></button> | |
| <div class="sp"></div> | |
| <button class="tb text-success" id="bsave"><i class="fas fa-check"></i></button> | |
| <button class="tb text-secondary" data-bs-dismiss="modal"><i class="fas fa-times"></i></button> | |
| </div> | |
| <div id="ncw"><canvas id="nc"></canvas></div> | |
| <div id="rpnl"> | |
| <small class="text-muted d-flex justify-content-between mb-1">Reference <i class="fas fa-times" style="cursor:pointer" onclick="this.closest('#rpnl').classList.add('hide')"></i></small> | |
| <img id="nref" src=""> | |
| </div> | |
| </div></div></div></div> | |
| <script> | |
| (function(){ | |
| const Q=s=>document.querySelector(s),QA=s=>document.querySelectorAll(s); | |
| const cv=Q('#nc'),cx=cv.getContext('2d',{willReadFrequently:false}),w=Q('#ncw'); | |
| let imgId,drawing=false,lx=0,ly=0; | |
| let tool='pen',color='#222',size=2; | |
| let hist=[],hidx=-1; | |
| window.openNotesModal=function(id,ref){ | |
| imgId=id; | |
| Q('#nref').src=ref; | |
| Q('#rpnl').classList.remove('hide'); | |
| bootstrap.Modal.getOrCreateInstance(Q('#notesModal')).show(); | |
| }; | |
| Q('#notesModal').addEventListener('shown.bs.modal',init); | |
| function init(){ | |
| // Set canvas size | |
| cv.width=w.clientWidth; | |
| cv.height=w.clientHeight; | |
| // Optimize context | |
| cx.lineCap='round'; | |
| cx.lineJoin='round'; | |
| cx.imageSmoothingEnabled=true; | |
| // Reset state | |
| hist=[]; | |
| hidx=-1; | |
| cx.fillStyle='#fff'; | |
| cx.fillRect(0,0,cv.width,cv.height); | |
| loadData(); | |
| } | |
| window.onresize=()=>{ | |
| if(!cv.width)return; | |
| const img=cx.getImageData(0,0,cv.width,cv.height); | |
| cv.width=w.clientWidth; | |
| cv.height=w.clientHeight; | |
| cx.putImageData(img,0,0); | |
| cx.lineCap='round'; | |
| cx.lineJoin='round'; | |
| }; | |
| // Unified pointer events - works for mouse, touch, pen | |
| cv.onpointerdown=e=>{ | |
| e.preventDefault(); | |
| drawing=true; | |
| lx=e.offsetX; | |
| ly=e.offsetY; | |
| cx.beginPath(); | |
| cx.arc(lx,ly,getSize()/2,0,Math.PI*2); | |
| cx.fillStyle=getColor(); | |
| cx.fill(); | |
| }; | |
| cv.onpointermove=e=>{ | |
| if(!drawing)return; | |
| e.preventDefault(); | |
| const x=e.offsetX,y=e.offsetY; | |
| cx.beginPath(); | |
| cx.moveTo(lx,ly); | |
| cx.lineTo(x,y); | |
| cx.strokeStyle=getColor(); | |
| cx.lineWidth=getSize(); | |
| cx.stroke(); | |
| lx=x;ly=y; | |
| }; | |
| cv.onpointerup=cv.onpointerleave=e=>{ | |
| if(drawing){drawing=false;saveState();} | |
| }; | |
| cv.ontouchmove=e=>e.preventDefault(); | |
| function getColor(){ | |
| if(tool==='eraser')return '#ffffff'; | |
| if(tool==='marker'){ | |
| const v=parseInt(color.slice(1),16); | |
| return `rgba(${(v>>16)&255},${(v>>8)&255},${v&255},0.35)`; | |
| } | |
| return color; | |
| } | |
| function getSize(){ | |
| if(tool==='eraser')return size*8; | |
| if(tool==='marker')return size*6; | |
| return size; | |
| } | |
| // UI Bindings | |
| QA('[data-tool]').forEach(b=>{ | |
| b.onclick=()=>{ | |
| tool=b.dataset.tool; | |
| QA('[data-tool]').forEach(x=>x.classList.remove('on')); | |
| b.classList.add('on'); | |
| }; | |
| }); | |
| QA('.cd').forEach(b=>{ | |
| b.onclick=()=>{ | |
| color=b.dataset.c; | |
| QA('.cd').forEach(x=>x.classList.remove('on')); | |
| b.classList.add('on'); | |
| }; | |
| }); | |
| Q('#bsize').oninput=function(){size=+this.value||2;}; | |
| Q('#bundo').onclick=undo; | |
| Q('#bclear').onclick=()=>{ | |
| if(!confirm('Clear all?'))return; | |
| cx.fillStyle='#fff'; | |
| cx.fillRect(0,0,cv.width,cv.height); | |
| saveState(); | |
| }; | |
| Q('#bsave').onclick=save; | |
| function saveState(){ | |
| hidx++; | |
| hist=hist.slice(0,hidx); | |
| hist.push(cv.toDataURL('image/png')); | |
| if(hist.length>20){hist.shift();hidx--;} | |
| } | |
| function undo(){ | |
| if(hidx>0){ | |
| hidx--; | |
| const img=new Image(); | |
| img.onload=()=>{ | |
| cx.fillStyle='#fff'; | |
| cx.fillRect(0,0,cv.width,cv.height); | |
| cx.drawImage(img,0,0); | |
| }; | |
| img.src=hist[hidx]; | |
| } | |
| } | |
| async function loadData(){ | |
| try{ | |
| const r=await fetch('/get_note_json/'+imgId); | |
| if(r.ok){ | |
| const d=await r.json(); | |
| if(d.success&&d.image_data){ | |
| const img=new Image(); | |
| img.onload=()=>{cx.drawImage(img,0,0);saveState();}; | |
| img.src=d.image_data; | |
| return; | |
| } | |
| } | |
| }catch(e){} | |
| saveState(); | |
| } | |
| async function save(){ | |
| const img=cv.toDataURL('image/png'); | |
| try{ | |
| const r=await fetch('/save_note_json',{ | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify({ | |
| image_id:imgId, | |
| session_id:'{{ session_id }}', | |
| json_data:'{}', | |
| image_data:img | |
| }) | |
| }); | |
| const d=await r.json(); | |
| if(d.success){ | |
| bootstrap.Modal.getInstance(Q('#notesModal')).hide(); | |
| if(window.showStatus) showStatus('Saved!','success'); | |
| setTimeout(()=>location.reload(),300); | |
| }else if(window.showStatus) showStatus('Error: '+d.error,'danger'); | |
| }catch(e){if(window.showStatus) showStatus(e.message,'danger');} | |
| } | |
| window.toggleNoteInPdf=async(id,inc)=>{ | |
| try{await fetch('/toggle_note_in_pdf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({image_id:id,include:inc})});}catch(e){} | |
| }; | |
| window.deleteNote=async id=>{ | |
| if(!confirm('Delete?'))return; | |
| try{ | |
| const r=await fetch('/delete_note',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({image_id:id})}); | |
| const d=await r.json(); | |
| if(d.success){location.reload();} | |
| }catch(e){} | |
| }; | |
| })(); | |
| </script> | |