first commit

This commit is contained in:
liyunze 2023-08-02 22:16:30 +08:00
commit dae6c59363
13 changed files with 1637 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.vscode/*
styles/taskList.css.map

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 RyanPython
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

207
README.md Normal file
View file

@ -0,0 +1,207 @@
# Multi-Task widget Infinity (PRIVATE)
![Multi Task Widget](./images/preview.png "Multi-Task widget")
It's basically the same as the original "Multi-Task widget" but with infinite scroll! (more aesthetic, hehe)
## Logs
---
I'll just update you guys on the Discord
---
<!-- directory -->
## Content
- [Multi-Task widget Infinity (PRIVATE)](#multi-task-widget-infinity-private)
- [Logs](#logs)
- [Content](#content)
- [Commands](#commands)
- [Moderators only](#moderators-only)
- [Installation](#installation)
- [Method 1 (fastest, easiest way)](#method-1-fastest-easiest-way)
- [Method 2 (better if you're using an alt account as a bot)](#method-2-better-if-youre-using-an-alt-account-as-a-bot)
- [Customization settings](#customization-settings)
- [settings](#settings)
- [task list styles](#task-list-styles)
- [header](#header)
- [body](#body)
- [task](#task)
- [Credits](#credits)
---
## Commands
### Moderators only
- !clear - Clear all tasks
- !cleardone - Clear all done tasks
- !adel @user - Remove all tasks from a user
---
## Installation
### Method 1 (fastest, easiest way)
1. Install the zip or clone the repository
2. Go to https://twitchapps.com/tmi/ to create an oauth token (it acts as a password)
3. Setup authentication in `auth.js`
4. Setup `Browser Source` in OBS studio or other streaming software with the following settings:
- Local File: `checked`
- Local File: `index.html`
### Method 2 (better if you're using an alt account as a bot)
1. Install the zip or clone the repository
2. Create a Twitch application [here](https://dev.twitch.tv/console/apps) \(Log in with your alternate Twitch account if you wish to use a different account as a bot account\)
3. Copy the `Client ID` from the application
4. Create token using `get_token.txt`. Replace `CLIENT_ID` with the `Client ID` from the application
5. Setup authentication in `auth.js`
6. Setup `Browser Source` in OBS studio or other streaming software with the following settings:
- Local File: `checked`
- Local File: `index.html`
---
## Customization settings
Edit `configs.js` to edit the style of the task list
![Labels](./images/labels.png "Labels")
---
### settings
`showDoneTasks`:
**true**: show the done tasks
**false**: hide (and remove) the done tasks
`enableLimit`:
**true**: limit the number of incomplete tasks stored in the list
**false**: unlimited incomplete tasks
`limit`: The number of incomplete tasks to store in the list (integer)
`OneScrollPerMS`: Per MS, how many pixels to scroll (integer)
`pauseBetweenScrolls`: How many MS to pause between each end of the scroll (integer)
### task list styles
`taskListWidth`: width of the task list (px)
`taskListHeight`: height of the task list (px)
`taskListBackgroundColor`: background color of the task list (hex only)
`taskListBackgroundOpacity`: opacity of the task list background (0.0 - 1.0)
`taskListBorderWidth`: width of the task list border (px)
`taskListBorderColor`: color of the task list border (hex or name)
`taskListBorderRadius`: how round the task list border should be (px)
`taskListHorizontalPadding`: horizontal padding of the task list (px)
`taskListVerticalPadding`: vertical padding of the task list (px)
### header
`headerBackgroundColor`: background color of the header (hex only)
`headerBackgroundOpacity`: opacity of the header background (0.0 - 1.0)
`headerBorderWidth`: width of the header border (px)
`headerBorderColor`: color of the header border (hex or name)
`headerBorderRadius`: how round the header border should be (px)
`headerFontFamily`: font family of the header (font name)
`headerGoogleFont`: whether to use Google font for the header (true or false)
`headerFontSize`: font size of the header (px)
`headerFontWeight`: font weight of the header (normal, bold, or number)
`headerFontColor`: font color of the header (hex or name)
### body
`bodyBackgroundColor`: background color of the body (hex only)
`bodyBackgroundOpacity`: opacity of the body background (0.0 - 1.0)
`bodyBorderWidth`: width of the body border (px)
`bodyBorderColor`: color of the body border (hex or name)
`bodyBorderRadius`: how round the body border should be (px)
`bodyHorizontalPadding`: horizontal padding of the body (px)
`bodyVerticalPadding`: vertical padding of the body (px)
### task
`lineHeight`: line height of the task (number)
`usernameFontWeight`: font weight of the username (normal, bold, or number)
`usernameColor`: color of the username (hex or name or "" for Twitch user color)
`taskBackgroundColor`: background color of the task (hex only)
`taskBackgroundOpacity`: opacity of the task background (0.0 - 1.0)
`taskFontFamily`: font family of the task (font name)
`taskGoogleFont`: whether to use Google font for the task (true or false)
`taskFontSize`: font size of the task (px)
`taskFontColor`: font color of the task (hex or name)
`taskBorderColor`: color of the task border (hex or name)
`taskBorderWidth`: width of the task border (px)
`taskBorderRadius`: how round the task border should be (px)
`taskMarginBottom`: bottom margin of the task (px)
`taskHorizontalPadding`: horizontal padding of the task (px)
`taskVerticalPadding`: vertical padding of the task (px)
---
## Credits
**Author:** [**@RyanPython**](https://twitch.tv/RyanPython)
**Special thanks to:**
- [**@Instafluff**](https://twitch.tv/Instafluff) \(for the Comfy JS library\)

12
auth.js Normal file
View file

@ -0,0 +1,12 @@
const auth = (function () {
// Authentication and channels - required
const channel = ""; // your channel
const username = ""; // bot account
const oauth = ""; // should be oauth:xxxxxxxxxxxx from the bot account DO NOT INCLUDE oauth:
return {
channel,
username,
oauth,
};
})();

313
configs.js Normal file
View file

@ -0,0 +1,313 @@
const configs = (function () {
"use strict";
// settings
const enableLimit = true; // true or false
const limit = 20; // integer
// animation
const scrollSpeed = 30; // milliseconds (DO NOT PUT BELOW 5, COMPUTER MIGHT CRASH) the lower the value, the faster the scroll
const pauseBetweenScrolls = 2000; // milliseconds
const gapBetweenScrolls = "100px"; // px
// STYLES
// task list
const taskListWidth = "400px"; // px
const taskListHeight = "300px"; // px
const taskListBackgroundColor = "#000000"; // hex only
const taskListBackgroundOpacity = 0; // 0.0 - 1.0
const taskListBorderWidth = "0px"; // px
const taskListBorderColor = "#ffffff"; // hex or name
const taskListBorderRadius = "5px"; // px
const taskListHorizontalPadding = "0px"; // px
const taskListVerticalPadding = "0px"; // px
// header
const headerHorizontalPadding = "10px"; // px
const headerVerticalPadding = "10px"; // px
const headerBackgroundColor = "#000000"; // hex only
const headerBackgroundOpacity = 0.8; // 0.0 - 1.0
const headerBorderWidth = "2px"; // px
const headerBorderColor = "#ffffff"; // hex or name
const headerBorderRadius = "5px"; // px
const headerFontFamily = "Fredoka"; // font name
const headerGoogleFont = true; // true: use google font, false: use system font
const headerFontSize = "25px"; // px
const headerFontWeight = "bold"; // normal or bold or number
const headerFontColor = "#ffffff"; // hex or name
// body
const bodyBackgroundColor = "#000000"; // hex only
const bodyBackgroundOpacity = 0; // 0.0 - 1.0
const bodyBorderWidth = "0px"; // px
const bodyBorderColor = "#fff"; // hex or name
const bodyBorderRadius = "5px"; // px
// task
const lineHeight = 1.5; // number
const usernameFontWeight = "bold"; // normal or bold or number
const usernameColor = "#ffc0cb"; // hex or name or "" for twitch user color
const taskWidth = "98%";
const taskBackgroundColor = "#000000"; // hex only
const taskBackgroundOpacity = 0.7; // 0.0 - 1.0
const taskFontFamily = "Poppins"; // font name
const taskGoogleFont = true; // true: use google font, false: use system font
const taskFontSize = "20px"; // px
const taskFontColor = "#ffffff"; // hex or name
const taskBorderColor = "#000000"; // hex or name
const taskBorderWidth = "0px"; // px
const taskBorderRadius = "5px"; // px
const taskMarginBottom = "5px"; // px
const taskHorizontalPadding = "20px"; // px
const taskVerticalPadding = "10px";
// Add task commands - please add commands in the exact format
const addTaskCommands = [
"!taska",
"!taskadd",
"!atask",
"!addtask",
"!task",
"!add",
"!todo",
];
// Edit task commands - please add commands in the exact format
const editTaskCommands = [
"!taske",
"!taskedit",
"!etask",
"!edittask",
"!edit",
];
// Delete task commands - please add commands in the exact format
const deleteTaskCommands = [
"!taskd",
"!taskdel",
"!taskdelete",
"!deltask",
"!deletetask",
"!taskr",
"!taskremove",
"!rtask",
"!removetask",
"!remove",
"!delete",
];
// Finish task commands - please add commands in the exact format
const finishTaskCommands = [
"!taskf",
"!taskfinish",
"!ftask",
"!finishtask",
"!taskd",
"!taskdone",
"!donetask",
"!dtask",
"!finish",
"!done",
"!finished",
];
// Check task commands - please add commands in the exact format
const checkCommands = [
"!taskc",
"!taskcheck",
"!ctask",
"!checktask",
"!mytask",
"!check",
];
// Help commands - please add commands in the exact format
const helpCommands = [
"!taskh",
"!taskhelp",
"!htask",
"!helptask",
"!tasks",
"!help",
];
// Admin delete - please add commands following the exact format
const adminDeleteCommands = [
"!taskadel",
"!adel",
"!adelete",
"!admindelete",
];
// Admin clear done - please add commands following the exact format
const adminClearDoneCommands = [
"!acleardone",
"!admincleardone",
"!cleardone",
];
const adminClearAllCommands = [
"!clearall",
"!allclear",
"!adminclearall",
"!adminallclear",
"!aclearall",
"!aclear",
"!clear",
];
// Responses
const taskAdded = 'The task "{message}" has been added, {user}!';
const noTaskAdded =
"Looks like you already hit the limit of incomplete tasks, {user}";
const noTaskContent = "Try using !task the-task-you-are-working-on {user}";
const taskDeleted = "Task '{task}' has been deleted successfully, {user}";
const specifyTaskIndex = "Try specifying the index of the task {user}";
const duplicateTask =
"Looks like you already have the task '{message}' up there {user}!";
const adminDeleteTasks = "All of {mentioned}'s tasks have been deleted";
const taskFinished = "Good job on finishing the task '{task}', {user}!";
const taskEdited =
"Task '{originalTask}' has been edited to '{task}' successfully, {user}";
const noTaskEdit = "Try doing !edit [index] [new task] {user}";
const noTask = "Looks like you don't have a task up there {user}";
const noTaskA = "Looks like there is no task from that user there {user}";
const notMod = "Permission denied, {user}; Mods only";
const clearedAll = "All tasks have been cleared!";
const clearedDone = "All completed tasks have been cleared!";
const specifyUser = "Try specifying a user to delete their tasks {user}";
const help = `{user} Use the following commands to help you out - !task !edit !remove !done. For mods, you can do !adel @user. More commmands here: https://github.com/liyunze-coding/Chat-Task-Tic-Overlay/blob/main/MultiTask.md/`;
// to edit check task command, go to
// scripts/taskList.js
// function checkTasks(username)
const additionalCommands = {
"!botcred":
"{user} Ryan is the creator of this bot! You can find him on https://github.com/liyunze-coding or https://www.twitch.tv/ryanpython",
};
const titles = [
"!taskhelp",
"!task",
"!edit",
"!remove",
"!done",
"!botcred",
];
const settings = {
enableLimit,
limit,
headerGoogleFont,
taskGoogleFont,
};
const styles = {
taskListWidth,
taskListHeight,
taskListBackgroundColor,
taskListBackgroundOpacity,
taskListBorderWidth,
taskListBorderColor,
taskListBorderRadius,
taskListHorizontalPadding,
taskListVerticalPadding,
headerHorizontalPadding,
headerVerticalPadding,
headerBackgroundColor,
headerBackgroundOpacity,
headerBorderWidth,
headerBorderColor,
headerBorderRadius,
headerFontSize,
headerFontWeight,
headerFontFamily,
headerFontColor,
bodyBackgroundColor,
bodyBackgroundOpacity,
bodyBorderWidth,
bodyBorderColor,
bodyBorderRadius,
taskWidth,
lineHeight,
usernameFontWeight,
usernameColor,
taskBackgroundColor,
taskBackgroundOpacity,
taskFontFamily,
taskFontSize,
taskFontColor,
taskBorderColor,
taskBorderWidth,
taskBorderRadius,
taskMarginBottom,
taskHorizontalPadding,
taskVerticalPadding,
};
const animation = {
scrollSpeed,
pauseBetweenScrolls,
gapBetweenScrolls,
titles,
};
const commands = {
addTaskCommands,
editTaskCommands,
deleteTaskCommands,
finishTaskCommands,
helpCommands,
checkCommands,
adminDeleteCommands,
adminClearDoneCommands,
adminClearAllCommands,
additionalCommands,
};
const responses = {
taskAdded,
noTaskAdded,
noTaskContent,
taskDeleted,
specifyTaskIndex,
duplicateTask,
taskFinished,
taskEdited,
noTaskEdit,
noTask,
noTaskA,
notMod,
help,
adminDeleteTasks,
clearedAll,
clearedDone,
specifyUser,
};
return {
settings,
styles,
animation,
commands,
responses,
};
})();

5
get_token.txt Normal file
View file

@ -0,0 +1,5 @@
https://id.twitch.tv/oauth2/authorize
?client_id=CLIENT_ID_HERE
&redirect_uri=http://localhost
&response_type=token
&scope=channel:moderate+chat:edit+chat:read+channel:manage:broadcast+user:edit:broadcast+channel:read:redemptions+user:read:email

BIN
images/labels.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

BIN
images/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

44
index.html Normal file
View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="http://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js"></script>
<script src="https://cdn.jsdelivr.net/npm/comfy.js@latest/dist/comfy.min.js"></script>
<script src="./auth.js"></script>
<script src="./configs.js"></script>
<script defer src="scripts/index.js"></script>
<script defer src="scripts/taskList.js"></script>
<link rel="stylesheet" type="text/css" href="styles/taskList.css" />
<title>Task List</title>
</head>
<body>
<div id="main-container">
<div class="header">
<div class="title">!taskhelp</div>
<div class="task-count">0/0</div>
</div>
<div class="task-wrapper">
<div class="task-container primary">
<!--
<div class="task">
<div class="username">RyanPython</div>
<ol class="user-tasks">
<li class="todo">complete website</li>
<li class="todo">work on overlay</li>
</ol>
</div>
-->
</div>
<div class="task-container secondary"></div>
</div>
</div>
</body>
</html>

147
scripts/index.js Normal file
View file

@ -0,0 +1,147 @@
const commands = configs.commands;
const responses = configs.responses;
let params = {};
function respond(template, params) {
ComfyJS.Say(
template
.replace("{user}", `@${params.user}`)
.replace("{message}", params.message)
.replace("{mentioned}", `@${params.mentioned}`)
.replace("{task}", params.task)
.replace("{originalTask}", params.originalTask)
);
}
function isMod(flags) {
return flags.broadcaster || flags.mod;
}
ComfyJS.onCommand = (user, command, message, flags, extra) => {
command = `!${command.toLowerCase()}`;
params = {
user: user,
message: message,
mentioned: "",
task: "",
originalTask: "",
};
if (commands.addTaskCommands.includes(command)) {
let addStatus = addTask(user, extra.userColor, message);
if (addStatus === 0) {
// limit has reached
respond(responses.noTaskAdded, params);
} else if (addStatus === 1) {
// duplicate task
respond(responses.duplicateTask, params);
} else if (addStatus === 2) {
// task has no content
respond(responses.noTaskContent, params);
} else if (addStatus === 3) {
// task added
respond(responses.taskAdded, params);
}
} else if (commands.editTaskCommands.includes(command)) {
let editStatus = editTask(user, message);
if (editStatus === 0) {
// no task
respond(responses.noTask, params);
} else if (editStatus === 1) {
// invalid input
respond(responses.noTaskEdit, params);
} else {
// task edited
params.task = editStatus[1];
params.originalTask = editStatus[0];
respond(responses.taskEdited, params);
}
} else if (commands.deleteTaskCommands.includes(command)) {
let removeStatus = removeTask(user, message);
if (removeStatus === 0) {
// no task
respond(responses.noTask, params);
} else if (removeStatus === 1) {
// invalid input
respond(responses.specifyTaskIndex, params);
} else {
// task deleted
params.task = removeStatus;
respond(responses.taskDeleted, params);
}
} else if (commands.finishTaskCommands.includes(command)) {
let finishStatus = markTaskDone(user, message);
if (finishStatus === 0) {
// user has no tasks
respond(responses.noTask, params);
} else if (finishStatus === 1) {
// invalid input
respond(responses.specifyTaskIndex, params);
} else {
// task finished
params.task = finishStatus;
respond(responses.taskFinished, params);
}
} else if (commands.checkCommands.includes(command)) {
if (message === "") {
let response = checkTasks(user);
if (response === 0) {
return respond(responses.noTask, params);
}
return ComfyJS.Say(response);
} else {
let mentioned = message.replace("@", "");
let response = checkTasks(mentioned);
if (response === 0) {
// no tasks
return respond(responses.noTaskA, params);
}
return ComfyJS.Say(response);
}
} else if (commands.adminDeleteCommands.includes(command)) {
if (!isMod(flags)) {
respond(responses.notMod, params);
return;
}
let mentioned = message.replace("@", "");
if (mentioned === "") {
respond(responses.specifyUser, params);
return;
}
clearUserTasks(mentioned);
params.mentioned = mentioned;
respond(responses.adminDeleteTasks, params);
return mentioned;
} else if (commands.adminClearDoneCommands.includes(command)) {
if (!isMod(flags)) {
respond(responses.notMod, params);
return;
} else {
clearAllDoneTasks();
respond(responses.clearedDone, params);
}
} else if (commands.adminClearAllCommands.includes(command)) {
if (!isMod(flags)) {
respond(responses.notMod, params);
return;
} else {
clearAllTasks();
respond(responses.clearedAll, params);
}
} else if (commands.helpCommands.includes(command)) {
respond(responses.help, params);
} else if (commands.additionalCommands[command]) {
respond(commands.additionalCommands[command], params);
} else {
// command not found
}
};
ComfyJS.Init(auth.username, `oauth:${auth.oauth}`, [auth.channel]);

592
scripts/taskList.js Normal file
View file

@ -0,0 +1,592 @@
/*
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();
})();

123
styles/taskList.css Normal file
View file

@ -0,0 +1,123 @@
:root {
--task-list-width: 400px;
--task-list-height: 300px;
--task-list-background-color: rgba(0, 0, 0, 0.8);
--task-list-border-width: 2px;
--task-list-border-color: white;
--task-list-border-radius: 5px;
--task-list-horizontal-padding: 0px;
--task-list-vertical-padding: 0px;
--header-horizontal-padding: 10px;
--header-vertical-padding: 10px;
--header-background-color: rgba(0, 0, 0, 0.8);
--header-border-width: 2px;
--header-border-color: white;
--header-border-radius: 5px;
--header-font-size: 20px;
--header-font-family: sans-serif;
--header-font-color: white;
--header-font-weight: bold;
--body-background-color: rgba(0, 0, 0, 0.7);
--body-border-width: 5px;
--body-border-color: white;
--body-border-radius: 5px;
--line-height: 1.5;
--username-font-weight: bold;
--task-background-color: rgba(0, 0, 0, 0.7);
--task-font-family: sans-serif;
--task-font-size: 25px;
--task-font-color: white;
--task-border-color: black;
--task-border-width: 0px;
--task-border-radius: 5px;
--task-horizontal-padding: 0px;
--task-vertical-padding: 0px;
--task-margin-bottom: 5px;
--task-horizontal-padding: 5px;
--task-vertical-padding: 5px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: gray;
}
#main-container {
width: var(--task-list-width);
height: var(--task-list-height);
display: flex;
flex-direction: column;
padding: var(--task-list-vertical-padding) var(--task-list-horizontal-padding);
background-color: var(--task-list-background-color);
border: solid var(--task-list-border-width) var(--task-list-border-color);
border-radius: var(--task-list-border-radius);
}
#main-container .header {
display: flex;
justify-content: space-between;
padding: var(--header-vertical-padding) var(--header-horizontal-padding);
border: solid var(--header-border-width) var(--header-border-color);
border-radius: var(--header-border-radius);
color: var(--header-font-color);
background: var(--header-background-color);
font-family: var(--header-font-family);
font-size: var(--header-font-size);
font-weight: var(--header-font-weight);
}
#main-container .header .title {
transition: opacity 0.5s ease;
}
#main-container .header .fade {
opacity: 0;
}
#main-container .task-wrapper {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
background: var(--body-background-color);
color: var(--task-font-color);
font-family: var(--task-font-family);
font-size: var(--task-font-size);
line-height: var(--line-height);
border: var(--body-border-width) solid var(--body-border-color);
border-radius: var(--body-border-radius);
z-index: 1;
}
#main-container .task-wrapper .task-container {
position: absolute;
width: inherit;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
margin-left: auto;
margin-right: auto;
}
#main-container .task-wrapper .task-container .task {
padding: var(--task-vertical-padding) var(--task-horizontal-padding);
background: var(--task-background-color);
margin-bottom: var(--task-margin-bottom);
width: var(--task-width);
border: solid var(--task-border-width) var(--task-border-color);
border-radius: var(--task-border-radius);
}
#main-container .task-wrapper .task-container .task .username {
font-weight: var(--username-font-weight);
}
#main-container .task-wrapper .task-container .task .user-tasks {
list-style-position: inside;
}
#main-container .task-wrapper .task-container .task .user-tasks .todo {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#main-container .task-wrapper .task-container .task .user-tasks .done {
text-decoration: line-through;
}/*# sourceMappingURL=taskList.css.map */

171
styles/taskList.scss Normal file
View file

@ -0,0 +1,171 @@
:root {
--task-list-width: 400px;
--task-list-height: 300px;
--task-list-background-color: rgba(0, 0, 0, 0.8);
--task-list-border-width: 2px;
--task-list-border-color: white;
--task-list-border-radius: 5px;
--task-list-horizontal-padding: 0px;
--task-list-vertical-padding: 0px;
--header-horizontal-padding: 10px;
--header-vertical-padding: 10px;
--header-background-color: rgba(0, 0, 0, 0.8);
--header-border-width: 2px;
--header-border-color: white;
--header-border-radius: 5px;
--header-font-size: 20px;
--header-font-family: sans-serif;
--header-font-color: white;
--header-font-weight: bold;
--body-background-color: rgba(0, 0, 0, 0.7);
--body-border-width: 5px;
--body-border-color: white;
--body-border-radius: 5px;
--line-height: 1.5;
--username-font-weight: bold;
--task-background-color: rgba(0, 0, 0, 0.7);
--task-font-family: sans-serif;
--task-font-size: 25px;
--task-font-color: white;
--task-border-color: black;
--task-border-width: 0px;
--task-border-radius: 5px;
--task-horizontal-padding: 0px;
--task-vertical-padding: 0px;
--task-margin-bottom: 5px;
--task-horizontal-padding: 5px;
--task-vertical-padding: 5px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: gray;
}
#main-container {
width: var(--task-list-width);
height: var(--task-list-height);
display: flex;
flex-direction: column;
padding: var(--task-list-vertical-padding)
var(--task-list-horizontal-padding);
background-color: var(--task-list-background-color);
border: solid var(--task-list-border-width) var(--task-list-border-color);
border-radius: var(--task-list-border-radius);
.header {
display: flex;
justify-content: space-between;
padding: var(--header-vertical-padding) var(--header-horizontal-padding);
border: solid var(--header-border-width) var(--header-border-color);
border-radius: var(--header-border-radius);
color: var(--header-font-color);
background: var(--header-background-color);
font-family: var(--header-font-family);
font-size: var(--header-font-size);
font-weight: var(--header-font-weight);
.title {
transition: opacity 0.5s ease;
}
.fade {
opacity: 0;
}
}
.task-wrapper {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
background: var(--body-background-color);
color: var(--task-font-color);
font-family: var(--task-font-family);
font-size: var(--task-font-size);
line-height: var(--line-height);
border: var(--body-border-width) solid var(--body-border-color);
border-radius: var(--body-border-radius);
z-index: 1;
.task-container {
position: absolute;
width: inherit;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
margin-left: auto;
margin-right: auto;
.task {
padding: var(--task-vertical-padding)
var(--task-horizontal-padding);
background: var(--task-background-color);
margin-bottom: var(--task-margin-bottom);
width: var(--task-width);
border: solid var(--task-border-width) var(--task-border-color);
border-radius: var(--task-border-radius);
.username {
font-weight: var(--username-font-weight);
}
.user-tasks {
// put list inner
list-style-position: inside;
.todo {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.done {
text-decoration: line-through;
}
}
}
}
}
}