Files
hyperfocus-widget/main.js

468 lines
12 KiB
JavaScript

let scrolling = false;
let primaryAnimation, secondaryAnimation;
let focusing = {};
window.addEventListener("DOMContentLoaded", () => {
importStyles();
const websocket = new ReconnectingWebSocket("ws://adamwalsh.name:9525");
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";
}
}