Initial commit of hyperfocus scrolling list.
This commit is contained in:
467
main.js
Normal file
467
main.js
Normal file
@@ -0,0 +1,467 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user