468 lines
12 KiB
JavaScript
468 lines
12 KiB
JavaScript
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";
|
|
}
|
|
}
|