Report-Generator / templates /_revision_notes.html
root
Working CHanges to revesion ; add preact support
e8a57cb
{# 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>