commit dae6c593637e075f28c152eb82fb97d973bc67ea Author: liyunze <50455574+liyunze-coding@users.noreply.github.com> Date: Wed Aug 2 22:16:30 2023 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55136ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/* +styles/taskList.css.map \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..211a20d --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d534834 --- /dev/null +++ b/README.md @@ -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 + +--- + + + +## 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\) diff --git a/auth.js b/auth.js new file mode 100644 index 0000000..f4de491 --- /dev/null +++ b/auth.js @@ -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, + }; +})(); diff --git a/configs.js b/configs.js new file mode 100644 index 0000000..e1a2d4b --- /dev/null +++ b/configs.js @@ -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, + }; +})(); diff --git a/get_token.txt b/get_token.txt new file mode 100644 index 0000000..69978cd --- /dev/null +++ b/get_token.txt @@ -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 diff --git a/images/labels.png b/images/labels.png new file mode 100644 index 0000000..7390580 Binary files /dev/null and b/images/labels.png differ diff --git a/images/preview.png b/images/preview.png new file mode 100644 index 0000000..80960dc Binary files /dev/null and b/images/preview.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..cca9678 --- /dev/null +++ b/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + Task List + + + +
+
+
!taskhelp
+
0/0
+
+
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/scripts/index.js b/scripts/index.js new file mode 100644 index 0000000..dea6911 --- /dev/null +++ b/scripts/index.js @@ -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]); diff --git a/scripts/taskList.js b/scripts/taskList.js new file mode 100644 index 0000000..5af4865 --- /dev/null +++ b/scripts/taskList.js @@ -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(); +})(); diff --git a/styles/taskList.css b/styles/taskList.css new file mode 100644 index 0000000..57d374d --- /dev/null +++ b/styles/taskList.css @@ -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 */ \ No newline at end of file diff --git a/styles/taskList.scss b/styles/taskList.scss new file mode 100644 index 0000000..6012694 --- /dev/null +++ b/styles/taskList.scss @@ -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; + } + } + } + } + } +}