/* DB structure: - tasks - username: - todos (array) - [{text, done}, {text, done}] - done - userColor */ const settings = configs.settings; const styles = configs.styles; const scrollSpeed = configs.animation.scrollSpeed; let scrolling = false; 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}`; } // hex to rgb that accepts 3 or 6 digits 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}`; } // import styles from configs function importStyles() { const styles = configs.styles; // fonts if (configs.settings.headerGoogleFont) { loadGoogleFont(styles.headerFontFamily); } if (configs.settings.taskGoogleFont) { loadGoogleFont(styles.taskFontFamily); } const stylesToImport = Object.keys(styles).filter((style) => { return !style.includes("Background"); }); stylesToImport.forEach((style) => { document.documentElement.style.setProperty( convertToCSSVar(style), styles[style] ); }); let backgroundStyles = Object.keys(styles).filter((style) => { return style.includes("Background"); }); // use regex to filter out after "Background" backgroundStyles = backgroundStyles.map((style) => { return style.replace(/Background.*/, ""); }); // 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})` ); }); // interval the task title setInterval(async () => { let taskTitle = document.querySelector(".title"); let taskTitleText = taskTitle.innerText; // cycle through a list of titles let titles = configs.animation.titles; // get current title let currentTitle = titles.indexOf(taskTitleText); // if current title is the last title, set it to the first title if (currentTitle === titles.length - 1) { currentTitle = 0; } else { currentTitle++; } // on change title, add fade animation taskTitle.classList.add("fade"); await sleep(500); // set new title taskTitle.innerText = titles[currentTitle]; await sleep(100); // remove fade animation taskTitle.classList.remove("fade"); }, 8000); } function resetDB() { localStorage.clear(); setupDB(); } function setupDB() { if (!localStorage.tasks) { localStorage.setItem(`tasks`, "{}"); } } // 0: limit has reached // 1: duplicate task // 2: invalid input // 3: success function addTask(username, userColor, task) { const tasks = JSON.parse(localStorage.tasks); if (!tasks[username]) { tasks[username] = { todos: [], done: [], userColor: userColor, }; } if ( incompleteTasksCount(username) >= settings.limit && settings.enableLimit ) { return 0; } if ( tasks[username].todos.find( (t) => t.text.toLowerCase() === task.toLowerCase() ) ) { return 1; } // if task is whitespace or empty if (!task || !task.trim()) { return 2; } tasks[username].todos.push({ text: task, done: false }); localStorage.setItem(`tasks`, JSON.stringify(tasks)); renderTaskListToDOM(); return 3; } // 0: user has no tasks // 1: invalid input // removed task: success function removeTask(username, task) { const tasks = JSON.parse(localStorage.tasks); if (!tasks[username] || tasks[username].todos.length === 0) { return 0; } let index = tasks[username].todos.findIndex( (t) => t.text.toLowerCase() === task.toLowerCase() ); if (index === -1) { if (isInt(task)) { index = parseInt(task) - 1; // ACTUAL INDEX } else if (tasks[username].todos.length === 1) { index = 0; } else { return 1; } if (index < 0 || index > tasks[username].todos.length - 1) { return 1; } } let removedTask = tasks[username].todos[index].text; tasks[username].todos.splice(index, 1); localStorage.setItem(`tasks`, JSON.stringify(tasks)); renderTaskListToDOM(); return removedTask; } // function to determine if string is integer function isInt(value) { return ( !isNaN(value) && parseInt(Number(value)) == value && !isNaN(parseInt(value, 10)) ); } // 0: user has no tasks // 1: invalid input // task: succcess function markTaskDone(username, task) { const tasks = JSON.parse(localStorage.tasks); if (!tasks[username] || tasks[username].todos.length === 0) { return 0; } let index = tasks[username].todos.findIndex( (t) => t.text.toLowerCase() === task.toLowerCase() ); // filter by t.done is false let incompleteTaskCount = tasks[username].todos.filter( (t) => !t.done ).length; if (incompleteTaskCount === 1) { index = tasks[username].todos.findIndex((t) => !t.done); } if (index === -1) { if (isInt(task)) { index = parseInt(task) - 1; // ACTUAL INDEX } else { return 1; } if (index < 0 || index > tasks[username].todos.length - 1) { return 1; } } let completedTask = tasks[username].todos[index].text; tasks[username].todos[index].done = true; localStorage.setItem(`tasks`, JSON.stringify(tasks)); renderTaskListToDOM(); return completedTask; } // 0: user has no tasks // 1: invalid input // [original task, new task]: succcess function editTask(username, message) { const tasks = JSON.parse(localStorage.tasks); let noSpecifiedIndex = false; if (!tasks[username] || tasks[username].todos.length === 0) { return 0; } // example 'message': 1 new task let index = parseInt(message.split(" ")[0]) - 1; // ACTUAL INDEX let incompleteTaskCount = tasks[username].todos.filter( (t) => !t.done ).length; if (incompleteTaskCount === 1 && index !== 0) { // find index of incomplete task index = tasks[username].todos.findIndex((t) => !t.done); noSpecifiedIndex = true; } if (isNaN(index)) { return 1; } let newTask = message.split(" ").slice(1).join(" "); if (noSpecifiedIndex) { newTask = message; } // if newTask is empty or whitespace, return 1 if (!newTask || !newTask.trim()) { return 1; } if (index < 0 || index > tasks[username].todos.length - 1) { return 1; } let originalTask = tasks[username].todos[index].text; tasks[username].todos[index].text = newTask; localStorage.setItem(`tasks`, JSON.stringify(tasks)); renderTaskListToDOM(); return [originalTask, newTask]; } // 0: user has no tasks function checkTasks(username) { const tasks = JSON.parse(localStorage.tasks); if (!tasks[username]) { return 0; } // filter completed tasks const incompleteTasks = tasks[username].todos.filter((t) => !t.done); const completedTasks = tasks[username].todos.filter((t) => t.done); // format incomplete tasks into string: 1. task 1 | 2. task 2 | 3. task 3... let reply = `@${username} incomplete tasks (${incompleteTasks.length}) : `; let taskIndex; for (let i = 0; i < incompleteTasks.length; i++) { // get index of task by task name taskIndex = tasks[username].todos.findIndex( (t) => t.text.toLowerCase() === incompleteTasks[i].text.toLowerCase() ) + 1; reply += `${taskIndex}. ${incompleteTasks[i].text} | `; } reply = reply.slice(0, -3); // format completed tasks into string: 1. task 1 | 2. task 2 | 3. task 3... reply += `\n|| completed tasks (${completedTasks.length}) : `; for (let j = 0; j < completedTasks.length; j++) { taskIndex = tasks[username].todos.findIndex( (t) => t.text.toLowerCase() === completedTasks[j].text.toLowerCase() ) + 1; reply += `${taskIndex}. ${completedTasks[j].text} | `; } reply = reply.slice(0, -3); return reply; } function incompleteTasksCount(username) { const tasks = JSON.parse(localStorage.tasks); if (!tasks[username]) { return 0; } let count = 0; for (const task of tasks[username].todos) { if (!task.done) { count++; } } return count; } function clearUserTasks(username) { const tasks = JSON.parse(localStorage.tasks); if (!tasks[username]) { return; } tasks[username].todos = []; localStorage.setItem(`tasks`, JSON.stringify(tasks)); renderTaskListToDOM(); } function clearAllDoneTasks() { const tasks = JSON.parse(localStorage.tasks); for (const user in tasks) { tasks[user].todos = tasks[user].todos.filter((t) => !t.done); } localStorage.setItem(`tasks`, JSON.stringify(tasks)); renderTaskListToDOM(); } function clearAllTasks() { resetDB(); renderTaskListToDOM(); } function smutOnly(streamer) { // clear all tasks except for broadcasters const tasks = JSON.parse(localStorage.tasks); for (const user in tasks) { if (user.toLowerCase() !== streamer.toLowerCase()) { tasks[user].todos = []; } } localStorage.setItem(`tasks`, JSON.stringify(tasks)); clearAllDoneTasks(); renderTaskListToDOM(); } function renderTaskListToDOM() { const tasks = JSON.parse(localStorage.tasks); // console.log(tasks); const taskContainers = document.querySelectorAll(".task-container"); taskContainers.forEach((taskList) => { taskList.innerHTML = ""; let totalTaskCount = 0; let completedTasksCount = 0; for (const user in tasks) { const userTasks = tasks[user]; if (userTasks.todos.length === 0) { continue; } const userColor = userTasks.userColor; const taskDiv = document.createElement("div"); taskDiv.classList.add("task"); taskList.appendChild(taskDiv); const usernameDiv = document.createElement("div"); usernameDiv.classList.add("username"); usernameDiv.innerText = user; if (styles.usernameColor === "") { usernameDiv.style.color = userColor; } else { usernameDiv.style.color = styles.usernameColor; } taskDiv.appendChild(usernameDiv); const olListDiv = document.createElement("ol"); olListDiv.classList.add("user-tasks"); taskDiv.appendChild(olListDiv); for (const task of userTasks.todos) { const taskElement = document.createElement("li"); taskElement.classList.add("todo"); totalTaskCount++; if (task.done) { taskElement.classList.add("done"); completedTasksCount++; } taskElement.innerText = task.text; olListDiv.appendChild(taskElement); } } document.querySelector( ".task-count" ).innerText = `${completedTasksCount}/${totalTaskCount}`; }); checkToAnimate(); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } // unit tests function tests1() { let listOfStreamers = [ `cloudydayzzz`, `berryspace`, `MohFocus`, `xeno_hiraeth`, `euphie___`, `unknownnie`, `theyolotato`, `charliosaurus`, `jutstreams`, `mikewhatwhere`, `studypaws`, `pcc_lanezzz`, `workwithjandj`, `studylena`, ]; for (let i = 0; i < listOfStreamers.length; i++) { addTask(listOfStreamers[i], "#ffc0cb", `task ${i + 1}`); addTask(listOfStreamers[i], "#ffc0cb", `another task ${i + 1}`); } } function tests2() { smutOnly("theyolotato"); } async function checkToAnimate() { // compare task container height and task wrapper height let taskContainer = document.querySelector(".task-container"); let taskContainerHeight = taskContainer.scrollHeight; let taskWrapper = document.querySelector(".task-wrapper"); let taskWrapperHeight = taskWrapper.clientHeight; console.log(taskContainerHeight > taskWrapperHeight); // scroll task wrapper up and down once if (taskContainerHeight > taskWrapperHeight) { if (!scrolling) { document.querySelector(".secondary").style.display = "flex"; let finalHeight = taskContainerHeight + 100; let primaryKeyFrames = [ { transform: `translateY(0)` }, { transform: `translateY(-${finalHeight}px)` }, ]; let secondaryKeyFrames = [ { transform: `translateY(${finalHeight}px)` }, { transform: `translateY(0)` }, ]; let scrollingSpeed = (finalHeight / scrollSpeed) * 1000; let options = { duration: scrollingSpeed, iterations: 1, easing: "linear", }; let primaryAnimation = document .querySelector(".primary") .animate(primaryKeyFrames, options); let secondaryAnimation = document .querySelector(".secondary") .animate(secondaryKeyFrames, options); primaryAnimation.play(); secondaryAnimation.play(); scrolling = true; await sleep(scrollingSpeed); scrolling = false; checkToAnimate(); } } else if (!scrolling) { document.querySelector(".secondary").style.display = "none"; // cancel animations document .querySelector(".task-container") .getAnimations() .forEach((a) => { a.cancel(); }); scrolling = false; } } (function () { setupDB(); importStyles(); renderTaskListToDOM(); })();