var _cap={
list:[],
selectedSet:new Set(),
lastClickedIndex:-1,
rate:1,
layout:0,
size:0,
timeFormat:0,
datePicker:null,
controls:null,
scriptCount:0,
navDebounce:null,
};
const FF_TYPES=['','MOV','JPG','MP4'];
function CapLoad(){
const scripts=['VideoControls'];
if(_cap.scriptCount){
CapDoLoad();
}else{
for(let name of scripts){
LoadScript(name,function(){
if(++_cap.scriptCount==scripts.length)CapDoLoad();
});
}
}
}
function CapDoLoad(){
const sidebar=document.getElementById('capSidebar');
const content=document.getElementById('capContent');
sidebar.style.display='flex';
content.style.display='block';
if(!_cap.datePicker){
let today=new Date();
_cap.datePicker=new Litepicker({element: document.getElementById('capDate'),singleMode:false,format:_dateFormat,startDate:today,endDate:today,maxDate:today,setup:(picker)=>{
picker.on('selected',CapObtainFileList);
}});
}
CapCreateGroupPicker();
CapCreateCamPicker();
CapObtainFileList();
CapShowInstructions();
window.addEventListener('keydown',CapKeyPress);
window.addEventListener('resize',CapResizeFileListItems);
}
function CapUnload(){
const sidebar=document.getElementById('capSidebar');
const content=document.getElementById('capContent');
sidebar.style.display='none';
content.style.display='none';
window.removeEventListener('keydown',CapKeyPress);
window.removeEventListener('resize',CapResizeFileListItems);
if(_cap.navDebounce){
clearTimeout(_cap.navDebounce);
_cap.navDebounce=null;
}
if(_cap.controls){
_cap.controls.cleanup();
_cap.controls=null;
}
CapAbortVideo();
CapHideMedia();
document.getElementById('capContent').innerHTML='';
}
function CapGetCookies(){
let cVal=allCookies.getItem('capLayout');
if(cVal!=null)_cap.layout=parseInt(cVal);
cVal=allCookies.getItem('capSize');
if(cVal!=null)_cap.size=parseInt(cVal);
cVal=allCookies.getItem('timeFormat');
if(cVal!=null)_cap.timeFormat=parseInt(cVal);
else _cap.timeFormat=DefaultTimeFormat();
if(_cap.layout)document.getElementById('filesDiv').innerHTML='';
document.getElementById('filesDiv').style.display=_cap.layout?'none':'block';
document.getElementById('capCamSelectDiv').style.flexGrow=_cap.layout?'1':'';
document.getElementById('capCamSelectDiv').style.maxHeight=_cap.layout?'':'160px';
document.getElementById('capContent').style.overflowY=_cap.layout?'auto':'hidden';
}
function CapCreateGroupPicker(){
var html="";
for(let i=0;i<_groups.length;i++)html+='<option value="'+_groups[i].id+'">'+_groups[i].name+'</option>';
const sel=document.getElementById('capGroup');
sel.innerHTML=html;
var capGroup=allCookies.getItem('capGroup');
if(capGroup && SelectValueIsValid(sel,capGroup))sel.value=capGroup;
}
function CapGroupSelectionChanged(){
allCookies.setItem('capGroup',document.getElementById("capGroup").value,Infinity);
CapCreateCamPicker();
}
function CapCreateCamPicker(){
var html="",isOneChecked=false,firstCam=null;
let members=_groups[document.getElementById('capGroup').selectedIndex].members;
let list=allCookies.getItem('capChecks'+document.getElementById('capGroup').value);
let checkedArr=list?list.split(',').map(Number):[];
for(let i=0;i<_cams.length;i++){
if(!_cams[i].demo && _cams[i].perm&PERM_FILES && members.includes(_cams[i].num)){
let checked=checkedArr.includes(_cams[i].num);
if(checked)isOneChecked=true;
if(firstCam==null)firstCam=_cams[i].num;
html+='<label><input type="checkbox" id="capCam'+_cams[i].num+'" class="camCheck" onclick="CapCamSelectionChanged(event)"'+(checked?' checked':'')+'> '+_cams[i].name+'</label>';
}
}
if(firstCam==null){
document.getElementById('capCamSelectDiv').innerHTML='<span>'+Loc('No cameras are available')+'</span>';
}else{
document.getElementById('capCamSelectDiv').innerHTML=html;
if(_cap.pendingCam!==undefined){
for(let m of members){
let cb=document.getElementById('capCam'+m);
if(cb)cb.checked=(m==_cap.pendingCam);
}
let today=new Date();
_cap.datePicker.setDateRange(today,today);
document.getElementById('capFilter').value='0';
_cap.pendingCam=undefined;
}else if(!isOneChecked){
document.getElementById('capCam'+firstCam).checked=true;
}
}
CapCamSelectionChanged();
}
function CapCamSelectionChanged(event){
let members=_groups[document.getElementById('capGroup').selectedIndex].members;
let list='';
if(event && event.altKey)for(m of members)if(document.getElementById('capCam'+m))document.getElementById('capCam'+m).checked=event.target.checked;
for(m of members)if(document.getElementById('capCam'+m)&& document.getElementById('capCam'+m).checked)list+=(list?',':'')+m;
allCookies.setItem('capChecks'+document.getElementById('capGroup').value,list,Infinity);
CapObtainFileList();
}
async function CapObtainFileList(){
if(!_cap.datePicker)return;
let resultsDiv=_cap.layout?document.getElementById('capContent'):document.getElementById('filesDiv');
let cams='';
for(let i=0;i<_cams.length;i++){
const checkbox=document.getElementById('capCam'+_cams[i].num);
if(checkbox && checkbox.checked)cams+=_cams[i].num+',';
}
_cap.selectedSet.clear();
_cap.lastClickedIndex=-1;
CapShowInstructions();
if(cams===''){
resultsDiv.innerHTML='<span>'+Loc('Please select a camera')+'</span>';
}else{
resultsDiv.innerHTML='<span>'+Loc('Obtaining file list…')+'</span>';
let startDate=_cap.datePicker.getStartDate().format('YYYY-MM-DD');
let endDate=_cap.datePicker.getEndDate().format('YYYY-MM-DD');
let req='caplist?cams='+cams+'&startDate='+startDate+'&endDate='+endDate+'&filter='+document.getElementById('capFilter').value;
let response=await fetch(req,{credentials:'same-origin'});
let contentType=response.headers.get('content-type');
if(contentType && contentType.startsWith('text/html')){
const htmlContent=await response.text();
document.getElementById('capContent').innerHTML=htmlContent;
resultsDiv.innerHTML='<span>'+Loc('Invalid response from server')+'</span>';
}else{
try{
_cap.list=await response.json();
CapProcessFileList();
}catch(error){
resultsDiv.innerHTML='<span>'+Loc('Invalid response from server')+'</span>';
}
}
}
}
function CapProcessFileList(){
let filter=document.getElementById('capFilter').value;
let date=new Date();
let timeFormatter=new Intl.DateTimeFormat('default',{hour:'numeric',minute:'numeric',second:'numeric'});
let html='',size='';
_cap.selectedSet.clear();
_cap.lastClickedIndex=-1;
CapGetCookies();
let thumbNames=['capThumbS','capThumbM','capThumbL'];
let thumbClass=thumbNames[_cap.layout?_cap.size:0];
for(let i=0;i<_cap.list.length;i++){
let file=_cap.list[i];
if(file.deleted)continue;
let cam=CamFromNum(file.c);
let prevSrc=`getpreview?/${file.c}/${file.f}/${file.n}?archive=${file.a}`;
if(filter==5)prevSrc+='&aiTypeHuman=1';
if(filter==6)prevSrc+='&aiTypeVehicle=1';
if(filter==7)prevSrc+='&aiTypeAnimal=1';
html+='<div id="f'+i+'" class="fileListItem" onclick="CapClick(event,this)">';
html+='<img class="'+thumbClass+'" src="'+prevSrc+'">';
html+='<div class="capDuration">'+((file.t==2)?'JPG':CapDurationStr(file.d))+'</div>';
html+='<div class="capInfo'+(file.u?' italic':'')+'">';
html+=DateStr(file.f)+'<br>';
html+=TimeStr(file.s,_cap.timeFormat)+'<br>';
html+=SizeStr(file.m)+'<br>';
html+='<button onclick="CapTagClick(event,'+i+',this);event.stopPropagation();"><img src="/img/tag-'+file.g+'.png"></button>';
html+='<button onclick="CapDownloadClick(event,'+i+');event.stopPropagation();"><img src="/img/download.svg" class="invertableImg"></button>';
if(cam.perm&PERM_FILEDEL)html+='<button onclick="CapBinClick(event,this.parentNode.parentNode,'+i+');event.stopPropagation();"><img src="/img/bin.svg" class="invertableImg"></button>';
html+='</div></div>'
}
let resultsDiv=_cap.layout?document.getElementById('capContent'):document.getElementById('filesDiv');
if(_cap.list.length){
resultsDiv.innerHTML=html;
CapResizeFileListItems();
}
else resultsDiv.innerHTML='<span>'+Loc('No files found')+'</span>';
CapShowInstructions();
}
function CapResizeFileListItems(){
if(_cap.layout){
let minWidths=[242,316,388];
let cols=Math.floor(document.getElementById('capContent').offsetWidth/minWidths[_cap.size]);
let pc=100/(cols?cols:1);
document.documentElement.style.setProperty('--fileListItemWidth',pc+'%');
}else{
document.documentElement.style.setProperty('--fileListItemWidth','100%');
}
}
function CapTagClick(event,fileIndex,button){
if(!_cap.selectedSet.has(fileIndex)){CapClearSelection();_cap.selectedSet.add(fileIndex);CapUpdateSelectionUI();}
let count=_cap.selectedSet.size;
let doTag=function(){
try{
let menu=new PopMenu(event);
menu.addSectionTitle('Tag');
for(let i=0;i<7;i++)menu.addItem('<img src="/img/tag-'+i+'.png" width="16" height="16">',false,()=>{CapSetTag(i);});
}catch(error){}
};
if(count>10){
let alert=new Alert(Loc('Apply tag to {0} files?',count),'');
alert.addButton('Continue',doTag);
alert.addButton('Cancel',null);
}else doTag();
}
function CapSetTag(tagId){
let indices=Array.from(_cap.selectedSet);
if(!indices.length)return;
let body=indices.map(i=>'setTag='+encodeURIComponent(CapBuildFileParam(i))).join('&');
fetch('setTags?tagId='+tagId,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body,credentials:'same-origin'});
indices.forEach(i=>{
_cap.list[i].g=tagId;
let btn=document.querySelector('#f'+i+' button img[src*="tag-"]');
if(btn)btn.src='/img/tag-'+tagId+'.png';
});
}
function CapDownloadClick(event,fileIndex){
if(!_cap.selectedSet.has(fileIndex)){CapClearSelection();_cap.selectedSet.add(fileIndex);CapUpdateSelectionUI();}
let indices=Array.from(_cap.selectedSet);
let lowBW=event.altKey;
if(indices.length>10){
let alert=new Alert(Loc('Download {0} files?',indices.length),'');
alert.addButton('Download',()=>{CapDoDownloads(indices,lowBW);});
alert.addButton('Cancel',null);
}else CapDoDownloads(indices,lowBW);
}
async function CapDoDownloads(indices,lowBW){
for(let i=0;i<indices.length;i++){
let f=_cap.list[indices[i]];
let a=document.createElement('a');
a.href='getfile/'+f.c+'/'+f.f+'/'+f.n+'?lowBandwidth='+(lowBW?'1':'0')+'&forceDownload=1&archive='+f.a;
a.download='';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
if(i<indices.length-1)await new Promise(r=>setTimeout(r,500));
}
}
function CapBinClick(event,div,fileIndex){
if(!_cap.selectedSet.has(fileIndex)){CapClearSelection();_cap.selectedSet.add(fileIndex);CapUpdateSelectionUI();}
let indices=Array.from(_cap.selectedSet);
let count=indices.length;
let msg=(count===1)?Loc('Delete file {0}?',_cap.list[indices[0]].n):Loc('Delete {0} files?',count);
let alert=new Alert(msg,Loc('This operation cannot be undone'));
alert.addButton('Delete',function(){
let body=indices.map(i=>'delete='+encodeURIComponent(CapBuildFileParam(i))).join('&');
fetch('delete?v6=1',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body,credentials:'same-origin'});
indices.forEach(i=>{_cap.list[i].deleted=true;let el=document.getElementById('f'+i);if(el)el.remove();});
_cap.selectedSet.clear();
CapSelectNearestFile(event,Math.min(...indices));
});
alert.addButton('Cancel',null);
}
function CapSelectAdjacentFile(event,delta){
let current=_cap.lastClickedIndex>=0?_cap.lastClickedIndex:CapGetTopmostSelected();
if(current<0)return;
let newIndex=-1;
if(delta>0){
for(let i=current+1;i<_cap.list.length;i++)if(!_cap.list[i].deleted){newIndex=i;break;}
}else if(delta<0){
for(let i=current-1;i>=0;i--)if(!_cap.list[i].deleted){newIndex=i;break;}
}
if(newIndex>-1){
let div=document.getElementById('f'+newIndex);
CapClick(event,div,true);
CapScrollToItemIfNeeded(div);
}
}
function CapScrollToItemIfNeeded(div){
let container=_cap.layout?document.getElementById('capContent'):document.getElementById('filesDiv');
let divRect=div.getBoundingClientRect();
let containerRect=container.getBoundingClientRect();
if(divRect.top < containerRect.top || divRect.bottom > containerRect.bottom){
div.scrollIntoView({block:'center',behavior:'auto'});
}
}
function CapSelectNearestFile(event,fileIndex){
let newIndex=-1;
for(let i=fileIndex;i<_cap.list.length;i++)if(!_cap.list[i].deleted){newIndex=i;break;}
if(newIndex<0)for(let i=fileIndex;i>=0;i--)if(!_cap.list[i].deleted){newIndex=i;break;}
if(newIndex>-1)CapClick(event,document.getElementById('f'+newIndex),false);
else CapShowInstructions();
}
function CapDurationStr(d){
if(d<1)return '';
if(d<60)return d+'s';
if(d<3600)return parseInt(d/60)+'m '+(d%60)+'s';
return parseInt(d/3600)+'h '+parseInt((d%3600)/60)+'m '+(d%60)+'s';
}
function CapClearSelection(){
_cap.selectedSet.forEach(i=>{let el=document.getElementById('f'+i);if(el)el.className='fileListItem';});
_cap.selectedSet.clear();
}
function CapUpdateSelectionUI(){
for(let i=0;i<_cap.list.length;i++){
let el=document.getElementById('f'+i);
if(el)el.className=_cap.selectedSet.has(i)?'fileListItem fileListItemSelected':'fileListItem';
}
}
function CapGetTopmostSelected(){
let min=-1;
_cap.selectedSet.forEach(i=>{if(min<0||i<min)min=i;});
return min;
}
function CapBuildFileParam(i){
let f=_cap.list[i];
return f.c+'/'+f.f+'/'+f.n+'?archive='+f.a;
}
function CapClick(event,div,debounce){
let index=parseInt(div.id.substring(1));
if(event.shiftKey&&_cap.lastClickedIndex>=0){
let from=Math.min(_cap.lastClickedIndex,index),to=Math.max(_cap.lastClickedIndex,index);
for(let i=from;i<=to;i++)if(!_cap.list[i].deleted)_cap.selectedSet.add(i);
}else if(event.metaKey||event.ctrlKey){
if(_cap.selectedSet.has(index))_cap.selectedSet.delete(index);
else _cap.selectedSet.add(index);
}else{
CapClearSelection();
_cap.selectedSet.add(index);
}
_cap.lastClickedIndex=index;
CapUpdateSelectionUI();
let topmost=CapGetTopmostSelected();
if(topmost<0){CapShowInstructions();return;}
if(_cap.navDebounce){clearTimeout(_cap.navDebounce);_cap.navDebounce=null;}
if(debounce){
_cap.navDebounce=setTimeout(function(){
_cap.navDebounce=null;
if(_cap.selectedSet.has(topmost))DisplayFile(event,_cap.list[topmost]);
},250);
}else DisplayFile(event,_cap.list[topmost]);
}
function CapAbortVideo(){
let video = document.getElementById('capVideo');
if(video){
video.pause();
video.removeAttribute('src');
video.load();
}
}
function DisplayFile(event,file){
let html='';
if(_cap.controls){
_cap.controls.cleanup();
_cap.controls=null;
}
CapAbortVideo();
let path=file.c+'/'+file.f+'/'+file.n;
if(file.t==1){
let ok265=(CheckCodecSupport('video/mp4; codecs="hvc1"')|| CheckCodecSupport('video/mp4; codecs="hvc1.1.6.L93"'));
html+='<span>'+(ok265 ? Loc('MovErrWithH265'): Loc('MovErrWithoutH265'))+'</span>';
}else if(file.t==2){
html='<img id="capImg" src="getfile/'+path+'?lowBandwidth='+(event.altKey?'1':'0')+'">';
}else if(file.t==3){
html='<video id="capVideo" autoplay muted><source src="getfile/'+path+'?lowBandwidth='+(event.altKey?'1':'0')+'&archive='+file.a+'&session-id='+RandomCharStr(12)+'" type="video/mp4"></video><div id="videoControls"></div>';
}
if(_cap.layout){
document.getElementById('mediaContent').innerHTML=html;
document.getElementById('mediaContainer').style.display='block';
}
else document.getElementById('capContent').innerHTML=html;
if(document.getElementById('capVideo')){
_cap.controls=new VideoControls(document.getElementById('videoControls'),document.getElementById('capVideo'));
}
}
function CapShowInstructions(){
if(_cap.layout!=0 || _cap.selectedSet.size>0)return;
let msg=Loc('Select a file in the list on the left to display it here');
if(!CheckCodecSupport('video/mp4; codecs="hvc1"')&& !CheckCodecSupport('video/mp4; codecs="hvc1.1.6.L93"')){
let cams='',count=0;
for(let i=0;i<_cams.length && count<8;i++)if(_cams[i].h265)cams+=((count++)?', ':'')+'&quot;'+_cams[i].name+'&quot;';
if(count)msg+='<br><br>'+Loc('H265Err',cams);
}
document.getElementById('capContent').innerHTML='<span>'+msg+'</span>';
}
function CapKeyPress(event){
switch(event.key){
case 'r':case 'R':
CapObtainFileList();
event.preventDefault();
break;
case 'Escape':case 'Esc':
CapHideMedia();
event.preventDefault();
break;
case 'ArrowUp':
CapSelectAdjacentFile(event,-1);
event.preventDefault();
break;
case 'ArrowDown':
CapSelectAdjacentFile(event,1);
event.preventDefault();
break;
}
}
function CapHideMedia(){
if(_cap.navDebounce){
clearTimeout(_cap.navDebounce);
_cap.navDebounce=null;
}
if(_cap.controls){
_cap.controls.cleanup();
_cap.controls=null;
}
CapAbortVideo();
if(_cap.layout){
document.getElementById('mediaContainer').style.display='none';
document.getElementById('mediaContent').innerHTML='';
}
}
