${e.description}
\n${e.timestamp.toLocaleString(this.meta.locale)}
\n `;new l(t,i)}}onCanvasMouseUp(){this.canvas.removeEventListener("mousemove",this.canvasMouseMoveCallback),document.removeEventListener("mouseup",this.canvasMouseUpCallback),this.canvas.style.cursor="default"}onCanvasDrop(t){const e=t.dataTransfer.files.item(0);this.handleFileBeforeParse(e)}onResize(){this.hasData()?this.render():this.renderLandingPage()}menuOnLandingPage(){this.days=f.days,this.style=f.style,this.meta=f.meta,this.renderLandingPage()}menuOnAlignStart(){this.scrollTimeline(v.Start)}menuOnAlignEnd(){this.scrollTimeline(v.End)}menuOnAlignCenter(){this.scrollTimeline(v.Center)}menuOnExportPNG(){if(!this.hasData())return;const t=this.canvas.toDataURL("image/png",1);u(this.fileData.name+".png",t)}menuOnGitHub(){new c('\nDeveloped by Qulle github.com/qulle/activity-timeline
\n ')}menuOnInfo(){if(!this.hasData())return;let t=0,e=Number.MAX_VALUE,i=0;this.days.forEach((n=>{const s=n.activities.length;s>t&&(t=s),sFile: ${this.fileData.name}.${this.fileData.extension}
\nOpened: ${this.fileData.opened.toLocaleTimeString(this.meta.locale)}
\nLocalization: ${this.meta.locale}
\nFirst date: ${n.toLocaleDateString(this.meta.locale)}
\nLast date: ${s.toLocaleDateString(this.meta.locale)}
\nTime period: ${r} days
\nActivities on: ${this.days.length} days corresponding to ${Number(h.toFixed(2))}% of time period
\nTotal activites: ${i} st
\nMost activites in a day: ${t} st
\nLeast activites in a day: ${e} st
\nAvarage activites in a day: ${Number(o.toFixed(2))} st
\n `;new l("Current Timeline",d)}menuOnZoomReset(){this.hasData()&&this.resetZoom()}menuOnZoomDelta(t){if(!this.hasData())return;const e=window.innerWidth/2,i=window.innerHeight/2;this.zoomTimeline(e,i,t)}menuOnDataImport(){const t=document.createElement("input");t.className="at-d-none",t.setAttribute("type","file"),t.setAttribute("accept",".json, .csv"),t.addEventListener("change",(t=>{const e=t.target.files[0];this.handleFileBeforeParse(e)})),t.click()}menuOnDataExport(){if(!this.hasData())return;const t={json:this.exportAsCSV.bind(this),csv:this.exportAsJSON.bind(this)}[this.fileData.extension];if(t)t.call();else{new c('\nCould not decide fileformat - report as bug
\n ')}}menuOnFetchNotification(){const t=new l("Notifications","Loading notifications...
"),e=(new Date).getTime().toString();fetch(`https://raw.githubusercontent.com/qulle/notification-endpoints/main/endpoints/activity-timeline.json?cache=${e}`,{method:"GET",headers:{Accept:"application/json"}}).then((t=>Boolean(t.ok)?t.json():Promise.reject(`${t.status} ${t.statusText}`))).then((e=>{let i="";0===e.features.length?i="No features currently under development
":e.features.forEach((t=>{i+=`${t}
`}));const n={message:e.message,latest:{version:e.latest.version,released:e.latest.released},features:i};this.setNotificationModalContent(t,n)})).catch((e=>{this.setNotificationModalContent(t,{message:"Glad you are using my App, hope you find it useful!",error:"Data from the GitHub repo could not be fetched"}),console.error(`Fetch error [${e}]`)}))}setNotificationModalContent(t,e){const i=`\n${e.message}
\n\n \n v${k}\n \n
\n ${Boolean(e.latest)&&Boolean(e.latest.version)&&Boolean(e.latest.released)?`\n\n \n v${e.latest.version} - ${new Date(e.latest.released).toLocaleDateString(this.meta.locale)}\n \n
\n `:""}\n ${Boolean(e.features)&&e.features.length>0?`\n${e.error}
\n `:""}\n `;t.setContent(i)}exportAsCSV(){const t={meta:{...this.meta},style:{...this.style},days:[...this.days]};let e=`Timestamp;Title;Description;Fill Color;Stroke Color;${JSON.stringify(t,((t,e)=>("days"===t&&(e=void 0),e)))}\r\n`;t.days.forEach((i=>{i.activities.forEach((i=>{e+=`${i.timestamp.toLocaleString(t.meta.locale)};${i.title};${i.description};${i.fillColor};${i.strokeColor}\r\n`}))})),e=e.replace(/\r\n*$/,""),u(this.fileData.name+".csv",e)}exportAsJSON(){const t={meta:{...this.meta},style:{...this.style},days:[...this.days]},e=JSON.stringify(t,((e,i)=>("x"!==e&&"y"!==e||(i=void 0),"date"===e&&(i=new Date(i).toLocaleDateString(t.meta.locale)),"timestamp"===e&&(i=new Date(i).toLocaleTimeString(t.meta.locale)),i)),4);u(this.fileData.name+".json",e)}handleFileBeforeParse(t){const e=t.name.lastIndexOf("."),i=t.name.substring(0,e)||t.name,n=t.name.substring(e+1).toLowerCase()||t.name;this.fileData={name:i,extension:n,opened:new Date};const s={json:this.parseJSONFile,csv:this.parseCSVFile}[n];if(s)s.call(this,t);else{new c('\nCan only parse .json or .csv files
\n ')}}zoomTimeline(t,e,i){const n=Math.sign(i);let s=(t+E.scrollLeft)/this.zoom.value,a=(e+E.scrollTop)/this.zoom.value;this.zoom.value+=n*this.zoom.factor*this.zoom.value,this.zoom.value=Math.max(this.zoom.min,Math.min(this.zoom.max,this.zoom.value));let r={x:s*this.zoom.value-t,y:a*this.zoom.value-e};this.render(),E.scrollLeft=r.x,E.scrollTop=r.y}scrollTimeline(t){if(!this.hasData())return;const e=window.innerHeight/2,i=parseInt(this.canvas.style.height,10)/2,n=Math.abs(e-i),s=parseInt(this.canvas.style.width,10),a=window.innerWidth/2,r={start:0,end:s,center:Math.abs(a-s/2)}[t];window.scrollTo({top:n,left:r,behavior:"smooth"})}resetZoom(){this.zoom.value=1,this.hasData()&&this.render()}highlight(){this.canvas.classList.add(C.highlightClass)}unhighlight(){this.canvas.classList.remove(C.highlightClass)}parseJSONFile(t){const e=new FileReader;e.readAsText(t),e.addEventListener("loadend",(()=>{let t;try{const i=e.result,n=JSON.parse(i,(function(e,i){return"date"===e?(t=i,i=new Date(i)):"timestamp"===e&&(i=new Date(t+" "+i)),"string"==typeof i&&i.trim(),i})),s={meta:{...f.meta,...n.meta||{}},style:{...f.style,...n.style||{}},days:[...f.days,...n.days||[]]};this.setData(s),this.render(),this.scrollTimeline(v.End)}catch(t){console.error(`JSON parsing error [${t}]`);new c('\nError parsing the JSON file - check the syntax
\n ')}}))}parseCSVFile(e){const i=new FileReader;i.readAsText(e,"iso-8859-1"),i.addEventListener("loadend",(()=>{try{let e={},n={};const s=i.result.replace(";;;;;","").replace(";;;;","").trim(),a=["timestamp","title","description","fillColor","strokeColor","json"],r=t(w).parse(s,{header:!0,delimiter:";",skipEmptyLines:!0,transformHeader:function(t,i){if(5===i){const i=JSON.parse(t);e=i.meta,n=i.style}return a[i]},transform:function(t,e){return t.trim()}});r.data.forEach((t=>{delete t.json}));const o=r.data.reduce(((t,e)=>{const i=new Date(e.timestamp),n=i.toDateString();return e.timestamp=i,t[n]||(t[n]=[]),t[n].push(e),t}),{}),l=Object.keys(o).map((t=>({date:new Date(t),activities:o[t]}))),h={meta:{...f.meta,...e||{}},style:{...f.style,...n||{}},days:[...f.days,...l||[]]};this.setData(h),this.render(),this.scrollTimeline(v.End)}catch(t){console.error(`CSV parsing error [${t}]`);new c('\nError parsing the CSV file - check the syntax
\n ')}}))}hasData(){return Array.isArray(this.days)&&this.days.length>0}calculateWidth(t){let e=window.innerWidth*t,i=0;return i+=C.xPadding*t*(this.zoom.value>1?this.zoom.value:1),i+=C.stepDistanceXAxis*t*this.zoom.value*this.days.length,i>e&&(e=i),e}calculateHeight(t){let e=window.innerHeight*t,i=0;this.days.forEach((t=>{t.activities.length>i&&(i=t.activities.length)}));let n=0;return n+=C.yPadding*t*(this.zoom.value>1?this.zoom.value:1),n+=C.stepDistanceYAxis*t*this.zoom.value*i*2,n>e&&(e=n),e}getVerticalMid(){return(this.canvas.height/2-this.style.lineThickness/2)/(this.zoom.value*x)}isToday(t){const e=new Date;return t.getFullYear()===e.getFullYear()&&t.getMonth()===e.getMonth()&&t.getDate()===e.getDate()}getWeekDayName(t){const e=t.toLocaleString(this.meta.locale,{weekday:"long"});return e.charAt(0).toUpperCase()+e.slice(1)}isTop(t){return t%2==0}isBottom(t){return!this.isTop(t)}hitDetection(t,e){if(!this.hasData())return;const i=this.canvas.getBoundingClientRect(),n=(t-i.left)/(this.zoom.value*x),s=(e-i.top)/(this.zoom.value*x),a=C.radius/x;for(let t=0;tApplication version v${k} and JSON version v${this.meta.version}
\n `)}}renderCircle(t,e,i,n,s,a){t.fillStyle=s,t.strokeStyle=a,t.beginPath(),t.arc(e,i,n,0,2*Math.PI),t.fill(),t.stroke()}renderLandingPage(){this.resetZoom();const t=this.canvas.getContext("2d");this.canvas.width=window.innerWidth*x,this.canvas.height=window.innerHeight*x,t.clearRect(0,0,this.canvas.width,this.canvas.height),t.scale(this.zoom.value*x,this.zoom.value*x),this.canvas.style.width=window.innerWidth+"px",this.canvas.style.height=window.innerHeight+"px";const e=window.innerWidth