let scrolling = false; let primaryAnimation, secondaryAnimation; let focusing = {}; window.addEventListener("DOMContentLoaded", () => { importStyles(); const websocket = new ReconnectingWebSocket("wss://croccyfocus.thewisewolf.dev/ws"); setInterval(updateTimers, 1000); communicate(websocket); }); class ReconnectingWebSocket { constructor(url) { this.url = url; this.ws = null; this.attempt = 0; this.connect(); this.closing = false; this.onopen = null; this.onmessage = null; this.onclose = null; this.onerror = null; } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log('WebSocket connected'); this.attempt = 0; if (this.onopen != null) { this.onopen(); } }; this.ws.onmessage = (msg) => { console.log('Received:', msg.data); if (this.onmessage != null) { this.onmessage(msg); } }; this.ws.onclose = () => { if (this.closing) { console.log('Closing Websocket.') } { console.warn('WebSocket closed. Reconnecting...'); this.reconnect(); } }; this.ws.onerror = (err) => { console.error('WebSocket error:', err); if (this.onerror != null) { this.onerror(); } this.close(); }; } reconnect() { const delay = this.getBackoffDelay(this.attempt); console.log(`Reconnecting in ${delay}ms`); setTimeout(() => { this.attempt++; this.connect(); }, delay); } close() { this.closing = true; this.ws.close(); if (this.onclose != null) { this.onclose(); } } getBackoffDelay(attempt) { const base = 500; // 0.5 second const max = 30000; // 30 seconds const jitter = Math.random() * 1000; return Math.min(base * 2 ** attempt + jitter, max); } send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(data); } else { console.warn('WebSocket not connected'); } } } function communicate(websocket) { console.log("Communicating"); const params = new URLSearchParams(window.location.search); websocket.onopen = () => { websocket.send( JSON.stringify( { type: "init", channel: "HyperFocus", community: params.get('community') } ) ); }; websocket.onmessage = ({ data }) => { const event = JSON.parse(data); switch (event.type) { case "DO": let args = event.args; // Call the specified method switch (event.method) { case "patchFocus": setFocus(focusing, args.userid, args.user_name, true, args.ends_at); break; case "delFocus": delFocus(focusing, args.userid); break; case "putFocus": clearFocus(focusing); args.forEach(focuser => { setFocus(focusing, focuser.userid, focuser.user_name, true, focuser.ends_at); }); break; default: throw new Error(`Unsupported method requested: ${event.method}.`) } if (!scrolling) { renderList(); } break; default: throw new Error(`Unsupported event type: ${event.type}.`); } }; } function setFocus(focusing, userid, name, hyper, end_at) { console.log(`Setting Focus ${userid}, ${name}, ${hyper}, ${end_at}`); focusing[userid] = { name: name, hyper: hyper, end_at: new Date(end_at) }; } function delFocus(focusing, userid) { console.log(`Del Focus ${userid}`); if (focusing.hasOwnProperty(userid)) { delete focusing[userid]; } } function clearFocus(tasks) { for (var prop in focusing) { delete focusing[prop]; } } function renderList() { let containers = document.querySelectorAll(".task-container") let now = Date.now(); let infos = Object.values(focusing); infos.sort((info1, info2) => (info2.end_at - info1.end_at)); console.log(infos); console.log(focusing); containers.forEach(function (tasklist) { tasklist.innerHTML = ""; for (const info of infos) { if (info.end_at < now) { continue; } let newTask = document.createElement("div"); newTask.className = "task-div"; let newFocus = document.createElement("div"); newFocus.className = "task-div"; let usernameTask = document.createElement("div"); usernameTask.className = "username-div"; let usernameDiv = document.createElement("div"); usernameDiv.className = "username"; usernameDiv.innerText = info.name; usernameDiv.style.color = "pink"; usernameTask.appendChild(usernameDiv); newTask.appendChild(usernameTask); let colon = document.createElement("div"); colon.className = "colon"; colon.innerText = ":"; usernameTask.appendChild(colon); let timerDiv = document.createElement("div"); timerDiv.dataset.endat = info.end_at.toISOString(); timerDiv.className = "task-timer-running"; // timerDiv.style.color = 'grey'; timerDiv.innerText = formatDur(info.end_at - Date.now()); newTask.appendChild(timerDiv); tasklist.appendChild(newTask); } }); animate(); } function formatDur(duration) { var dur_seconds = Math.floor(duration / 1000); 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; if (hours > 0) { return String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0') } else { return String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0') } } function updateTimers() { let timers = document.querySelectorAll('.task-timer-running'); timers.forEach(function (timer) { if (new Date(timer.dataset.endat) < Date.now()) { if (!scrolling) { renderList(); } } else { timer.innerText = formatDur(new Date(timer.dataset.endat) - Date.now()); } }); } /** * This function animates the task list by scrolling it up and down once. * It first calculates the heights of the task container and task wrapper. * If the task container height is greater than the task wrapper height and the task list is not currently scrolling, it calculates the final height and duration of the animation, * creates the keyframes and options for the animation, and then creates and plays the animation. * If the task list is currently scrolling, it hides the secondary task list and cancels the animation. */ async function animate() { // task container height let taskContainer = document.querySelector(".task-container"); let taskContainerHeight = taskContainer.scrollHeight; let taskWrapper = document.querySelector(".task-wrapper"); let taskWrapperHeight = taskWrapper.clientHeight; // scroll task wrapper up and down once if (taskContainerHeight > taskWrapperHeight && !scrolling) { let secondaryElement = document.querySelector(".secondary"); secondaryElement.style.display = "flex"; let finalHeight = taskContainerHeight + configs.styles.gapBetweenScrolls; let duration = (finalHeight / configs.styles.pixelsPerSecond) * 1000; // keyframes object in css scroll let primaryKeyFrames = [ { transform: `translateY(0)` }, { transform: `translateY(-${finalHeight}px)` }, ]; let secondaryKeyFrames = [ { transform: `translateY(${finalHeight}px)` }, { transform: `translateY(0)` }, ]; let options = { duration: duration, iterations: 1, easing: "linear", }; // create animation object and play it primaryAnimation = document .querySelector(".primary") .animate(primaryKeyFrames, options); secondaryAnimation = document .querySelector(".secondary") .animate(secondaryKeyFrames, options); primaryAnimation.play(); secondaryAnimation.play(); // wait for animation to finish scrolling = true; addAnimationListeners(); } else if (!scrolling) { document.querySelector(".secondary").style.display = "none"; // cancel animations cancelAnimation(); } } /** * This function adds event listeners to the primary animation. * If the primary animation exists, it adds 'finish' and 'cancel' event listeners that both trigger the animationFinished function. */ function addAnimationListeners() { if (primaryAnimation) { primaryAnimation.addEventListener("finish", animationFinished); primaryAnimation.addEventListener("cancel", animationFinished); } } /** * This function is triggered when the primary animation finishes or is cancelled. * It sets the scrolling flag to false, re-renders the task list, and then starts the animation again. */ function animationFinished() { scrolling = false; renderList(); animate(); } /** * This function cancels the primary and secondary animations if they exist. * It also sets the scrolling flag to false. */ function cancelAnimation() { if (primaryAnimation) { primaryAnimation.cancel(); } if (secondaryAnimation) { secondaryAnimation.cancel(); } scrolling = false; } /** * This function converts a hex color code to an RGB color code. * It first checks if the hex code includes the '#' character and removes it if present. * Then, it checks the length of the hex code. * If it's 3, it duplicates each character to create a 6-digit hex code. * If it's 6, it uses the hex code as is. * Finally, it converts each pair of hex digits to decimal to get the RGB values and returns them as a string. * * @param {string} hex - The hex color code to be converted. * @returns {string} The RGB color code. */ function hexToRgb(hex) { // remove # if present if (hex[0] === "#") { hex = hex.slice(1); } let r = 0, g = 0, b = 0; if (hex.length == 3) { // 3 digits r = "0x" + hex[0] + hex[0]; g = "0x" + hex[1] + hex[1]; b = "0x" + hex[2] + hex[2]; } else if (hex.length == 6) { // 6 digits r = "0x" + hex[0] + hex[1]; g = "0x" + hex[2] + hex[3]; b = "0x" + hex[4] + hex[5]; } // interger value of rgb r = +r; g = +g; b = +b; return `${r}, ${g}, ${b}`; } /** * * @param {string} font */ function loadGoogleFont(font) { WebFont.load({ google: { families: [font], }, }); } /** * convert taskListBorderColor to task-list-border-color */ function convertToCSSVar(name) { let cssVar = name.replace(/([A-Z])/g, "-$1").toLowerCase(); return `--${cssVar}`; } /** * This function imports styles from the configs object and applies them to the document. * It loads the Google fonts specified in the configs, applies all styles that do not include 'Background' in their keys, * and applies background colors and opacities for specific elements. * If the 'showTasksNumber' setting in configs is false, it hides the task count element. */ function importStyles() { const styles = configs.styles; loadGoogleFont(styles.headerFontFamily); loadGoogleFont(styles.bodyFontFamily); const stylesToImport = Object.keys(styles).filter((style) => { return !style.includes("Background"); }); const backgroundStyles = [ "taskList", "header", "body", "task", "checkBox", "doneTask", ]; stylesToImport.forEach((style) => { document.documentElement.style.setProperty( convertToCSSVar(style), styles[style] ); }); // loop through backgroundstyles backgroundStyles.forEach((style) => { // get background color and opacity let backgroundColor = styles[`${style}BackgroundColor`]; let backgroundOpacity = styles[`${style}BackgroundOpacity`]; let cssStyle = convertToCSSVar(style); // set background color document.documentElement.style.setProperty( `${cssStyle}-background-color`, `rgba(${hexToRgb(backgroundColor)}, ${backgroundOpacity})` ); }); if (!configs.settings.showTasksNumber) { document.getElementById("task-count").style.display = "none"; } }