multi-task-widget-cross-pla.../scripts/taskList.js
2023-12-01 10:14:20 +08:00

1586 lines
34 KiB
JavaScript

/*
DB structure:
- tasks
- [username]:
- todos (array)
- [{text, done, focus}, {text, done, focus}]
- userColor
- counts
- users
- [username]:
- completeCount
- totalCompleteCount
*/
const settings = configs.settings;
const styles = configs.styles;
const scrollSpeed = configs.animation.scrollSpeed;
const responses = configs.responses;
let scrolling = false;
let primaryAnimation, secondaryAnimation;
const taskSeparator = configs.settings.taskSeparator;
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})`
);
});
let currentTitle = 0;
// interval the task title
setInterval(async () => {
let taskTitle = document.querySelector(".title");
// cycle through a list of titles
let titles = configs.animation.titles;
// 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`, "{}");
}
if (!localStorage.counts) {
localStorage.setItem(`counts`, JSON.stringify({ users: {} }));
}
}
function incompleteTasksCount(username) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username]) {
return 0;
}
return tasks[username].todos.filter((t) => !t.done).length;
}
function completedTasksCount(username) {
const counts = JSON.parse(localStorage.counts);
if (!counts.users[username.toLowerCase()]) {
return 0;
}
return counts.users[username.toLowerCase()].completeCount;
}
function getUserTotalTaskCount(username) {
return incompleteTasksCount(username) + completedTasksCount(username);
}
function addDoneCount(username, value) {
const counts = JSON.parse(localStorage.counts);
if (!counts.users[username.toLowerCase()]) {
counts.users[username.toLowerCase()] = {
completeCount: 0,
};
}
if (!counts.totalCompleteCount) {
counts.totalCompleteCount = 0;
}
counts.users[username.toLowerCase()].completeCount += value;
counts.totalCompleteCount += value;
localStorage.setItem(`counts`, JSON.stringify(counts));
}
function setTotalCompleteCount(value) {
const counts = JSON.parse(localStorage.counts);
counts.totalCompleteCount = parseInt(value);
localStorage.setItem(`counts`, JSON.stringify(counts));
}
function calculatePoints(username) {
const counts = JSON.parse(localStorage.counts);
if (!counts.users[username.toLowerCase()]) {
return 0;
}
return (
counts.users[username.toLowerCase()].completeCount *
settings.pointsPerTask
);
}
function getBoardTotalTaskCount() {
const counts = JSON.parse(localStorage.counts);
if (!counts.totalCompleteCount) {
return 0;
}
return counts.totalCompleteCount;
}
function resetBoardCount() {
const counts = JSON.parse(localStorage.counts);
counts.totalCompleteCount = 0;
localStorage.setItem(`counts`, JSON.stringify(counts));
}
function resetUsersCount() {
const counts = JSON.parse(localStorage.counts);
counts.users = {};
localStorage.setItem(`counts`, JSON.stringify(counts));
}
// status, body
// 0: limit has reached
// 1: duplicate task
// 2: invalid input
// 200: 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 {
status: 0,
body: {
"error message": `@${username} has reached the limit of ${settings.limit} tasks`,
error: responses.noTaskAdded,
},
};
}
if (
tasks[username].todos.find(
(t) => t.text.toLowerCase() === task.toLowerCase()
)
) {
return {
status: 1,
body: {
"error message": `@${username} already has this task`,
error: responses.duplicateTask,
},
};
}
// if task is whitespace or empty
if (!task || !task.trim()) {
return {
status: 2,
body: {
"error message": `@${username} empty task`,
error: responses.noTaskContent,
},
};
}
// if task is "all", return error
if (task.toLowerCase() === "all") {
return {
status: 2,
body: {
"error message": `@${username} all is a reserved keyword`,
error: responses.noTaskContent,
},
};
}
let tasksFailedToAdd = [];
// if task has commas
if (taskSeparator.some((char) => task.includes(char))) {
let tasksToAdd = [];
let char = taskSeparator.find((element) => task.includes(element));
tasksToAdd = task.split(char).map((t) => t.trim());
if (
incompleteTasksCount(username) + tasksToAdd.length >
settings.limit &&
settings.enableLimit
) {
tasksFailedToAdd = tasksToAdd.slice(
settings.limit - incompleteTasksCount(username),
tasksToAdd.length
);
tasksToAdd = tasksToAdd.slice(
0,
settings.limit - incompleteTasksCount(username)
);
}
for (const t of tasksToAdd) {
if (t === "") {
continue;
}
if (t === "all") {
tasksFailedToAdd.push(t);
// remove from tasksToAdd
tasksToAdd = tasksToAdd.filter((task) => task !== t);
continue;
}
if (tasks[username].todos.find((task) => task.text === t)) {
tasksToAdd = tasksToAdd.filter((task) => task !== t);
tasksFailedToAdd.push(t);
continue;
}
tasks[username].todos.push({
text: t,
done: false,
focus: false,
});
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
body: {
task: tasksToAdd.join('", "'),
tasksFailedToAdd: tasksFailedToAdd.join('", "') || "",
},
};
}
tasks[username].todos.push({ text: task, done: false, focus: false });
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
body: {
task: task,
tasksFailedToAdd: "",
},
};
}
// 0: user has no tasks
// 1: invalid input
// 200: success
function focusTask(username, task) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username] || tasks[username].todos.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
const incompleteTasks = tasks[username].todos.filter((t) => !t.done);
if (incompleteTasks.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no incomplete tasks`,
error: responses.noTask,
},
};
}
if (task.includes(",")) {
return {
status: 1,
body: {
"error message": `@${username} need to specify ONLY ONE task`,
error: responses.onlyOneFocus,
},
};
}
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 (incompleteTasks.length === 1) {
index = tasks[username].todos.findIndex((t) => !t.done);
} else {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyFocusTask,
},
};
}
if (index < 0 || index > tasks[username].todos.length - 1) {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyFocusTask,
},
};
}
} else {
if (tasks[username].todos[index].done) {
return {
status: 1,
body: {
"error message": `@${username} task is already completed`,
error: responses.specifyFocusTask,
},
};
}
}
let focusedTask = tasks[username].todos[index].text;
// if task is already focused, return 1
if (tasks[username].todos[index].focus) {
return {
status: 1,
body: {
"error message": `@${username} task is already focused`,
error: responses.alreadyFocusedTask,
},
};
}
// set all tasks to unfocused
for (const task of tasks[username].todos) {
task.focus = false;
}
// set task to focused
tasks[username].todos[index].focus = true;
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
body: {
focusedTask: focusedTask,
},
};
}
// 0: user has no tasks
// 1: invalid input
// 200: success
function unfocusTask(username) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username] || tasks[username].todos.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
// if no tasks are focused, return 1
if (!tasks[username].todos.find((t) => t.focus)) {
return {
status: 1,
body: {
"error message": `@${username} no tasks are focused`,
error: responses.noFocusedTask,
},
};
}
// set all tasks to unfocused
for (const task of tasks[username].todos) {
task.focus = false;
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
};
}
// 0: user has no tasks
// 1: invalid input
// 200: success
function removeTask(username, task) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username] || tasks[username].todos.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
const incompleteTasks = tasks[username].todos.filter((t) => !t.done);
// match regex: integers separated by space e.g. '1 2 3 4'
if (task.match(/^(\d+ )*\d+$/)) {
// insert commas between integers
task = task.replace(/(\d+)/g, "$1,");
// remove trailing comma
task = task.slice(0, -1);
}
let removedTaskIndex = [];
let tasksRemoved = [];
let tasksFailedToRemove = [];
// check if there's a comma in the task (multiple tasks)
if (task.includes(",") || task.includes(";")) {
let tasksToRemove = [];
let char = taskSeparator.find((element) => task.includes(element));
tasksToRemove = task.split(char).map((t) => t.trim());
for (const t of tasksToRemove) {
let index = tasks[username].todos.findIndex(
(task) => task.text.toLowerCase() === t.toLowerCase()
);
if (index === -1) {
if (isInt(t)) {
index = parseInt(t) - 1; // ACTUAL INDEX
} else if (incompleteTasks.length === 1) {
index = tasks[username].todos.findIndex(
(t) => !t.done
);
tasksRemoved.push(tasks[username].todos[index].text);
removedTaskIndex.push(index);
} else {
tasksFailedToRemove.push(t);
continue;
}
if (index < 0 || index > tasks[username].todos.length - 1) {
tasksFailedToRemove.push(t);
continue;
} else {
tasksRemoved.push(tasks[username].todos[index].text);
removedTaskIndex.push(index);
}
} else {
tasksRemoved.push(tasks[username].todos[index].text);
removedTaskIndex.push(index);
}
}
if (tasksRemoved.length === 0) {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
// sort removedTaskIndex in descending order
removedTaskIndex.sort((a, b) => b - a);
// remove tasks from tasks array
for (const index of removedTaskIndex) {
tasks[username].todos.splice(index, 1);
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
body: {
removedTasks: tasksRemoved.join('", "'),
failedTasks: tasksFailedToRemove.join('", "'),
},
};
} else {
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 (incompleteTasks.length === 1) {
index = tasks[username].todos.findIndex((t) => !t.done);
} else {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
if (index < 0 || index > tasks[username].todos.length - 1) {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
}
task = tasks[username].todos[index].text;
tasks[username].todos.splice(index, 1);
localStorage.setItem(`tasks`, JSON.stringify(tasks));
renderTaskListToDOM();
return {
status: 200,
body: {
removedTasks: task,
failedTasks: "",
},
};
}
}
// 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
// 200: success
function clearOwnDoneTasks(username) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username] || tasks[username].todos.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
let incompleteUserTasks = tasks[username].todos.filter((t) => !t.done);
// replace user's task with incomplete tasks
tasks[username].todos = incompleteUserTasks;
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
};
}
// 0: user has no tasks
// 1: invalid input
// 2: task is already completed
// 200: succcess
function markTaskDone(username, task) {
const tasks = JSON.parse(localStorage.tasks);
// user does not have any tasks
if (!tasks[username] || tasks[username].todos.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
const incompleteTasks = tasks[username].todos.filter((t) => !t.done);
if (incompleteTasks.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no incomplete tasks`,
error: responses.noTask,
},
};
}
// match regex: integers separated by space e.g. '1 2 3 4'
if (task.match(/^(\d+ )*\d+$/)) {
// insert commas between integers
task = task.replace(/(\d+)/g, "$1,");
// remove trailing comma
task = task.slice(0, -1);
}
let tasksMarkedComplete = [];
let tasksFailedToComplete = [];
// check if there's a comma in the task (multiple tasks)
if (task.includes(",") || task.includes(";")) {
// let tasksToMarkDone = task.split(",").map((t) => t.trim());
let tasksToMarkDone = [];
let char = taskSeparator.find((element) => task.includes(element));
tasksToMarkDone = task.split(char).map((t) => t.trim());
for (const t of tasksToMarkDone) {
let index = tasks[username].todos.findIndex(
(task) => task.text.toLowerCase() === t.toLowerCase()
);
// check if task is already marked done
if (index !== -1 && tasks[username].todos[index].done) {
tasksFailedToComplete.push(t);
continue;
}
if (index === -1) {
if (isInt(t)) {
index = parseInt(t) - 1; // ACTUAL INDEX
} else if (incompleteTasks.length === 1) {
index = tasks[username].todos.findIndex(
(t) => !t.done
);
tasksMarkedComplete.push(
tasks[username].todos[index].text
);
// increment count
addDoneCount(username, 1);
} else {
tasksFailedToComplete.push(t);
continue;
}
if (index < 0 || index > tasks[username].todos.length - 1) {
tasksFailedToComplete.push(t);
continue;
} else if (tasks[username].todos[index].done) {
tasksFailedToComplete.push(t);
continue;
} else {
tasksMarkedComplete.push(
tasks[username].todos[index].text
);
// increment count
addDoneCount(username, 1);
}
} else {
tasksMarkedComplete.push(tasks[username].todos[index].text);
// increment count
addDoneCount(username, 1);
}
tasks[username].todos[index].done = true;
tasks[username].todos[index].focus = false;
}
if (tasksMarkedComplete.length === 0) {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
body: {
markedTasks: tasksMarkedComplete.join('", "'),
failedTasks: tasksFailedToComplete.join('", "'),
markedTasksCount: tasksMarkedComplete.length,
},
};
} else {
// if there's no comma in the task (single task)
let index = tasks[username].todos.findIndex(
(t) => t.text.toLowerCase() === task.toLowerCase()
);
// is there a task with focus on?
let focusedTask = tasks[username].todos.find((t) => t.focus);
if (index === -1) {
if (isInt(task)) {
index = parseInt(task) - 1; // ACTUAL INDEX
} else if (incompleteTasks.length === 1) {
index = tasks[username].todos.findIndex((t) => !t.done);
} else if (focusedTask) {
index = tasks[username].todos.findIndex(
(t) =>
t.text.toLowerCase() ===
focusedTask.text.toLowerCase()
);
} else if (settings.automaticDoneIndex) {
// index is the first incomplete task
index = tasks[username].todos.findIndex((t) => !t.done);
} else {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
if (index < 0 || index > tasks[username].todos.length - 1) {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
}
// task is already marked done
if (tasks[username].todos[index].done) {
return {
status: 2,
body: {
"error message": `@${username} task is already completed`,
error: responses.alreadyDoneTask,
},
};
}
task = tasks[username].todos[index].text;
tasks[username].todos[index].done = true;
addDoneCount(username, 1);
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
body: {
markedTasks: task,
failedTasks: "",
markedTasksCount: 1,
},
};
}
}
// 0: user has no tasks
// 200: success
function markAllTasksAsDone(username) {
const tasks = JSON.parse(localStorage.tasks);
// user does not have any tasks
if (!tasks[username] || tasks[username].todos.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
for (const task of tasks[username].todos) {
if (!task.done) {
addDoneCount(username, 1);
}
task.done = true;
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
};
}
// 0: user has no tasks
// 1: invalid input
// 2: task is already incomplete
// 200: succcess
function markTaskUndone(username, task) {
const tasks = JSON.parse(localStorage.tasks);
// user does not have any tasks
if (!tasks[username] || tasks[username].todos.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
const completedTasks = tasks[username].todos.filter((t) => t.done);
// match regex: integers separated by space e.g. '1 2 3 4'
if (task.match(/^(\d+ )*\d+$/)) {
// insert commas between integers
task = task.replace(/(\d+)/g, "$1,");
// remove trailing comma
task = task.slice(0, -1);
}
let tasksMarkedUndone = [];
let tasksFailedToMarkUndone = [];
// check if there's a comma in the task (multiple tasks)
if (task.includes(",") || task.includes(";")) {
let tasksToMarkUndone;
let char = taskSeparator.find((element) => task.includes(element));
tasksToMarkUndone = task.split(char).map((t) => t.trim());
for (const t of tasksToMarkUndone) {
let index = tasks[username].todos.findIndex(
(task) => task.text.toLowerCase() === t.toLowerCase()
);
if (index === -1) {
// user input isn't the task name
if (isInt(t)) {
index = parseInt(t) - 1; // ACTUAL INDEX
} else {
tasksFailedToMarkUndone.push(t);
continue;
}
if (index < 0 || index > tasks[username].todos.length - 1) {
tasksFailedToMarkUndone.push(t);
continue;
} else {
tasksMarkedUndone.push(
tasks[username].todos[index].text
);
}
} else {
tasksMarkedUndone.push(tasks[username].todos[index].text);
}
addDoneCount(username, -1);
tasks[username].todos[index].done = false;
}
if (tasksMarkedUndone.length === 0) {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
body: {
markedTasks: tasksMarkedUndone.join('", "'),
failedTasks: tasksFailedToMarkUndone.join('", "'),
},
};
} else {
let index = tasks[username].todos.findIndex(
(t) => t.text.toLowerCase() === task.toLowerCase() && t.done
);
if (index === -1) {
if (isInt(task)) {
index = parseInt(task) - 1; // ACTUAL INDEX
} else if (completedTasks.length === 1) {
index = tasks[username].todos.findIndex((t) => t.done);
} else {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
if (index < 0 || index > tasks[username].todos.length - 1) {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
tasks[username].todos[index].done = false;
addDoneCount(username, -1);
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
body: {
markedTasks: tasks[username].todos[index].text,
failedTasks: "",
},
};
} else {
tasks[username].todos[index].done = false;
addDoneCount(username, -1);
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
body: {
markedTasks: tasks[username].todos[index].text,
failedTasks: "",
},
};
}
}
}
// 0: user has no tasks
// 1: invalid input
// 200: success
function editTask(username, message) {
const tasks = JSON.parse(localStorage.tasks);
let noSpecifiedIndex = false;
if (!tasks[username] || tasks[username].todos.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
// 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 {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
let newTask = message.split(" ").slice(1).join(" ");
if (noSpecifiedIndex) {
newTask = message;
}
// if newTask is empty or whitespace, return 1
if (!newTask || !newTask.trim()) {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.noTaskContent,
},
};
}
if (index < 0 || index > tasks[username].todos.length - 1) {
return {
status: 1,
body: {
"error message": `@${username} invalid input`,
error: responses.specifyTaskIndex,
},
};
}
let originalTask = tasks[username].todos[index].text;
tasks[username].todos[index].text = newTask;
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
status: 200,
body: {
originalTask: originalTask,
newTask: newTask,
},
};
}
// 0: user has no tasks
function checkTasks(username) {
const tasks = JSON.parse(localStorage.tasks);
// Go through keys of tasks, find match of lowercased username
username = Object.keys(tasks).find(
(user) => user.toLowerCase() === username.toLowerCase()
);
if (!tasks[username]) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
// 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].focus ? "(FOCUSED)" : ""
} ${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 {
status: 200,
body: {
reply: reply,
},
};
}
// 0: user has no tasks
function listTasks(username) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username]) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
// filter completed tasks
const incompleteTasks = tasks[username].todos.filter((t) => !t.done);
if (incompleteTasks.length === 0) {
return {
status: 0,
body: {
"error message": `@${username} has no incomplete tasks`,
error: responses.noTask,
},
};
}
// format incomplete tasks into string: 1. task 1 | 2. task 2 | 3. task 3...
let reply = `@${username} `;
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 += `${incompleteTasks[i].text}, `;
}
reply = reply.slice(0, -2);
return {
status: 200,
body: {
reply: reply,
},
};
}
// 0: user has no tasks
function clearUserTasks(username) {
const tasks = JSON.parse(localStorage.tasks);
// find username where lowercase matches username
username = Object.keys(tasks).find(
(user) => user.toLowerCase() === username.toLowerCase()
);
if (!username) {
return {
status: 0,
body: {
"error message": `@${username} has no tasks`,
error: responses.noTask,
},
};
}
tasks[username].todos = [];
localStorage.setItem(`tasks`, JSON.stringify(tasks));
return {
status: 200,
};
}
function clearAllDoneTasks() {
const tasks = JSON.parse(localStorage.tasks);
for (const user in tasks) {
tasks[user].todos = tasks[user].todos.filter((t) => !t.done);
}
cancelAnimation();
checkToAnimate();
localStorage.setItem(`tasks`, JSON.stringify(tasks));
renderTaskListToDOM();
return {
status: 200,
};
}
function clearAllTasks() {
localStorage.setItem(`tasks`, "{}");
cancelAnimation();
checkToAnimate();
renderTaskListToDOM();
return {
status: 200,
};
}
function clearAll() {
resetDB();
cancelAnimation();
checkToAnimate();
renderTaskListToDOM();
return {
status: 200,
};
}
// 200: success
function clearAllExceptStreamer(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();
cancelAnimation();
checkToAnimate();
renderTaskListToDOM();
return {
status: 200,
};
}
function renderTaskListToDOM() {
const tasks = JSON.parse(localStorage.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) {
// remove user from tasks
delete tasks[user];
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++;
} else if (task.focus) {
taskElement.classList.add("focus");
}
taskElement.innerText = task.text;
olListDiv.appendChild(taskElement);
}
}
document.querySelector(
".task-count"
).innerText = `${completedTasksCount}/${totalTaskCount}`;
});
localStorage.setItem(`tasks`, JSON.stringify(tasks));
checkToAnimate();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// unit tests
async 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 1, task 2, task 3, task 4, task 5`
);
await sleep(1000);
markTaskDone(listOfStreamers[i], `1, 2, 3, 4, 5`);
await sleep(1000);
}
}
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;
// scroll task wrapper up and down once
if (taskContainerHeight > taskWrapperHeight && !scrolling) {
if (!scrolling) {
document.querySelector(".secondary").style.display = "flex";
let finalHeight =
taskContainerHeight + configs.animation.gapBetweenScrolls;
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",
};
primaryAnimation = document
.querySelector(".primary")
.animate(primaryKeyFrames, options);
secondaryAnimation = document
.querySelector(".secondary")
.animate(secondaryKeyFrames, options);
primaryAnimation.play();
secondaryAnimation.play();
scrolling = true;
addAnimationListeners();
}
} else if (!scrolling) {
document.querySelector(".secondary").style.display = "none";
// cancel animations
if (primaryAnimation) {
primaryAnimation.cancel();
}
if (secondaryAnimation) {
secondaryAnimation.cancel();
}
scrolling = false;
}
}
function addAnimationListeners() {
if (primaryAnimation) {
primaryAnimation.addEventListener("finish", animationFinished);
primaryAnimation.addEventListener("cancel", animationFinished);
}
}
function animationFinished() {
scrolling = false;
renderTaskListToDOM();
checkToAnimate();
}
function cancelAnimation() {
if (primaryAnimation) {
primaryAnimation.cancel();
}
if (secondaryAnimation) {
secondaryAnimation.cancel();
}
scrolling = false;
}
(function () {
setupDB();
importStyles();
renderTaskListToDOM();
})();