var renderer; var timer; window.addEventListener("DOMContentLoaded", () => { renderer = new TimerRenderer(); const queryString = window.location.search; console.log(queryString); // Set colours from querystring too, background, focus, break if (queryString) { updateTimerFromQuery(queryString) } else { const websocket = new WebSocket("wss://croccy.thewisewolf.dev/ws"); websocket.addEventListener("open", () => { communicate(websocket); }); } }); function updateTimerFromQuery(queryString) { const params = new URLSearchParams(queryString); const block_number = parseInt(params.get('block') || 1); const focus_length = parseInt(params.get('focus') || 50) * 60; const break_length = parseInt(params.get('break') || 10) * 60; const block_goal = parseInt(params.get('goal') || 0); const focus_colour = "#" + (params.get('focuscolour') || "EED59F"); const break_colour = "#" + (params.get('breakcolour') || "8CD5CA"); const background = "#" + (params.get('background') || "1E202F"); const root = document.documentElement; root.style.setProperty('--focus-color', focus_colour); root.style.setProperty('--break-color', break_colour); root.style.setProperty('--bg-color', background); const now = new Date(); const start_at = new Date(now.getTime() - (focus_length + break_length) * 1000 * (block_number - 1)); update_timer(start_at, focus_length, break_length, block_goal); } class Timer { constructor(renderer, start_at, focus_length, break_length, block_goal) { this.renderer = renderer; this.start_at = start_at; this.focus_length = focus_length; this.break_length = break_length; this.block_goal = block_goal; this.counter = this.count_now; this.break_notified = true; this.destroying = false; } get block_length() { // Return total block duration in seconds return this.focus_length + this.break_length; } get block_start() { // Get the Date when this block started. return new Date(this.start_at.getTime() + (this.counter * this.block_length) * 1000); } get count_now() { // Current actual block number. 0 for the first block, -1 if not running var diff = Math.floor( (new Date() - this.start_at) / 1000 ); if (diff < 0) { return -1; } else { var stage = Math.floor( diff / this.block_length ); return stage; } } get is_break() { // Whether the current block is on break. if (this.counter < 0){ return true } else{ return ((new Date() - this.block_start) / 1000) > this.focus_length; } } render_time() { // Render timer status to the document let timestr; let proportion; if (this.counter < 0) { timestr = "--:--"; proportion = 0; } else { // How many seconds have passed in this block var dur_seconds = Math.floor( (new Date() - this.block_start) / 1000); if (dur_seconds < this.focus_length) { // How many seconds are left in the focus session dur_seconds = this.focus_length - dur_seconds; // What proportion of the focus session remains proportion = dur_seconds / this.focus_length; } else { dur_seconds = this.block_length - dur_seconds; proportion = dur_seconds / (this.break_length); } var seconds = dur_seconds % 60; dur_seconds = dur_seconds - seconds; var minutes = Math.floor(dur_seconds / 60); var hours = Math.floor(minutes / 60); minutes = minutes - 60 * hours; timestr = String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0'); } this.renderer.update_time(timestr); this.renderer.update_proportion(proportion); let goal; if (this.block_goal <= 0) { goal = "??"; } else { goal = String(this.block_goal); } this.renderer.update_block(String(this.counter + 1) + '/' + String(goal)); if (this.is_break){ this.renderer.update_stage("BREAK"); this.renderer.set_break_color(); } else { this.renderer.update_stage("FOCUS"); this.renderer.set_focus_color(); } } tick() { // Recursive call run per second to update and change stage if (this.destroying) { return } if (this.is_break && !this.break_notified && this.counter >= 0) { // Notify for break // TODO voice alert this.renderer.break_alert.play(); // TODO chat message? console.log("Notifying Break.") this.break_notified = true; } else if (this.count_now > this.counter) { this.counter++; this.break_notified = false; // Notify for focus // TODO voice alert this.renderer.focus_alert.play(); // TODO chat message? console.log("Notifying Focus.") } this.render_time(); setTimeout(this.tick.bind(this), 1000); } } class TimerRenderer { constructor (){ this.background = document.getElementsByClassName("timer-bg")[0]; this.time_element = document.getElementsByClassName("timer-time")[0]; this.stage_element = document.getElementsByClassName("timer-details-stage-text")[0]; this.block_element = document.getElementsByClassName("timer-details-block")[0]; this.break_alert = new Audio('audio/break_alert.wav'); this.focus_alert = new Audio('audio/focus_alert.wav'); this.bg_width = parseInt( window.getComputedStyle( this.background, null ).getPropertyValue('width'), 10 ); this.bg_height = parseInt( window.getComputedStyle( this.background, null ).getPropertyValue('height'), 10 ) } update_time (timestr) { this.time_element.textContent = timestr; } update_proportion (proportion) { var bounds = rectBounds( proportion, this.bg_width, this.bg_height ); this.background.style.setProperty( '--clip-path', "polygon(" + bounds.join(', ') + ")" ); } update_block (blockstr) { this.block_element.textContent = blockstr; } update_stage (stagestr) { this.stage_element.textContent = stagestr; } set_break_color () { this.background.style.setProperty('--stage-color', 'var(--break-color)'); this.time_element.style.color = "var(--break-color)"; } set_focus_color () { this.background.style.setProperty('--stage-color', 'var(--focus-color)'); this.time_element.style.color = "var(--focus-color)"; } } function rectBounds(prop, rectHeight, rectWidth) { var totalCircum = 2 * rectHeight + 2 * rectWidth; var propCircum = prop * totalCircum; let bounds; if (propCircum < rectWidth / 2) { var xprop = Math.floor(( (rectWidth/2 + propCircum) / rectWidth ) * 1000)/10; bounds = [ "50% -5%", `${xprop}% -5%`, `${xprop}% 4%`, "50% 4%" ]; } else if (propCircum < rectWidth / 2 + rectHeight) { var yprop = ( (propCircum - rectWidth / 2) / rectHeight ) * 100; bounds = [ "50% -5%", "110% -5%", `110% ${yprop}%`, `96% ${yprop}%`, "95% 4%", "50% 4%" ]; } else if (propCircum < 3 * rectWidth / 2 + rectHeight) { var xprop = ( ((3 * rectWidth/2 + rectHeight) - propCircum) / rectWidth ) * 100; bounds = [ "50% -5%", "110% -5%", "110% 110%", `${xprop}% 110%`, `${xprop}% 96%`, "95% 96%", "95% 4%", "50% 4%" ]; } else if (propCircum < 3 * rectWidth / 2 + 2 * rectHeight) { var yprop = ( ((3 * rectWidth/2 + 2 * rectHeight) - propCircum) / rectHeight ) * 100; bounds = [ "50% -5%", "110% -5%", "110% 110%", "-5% 110%", `-5% ${yprop}%`, `4% ${yprop}%`, "5% 96%", "95% 96%", "95% 4%", "50% 4%" ]; } else { var xprop = ( (propCircum - (3 * rectWidth/2 + 2 * rectHeight)) / rectWidth ) * 100; bounds = [ "50% -5%", "110% -5%", "110% 110%", "-5% 110%", "-5% -5%", `${xprop}% -5%`, `${xprop}% 4%`, "4% 4%", "5% 96%", "95% 96%", "95% 4%", "50% 4%" ]; } return bounds; } function communicate(websocket) { console.log("Communicating"); websocket.send(JSON.stringify({type: "init", channel: "Timer"})); websocket.addEventListener("message", ({ data }) => { console.log("Rec Event " + data); const event = JSON.parse(data); switch (event.type) { case "DO": let args = event.args; // Call the specified method switch (event.method) { case "setTimer": update_timer( new Date(args.start_at), args.focus_length, args.break_length, args.block_goal ) break; default: throw new Error(`Unsupported method requested: ${event.method}.`) } break; default: throw new Error(`Unsupported event type: ${event.type}.`); } }); } function update_timer(start_at, focus_length, break_length, goal) { if (timer != null) { timer.destroying = true; } timer = new Timer( renderer, start_at, focus_length, break_length, goal ) timer.tick(); }