Compare commits

..

7 commits

Author SHA1 Message Date
liyunze
d8df9b2ca1 update no chat version 2024-07-05 21:53:23 +10:00
liyunze
72da262759 version 2.0 2024-05-25 19:02:08 +08:00
liyunze
de75b3cd4c added usernameFontsize and doneTaskFontColor 2023-11-23 21:04:38 +08:00
liyunze
7c9869e967 updated with focus task 2023-11-22 22:14:50 +08:00
liyunze
7fc656680e Removed unnecessary configs 2023-11-14 17:27:48 +08:00
liyunze
52bbde40bd Added local storage event listener 2023-11-12 13:36:12 +08:00
liyunze
efcc8000f8 First commit, might not be stable 2023-11-12 13:24:00 +08:00
28 changed files with 793 additions and 4505 deletions

5
.gitignore vendored
View file

@ -1,4 +1,3 @@
.vscode/*
to-do.md
updates
taskList.css.map
styles/taskList.css.map
styles/taskList.scss

202
README.md
View file

@ -1,3 +1,203 @@
# Multi-Task widget Infinity
# 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)
### 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\)

View file

@ -2,7 +2,7 @@ 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
const oauth = ""; // should be oauth:xxxxxxxxxxxx from the bot account DO NOT INCLUDE oauth:
return {
channel,

View file

@ -2,24 +2,11 @@ const configs = (function () {
"use strict";
const settings = {
replyInChat: true, // true or false
enableLimit: false, // true or false
limit: 10, // integer
automaticDoneIndex: false, // true or false - Automatically assume first unfinished task is complete
taskName: "task", // string (singular)
pointsName: "points", // string
pointsPerTask: 10, // integer
taskSeparator: [";", ","], // array of strings
openQuote: '"', // string
closeQuote: '"', // string
streamer: "RythonDev",
testTasks: false, // true or false
headerGoogleFont: true, // true: use google font, false: use system font
taskGoogleFont: true, // true: use google font, false: use system font
displayTaskCount: true, // true or false
enableTaskMaster: true, // true or false
hideWhenNoTasks: false, // true or false
taskCharacterLimit: 15, // integer
taskCharacterLimitEnabled: false, // true or false
showStreamersTasksOnly: false, // true or false
};
const animation = {
@ -34,6 +21,7 @@ const configs = (function () {
"!undone",
"!botcred",
"!count",
"!points",
"!boardcount",
],
};
@ -54,19 +42,19 @@ const configs = (function () {
headerHorizontalPadding: "10px", // px
headerVerticalPadding: "10px", // px
headerBackgroundColor: "#000", // hex only
headerBackgroundOpacity: 0.9, // 0.0 - 1.0
headerBackgroundColor: "#fff", // hex only
headerBackgroundOpacity: 1, // 0.0 - 1.0
headerBorderWidth: "0px", // px
headerBorderColor: "#ffffff", // hex or name
headerBorderRadius: "10px", // px
headerBorderRadius: "3px", // px
headerFontFamily: "Fredoka", // font name
headerFontSize: "25px", // px
headerFontWeight: "bold", // normal or bold or number
headerFontColor: "#fff", // hex or name
headerFontColor: "#000", // hex or name
// body
bodyBackgroundColor: "#fff", // hex only
@ -76,19 +64,16 @@ const configs = (function () {
bodyBorderColor: "#fff", // hex or name
bodyBorderRadius: "5px", // px
bodyMarginTop: "1px", // px
// task
numberOfLines: 2, // number
lineHeight: 1.5, // number
usernameFontWeight: "bold", // normal or bold or number
usernameColor: "white", // hex or name or "" for twitch user color
usernameColor: "pink", // hex or name or "" for twitch user color
usernameFontSize: "20px", // px
taskWidth: "100%",
taskBackgroundColor: "#000", // hex only
taskBackgroundOpacity: 0.8, // 0.0 - 1.0
taskBackgroundOpacity: 0, // 0.0 - 1.0
taskFontFamily: "Poppins", // font name
@ -117,310 +102,9 @@ const configs = (function () {
focusTaskVerticalPadding: "0px", // px
};
// STYLES
const commands = {
// Add task commands - please add commands in the exact format
addTaskCommands: [
"!taska",
"!taskadd",
"!atask",
"!addtask",
"!task",
"!add",
"!todo",
"!a",
],
// Edit task commands - please add commands in the exact format
editTaskCommands: [
"!taske",
"!taskedit",
"!etask",
"!edittask",
"!edit",
"!e",
],
// Delete task commands - please add commands in the exact format
deleteTaskCommands: [
"!taskdel",
"!taskdelete",
"!deltask",
"!deletetask",
"!taskr",
"!taskremove",
"!rtask",
"!removetask",
"!remove",
"!delete",
"!r",
],
// Finish task commands - please add commands in the exact format
finishTaskCommands: [
"!taskf",
"!taskfinish",
"!ftask",
"!finishtask",
"!taskd",
"!taskdone",
"!donetask",
"!dtask",
"!finish",
"!done",
"!finished",
"!f",
],
// Unfinish task commands - please add commands in the exact format
unfinishTaskCommands: [
"!tasku",
"!taskunfinish",
"!utask",
"!unfinishtask",
"!taskud",
"!taskundone",
"!undonetask",
"!undone",
"!unfinish",
"!unfinished",
"!u",
],
nextTaskCommands: [
"!taskn",
"!tasknext",
"!ntask",
"!nexttask",
"!next",
"!n",
],
logTaskCommands: ["!log", "!tasklog", "!logtask", "!logtasks"],
// focus task commands - please add commands in the exact format
focusTaskCommands: ["!focus", "!taskfocus", "!focustask"],
focusedTaskCommands: ["!focused", "!ongoing", "!checkfocus"],
unfocusTaskCommands: ["!unfocus", "!taskunfocus", "!unfocustask"],
nowTaskCommands: ["!now", "!tasknow", "!nowtask"],
// Check task commands - please add commands in the exact format
checkCommands: [
"!taskc",
"!taskcheck",
"!ctask",
"!checktask",
"!mytask",
"!check",
"!mytasks",
"!c",
],
checkCompletedCommands: [
"!completed",
"!taskcompleted",
"!donetasks",
"!completedtasks",
],
// List tasks commands - please add commands in the exact format
listCommands: ["!taskl", "!tasklist", "!listtasks", "!list"],
// Clear my done commands - please add commands in the exact format
clearMyDoneCommands: ["!clearmydone"],
// check count commands - please add commands in the exact format
checkCountCommands: ["!taskcount", "!count", "!checkcount", "!mycount"],
checkAllCountCommands: [
"!taskallcount",
"!allcount",
"!checkallcount",
"!boardcount",
"!checkboardcount",
"!taskboardcount",
],
checkMyPointsCommands: [
],
// sync points with count commands - please add commands in the exact format
syncCountPointsCommands: [],
addPointsCommands: [],
reducePointsCommands: [],
setUserPointsCommands: [
],
setUserTaskCountCommands: [
"!setusertaskcount",
"!settaskcount",
"!settaskcountuser",
"!setusertask",
],
leaderboardCommands: ["!leaderboard", "!lb", "!top"],
taskMasterCommands: ["!taskmaster", "!taskchampion"],
resetTaskMasterCommands: ["!resettaskmaster"],
// Help commands - please add commands in the exact format
helpCommands: [
"!taskh",
"!taskhelp",
"!htask",
"!helptask",
"!tasks",
"!help",
],
// Admin clear all except streamer - please add commands following the exact format
adminClearNotStreamerCommands: [
"!clearnotstreamer",
"!aclearnotstreamer",
"!adminclearnotstreamer",
"!clearns",
],
// Admin delete - please add commands following the exact format
adminDeleteCommands: ["!taskadel", "!adel", "!adelete", "!admindelete"],
// Admin clear done - please add commands following the exact format
adminClearDoneCommands: [
"!acleardone",
"!admincleardone",
"!cleardone",
],
adminClearTasksCommands: ["!cleartasks"],
adminResetBoardCountCommands: ["!resetboardcount", "!resetallcount"],
adminResetUsersCountCommands: ["!resetuserscount"],
adminClearAllCommands: ["!clearallnoregrets"],
adminSetBoardCountCommands: ["!setboardcount", "!setallcount"],
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/RythonDev",
},
};
const responses = {
// Responses related to task addition
taskAdded: "The {taskName}(s) {task} has been added, {user}!",
noTaskAdded:
"Looks like you already hit the limit of incomplete {taskName}s, {user}",
noTaskContent:
"Try using !task the-{taskName}-you-are-working-on {user}",
duplicateTask:
"Looks like you already have the {taskName} {task} up there {user}!",
taskEdited:
"{taskName} {originalTask} has been edited to {task} successfully, {user}",
noTaskEdit: "Try doing !edit [index] [new task] {user}",
nowTask:
"{taskName} {task} is now the task you are working on, {user}!",
taskLogged: "Task(s) {task} has been logged, {user}!",
taskAlreadyCompleted:
"Looks like you already completed that {taskName} {task} up there {user}!",
// Grouped by task progression responses
taskNext:
"Good job on finishing the task {oldTask}! Now moving onto {newTask}, {user}!",
nextNoContent: "Try using !next the-task-you-want-to-do-next {user}",
taskNextFailed:
"Unable to perform command with multiple incomplete {taskName}s, {user}!",
// Grouped by task deletion responses
clearedMyDone:
"All of your completed {taskName}s have been cleared, {user}!",
taskDeleted:
"{taskName}(s) {task} has been deleted successfully, {user}",
specifyTaskIndex:
"Try specifying the index of the incomplete {taskName}(s) {user}",
clearTasksExceptBroadcaster:
"All {taskName}s have been cleared except for the streamer's, {user}!",
clearedTasks: "All {taskName}s have been cleared, {user}!",
adminDeleteTasks: "All of {mentioned}'s {taskName}s have been deleted",
clearedDone: "All completed {taskName}s have been cleared, {user}!",
clearedAll: "All {taskName}s and points have been cleared, {user}!",
// Grouped by task completion responses
taskFinished:
"Good job on finishing the {taskName}(s) {task}, {user}! You have completed {doneCount} {taskName}(s) so far!", // {pointCount} {pointName} vars available
allTasksFinished:
"Good job on finishing all your {taskName}s, {user}! You have completed {doneCount} {taskName}(s) so far!",
taskUnfinished:
"{taskName}(s) {task} has been unmarked as done, {user}!",
taskAlreadyFinished:
"Looks like you already finished that {taskName} {user}",
// Grouped by task focus responses
clearFocused: "{taskName} has been unfocused, {user}!",
noFocusedTask: "Looks like you don't have a focused {taskName} {user}!",
alreadyFocusedTask:
"Looks like you already have that task set to focus {user}!",
onlyOneFocus: "You can only focus one task at a time {user}!",
specifyFocusTask:
"Try specifying an incomplete {taskName} to focus {user}!",
taskFocused: "Task {task} has been focused, {user}!",
focusedTask: "{user} Focused {taskName}: {task}",
// Grouped by task existence responses
noTask: "Looks like you don't have a task up there {user}",
noTaskA: "Looks like there is no task from that user there {user}",
// Grouped by point-related responses
checkMyPoints: "You have {pointCount} {pointName}, {user}!",
checkUserPoints: "{mentioned} has {pointCount} {pointName}, {user}!",
syncCountPoints: "Points have been synced with count, {user}!",
addPoints: "{mentioned} has been given {pointCount} {pointName}!",
reducePoints: "{mentioned} has lost {pointCount} {pointName}!",
setUserPoints: "{mentioned} now has {pointCount} {pointName}!",
specifyPoints: "Try specifying a number of points {user}",
// Grouped by count-related responses
checkYourCount:
"You have completed {doneCount} {taskName}(s) so far, {user}!",
checkUserCount:
"{mentioned} has completed {doneCount} {taskName}(s) so far, {user}!",
checkAllCount:
"Everyone has completed {doneCount} {taskName}(s) so far, {user}!",
noCountAll: "Looks like no one has completed a {taskName} yet {user}",
leaderboard: "Leaderboard: {leaderboard}",
setUserTaskCount: "{mentioned} now has {taskCount} tasks!",
// Grouped by taskmaster-related responses
taskMaster: "{user} Task Master: {taskMaster} [{taskMasterCount}]",
noTaskMaster: "No task master yet, {user}!",
resetTaskMaster: "Task Master has been reset, {user}!",
// Grouped by permission-related responses
notMod: "Permission denied, {user}; Mods only",
notStreamer: "Permission denied, {user}; Streamer only",
// Help response
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/`,
"chat-task-tic":
"Chat-Task-Tic widget is a free and open source task widget developed by @RythonDev ! https://github.com/liyunze-coding/chat-task-tic-overlay-infinity",
multitask:
"If you want to add the multi-task widget to your stream, you can buy it from ko-fi! https://ko-fi.com/s/94e7e8dc81",
// to edit check task command, go to
// scripts/taskList.js
// function checkTasks(username)
};
return {
settings,
styles,
animation,
commands,
responses,
};
})();

View file

@ -4,33 +4,14 @@
<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="./lib/webfont.js"></script>
<script src="./lib/comfy.min.js"></script>
<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/core/defaultConfigs.js"></script>
<script defer src="./scripts/core/constants.js"></script>
<script defer src="./scripts/core/functions.js"></script>
<script defer src="./scripts/core/db.js"></script>
<script defer src="./scripts/core/utils.js"></script>
<script defer src="./scripts/ui/styles.js"></script>
<script defer src="./scripts/tasks/taskMaster.js"></script>
<script defer src="./scripts/tasks/counts.js"></script>
<script defer src="./scripts/ui/renderer.js"></script>
<script defer src="./scripts/tasks/taskCleanup.js"></script>
<script defer src="./scripts/tasks/taskCore.js"></script>
<script defer src="./scripts/testing/tests.js"></script>
<script defer src="./scripts/chat/processCommand.js"></script>
<script defer src="./scripts/chat/index.js"></script>
<script defer src="./scripts/ui/taskList.js"></script>
<script defer src="scripts/defaultConfigs.js"></script>
<script defer src="scripts/taskList.js"></script>
<link rel="stylesheet" type="text/css" href="./styles/taskList.css" />
<link
rel="stylesheet"
type="text/css"
href="./styles/extraStyles.css"
/>
<link rel="stylesheet" type="text/css" href="styles/taskList.css" />
<title>Task List</title>
</head>
@ -42,52 +23,9 @@
<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 primary"></div>
<div class="task-container secondary"></div>
</div>
</div>
<div
id="oauth-expired-modal"
class="oauth-modal hidden"
role="alertdialog"
aria-labelledby="oauth-modal-title"
aria-modal="true"
>
<div class="oauth-modal__content">
<h2 id="oauth-modal-title">Twitch OAuth token expired</h2>
<p>
The token in <code>auth.js</code> is no longer valid. Chat
commands will not work until you replace it.
</p>
<ol>
<li>
Open
<a
href="https://sarayalth.github.io/TwitchOAuthGenerator/"
target="_blank"
rel="noopener noreferrer"
>Twitch OAuth Generator</a
>
</li>
<li>Click <strong>Set Default Scopes</strong></li>
<li>
Click <strong>Get OAuth Token</strong>, then paste the
token into <code>auth.js</code>
</li>
<li>Reload this browser source</li>
</ol>
</div>
</div>
</body>
</html>

1
lib/comfy.min.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,33 +0,0 @@
/*
* Copyright 2016 Small Batch, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
/* Web Font Loader v1.6.26 - (c) Adobe Systems, Google. License: Apache 2.0 */(function(){function aa(a,b,c){return a.call.apply(a.bind,arguments)}function ba(a,b,c){if(!a)throw Error();if(2<arguments.length){var d=Array.prototype.slice.call(arguments,2);return function(){var c=Array.prototype.slice.call(arguments);Array.prototype.unshift.apply(c,d);return a.apply(b,c)}}return function(){return a.apply(b,arguments)}}function p(a,b,c){p=Function.prototype.bind&&-1!=Function.prototype.bind.toString().indexOf("native code")?aa:ba;return p.apply(null,arguments)}var q=Date.now||function(){return+new Date};function ca(a,b){this.a=a;this.m=b||a;this.c=this.m.document}var da=!!window.FontFace;function t(a,b,c,d){b=a.c.createElement(b);if(c)for(var e in c)c.hasOwnProperty(e)&&("style"==e?b.style.cssText=c[e]:b.setAttribute(e,c[e]));d&&b.appendChild(a.c.createTextNode(d));return b}function u(a,b,c){a=a.c.getElementsByTagName(b)[0];a||(a=document.documentElement);a.insertBefore(c,a.lastChild)}function v(a){a.parentNode&&a.parentNode.removeChild(a)}
function w(a,b,c){b=b||[];c=c||[];for(var d=a.className.split(/\s+/),e=0;e<b.length;e+=1){for(var f=!1,g=0;g<d.length;g+=1)if(b[e]===d[g]){f=!0;break}f||d.push(b[e])}b=[];for(e=0;e<d.length;e+=1){f=!1;for(g=0;g<c.length;g+=1)if(d[e]===c[g]){f=!0;break}f||b.push(d[e])}a.className=b.join(" ").replace(/\s+/g," ").replace(/^\s+|\s+$/,"")}function y(a,b){for(var c=a.className.split(/\s+/),d=0,e=c.length;d<e;d++)if(c[d]==b)return!0;return!1}
function z(a){if("string"===typeof a.f)return a.f;var b=a.m.location.protocol;"about:"==b&&(b=a.a.location.protocol);return"https:"==b?"https:":"http:"}function ea(a){return a.m.location.hostname||a.a.location.hostname}
function A(a,b,c){function d(){k&&e&&f&&(k(g),k=null)}b=t(a,"link",{rel:"stylesheet",href:b,media:"all"});var e=!1,f=!0,g=null,k=c||null;da?(b.onload=function(){e=!0;d()},b.onerror=function(){e=!0;g=Error("Stylesheet failed to load");d()}):setTimeout(function(){e=!0;d()},0);u(a,"head",b)}
function B(a,b,c,d){var e=a.c.getElementsByTagName("head")[0];if(e){var f=t(a,"script",{src:b}),g=!1;f.onload=f.onreadystatechange=function(){g||this.readyState&&"loaded"!=this.readyState&&"complete"!=this.readyState||(g=!0,c&&c(null),f.onload=f.onreadystatechange=null,"HEAD"==f.parentNode.tagName&&e.removeChild(f))};e.appendChild(f);setTimeout(function(){g||(g=!0,c&&c(Error("Script load timeout")))},d||5E3);return f}return null};function C(){this.a=0;this.c=null}function D(a){a.a++;return function(){a.a--;E(a)}}function F(a,b){a.c=b;E(a)}function E(a){0==a.a&&a.c&&(a.c(),a.c=null)};function G(a){this.a=a||"-"}G.prototype.c=function(a){for(var b=[],c=0;c<arguments.length;c++)b.push(arguments[c].replace(/[\W_]+/g,"").toLowerCase());return b.join(this.a)};function H(a,b){this.c=a;this.f=4;this.a="n";var c=(b||"n4").match(/^([nio])([1-9])$/i);c&&(this.a=c[1],this.f=parseInt(c[2],10))}function fa(a){return I(a)+" "+(a.f+"00")+" 300px "+J(a.c)}function J(a){var b=[];a=a.split(/,\s*/);for(var c=0;c<a.length;c++){var d=a[c].replace(/['"]/g,"");-1!=d.indexOf(" ")||/^\d/.test(d)?b.push("'"+d+"'"):b.push(d)}return b.join(",")}function K(a){return a.a+a.f}function I(a){var b="normal";"o"===a.a?b="oblique":"i"===a.a&&(b="italic");return b}
function ga(a){var b=4,c="n",d=null;a&&((d=a.match(/(normal|oblique|italic)/i))&&d[1]&&(c=d[1].substr(0,1).toLowerCase()),(d=a.match(/([1-9]00|normal|bold)/i))&&d[1]&&(/bold/i.test(d[1])?b=7:/[1-9]00/.test(d[1])&&(b=parseInt(d[1].substr(0,1),10))));return c+b};function ha(a,b){this.c=a;this.f=a.m.document.documentElement;this.h=b;this.a=new G("-");this.j=!1!==b.events;this.g=!1!==b.classes}function ia(a){a.g&&w(a.f,[a.a.c("wf","loading")]);L(a,"loading")}function M(a){if(a.g){var b=y(a.f,a.a.c("wf","active")),c=[],d=[a.a.c("wf","loading")];b||c.push(a.a.c("wf","inactive"));w(a.f,c,d)}L(a,"inactive")}function L(a,b,c){if(a.j&&a.h[b])if(c)a.h[b](c.c,K(c));else a.h[b]()};function ja(){this.c={}}function ka(a,b,c){var d=[],e;for(e in b)if(b.hasOwnProperty(e)){var f=a.c[e];f&&d.push(f(b[e],c))}return d};function N(a,b){this.c=a;this.f=b;this.a=t(this.c,"span",{"aria-hidden":"true"},this.f)}function O(a){u(a.c,"body",a.a)}function P(a){return"display:block;position:absolute;top:-9999px;left:-9999px;font-size:300px;width:auto;height:auto;line-height:normal;margin:0;padding:0;font-variant:normal;white-space:nowrap;font-family:"+J(a.c)+";"+("font-style:"+I(a)+";font-weight:"+(a.f+"00")+";")};function Q(a,b,c,d,e,f){this.g=a;this.j=b;this.a=d;this.c=c;this.f=e||3E3;this.h=f||void 0}Q.prototype.start=function(){var a=this.c.m.document,b=this,c=q(),d=new Promise(function(d,e){function k(){q()-c>=b.f?e():a.fonts.load(fa(b.a),b.h).then(function(a){1<=a.length?d():setTimeout(k,25)},function(){e()})}k()}),e=new Promise(function(a,d){setTimeout(d,b.f)});Promise.race([e,d]).then(function(){b.g(b.a)},function(){b.j(b.a)})};function R(a,b,c,d,e,f,g){this.v=a;this.B=b;this.c=c;this.a=d;this.s=g||"BESbswy";this.f={};this.w=e||3E3;this.u=f||null;this.o=this.j=this.h=this.g=null;this.g=new N(this.c,this.s);this.h=new N(this.c,this.s);this.j=new N(this.c,this.s);this.o=new N(this.c,this.s);a=new H(this.a.c+",serif",K(this.a));a=P(a);this.g.a.style.cssText=a;a=new H(this.a.c+",sans-serif",K(this.a));a=P(a);this.h.a.style.cssText=a;a=new H("serif",K(this.a));a=P(a);this.j.a.style.cssText=a;a=new H("sans-serif",K(this.a));a=
P(a);this.o.a.style.cssText=a;O(this.g);O(this.h);O(this.j);O(this.o)}var S={D:"serif",C:"sans-serif"},T=null;function U(){if(null===T){var a=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent);T=!!a&&(536>parseInt(a[1],10)||536===parseInt(a[1],10)&&11>=parseInt(a[2],10))}return T}R.prototype.start=function(){this.f.serif=this.j.a.offsetWidth;this.f["sans-serif"]=this.o.a.offsetWidth;this.A=q();la(this)};
function ma(a,b,c){for(var d in S)if(S.hasOwnProperty(d)&&b===a.f[S[d]]&&c===a.f[S[d]])return!0;return!1}function la(a){var b=a.g.a.offsetWidth,c=a.h.a.offsetWidth,d;(d=b===a.f.serif&&c===a.f["sans-serif"])||(d=U()&&ma(a,b,c));d?q()-a.A>=a.w?U()&&ma(a,b,c)&&(null===a.u||a.u.hasOwnProperty(a.a.c))?V(a,a.v):V(a,a.B):na(a):V(a,a.v)}function na(a){setTimeout(p(function(){la(this)},a),50)}function V(a,b){setTimeout(p(function(){v(this.g.a);v(this.h.a);v(this.j.a);v(this.o.a);b(this.a)},a),0)};function W(a,b,c){this.c=a;this.a=b;this.f=0;this.o=this.j=!1;this.s=c}var X=null;W.prototype.g=function(a){var b=this.a;b.g&&w(b.f,[b.a.c("wf",a.c,K(a).toString(),"active")],[b.a.c("wf",a.c,K(a).toString(),"loading"),b.a.c("wf",a.c,K(a).toString(),"inactive")]);L(b,"fontactive",a);this.o=!0;oa(this)};
W.prototype.h=function(a){var b=this.a;if(b.g){var c=y(b.f,b.a.c("wf",a.c,K(a).toString(),"active")),d=[],e=[b.a.c("wf",a.c,K(a).toString(),"loading")];c||d.push(b.a.c("wf",a.c,K(a).toString(),"inactive"));w(b.f,d,e)}L(b,"fontinactive",a);oa(this)};function oa(a){0==--a.f&&a.j&&(a.o?(a=a.a,a.g&&w(a.f,[a.a.c("wf","active")],[a.a.c("wf","loading"),a.a.c("wf","inactive")]),L(a,"active")):M(a.a))};function pa(a){this.j=a;this.a=new ja;this.h=0;this.f=this.g=!0}pa.prototype.load=function(a){this.c=new ca(this.j,a.context||this.j);this.g=!1!==a.events;this.f=!1!==a.classes;qa(this,new ha(this.c,a),a)};
function ra(a,b,c,d,e){var f=0==--a.h;(a.f||a.g)&&setTimeout(function(){var a=e||null,k=d||null||{};if(0===c.length&&f)M(b.a);else{b.f+=c.length;f&&(b.j=f);var h,m=[];for(h=0;h<c.length;h++){var l=c[h],n=k[l.c],r=b.a,x=l;r.g&&w(r.f,[r.a.c("wf",x.c,K(x).toString(),"loading")]);L(r,"fontloading",x);r=null;null===X&&(X=window.FontFace?(x=/Gecko.*Firefox\/(\d+)/.exec(window.navigator.userAgent))?42<parseInt(x[1],10):!0:!1);X?r=new Q(p(b.g,b),p(b.h,b),b.c,l,b.s,n):r=new R(p(b.g,b),p(b.h,b),b.c,l,b.s,a,
n);m.push(r)}for(h=0;h<m.length;h++)m[h].start()}},0)}function qa(a,b,c){var d=[],e=c.timeout;ia(b);var d=ka(a.a,c,a.c),f=new W(a.c,b,e);a.h=d.length;b=0;for(c=d.length;b<c;b++)d[b].load(function(b,d,c){ra(a,f,b,d,c)})};function sa(a,b){this.c=a;this.a=b}function ta(a,b,c){var d=z(a.c);a=(a.a.api||"fast.fonts.net/jsapi").replace(/^.*http(s?):(\/\/)?/,"");return d+"//"+a+"/"+b+".js"+(c?"?v="+c:"")}
sa.prototype.load=function(a){function b(){if(f["__mti_fntLst"+d]){var c=f["__mti_fntLst"+d](),e=[],h;if(c)for(var m=0;m<c.length;m++){var l=c[m].fontfamily;void 0!=c[m].fontStyle&&void 0!=c[m].fontWeight?(h=c[m].fontStyle+c[m].fontWeight,e.push(new H(l,h))):e.push(new H(l))}a(e)}else setTimeout(function(){b()},50)}var c=this,d=c.a.projectId,e=c.a.version;if(d){var f=c.c.m;B(this.c,ta(c,d,e),function(e){e?a([]):(f["__MonotypeConfiguration__"+d]=function(){return c.a},b())}).id="__MonotypeAPIScript__"+
d}else a([])};function ua(a,b){this.c=a;this.a=b}ua.prototype.load=function(a){var b,c,d=this.a.urls||[],e=this.a.families||[],f=this.a.testStrings||{},g=new C;b=0;for(c=d.length;b<c;b++)A(this.c,d[b],D(g));var k=[];b=0;for(c=e.length;b<c;b++)if(d=e[b].split(":"),d[1])for(var h=d[1].split(","),m=0;m<h.length;m+=1)k.push(new H(d[0],h[m]));else k.push(new H(d[0]));F(g,function(){a(k,f)})};function va(a,b,c){a?this.c=a:this.c=b+wa;this.a=[];this.f=[];this.g=c||""}var wa="//fonts.googleapis.com/css";function xa(a,b){for(var c=b.length,d=0;d<c;d++){var e=b[d].split(":");3==e.length&&a.f.push(e.pop());var f="";2==e.length&&""!=e[1]&&(f=":");a.a.push(e.join(f))}}
function ya(a){if(0==a.a.length)throw Error("No fonts to load!");if(-1!=a.c.indexOf("kit="))return a.c;for(var b=a.a.length,c=[],d=0;d<b;d++)c.push(a.a[d].replace(/ /g,"+"));b=a.c+"?family="+c.join("%7C");0<a.f.length&&(b+="&subset="+a.f.join(","));0<a.g.length&&(b+="&text="+encodeURIComponent(a.g));return b};function za(a){this.f=a;this.a=[];this.c={}}
var Aa={latin:"BESbswy","latin-ext":"\u00e7\u00f6\u00fc\u011f\u015f",cyrillic:"\u0439\u044f\u0416",greek:"\u03b1\u03b2\u03a3",khmer:"\u1780\u1781\u1782",Hanuman:"\u1780\u1781\u1782"},Ba={thin:"1",extralight:"2","extra-light":"2",ultralight:"2","ultra-light":"2",light:"3",regular:"4",book:"4",medium:"5","semi-bold":"6",semibold:"6","demi-bold":"6",demibold:"6",bold:"7","extra-bold":"8",extrabold:"8","ultra-bold":"8",ultrabold:"8",black:"9",heavy:"9",l:"3",r:"4",b:"7"},Ca={i:"i",italic:"i",n:"n",normal:"n"},
Da=/^(thin|(?:(?:extra|ultra)-?)?light|regular|book|medium|(?:(?:semi|demi|extra|ultra)-?)?bold|black|heavy|l|r|b|[1-9]00)?(n|i|normal|italic)?$/;
function Ea(a){for(var b=a.f.length,c=0;c<b;c++){var d=a.f[c].split(":"),e=d[0].replace(/\+/g," "),f=["n4"];if(2<=d.length){var g;var k=d[1];g=[];if(k)for(var k=k.split(","),h=k.length,m=0;m<h;m++){var l;l=k[m];if(l.match(/^[\w-]+$/)){var n=Da.exec(l.toLowerCase());if(null==n)l="";else{l=n[2];l=null==l||""==l?"n":Ca[l];n=n[1];if(null==n||""==n)n="4";else var r=Ba[n],n=r?r:isNaN(n)?"4":n.substr(0,1);l=[l,n].join("")}}else l="";l&&g.push(l)}0<g.length&&(f=g);3==d.length&&(d=d[2],g=[],d=d?d.split(","):
g,0<d.length&&(d=Aa[d[0]])&&(a.c[e]=d))}a.c[e]||(d=Aa[e])&&(a.c[e]=d);for(d=0;d<f.length;d+=1)a.a.push(new H(e,f[d]))}};function Fa(a,b){this.c=a;this.a=b}var Ga={Arimo:!0,Cousine:!0,Tinos:!0};Fa.prototype.load=function(a){var b=new C,c=this.c,d=new va(this.a.api,z(c),this.a.text),e=this.a.families;xa(d,e);var f=new za(e);Ea(f);A(c,ya(d),D(b));F(b,function(){a(f.a,f.c,Ga)})};function Ha(a,b){this.c=a;this.a=b}Ha.prototype.load=function(a){var b=this.a.id,c=this.c.m;b?B(this.c,(this.a.api||"https://use.typekit.net")+"/"+b+".js",function(b){if(b)a([]);else if(c.Typekit&&c.Typekit.config&&c.Typekit.config.fn){b=c.Typekit.config.fn;for(var e=[],f=0;f<b.length;f+=2)for(var g=b[f],k=b[f+1],h=0;h<k.length;h++)e.push(new H(g,k[h]));try{c.Typekit.load({events:!1,classes:!1,async:!0})}catch(m){}a(e)}},2E3):a([])};function Ia(a,b){this.c=a;this.f=b;this.a=[]}Ia.prototype.load=function(a){var b=this.f.id,c=this.c.m,d=this;b?(c.__webfontfontdeckmodule__||(c.__webfontfontdeckmodule__={}),c.__webfontfontdeckmodule__[b]=function(b,c){for(var g=0,k=c.fonts.length;g<k;++g){var h=c.fonts[g];d.a.push(new H(h.name,ga("font-weight:"+h.weight+";font-style:"+h.style)))}a(d.a)},B(this.c,z(this.c)+(this.f.api||"//f.fontdeck.com/s/css/js/")+ea(this.c)+"/"+b+".js",function(b){b&&a([])})):a([])};var Y=new pa(window);Y.a.c.custom=function(a,b){return new ua(b,a)};Y.a.c.fontdeck=function(a,b){return new Ia(b,a)};Y.a.c.monotype=function(a,b){return new sa(b,a)};Y.a.c.typekit=function(a,b){return new Ha(b,a)};Y.a.c.google=function(a,b){return new Fa(b,a)};var Z={load:p(Y.load,Y)};"function"===typeof define&&define.amd?define(function(){return Z}):"undefined"!==typeof module&&module.exports?module.exports=Z:(window.WebFont=Z,window.WebFontConfig&&Y.load(window.WebFontConfig));}());

View file

@ -1,74 +0,0 @@
let params = {};
function respond(template, params, parentId = null) {
if (getSetting("replyInChat")) {
Object.keys(params).forEach((key) => {
console.log(`{${key}}: ${params[key]}`);
template = template.replaceAll(`{${key}}`, params[key]);
});
template = template.charAt(0).toUpperCase() + template.slice(1);
if (!parentId) {
ComfyJS.Say(template);
} else {
ComfyJS.Reply(parentId, template);
}
}
}
function isMod(flags) {
return flags.broadcaster || flags.mod;
}
function isStreamer(flags) {
return flags.broadcaster;
}
ComfyJS.onCommand = (user, command, message, flags, extra) => {
if (!getSetting("listenAcrossSharedChat")) {
// Reject messages from another chat during shared chat if room information
// is present and doesn't match the current chat.
const { roomId, userState } = extra;
const sourceRoomId = userState["source-room-id"];
if (roomId && sourceRoomId && roomId !== sourceRoomId) return;
}
command = `!${command.toLowerCase()}`;
processCommand(user, command, message, flags, extra);
};
const oauth_token = auth.oauth.includes("oauth:")
? auth.oauth
: `oauth:${auth.oauth}`;
const auth_username = auth.username ? auth.username : auth.channel;
function showOAuthExpiredModal() {
const modal = document.getElementById("oauth-expired-modal");
if (modal) modal.classList.remove("hidden");
}
async function validateOAuthToken() {
const bareToken = oauth_token.replace(/^oauth:/i, "");
const response = await fetch("https://id.twitch.tv/oauth2/validate", {
headers: { Authorization: `OAuth ${bareToken}` },
});
return response.ok;
}
async function initChat() {
try {
const valid = await validateOAuthToken();
if (!valid) {
showOAuthExpiredModal();
return;
}
} catch (err) {
console.warn("Could not validate OAuth token; connecting anyway.", err);
}
ComfyJS.Init(auth_username, oauth_token, [auth.channel]);
}
initChat();

View file

@ -1,668 +0,0 @@
const commands = configs.commands;
async function processCommand(user, command, message, flags, extra) {
params = {
user: user,
message: message,
pointName: getSetting("pointsName"),
taskName: getSetting("taskName"),
};
command = command.toLowerCase();
if (getCommand("addTaskCommands").includes(command)) {
let addRequest = await addTask(user, extra.userColor, message);
if (addRequest.status !== 200) {
params.task = `${openQuote}${message}${closeQuote}`;
respond(addRequest.body.error, params, extra.id);
return;
}
let addedResponse = getResponse("taskAdded");
params.task = `${openQuote}${addRequest.body.task}${closeQuote}`;
if (addRequest.body.tasksFailedToAdd !== "") {
addedResponse += ` | Failed to add task(s): "${addRequest.body.tasksFailedToAdd}"`;
}
await respond(addedResponse, params, extra.id);
} else if (getCommand("editTaskCommands").includes(command)) {
let editRequest = await editTask(user, message);
if (editRequest.status !== 200) {
respond(editRequest.body.error, params, extra.id);
return;
}
let originalTask = editRequest.body.originalTask;
let newTask = editRequest.body.newTask;
params.task = `${openQuote}${newTask}${closeQuote}`;
params.originalTask = `${openQuote}${originalTask}${closeQuote}`;
respond(getResponse("taskEdited"), params, extra.id);
} else if (getCommand("deleteTaskCommands").includes(command)) {
let removeRequest = await removeTask(user, message);
if (removeRequest.status !== 200) {
respond(removeRequest.body.error, params, extra.id);
return;
}
let deletedResponse = getResponse("taskDeleted");
let removedTasks = `${openQuote}${removeRequest.body.removedTasks}${closeQuote}`;
let failedTasks = removeRequest.body.failedTasks;
params.task = removedTasks;
if (failedTasks !== "") {
deletedResponse += ` | Failed to delete task(s): ${openQuote}${failedTasks}${closeQuote}`;
}
respond(deletedResponse, params, extra.id);
} else if (getCommand("finishTaskCommands").includes(command)) {
if (message === "all") {
let finishAllRequest = await markAllTasksAsDone(user);
if (finishAllRequest.status !== 200) {
respond(finishAllRequest.body.error, params, extra.id);
return;
}
let finishedAllResponse = getResponse("allTasksFinished");
params.doneCount = await completedTasksCount(user);
finishedAllResponse = finishedAllResponse.replaceAll(
"{pointName}",
getSetting("pointsName")
);
finishedAllResponse = finishedAllResponse.replaceAll(
"{taskName}",
getSetting("taskName")
);
respond(finishedAllResponse, params, extra.id);
return;
}
let finishRequest = await markTaskDone(user, message);
if (finishRequest.status !== 200) {
respond(finishRequest.body.error, params, extra.id);
return;
}
let finishedResponse = getResponse("taskFinished");
params.task = `${openQuote}${finishRequest.body.markedTasks}${closeQuote}`;
params.doneCount = await completedTasksCount(user);
params.pointCount =
finishRequest.body.markedTasksCount * getSetting("pointsPerTask");
if (finishRequest.body.failedTasks !== "") {
finishedResponse += ` | Failed to finish {taskName}(s): "${finishRequest.body.failedTasks}"`;
}
finishedResponse = finishedResponse.replaceAll(
"{pointName}",
getSetting("pointsName")
);
finishedResponse = finishedResponse.replaceAll(
"{taskName}",
getSetting("taskName")
);
respond(finishedResponse, params, extra.id);
} else if (getCommand("unfinishTaskCommands").includes(command)) {
let unfinishRequest = await markTaskUndone(user, message);
if (unfinishRequest.status !== 200) {
respond(unfinishRequest.body.error, params, extra.id);
return;
}
let unfinishedResponse = getResponse("taskUnfinished");
params.task = `${openQuote}${unfinishRequest.body.markedTasks}${closeQuote}`;
if (unfinishRequest.body.failedTasks !== "") {
unfinishedResponse += ` | Failed to unfinish task(s): "${unfinishRequest.body.failedTasks}"`;
}
respond(unfinishedResponse, params, extra.id);
} else if (getCommand("nextTaskCommands").includes(command)) {
let nextRequest = await nextTask(user, message);
if (nextRequest.status !== 200) {
params.task = `${openQuote}${nextRequest.body.task}${closeQuote}`;
await respond(nextRequest.body.error, params, extra.id);
return;
}
let nextResponse = getResponse("taskNext");
params.oldTask = `${openQuote}${nextRequest.body.oldTask}${closeQuote}`;
params.newTask = `${openQuote}${nextRequest.body.newTask}${closeQuote}`;
respond(nextResponse, params, extra.id);
} else if (getCommand("logTaskCommands").includes(command)) {
let logRequest = await logTask(user, extra.userColor, message);
if (logRequest.status !== 200) {
respond(logRequest.body.error, params, extra.id);
return;
}
let loggedResponse = await getResponse("taskLogged");
params.task = `${openQuote}${logRequest.body.task}${closeQuote}`;
if (logRequest.body.tasksFailedToLog !== "") {
loggedResponse += ` | Failed to log task(s): ${openQuote}${logRequest.body.tasksFailedToLog}${closeQuote}`;
}
await respond(loggedResponse, params, extra.id);
} else if (
getCommand("focusTaskCommands").includes(command) ||
getCommand("nowTaskCommands").includes(command)
) {
let focusRequest = await focusTask(user, extra.userColor, message);
if (focusRequest.status !== 200) {
respond(focusRequest.body.error, params, extra.id);
return;
}
let focusedResponse = getResponse("nowTask");
params.task = `${openQuote}${focusRequest.body.focusedTask}${closeQuote}`;
respond(focusedResponse, params, extra.id);
} else if (getCommand("focusedTaskCommands").includes(command)) {
let focusedRequest = await focusedTask(user);
if (focusedRequest.status !== 200) {
respond(focusedRequest.body.error, params, extra.id);
return;
}
let focusedResponse = getResponse("focusedTask");
params.task = `${openQuote}${focusedRequest.body.focusedTask}${closeQuote}`;
await respond(focusedResponse, params, extra.id);
} else if (getCommand("unfocusTaskCommands").includes(command)) {
let unfocusRequest = await unfocusTask(user);
if (unfocusRequest.status !== 200) {
respond(unfocusRequest.body.error, params, extra.id);
return;
}
let unfocusedResponse = getResponse("clearFocused");
respond(unfocusedResponse, params, extra.id);
} else if (
getCommand("checkCommands").includes(command) ||
getCommand("checkCompletedCommands").includes(command)
) {
let completed = getCommand("checkCompletedCommands").includes(command);
if (message === "") {
let checkRequest = await checkTasks(user, completed);
if (checkRequest.status !== 200) {
respond(checkRequest.body.error, params, extra.id);
return;
}
let filteredTasks = checkRequest.body.reply.split(" | ");
for (let i = 0; i < filteredTasks.length; i += 10) {
let tasks = filteredTasks.slice(i, i + 10);
params.tasks = tasks
.join(" | ")
.replaceAll("{taskName}", getSetting("taskName"));
const checkingUser = user;
params.checkingUser = checkingUser;
await respond(`{tasks}`, params, extra.id);
await sleep(200);
}
} else {
let mentioned = message.replace("@", "");
if (mentioned.toLowerCase() === "id") {
await respond(getResponse("noTaskA"), params, extra.id);
return;
}
let checkRequest = await checkTasks(mentioned, completed);
let filteredTasks = checkRequest.body.reply.split(" | ");
if (checkRequest.status !== 200) {
respond(getResponse("noTaskA"), params, extra.id);
return;
}
for (let i = 0; i < filteredTasks.length; i += 10) {
let tasks = filteredTasks.slice(i, i + 10);
params.tasks = tasks
.join(" | ")
.replaceAll("{taskName}", getSetting("taskName"));
const checkingUser = user;
params.checkingUser = checkingUser;
await respond(`{checkingUser} {tasks}`, params, extra.id);
await sleep(200);
}
}
} else if (getCommand("adminDeleteCommands").includes(command)) {
if (!isMod(flags)) {
respond(getResponse("notMod"), params, extra.id);
return;
}
let mentioned = message.replace("@", "");
if (mentioned === "") {
respond(getResponse("specifyUser"), params, extra.id);
return;
}
let clearUserTaskResponse = await clearUserTasks(mentioned);
params.mentioned = mentioned;
if (clearUserTaskResponse.status === 0) {
respond(getResponse("noTaskA"), params, extra.id);
return;
}
respond(getResponse("adminDeleteTasks"), params, extra.id);
return;
} else if (getCommand("adminClearDoneCommands").includes(command)) {
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
await clearAllDoneTasks();
respond(getResponse("clearedDone"), params, extra.id);
return;
} else if (getCommand("adminClearAllCommands").includes(command)) {
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
await clearAll();
respond(getResponse("clearedAll"), params, extra.id);
return;
} else if (getCommand("clearMyDoneCommands").includes(command)) {
let clearOwnDoneResponse = await clearOwnDoneTasks(user);
if (clearOwnDoneResponse.status !== 200) {
respond(getResponse("noTask"), params, extra.id);
return;
}
respond(getResponse("clearedMyDone"), params, extra.id);
} else if (getCommand("adminClearNotStreamerCommands").includes(command)) {
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
await clearMemory();
await clearAllExceptStreamer(auth.channel);
respond(getResponse("clearTasksExceptBroadcaster"), params, extra.id);
} else if (getCommand("adminClearTasksCommands").includes(command)) {
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
await clearMemory();
await clearAllTasks();
respond(getResponse("clearedTasks"), params, extra.id);
} else if (getCommand("listCommands").includes(command)) {
const interval = 100;
if (message === "") {
let listTaskResponse = await listTasks(user);
if (listTaskResponse.status !== 200) {
respond(listTaskResponse.body.error, params, extra.id);
return;
}
for (reply of listTaskResponse.body.replies) {
params.tasks = reply;
await respond("{tasks}", params, extra.id);
await sleep(interval);
}
} else {
let separator = ",";
let args = message.split(" ");
let anotherUser = true;
if (args[0].length === 1) {
separator = args[0];
args.shift();
} else if (args[1] && args[1].length === 1) {
separator = args[1];
args.splice(1, 1);
} else {
}
let mentioned = args.join(" ").replace("@", "");
if (args.length === 0) {
mentioned = user;
anotherUser = false;
}
if (mentioned.toLowerCase() === "id") {
await respond(getResponse("noTaskA"), params, extra.id);
return;
}
let listTaskResponse = await listTasks(mentioned, separator);
if (listTaskResponse.status !== 200 && anotherUser) {
respond(getResponse("noTaskA"), params, extra.id);
return;
} else if (listTaskResponse.status !== 200 && !anotherUser) {
respond(getResponse("noTask"), params, extra.id);
return;
}
for (reply of listTaskResponse.body.replies) {
params.tasks = reply;
await respond("{tasks}", params, extra.id);
await sleep(interval);
}
}
} else if (getCommand("checkCountCommands").includes(command)) {
if (message === "") {
let count = await completedTasksCount(user);
params.doneCount = count;
return respond(getResponse("checkYourCount"), params, extra.id);
} else {
let mentioned = message.replace("@", "");
let count = await completedTasksCount(mentioned);
params.doneCount = count;
params.mentioned = mentioned;
return respond(getResponse("checkUserCount"), params, extra.id);
}
} else if (getCommand("checkAllCountCommands").includes(command)) {
let count = await getBoardTotalTaskCount();
if (count === 0) {
respond(getResponse("noCountAll"), params, extra.id);
return;
}
params.doneCount = count;
respond(getResponse("checkAllCount"), params, extra.id);
} else if (getCommand("checkMyPointsCommands").includes(command)) {
if (message === "") {
let points = await getUserPoints(user);
params.pointCount = points;
return respond(getResponse("checkMyPoints"), params, extra.id);
} else {
let mentioned = message.replace("@", "");
let points = await getUserPoints(mentioned);
params.pointCount = points;
params.mentioned = mentioned;
return respond(getResponse("checkUserPoints"), params, extra.id);
}
} else if (getCommand("syncCountPointsCommands").includes(command)) {
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
await syncPointsToCount();
respond(getResponse("syncCountPoints"), params, extra.id);
} else if (getCommand("addPointsCommands").includes(command)) {
if (!isMod(flags)) {
respond(getResponse("notMod"), params, extra.id);
return;
}
let mentioned = message.split(" ")[0].replace("@", "");
if (mentioned === "") {
respond(getResponse("specifyUser"), params, extra.id);
return;
}
let points = message.split(" ")[1];
if (points === undefined) {
respond(getResponse("specifyPoints"), params, extra.id);
return;
}
if (!/^\d+$/.test(points)) {
respond(getResponse("invalidNumber"), params, extra.id);
return;
}
points = parseInt(points);
await addPoints(mentioned, points);
params.mentioned = mentioned;
params.pointCount = points;
respond(getResponse("addPoints"), params, extra.id);
} else if (getCommand("reducePointsCommands").includes(command)) {
if (!isMod(flags)) {
respond(getResponse("notMod"), params, extra.id);
return;
}
let mentioned = message.split(" ")[0].replace("@", "");
if (mentioned === "") {
respond(getResponse("specifyUser"), params, extra.id);
return;
}
let points = message.split(" ")[1];
if (points == undefined) {
respond(getResponse("specifyPoints"), params, extra.id);
return;
}
if (!/^\d+$/.test(points)) {
respond(getResponse("invalidNumber"), params, extra.id);
return;
}
await reducePoints(mentioned, points);
params.mentioned = mentioned;
params.pointCount = points;
respond(getResponse("reducePoints"), params, extra.id);
} else if (getCommand("setUserPointsCommands").includes(command)) {
if (!isMod(flags)) {
respond(getResponse("notMod"), params, extra.id);
return;
}
let mentioned = message.split(" ")[0].replace("@", "");
if (mentioned === "") {
respond(getResponse("specifyUser"), params, extra.id);
return;
}
let points = message.split(" ")[1];
if (points === undefined) {
respond(getResponse("specifyPoints"), params, extra.id);
return;
}
if (!/^\d+$/.test(points)) {
respond(getResponse("invalidNumber"), params, extra.id);
return;
}
points = parseInt(points);
await setUserPoints(mentioned, points);
params.mentioned = mentioned;
params.pointCount = points;
respond(getResponse("setUserPoints"), params, extra.id);
} else if (getCommand("setUserTaskCountCommands").includes(command)) {
if (!isMod(flags)) {
respond(getResponse("notMod"), params, extra.id);
return;
}
let mentioned = message.split(" ")[0].replace("@", "");
if (mentioned === "") {
respond(getResponse("specifyUser"), params, extra.id);
return;
}
let count = message.split(" ")[1];
if (count === undefined) {
respond(getResponse("specifyCount"), params, extra.id);
return;
}
if (!/^\d+$/.test(count)) {
respond(getResponse("invalidNumber"), params, extra.id);
return;
}
let setUserTaskCountResponse = await setUserTaskCount(mentioned, count);
if (setUserTaskCountResponse.status !== 200) {
respond(setUserTaskCountResponse.body.message, params, extra.id);
return;
}
params.mentioned = mentioned;
params.taskCount = count;
respond(setUserTaskCountResponse.body.message, params, extra.id);
} else if (getCommand("leaderboardCommands").includes(command)) {
let leaderboardResponse = await leaderboardTaskCount(5);
if (leaderboardResponse.status !== 200) {
respond(leaderboardResponse.body.error, params, extra.id);
return;
}
let leaderboard = leaderboardResponse.body.leaderboard;
await respond(leaderboard, params, extra.id);
} else if (getCommand("adminSetBoardCountCommands").includes(command)) {
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
if (!/^\d+$/.test(message)) {
respond(getResponse("invalidNumber"), params, extra.id);
return;
}
params.count = message;
await setTotalCompleteCount(message);
respond(getResponse("setBoardCount"), params, extra.id);
} else if (getCommand("adminResetBoardCountCommands").includes(command)) {
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
await resetBoardCount();
respond(getResponse("clearedBoardCount"), params, extra.id);
} else if (getCommand("adminResetUsersCountCommands").includes(command)) {
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
await resetUsersCount();
respond(getResponse("clearedUsersCount"), params, extra.id);
} else if (getCommand("taskMasterCommands").includes(command)) {
if (!getSetting("enableTaskMaster")) {
return;
}
let champion = await getTaskMasterChampion();
if (champion === null) {
respond(getResponse("noTaskMaster"), params, extra.id);
return;
}
params.taskMaster = champion.username;
params.taskMasterCount = champion.count;
respond(getResponse("taskMaster"), params, extra.id);
} else if (getCommand("resetTaskMasterCommands").includes(command)) {
if (!getSetting("enableTaskMaster")) {
return;
}
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
await resetTaskMaster();
respond(getResponse("resetTaskMaster"), params, extra.id);
} else if (getCommand("helpCommands").includes(command)) {
respond(getResponse("help"), params, extra.id);
} else if (command === "!clearlocalstorage") {
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
clearLocalStorage();
respond(getResponse("clearLocalStorage"), params, extra.id);
} else if (["!transferdata"].includes(command)) {
if (!isStreamer(flags)) {
respond(getResponse("notStreamer"), params, extra.id);
return;
}
await transferLocalStorageToIndexedDB();
} else if (commands.additionalCommands[command]) {
respond(commands.additionalCommands[command], params, extra.id);
} else {
// command not found
}
}

View file

@ -1,38 +0,0 @@
const STATUS = Object.freeze({
ERROR: 0,
INVALID_INPUT: 1,
ALREADY_COMPLETED: 2,
SUCCESS: 200,
});
const DB_CONFIG = Object.freeze({
NAME: "tasksDB",
VERSION: 1,
STORE: "tasks",
KEY_TASKS: "tasks",
KEY_COUNTS: "counts",
KEY_TASKMASTER: "taskmaster",
});
const PAGINATION = Object.freeze({
SIZE: 10,
SLEEP_MS: 200,
});
const ANIMATION = Object.freeze({
TITLE_FADE_IN_MS: 500,
TITLE_FADE_OUT_MS: 100,
TITLE_CYCLE_INTERVAL_MS: 8000,
});
const TEST_CONFIG = Object.freeze({
RANDOM_TASK_MAX: 10,
TASK_DELAY_MS: 1000,
USER_COUNT: 7,
RANDOM_TASK_COUNT_MIN: 1,
RANDOM_TASK_COUNT_MAX: 5,
});
const RESERVED_KEYS = Object.freeze({
ID: "id",
});

View file

@ -1,165 +0,0 @@
/*
DB structure:
- tasks
- [username]:
- todos (array)
- [{text, done, focus}, {text, done, focus}]
- userColor
- counts
- users
- [username]:
- completeCount
- points
- totalCompleteCount
- taskmaster
- users
- [username]:
- taskMasterCompleteCount
- startDate
*/
let taskListMemory = {
doneTaskCount: 0,
totalTaskCount: 0,
};
const DBHandler = {
db: null,
open: function () {
return new Promise((resolve, reject) => {
let request = indexedDB.open("tasksDB", 1);
request.onupgradeneeded = function (e) {
let db = e.target.result;
if (!db.objectStoreNames.contains("tasks")) {
db.createObjectStore("tasks", { autoIncrement: true });
}
};
request.onsuccess = function (e) {
DBHandler.db = e.target.result;
resolve();
};
request.onerror = function (e) {
console.log("Error opening db", e);
reject(e);
};
});
},
get: function (key) {
return new Promise((resolve, reject) => {
let transaction = DBHandler.db.transaction(["tasks"], "readonly");
let store = transaction.objectStore("tasks");
let request = store.get(key);
request.onsuccess = function (e) {
resolve(e.target.result);
};
request.onerror = function (e) {
reject("Error getting item", e);
};
});
},
set: function (key, value) {
return new Promise((resolve, reject) => {
let transaction = DBHandler.db.transaction(["tasks"], "readwrite");
let store = transaction.objectStore("tasks");
let request = store.put(value, key);
request.onsuccess = function (e) {
resolve(e.target.result);
};
request.onerror = function (e) {
reject("Error setting item", e);
};
});
},
remove: function (key) {
return new Promise((resolve, reject) => {
let transaction = DBHandler.db.transaction(["tasks"], "readwrite");
let store = transaction.objectStore("tasks");
let request = store.delete(key);
request.onsuccess = function (e) {
resolve(e.target.result);
};
request.onerror = function (e) {
reject("Error removing item", e);
};
});
},
clear: function () {
return new Promise((resolve, reject) => {
let transaction = DBHandler.db.transaction(["tasks"], "readwrite");
let store = transaction.objectStore("tasks");
let request = store.clear();
request.onsuccess = function (e) {
resolve(e.target.result);
};
request.onerror = function (e) {
reject("Error clearing store", e);
};
});
},
};
async function transferLocalStorageToIndexedDB() {
let keys = Object.keys(localStorage);
for (let key of keys) {
let value = JSON.parse(localStorage.getItem(key));
await DBHandler.set(key, value);
}
console.log("Transfer complete");
await renderTaskListToDOM();
}
function clearLocalStorage() {
localStorage.removeItem("tasks");
localStorage.removeItem("counts");
}
async function resetDB() {
await DBHandler.clear();
await setupDB();
}
async function setupDB() {
const keys = ["tasks", "counts", "taskmaster"];
const defaultValues = [
{},
{ users: {} },
{
users: {},
startDate: new Date(),
taskMasterCompleteCount: 0,
},
];
for (let i = 0; i < keys.length; i++) {
let value = await DBHandler.get(keys[i]);
if (!value) {
value = defaultValues[i];
}
await DBHandler.set(keys[i], value);
}
return;
}
function clearMemory() {
taskListMemory = {
doneTaskCount: 0,
totalTaskCount: 0,
};
}

View file

@ -1,436 +0,0 @@
const defaultConfigs = (function () {
"use strict";
const settings = {
listenAcrossSharedChat: false,
replyInChat: true, // true or false
enableLimit: false, // true or false
limit: 10, // integer
automaticDoneIndex: false, // true or false - Automatically assume first unfinished task is complete
taskName: "task", // string (singular)
pointsName: "points", // string
pointsPerTask: 10, // integer
taskSeparator: [";", ","], // array of strings
openQuote: '"', // string
closeQuote: '"', // string
testTasks: false, // true or false
headerGoogleFont: true, // true: use google font, false: use system font
taskGoogleFont: true, // true: use google font, false: use system font
displayTaskCount: true, // true or false
enableTaskMaster: true, // true or false
hideWhenNoTasks: false, // true or false
taskCharacterLimit: 15, // integer
taskCharacterLimitEnabled: false, // true or false
};
const animation = {
scrollSpeed: 40, // milliseconds
gapBetweenScrolls: 0, // px
titles: [
"!taskhelp",
"!task",
"!edit",
"!remove",
"!done",
"!undone",
"!botcred",
"!count",
"!points",
"!boardcount",
],
};
const styles = {
// task list
taskListBackgroundColor: "#000000", // hex only
taskListBackgroundOpacity: 0, // 0.0 - 1.0
taskListBorderWidth: "0px", // px
taskListBorderColor: "#ffffff", // hex or name
taskListBorderRadius: "0px", // px
taskListHorizontalPadding: "0px", // px
taskListVerticalPadding: "0px", // px
// header
headerHorizontalPadding: "10px", // px
headerVerticalPadding: "10px", // px
headerBackgroundColor: "#000", // hex only
headerBackgroundOpacity: 0.9, // 0.0 - 1.0
headerBorderWidth: "0px", // px
headerBorderColor: "#ffffff", // hex or name
headerBorderRadius: "10px", // px
headerFontFamily: "Fredoka", // font name
headerFontSize: "25px", // px
headerFontWeight: "bold", // normal or bold or number
headerFontColor: "#fff", // hex or name
// body
bodyBackgroundColor: "#fff", // hex only
bodyBackgroundOpacity: 0, // 0.0 - 1.0
bodyBorderWidth: "0px", // px
bodyBorderColor: "#fff", // hex or name
bodyBorderRadius: "5px", // px
bodyMarginTop: "1px", // px
// task
numberOfLines: 2, // number
lineHeight: 1.5, // number
usernameFontWeight: "bold", // normal or bold or number
usernameColor: "white", // hex or name or "" for twitch user color
usernameFontSize: "20px", // px
taskWidth: "100%",
taskBackgroundColor: "#000", // hex only
taskBackgroundOpacity: 0.8, // 0.0 - 1.0
taskFontFamily: "Poppins", // font name
taskFontSize: "20px", // px
taskFontColor: "#fff", // hex or name
taskBorderColor: "#fff", // hex or name
taskBorderWidth: "0px", // px
taskBorderRadius: "10px", // px
taskMarginBottom: "10px", // px
taskHorizontalPadding: "20px", // px
taskVerticalPadding: "10px", // px
// done task
doneTaskFontColor: "#bbb", // hex or name
// focus task
focusTaskBackgroundColor: "#fff", // hex only
focusTaskBackgroundOpacity: 1, // 0.0 - 1.0
focusTaskBorderRadius: "5px", // px
focusTaskFontColor: "#000", // hex or name
focusTaskHorizontalPadding: "7px", // px
focusTaskVerticalPadding: "0px", // px
};
// STYLES
const commands = {
// Add task commands - please add commands in the exact format
addTaskCommands: [
"!taska",
"!taskadd",
"!atask",
"!addtask",
"!task",
"!add",
"!todo",
"!a",
],
// Edit task commands - please add commands in the exact format
editTaskCommands: [
"!taske",
"!taskedit",
"!etask",
"!edittask",
"!edit",
"!e",
],
// Delete task commands - please add commands in the exact format
deleteTaskCommands: [
"!taskdel",
"!taskdelete",
"!deltask",
"!deletetask",
"!taskr",
"!taskremove",
"!rtask",
"!removetask",
"!remove",
"!delete",
"!r",
],
// Finish task commands - please add commands in the exact format
finishTaskCommands: [
"!taskf",
"!taskfinish",
"!ftask",
"!finishtask",
"!taskd",
"!taskdone",
"!donetask",
"!dtask",
"!finish",
"!done",
"!finished",
"!f",
],
// Unfinish task commands - please add commands in the exact format
unfinishTaskCommands: [
"!tasku",
"!taskunfinish",
"!utask",
"!unfinishtask",
"!taskud",
"!taskundone",
"!undonetask",
"!undone",
"!unfinish",
"!unfinished",
"!u",
],
nextTaskCommands: [
"!taskn",
"!tasknext",
"!ntask",
"!nexttask",
"!next",
"!n",
],
logTaskCommands: ["!log", "!tasklog", "!logtask", "!logtasks"],
// focus task commands - please add commands in the exact format
focusTaskCommands: ["!focus", "!taskfocus", "!focustask"],
focusedTaskCommands: ["!focused", "!ongoing", "!checkfocus"],
unfocusTaskCommands: ["!unfocus", "!taskunfocus", "!unfocustask"],
nowTaskCommands: ["!now", "!tasknow", "!nowtask"],
// Check task commands - please add commands in the exact format
checkCommands: [
"!taskc",
"!taskcheck",
"!ctask",
"!checktask",
"!mytask",
"!check",
"!mytasks",
"!c",
],
checkCompletedCommands: [
"!completed",
"!taskcompleted",
"!donetasks",
"!completedtasks",
],
// List tasks commands - please add commands in the exact format
listCommands: ["!taskl", "!tasklist", "!listtasks", "!list"],
// Clear my done commands - please add commands in the exact format
clearMyDoneCommands: ["!clearmydone"],
// check count commands - please add commands in the exact format
checkCountCommands: ["!taskcount", "!count", "!checkcount", "!mycount"],
checkAllCountCommands: [
"!taskallcount",
"!allcount",
"!checkallcount",
"!boardcount",
"!checkboardcount",
"!taskboardcount",
],
checkMyPointsCommands: [
"!taskpoints",
"!points",
"!mypoints",
"!checkpoints",
],
// sync points with count commands - please add commands in the exact format
syncCountPointsCommands: ["!syncpoints", "!synccount"],
addPointsCommands: ["!addpoints", "!givepoints", "!adduserpoints"],
reducePointsCommands: ["!reducepoints", "!takepoints"],
setUserPointsCommands: [
"!setuserpoints",
"!setpoints",
"!setpointsuser",
"!setuserpoint",
],
setUserTaskCountCommands: [
"!setusertaskcount",
"!settaskcount",
"!settaskcountuser",
"!setusertask",
],
leaderboardCommands: ["!leaderboard", "!lb", "!top", "!toppoints"],
taskMasterCommands: ["!taskmaster", "!taskchampion"],
resetTaskMasterCommands: ["!resettaskmaster"],
// Help commands - please add commands in the exact format
helpCommands: [
"!taskh",
"!taskhelp",
"!htask",
"!helptask",
"!tasks",
"!help",
],
// Admin clear all except streamer - please add commands following the exact format
adminClearNotStreamerCommands: [
"!clearnotstreamer",
"!aclearnotstreamer",
"!adminclearnotstreamer",
"!clearns",
],
// Admin delete - please add commands following the exact format
adminDeleteCommands: ["!taskadel", "!adel", "!adelete", "!admindelete"],
// Admin clear done - please add commands following the exact format
adminClearDoneCommands: [
"!acleardone",
"!admincleardone",
"!cleardone",
],
adminClearTasksCommands: ["!cleartasks"],
adminResetBoardCountCommands: ["!resetboardcount", "!resetallcount"],
adminResetUsersCountCommands: ["!resetuserscount"],
adminClearAllCommands: ["!clearallnoregrets"],
adminSetBoardCountCommands: ["!setboardcount", "!setallcount"],
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/RythonDev",
},
};
const responses = {
// Responses related to task addition
taskAdded: "The {taskName}(s) {task} has been added, {user}!",
noTaskAdded:
"Looks like you already hit the limit of incomplete {taskName}s, {user}",
noTaskContent:
"Try using !task the-{taskName}-you-are-working-on {user}",
duplicateTask:
"Looks like you already have the {taskName} {task} up there {user}!",
taskEdited:
"{taskName} {originalTask} has been edited to {task} successfully, {user}",
noTaskEdit: "Try doing !edit [index] [new task] {user}",
nowTask:
"{taskName} {task} is now the task you are working on, {user}!",
taskLogged: "Task(s) {task} has been logged, {user}!",
taskAlreadyCompleted:
"Looks like you already completed that {taskName} {task} up there {user}!",
// Grouped by task progression responses
taskNext:
"Good job on finishing the task {oldTask}! Now moving onto {newTask}, {user}!",
nextNoContent: "Try using !next the-task-you-want-to-do-next {user}",
taskNextFailed:
"Unable to perform command with multiple incomplete {taskName}s, {user}!",
// Grouped by task deletion responses
clearedMyDone:
"All of your completed {taskName}s have been cleared, {user}!",
taskDeleted:
"{taskName}(s) {task} has been deleted successfully, {user}",
specifyTaskIndex:
"Try specifying the index of the incomplete {taskName}(s) {user}",
clearTasksExceptBroadcaster:
"All {taskName}s have been cleared except for the streamer's, {user}!",
clearedTasks: "All {taskName}s have been cleared, {user}!",
adminDeleteTasks: "All of {mentioned}'s {taskName}s have been deleted",
clearedDone: "All completed {taskName}s have been cleared, {user}!",
clearedAll: "All {taskName}s and points have been cleared, {user}!",
// Grouped by task completion responses
taskFinished:
"Good job on finishing the {taskName}(s) {task}, {user}! You have earned {pointCount} {pointName} and completed {doneCount} {taskName}(s) so far!",
allTasksFinished:
"Good job on finishing all your {taskName}s, {user}! You have completed {doneCount} {taskName}(s) so far!",
taskUnfinished:
"{taskName}(s) {task} has been unmarked as done, {user}!",
taskAlreadyFinished:
"Looks like you already finished that {taskName} {user}",
// Grouped by task focus responses
clearFocused: "{taskName} has been unfocused, {user}!",
noFocusedTask: "Looks like you don't have a focused {taskName} {user}!",
alreadyFocusedTask:
"Looks like you already have that task set to focus {user}!",
onlyOneFocus: "You can only focus one task at a time {user}!",
specifyFocusTask:
"Try specifying an incomplete {taskName} to focus {user}!",
taskFocused: "Task {task} has been focused, {user}!",
focusedTask: "{user} Focused {taskName}: {task}",
// Grouped by task existence responses
noTask: "Looks like you don't have a task up there {user}",
noTaskA: "Looks like there is no task from that user there {user}",
// Grouped by point-related responses
checkMyPoints: "You have {pointCount} {pointName}, {user}!",
checkUserPoints: "{mentioned} has {pointCount} {pointName}, {user}!",
syncCountPoints: "Points have been synced with count, {user}!",
addPoints: "{mentioned} has been given {pointCount} {pointName}!",
reducePoints: "{mentioned} has lost {pointCount} {pointName}!",
setUserPoints: "{mentioned} now has {pointCount} {pointName}!",
specifyPoints: "Try specifying a number of points {user}",
// Grouped by count-related responses
checkYourCount:
"You have completed {doneCount} {taskName}(s) so far, {user}!",
checkUserCount:
"{mentioned} has completed {doneCount} {taskName}(s) so far, {user}!",
checkAllCount:
"Everyone has completed {doneCount} {taskName}(s) so far, {user}!",
noCountAll: "Looks like no one has completed a {taskName} yet {user}",
leaderboard: "Leaderboard: {leaderboard}",
setUserTaskCount: "{mentioned} now has {taskCount} tasks!",
// Grouped by taskmaster-related responses
taskMaster: "{user} Task Master: {taskMaster} [{taskMasterCount}]",
noTaskMaster: "No task master yet, {user}!",
resetTaskMaster: "Task Master has been reset, {user}!",
// Grouped by permission-related responses
notMod: "Permission denied, {user}; Mods only",
notStreamer: "Permission denied, {user}; Streamer only",
// Help response
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/`,
"chat-task-tic":
"Chat-Task-Tic widget is a free and open source task widget developed by @RythonDev ! https://github.com/liyunze-coding/chat-task-tic-overlay-infinity",
multitask:
"If you want to add the multi-task widget to your stream, you can buy it from ko-fi! https://ko-fi.com/s/94e7e8dc81",
// to edit check task command, go to
// scripts/taskList.js
// function checkTasks(username)
};
return {
settings,
styles,
animation,
commands,
responses,
};
})();

View file

@ -1,18 +0,0 @@
function getResponse(responseName) {
return (
configs.responses[responseName] ??
defaultConfigs.responses[responseName]
);
}
function getCommand(commandName) {
return (
configs.commands[commandName] ?? defaultConfigs.commands[commandName]
);
}
function getSetting(settingName) {
return (
configs.settings[settingName] ?? defaultConfigs.settings[settingName]
);
}

View file

@ -1,64 +0,0 @@
function isInt(value) {
return /^\d+$/.test(String(value).trim());
}
function createErrorResponse(errorMessage, errorType, status = 0) {
return {
status: status,
body: {
"error message": errorMessage,
error: errorType,
},
};
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Converts a camelCase string to a CSS variable format.
*
* @param {string} name - The camelCase string to convert.
* @returns {string} The converted string in CSS variable format.
*/
function convertToCSSVar(name) {
let cssVar = name.replace(/([A-Z])/g, "-$1").toLowerCase();
return `--${cssVar}`;
}
/**
* Converts a hexadecimal color value to its RGB equivalent.
*
* @param {string} hex - The hexadecimal color value. Can be 3 or 6 digits, with or without a leading '#'.
* @returns {string} The RGB color value as a string in the format 'r, g, b'.
*/
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];
}
// integer value of rgb
r = +r;
g = +g;
b = +b;
return `${r}, ${g}, ${b}`;
}

110
scripts/defaultConfigs.js Normal file
View file

@ -0,0 +1,110 @@
const defaultConfigs = (function () {
"use strict";
const settings = {
streamer: "RythonDev",
testTasks: false, // true or false
headerGoogleFont: true, // true: use google font, false: use system font
taskGoogleFont: true, // true: use google font, false: use system font
showStreamersTasksOnly: true, // true or false
};
const animation = {
scrollSpeed: 40, // milliseconds
gapBetweenScrolls: 0, // px
titles: [
"!taskhelp",
"!task",
"!edit",
"!remove",
"!done",
"!undone",
"!botcred",
"!count",
"!points",
"!boardcount",
],
};
const styles = {
// task list
taskListBackgroundColor: "#000000", // hex only
taskListBackgroundOpacity: 0, // 0.0 - 1.0
taskListBorderWidth: "0px", // px
taskListBorderColor: "#ffffff", // hex or name
taskListBorderRadius: "0px", // px
taskListHorizontalPadding: "0px", // px
taskListVerticalPadding: "0px", // px
// header
headerHorizontalPadding: "10px", // px
headerVerticalPadding: "10px", // px
headerBackgroundColor: "#fff", // hex only
headerBackgroundOpacity: 1, // 0.0 - 1.0
headerBorderWidth: "0px", // px
headerBorderColor: "#ffffff", // hex or name
headerBorderRadius: "3px", // px
headerFontFamily: "Fredoka", // font name
headerFontSize: "25px", // px
headerFontWeight: "bold", // normal or bold or number
headerFontColor: "#000", // hex or name
// body
bodyBackgroundColor: "#fff", // hex only
bodyBackgroundOpacity: 0, // 0.0 - 1.0
bodyBorderWidth: "0px", // px
bodyBorderColor: "#fff", // hex or name
bodyBorderRadius: "5px", // px
// task
lineHeight: 1.5, // number
usernameFontWeight: "bold", // normal or bold or number
usernameColor: "pink", // hex or name or "" for twitch user color
usernameFontSize: "20px", // px
taskWidth: "100%",
taskBackgroundColor: "#000", // hex only
taskBackgroundOpacity: 0, // 0.0 - 1.0
taskFontFamily: "Poppins", // font name
taskFontSize: "20px", // px
taskFontColor: "#fff", // hex or name
taskBorderColor: "#fff", // hex or name
taskBorderWidth: "0px", // px
taskBorderRadius: "10px", // px
taskMarginBottom: "10px", // px
taskHorizontalPadding: "20px", // px
taskVerticalPadding: "10px", // px
// done task
doneTaskFontColor: "#bbb", // hex or name
// focus task
focusTaskBackgroundColor: "#fff", // hex only
focusTaskBackgroundOpacity: 1, // 0.0 - 1.0
focusTaskBorderRadius: "5px", // px
focusTaskFontColor: "#000", // hex or name
focusTaskHorizontalPadding: "7px", // px
focusTaskVerticalPadding: "0px", // px
};
return {
settings,
styles,
animation,
};
})();

459
scripts/taskList.js Normal file
View file

@ -0,0 +1,459 @@
/*
DB structure:
- tasks
- [username]:
- todos (array)
- [{text, done, focus}, {text, done, focus}]
- userColor
- counts
- users
- [username]:
- completeCount
- points
- totalCompleteCount
- taskmaster
- users
- [username]:
- taskMasterCompleteCount
- startDate
*/
var taskListMemory = {
doneTaskCount: 0,
totalTaskCount: 0,
};
function getSetting(setting) {
return configs.settings[setting] ?? defaultConfigs.settings[setting];
}
const DBHandler = {
db: null,
open: function () {
return new Promise((resolve, reject) => {
let request = indexedDB.open("tasksDB", 1);
request.onupgradeneeded = function (e) {
let db = e.target.result;
if (!db.objectStoreNames.contains("tasks")) {
db.createObjectStore("tasks", { autoIncrement: true });
}
};
request.onsuccess = function (e) {
DBHandler.db = e.target.result;
resolve();
};
request.onerror = function (e) {
console.log("Error opening db", e);
reject(e);
};
});
},
get: function (key) {
return new Promise((resolve, reject) => {
let transaction = DBHandler.db.transaction(["tasks"], "readonly");
let store = transaction.objectStore("tasks");
let request = store.get(key);
request.onsuccess = function (e) {
resolve(e.target.result);
};
request.onerror = function (e) {
reject("Error getting item", e);
};
});
},
set: function (key, value) {
return new Promise((resolve, reject) => {
let transaction = DBHandler.db.transaction(["tasks"], "readwrite");
let store = transaction.objectStore("tasks");
let request = store.put(value, key);
request.onsuccess = function (e) {
resolve(e.target.result);
};
request.onerror = function (e) {
reject("Error setting item", e);
};
});
},
remove: function (key) {
return new Promise((resolve, reject) => {
let transaction = DBHandler.db.transaction(["tasks"], "readwrite");
let store = transaction.objectStore("tasks");
let request = store.delete(key);
request.onsuccess = function (e) {
resolve(e.target.result);
};
request.onerror = function (e) {
reject("Error removing item", e);
};
});
},
clear: function () {
return new Promise((resolve, reject) => {
let transaction = DBHandler.db.transaction(["tasks"], "readwrite");
let store = transaction.objectStore("tasks");
let request = store.clear();
request.onsuccess = function (e) {
resolve(e.target.result);
};
request.onerror = function (e) {
reject("Error clearing store", e);
};
});
},
};
const settings = configs.settings;
const styles = configs.styles;
const scrollSpeed = configs.animation.scrollSpeed;
let scrolling = false;
let primaryAnimation, secondaryAnimation;
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);
}
async function renderTaskListToDOM() {
const tasks = await DBHandler.get("tasks");
const taskContainers = document.querySelectorAll(".task-container");
taskContainers.forEach((taskList) => {
taskList.innerHTML = "";
let totalTaskCount = 0;
let completedTasksCount = 0;
if (getSetting("showStreamersTasksOnly")) {
const streamer = getSetting("streamer");
if (tasks[streamer]) {
({ totalTaskCount, completedTasksCount } = handleTasks(
tasks,
streamer,
taskList,
totalTaskCount,
completedTasksCount
));
}
} else {
for (const user in tasks) {
if (user.toLowerCase() === "id" || !tasks[user].todos) continue;
({ totalTaskCount, completedTasksCount } = handleTasks(
tasks,
user,
taskList,
totalTaskCount,
completedTasksCount
));
}
}
updateTaskCount(totalTaskCount, completedTasksCount);
});
await DBHandler.set("tasks", tasks);
await checkToAnimate();
}
function handleTasks(
tasks,
user,
taskList,
totalTaskCount,
completedTasksCount
) {
const userTasks = tasks[user];
if (userTasks.todos.length === 0) {
delete tasks[user];
} else {
const taskDiv = createTaskDiv(userTasks, user);
taskList.appendChild(taskDiv);
for (const task of userTasks.todos) {
const taskElement = createTaskElement(task);
taskDiv.querySelector(".user-tasks").appendChild(taskElement);
totalTaskCount++;
if (task.done) completedTasksCount++;
}
}
return { totalTaskCount, completedTasksCount };
}
function createTaskDiv(userTasks, username) {
const taskDiv = document.createElement("div");
taskDiv.classList.add("task");
const usernameDiv = document.createElement("div");
usernameDiv.classList.add("username");
usernameDiv.innerText = username;
usernameDiv.style.color = styles.usernameColor || userTasks.userColor;
taskDiv.appendChild(usernameDiv);
const olListDiv = document.createElement("ol");
olListDiv.classList.add("user-tasks");
taskDiv.appendChild(olListDiv);
return taskDiv;
}
function createTaskElement(task) {
const taskElement = document.createElement("li");
taskElement.classList.add("todo");
taskElement.innerText = task.text;
if (task.done) taskElement.classList.add("done");
else if (task.focus) taskElement.classList.add("focus");
return taskElement;
}
function updateTaskCount(totalTaskCount, completedTasksCount) {
if (taskListMemory.doneTaskCount > completedTasksCount) {
completedTasksCount = taskListMemory.doneTaskCount;
totalTaskCount = taskListMemory.totalTaskCount;
}
if (totalTaskCount < completedTasksCount) {
totalTaskCount = completedTasksCount;
}
document.querySelector(
".task-count"
).innerText = `${completedTasksCount}/${totalTaskCount}`;
taskListMemory.doneTaskCount = completedTasksCount;
taskListMemory.totalTaskCount = totalTaskCount;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
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);
}
}
async function animationFinished() {
scrolling = false;
await renderTaskListToDOM();
await checkToAnimate();
}
function cancelAnimation() {
if (primaryAnimation) {
primaryAnimation.cancel();
}
if (secondaryAnimation) {
secondaryAnimation.cancel();
}
scrolling = false;
}
DBHandler.open()
.then(async () => {
importStyles();
await renderTaskListToDOM();
setInterval(async () => {
await renderTaskListToDOM();
}, 5000);
})
.catch((error) => {
console.error("Error opening database:", error);
});

View file

@ -1,402 +0,0 @@
/**
* Counts the number of incomplete tasks for a given user.
*
* @param {string} username - The name of the user whose tasks are to be counted.
* @returns {number} The number of incomplete tasks for the user.
*/
async function incompleteTasksCount(username) {
const tasks = await DBHandler.get("tasks");
if (!tasks[username]) {
return 0;
}
return tasks[username].todos.filter((t) => !t.done).length;
}
/**
* Counts the number of completed tasks for a given user.
*
* @param {string} username - The name of the user whose tasks are to be counted.
* @returns {number} The number of completed tasks for the user.
*/
async function completedTasksCount(username) {
const counts = await DBHandler.get("counts");
if (!counts.users[username.toLowerCase()]) {
return 0;
}
return counts.users[username.toLowerCase()].completeCount;
}
/**
* Gets the total number of tasks for a given user.
*
* @param {string} username - The name of the user whose tasks are to be counted.
* @returns {number} The total number of tasks for the user.
*/
async function getUserTotalTaskCount(username) {
return (
(await incompleteTasksCount(username)) +
(await completedTasksCount(username))
);
}
/**
* Calculates the total points for a given user based on the number of completed tasks and the points per task setting.
*
* @param {string} username - The name of the user whose points are to be calculated.
* @returns {number} The total points for the user.
*/
async function calculatePoints(username) {
const counts = await DBHandler.get("counts");
if (!counts.users[username.toLowerCase()]) {
return 0;
}
return (
counts.users[username.toLowerCase()].completeCount *
getSetting("pointsPerTask")
);
}
/**
* Retrieves the points of a specific user from local storage.
*
* @param {string} username - The name of the user.
* @returns {number} The points of the user. If the user does not exist, returns 0.
*/
async function getUserPoints(username) {
const counts = await DBHandler.get("counts");
if (!counts.users[username.toLowerCase()]) {
return 0;
}
return counts.users[username.toLowerCase()].points;
}
/**
* Adds a specified value to the completed task count for a given user and the total completed task count.
*
* @param {string} username - The name of the user whose completed task count is to be incremented.
* @param {number} value - The value to be added to the completed task count.
*/
async function addDoneCount(username, value) {
const counts = await DBHandler.get("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;
taskListMemory.doneTaskCount += value;
// check if user has points
if (!counts.users[username.toLowerCase()].points) {
counts.users[username.toLowerCase()].points = 0;
}
// add to points
counts.users[username.toLowerCase()].points +=
value * getSetting("pointsPerTask");
// add to taskmaster
addCountToTaskMasterUser(username, value);
await DBHandler.set("counts", counts);
}
/**
* Sets the completed task count for a given user.
*
* @param {string} username
* @param {int} value
* @returns {Object} An object with a status and body. The status is 200 if successful, and the body contains a success message.
*/
async function setUserCompleteCount(username, value) {
const counts = await DBHandler.get("counts");
if (!counts.users[username.toLowerCase()]) {
counts.users[username.toLowerCase()] = {
completeCount: 0,
};
}
counts.users[username.toLowerCase()].completeCount = parseInt(value);
await DBHandler.set("counts", counts);
return {
status: 200,
body: {
message: `Successfully set ${username}'s complete count to ${value}`,
},
};
}
/**
* Sets the total count of completed tasks in local storage.
*
* @param {number} value - The new total count of completed tasks.
* @returns {Object} An object with a status and body. The status is 200 if successful, and the body contains a success message.
*/
async function setTotalCompleteCount(value) {
const counts = await DBHandler.get("counts");
counts.totalCompleteCount = parseInt(value);
await DBHandler.set("counts", counts);
return {
status: 200,
body: {
message: `Successfully set total complete count to ${value}`,
},
};
}
/**
* Adds a specified number of points to a user's total in local storage.
*
* @param {string} username - The name of the user.
* @param {number} value - The number of points to add.
* @returns {Object} An object with a status and body. The status is 200 if successful, and the body contains a success message.
*/
async function addPoints(username, value) {
const counts = await DBHandler.get("counts");
if (!counts.users[username.toLowerCase()]) {
counts.users[username.toLowerCase()] = {
points: 0,
};
}
if (!counts.users[username.toLowerCase()].points) {
counts.users[username.toLowerCase()].points = 0;
}
counts.users[username.toLowerCase()].points += value;
await DBHandler.set("counts", counts);
return {
status: 200,
body: {
message: `Successfully added ${value} points to ${username}`,
},
};
}
/**
* Sets the points for a specific user in local storage.
*
* @param {string} username - The name of the user.
* @param {number} pointsCount - The new points count for the user.
* @returns {Object} An object with a status and body. The status is 200 if successful, and the body contains a success message.
*/
async function setUserPoints(username, pointsCount) {
const counts = await DBHandler.get("counts");
if (!counts.users[username.toLowerCase()]) {
counts.users[username.toLowerCase()] = {
points: 0,
};
}
counts.users[username.toLowerCase()].points = pointsCount;
await DBHandler.set("counts", counts);
return {
status: 200,
body: {
message: `Successfully set ${username}'s points to ${pointsCount}`,
},
};
}
/**
* Reduces a specified number of points from a user's total in local storage.
*
* @param {string} username - The name of the user.
* @param {number} value - The number of points to reduce.
* @returns {Object} An object with a status and body. The status is 200 if successful, and the body contains a success message.
*/
async function reducePoints(username, value) {
const counts = await DBHandler.get("counts");
if (!counts.users[username.toLowerCase()]) {
counts.users[username.toLowerCase()] = {
points: 0,
};
}
if (!counts.users[username.toLowerCase()].points) {
counts.users[username.toLowerCase()].points = 0;
}
counts.users[username.toLowerCase()].points -= parseInt(value);
await DBHandler.set("counts", counts);
return {
status: 200,
body: {
message: `Successfully reduced ${username}'s points by ${value}`,
},
};
}
/**
* Retrieves the total count of tasks from the board.
*
* @returns {number} The total count of tasks. If no tasks are found, returns 0.
*/
async function getBoardTotalTaskCount() {
const counts = await DBHandler.get("counts");
if (!counts.totalCompleteCount) {
return 0;
}
return counts.totalCompleteCount;
}
/**
* Sets the task count for a specific user in local storage and returns a success message.
*
* @param {string} username - The name of the user.
* @param {number} value - The new task count for the user.
* @returns {Object} An object with a status and body. The status is 200 if successful, and the body contains a success message.
*/
async function setUserTaskCount(username, value) {
const counts = await DBHandler.get("counts");
if (!counts.users[username.toLowerCase()]) {
counts.users[username.toLowerCase()] = {
completeCount: 0,
};
}
if (!counts.users[username.toLowerCase()].completeCount) {
counts.users[username.toLowerCase()].completeCount = 0;
}
counts.users[username.toLowerCase()].completeCount = parseInt(value);
await DBHandler.set("counts", counts);
return {
status: 200,
body: {
message: `Successfully set ${username}'s task count to ${value}`,
},
};
}
/**
* Synchronizes the points of each user with their task count in local storage. After the synchronization, the function returns an object with a status of 200 and a success message.
*
* @returns {Object} An object with a status and body. The status is 200 if the synchronization is successful, and the body contains a success message.
*/
async function syncPointsToCount() {
const counts = await DBHandler.get("counts");
for (const user in counts.users) {
counts.users[user].points = calculatePoints(user);
}
await DBHandler.set("counts", counts);
return {
status: 200,
body: {
message: "Successfully synced counts to points",
},
};
}
/**
* Gets the leaderboard of users based on task count.
*
* @param {number} limit - The maximum number of users to include in the leaderboard.
* @returns {Object} An object with a status and body. The status is 200 if successful, and the body contains an array of user objects, each with a username and task count, sorted in descending order of task count.
*/
async function leaderboardTaskCount(limit) {
const counts = await DBHandler.get("counts");
let leaderboardArray = [];
for (const user in counts.users) {
let userTaskCount = await completedTasksCount(user);
leaderboardArray.push({
username: user,
taskCount: userTaskCount,
});
}
leaderboardArray.sort((a, b) => b.taskCount - a.taskCount);
// shorten to limit
leaderboardArray = leaderboardArray.slice(0, limit);
let leaderboardstring = "";
leaderboardArray.map((user, index) => {
leaderboardstring += `${index + 1}. @${user.username} : ${
user.taskCount
} | `;
});
leaderboardstring = leaderboardstring.slice(0, -3);
return {
status: 200,
body: {
leaderboard: leaderboardstring,
},
};
}
/**
* Resets the total count of completed tasks in the board to zero.
*/
async function resetBoardCount() {
const counts = await DBHandler.get("counts");
counts.totalCompleteCount = 0;
await DBHandler.set("counts", counts);
return {
status: 200,
body: {
message: "Successfully reset board count",
},
};
}
/**
* Resets the task count for all users.
*/
async function resetUsersCount() {
const counts = await DBHandler.get("counts");
counts.users = {};
await DBHandler.set("counts", counts);
return {
status: 200,
body: {
message: "Successfully reset users count",
},
};
}

View file

@ -1,140 +0,0 @@
/**
* Clears all completed tasks for a given user. If the tasks are successfully cleared, the function returns an object with a status of 200. If the tasks are not cleared due to the user having no tasks, the function returns an object with a status indicating the error and an error message.
*
* @param {string} username - The name of the user whose completed tasks are to be cleared.
* @returns {Object} An object with a status and body. The status is 200 if the tasks are successfully cleared. If the tasks are not cleared, the status indicates the error and the body contains an error message.
*/
async function clearOwnDoneTasks(username) {
const tasks = await DBHandler.get("tasks");
if (!tasks[username] || tasks[username].todos.length === 0) {
return createErrorResponse(
`@${username} has no tasks`,
getResponse("noTask")
);
}
let incompleteUserTasks = tasks[username].todos.filter((t) => !t.done);
// replace user's task with incomplete tasks
tasks[username].todos = incompleteUserTasks;
await DBHandler.set("tasks", tasks);
if (!scrolling) {
await renderTaskListToDOM();
}
return {
status: 200,
};
}
/**
* Clears all tasks for a given user. If the tasks are successfully cleared, the function returns an object with a status of 200.
* If the tasks are not cleared due to the user having no tasks, the function returns an object with a status indicating the error and an error message.
*
* @param {string} username - The name of the user whose tasks are to be cleared.
* @returns {Object} An object with a status and body. The status is 200 if the tasks are successfully cleared. If the tasks are not cleared, the status indicates the error and the body contains an error message.
*/
async function clearUserTasks(username) {
const tasks = await DBHandler.get("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: getResponse("noTask"),
},
};
}
tasks[username].todos = [];
await DBHandler.set("tasks", tasks);
if (!scrolling) {
await renderTaskListToDOM();
}
return {
status: 200,
};
}
/**
* Clears all completed tasks for all users. If the tasks are successfully cleared, the function returns an object with a status of 200. If there are no tasks to clear, the function still returns an object with a status of 200 as it's considered a successful operation.
*
* @returns {Object} An object with a status. The status is 200 if the tasks are successfully cleared.
*/
async function clearAllDoneTasks() {
const tasks = await DBHandler.get("tasks");
for (const user in tasks) {
if (!tasks[user].todos) continue;
if (user.toLowerCase() === "id") continue;
tasks[user].todos = tasks[user].todos.filter((t) => !t.done);
}
cancelAnimation();
await DBHandler.set("tasks", tasks);
await renderTaskListToDOM();
return {
status: 200,
};
}
async function clearAllTasks() {
await DBHandler.set("tasks", {});
await setupDB();
cancelAnimation();
await renderTaskListToDOM();
return {
status: 200,
};
}
async function clearAll() {
await resetDB();
cancelAnimation();
checkToAnimate();
await renderTaskListToDOM();
return {
status: 200,
};
}
/**
* Clears all tasks from the local storage except for those belonging to specified streamer usernames.
* After clearing, it also clears all done tasks, cancels any ongoing animation and re-renders the task list to the DOM.
*
* @param {string[]} streamerUsername - An array of streamer usernames whose tasks should not be cleared.
* @returns {Object} An object with a status property indicating the success of the operation (200 for success).
*/
async function clearAllExceptStreamer(streamerUsername) {
const tasks = await DBHandler.get("tasks");
for (const user in tasks) {
if (user.toLowerCase() === "id") continue;
if (user.toLowerCase() !== streamerUsername.toLowerCase()) {
tasks[user].todos = [];
}
}
await DBHandler.set("tasks", tasks);
await clearAllDoneTasks();
cancelAnimation();
await renderTaskListToDOM();
return {
status: 200,
};
}

File diff suppressed because it is too large Load diff

View file

@ -1,72 +0,0 @@
/**
* adds a count to user for 'taskmaster'
*
* @param {string} username
*/
async function addCountToTaskMasterUser(username, count) {
const taskmaster = await DBHandler.get("taskmaster");
if (!taskmaster.users[username.toLowerCase()]) {
taskmaster.users[username.toLowerCase()] = {
taskMasterCompleteCount: 0,
};
}
taskmaster.users[username.toLowerCase()].taskMasterCompleteCount +=
parseInt(count);
await DBHandler.set("taskmaster", taskmaster);
}
/**
* gets the count of a user from 'taskmaster'
*
* @param {string} username
* @returns {number} the count of the user
*/
async function getUserTaskMasterCount(username) {
const taskmaster = await DBHandler.get("taskmaster");
if (!taskmaster.users[username.toLowerCase()]) {
return 0;
}
return taskmaster.users[username.toLowerCase()].taskMasterCompleteCount;
}
async function getTaskMasterChampion() {
const taskmaster = await DBHandler.get("taskmaster");
if (!taskmaster.users) {
return null;
}
let champion = {
username: "",
count: 0,
};
for (const user in taskmaster.users) {
if (
user.toLowerCase() !== auth.channel.toLowerCase() &&
taskmaster.users[user].taskMasterCompleteCount > champion.count
) {
champion.username = user;
champion.count = taskmaster.users[user].taskMasterCompleteCount;
}
}
if (champion.count === 0) {
return null;
}
return champion;
}
async function resetTaskMaster() {
await DBHandler.set("taskmaster", {
users: {},
startDate: new Date(),
totalCompleteCount: 0,
});
}

View file

@ -1,27 +0,0 @@
function randomTasks(num) {
let tasks = [];
for (let i = 0; i < num; i++) {
tasks.push(`task ${i + 1}`);
}
return tasks.join(", ");
}
async function tests() {
let listOfStreamers = [
`followRythonDev1`,
`followRythonDev2`,
`followRythonDev3`,
`followRythonDev4`,
`followRythonDev5`,
`followRythonDev6`,
`followRythonDev7`,
];
for (let i = 0; i < listOfStreamers.length; i++) {
let randomNum = Math.floor(Math.random() * 10) + 1;
addTask(listOfStreamers[i], "#ffc0cb", randomTasks(randomNum));
await sleep(1000);
}
}

View file

@ -1,180 +0,0 @@
async function renderTaskListToDOM() {
const tasks = await DBHandler.get("tasks");
const taskContainers = document.querySelectorAll(".task-container");
let hasTasks = false;
taskContainers.forEach((taskList) => {
taskList.innerHTML = "";
let totalTaskCount = 0;
let completedTasksCount = 0;
for (const user in tasks) {
if (user.toLowerCase() === "id") continue;
if (!tasks[user].todos) continue;
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");
}
const taskContent = document.createElement("div");
taskContent.classList.add("content");
taskContent.innerText = task.text;
taskElement.appendChild(taskContent);
olListDiv.appendChild(taskElement);
hasTasks = true;
}
}
if (taskListMemory.doneTaskCount > completedTasksCount) {
completedTasksCount = taskListMemory.doneTaskCount;
totalTaskCount = taskListMemory.totalTaskCount;
}
if (totalTaskCount < completedTasksCount) {
totalTaskCount = completedTasksCount;
}
document.querySelector(
".task-count"
).innerText = `${completedTasksCount}/${totalTaskCount}`;
taskListMemory.doneTaskCount = completedTasksCount;
taskListMemory.totalTaskCount = totalTaskCount;
});
if (getSetting("hideWhenNoTasks") && !hasTasks) {
document.querySelector("#main-container").style.opacity = "0";
visible = false;
} else if (getSetting("hideWhenNoTasks") && hasTasks && !visible) {
document.querySelector("#main-container").style.opacity = "1";
visible = true;
}
await DBHandler.set("tasks", tasks);
checkToAnimate();
}
async function checkToAnimate() {
let taskContainer = document.querySelector(".task-container");
let taskContainerHeight = taskContainer.scrollHeight;
let taskWrapper = document.querySelector(".task-wrapper");
let taskWrapperHeight = taskWrapper.clientHeight;
let secondary = document.querySelector(".secondary");
if (taskContainerHeight > taskWrapperHeight && !scrolling) {
if (!scrolling) {
secondary.style.display = "flex";
let finalHeight =
taskContainerHeight + configs.animation.gapBetweenScrolls;
let keyframes = [
{ transform: `translateY(0)` },
{ transform: `translateY(-${finalHeight}px)` },
];
let scrollingSpeed = (finalHeight / scrollSpeed) * 1000;
let options = {
duration: scrollingSpeed,
iterations: 1,
easing: "linear",
};
primaryAnimation = document
.querySelector(".primary")
.animate(keyframes, options);
secondaryAnimation = document
.querySelector(".secondary")
.animate(keyframes, options);
primaryAnimation.play();
secondaryAnimation.play();
scrolling = true;
addAnimationListeners();
}
} else if (!scrolling) {
secondary.style.display = "none";
if (primaryAnimation) {
primaryAnimation.cancel();
}
if (secondaryAnimation) {
secondaryAnimation.cancel();
}
scrolling = false;
}
}
function addAnimationListeners() {
if (primaryAnimation) {
primaryAnimation.addEventListener("finish", animationFinished);
primaryAnimation.addEventListener("cancel", animationFinished);
}
}
async function animationFinished() {
scrolling = false;
await renderTaskListToDOM();
checkToAnimate();
}
function cancelAnimation() {
if (primaryAnimation) {
primaryAnimation.cancel();
}
if (secondaryAnimation) {
secondaryAnimation.cancel();
}
scrolling = false;
}

View file

@ -1,94 +0,0 @@
function loadGoogleFont(font) {
WebFont.load({
google: {
families: [font],
},
});
}
/**
* import styles from configs
*/
function importStyles() {
const styles = configs.styles;
// fonts
if (getSetting("headerGoogleFont")) {
loadGoogleFont(styles.headerFontFamily);
}
if (getSetting("taskGoogleFont")) {
loadGoogleFont(styles.taskFontFamily);
}
document.documentElement.style.setProperty(
"--gap-between-scrolls",
`${configs.animation.gapBetweenScrolls}px` ?? "0px"
);
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})`
);
});
if (!getSetting("displayTaskCount")) {
document.querySelector(".task-count").style.display = "none";
}
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);
}

View file

@ -1,23 +0,0 @@
const styles = configs.styles;
const scrollSpeed = configs.animation.scrollSpeed;
let openQuote = getSetting("openQuote");
let closeQuote = getSetting("closeQuote");
let visible = false;
let scrolling = false;
let primaryAnimation, secondaryAnimation;
const taskSeparator = getSetting("taskSeparator");
DBHandler.open()
.then(async () => {
await setupDB();
importStyles();
if (getSetting("testTasks")) {
await resetDB();
await tests();
}
await renderTaskListToDOM();
})
.catch((error) => {
console.error("Error opening database:", error);
});

View file

@ -1,4 +0,0 @@
#main-container {
width: 100%;
height: 100vh;
}

View file

@ -17,13 +17,10 @@
--header-font-family: sans-serif;
--header-font-color: white;
--header-font-weight: bold;
--task-count-margin-right: 0px;
--body-background-color: rgba(0, 0, 0, 0.7);
--body-border-width: 5px;
--body-border-color: white;
--body-border-radius: 5px;
--body-margin-top: 0px;
--number-of-lines: 1;
--line-height: 1.5;
--username-font-weight: bold;
--task-background-color: rgba(0, 0, 0, 0.7);
@ -38,7 +35,6 @@
--task-margin-bottom: 5px;
--task-horizontal-padding: 5px;
--task-vertical-padding: 5px;
--gap-between-scrolls: 0;
}
* {
@ -47,18 +43,6 @@
box-sizing: border-box;
}
@keyframes scroll {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-100%);
}
}
.primary, .secondary {
animation: scroll linear infinite;
}
#main-container {
width: 100%;
height: 100vh;
@ -68,7 +52,6 @@
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);
transition: opacity 0.5s ease-in-out;
}
#main-container .header {
display: flex;
@ -88,12 +71,6 @@
#main-container .header .fade {
opacity: 0;
}
#main-container .header .task-count {
margin-right: var(--task-count-margin-right);
}
#main-container .task-wrapper, #main-container .task-container {
transition: height 0.5s ease-out;
}
#main-container .task-wrapper {
width: 100%;
height: 100%;
@ -108,10 +85,8 @@
border-radius: var(--body-border-radius);
z-index: 1;
}
#main-container .task-wrapper .task-container.secondary {
margin-top: var(--gap-between-scrolls);
}
#main-container .task-wrapper .task-container {
position: absolute;
width: inherit;
left: 0;
display: flex;
@ -119,8 +94,6 @@
align-items: center;
margin-left: auto;
margin-right: auto;
margin-top: var(--body-margin-top);
transition: transform 0.4s ease-in-out;
}
#main-container .task-wrapper .task-container .task {
padding: var(--task-vertical-padding) var(--task-horizontal-padding);
@ -135,27 +108,14 @@
font-size: var(--username-font-size);
}
#main-container .task-wrapper .task-container .task .user-tasks {
counter-reset: item;
list-style-position: inside;
}
#main-container .task-wrapper .task-container .task .user-tasks .todo {
display: flex;
}
#main-container .task-wrapper .task-container .task .user-tasks .todo:before {
counter-increment: item;
content: counters(item, ".") ". ";
margin-right: 5px;
}
#main-container .task-wrapper .task-container .task .user-tasks .todo .content {
display: -webkit-box;
line-clamp: var(--number-of-lines);
-webkit-line-clamp: var(--number-of-lines);
-webkit-box-orient: vertical;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#main-container .task-wrapper .task-container .task .user-tasks .done {
color: var(--done-task-font-color);
}
#main-container .task-wrapper .task-container .task .user-tasks .done .content {
text-decoration: line-through;
color: var(--done-task-font-color);
}
@ -166,60 +126,4 @@
border-radius: var(--focus-task-border-radius);
width: -moz-fit-content;
width: fit-content;
margin-left: calc(var(--focus-task-horizontal-padding) * -1);
}/*# sourceMappingURL=taskList.css.map */
.oauth-modal {
position: fixed;
inset: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
background: rgba(0, 0, 0, 0.75);
}
.oauth-modal.hidden {
display: none;
}
.oauth-modal__content {
max-width: 28rem;
padding: 1.5rem 1.75rem;
border-radius: 0.5rem;
background: #18181b;
color: #efeff1;
font-family: system-ui, -apple-system, Segoe UI, sans-serif;
font-size: 0.95rem;
line-height: 1.5;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.oauth-modal__content h2 {
margin-bottom: 0.75rem;
font-size: 1.15rem;
font-weight: 600;
}
.oauth-modal__content p {
margin-bottom: 0.75rem;
}
.oauth-modal__content ol {
margin-left: 1.25rem;
}
.oauth-modal__content li + li {
margin-top: 0.35rem;
}
.oauth-modal__content a {
color: #9147ff;
}
.oauth-modal__content code {
padding: 0.1em 0.35em;
border-radius: 0.2rem;
background: rgba(255, 255, 255, 0.1);
font-size: 0.9em;
}
}/*# sourceMappingURL=taskList.css.map */

View file

@ -1 +0,0 @@
{"version":3,"sources":["taskList.scss","taskList.css"],"names":[],"mappings":"AAAA;EACC,uBAAA;EACA,yBAAA;EAEA,gDAAA;EAEA,6BAAA;EACA,+BAAA;EACA,8BAAA;EAEA,mCAAA;EACA,iCAAA;EAEA,iCAAA;EACA,+BAAA;EAEA,6CAAA;EAEA,0BAAA;EACA,4BAAA;EACA,2BAAA;EAEA,wBAAA;EACA,gCAAA;EACA,0BAAA;EACA,0BAAA;EAEA,8BAAA;EAEA,2CAAA;EAEA,wBAAA;EACA,0BAAA;EACA,yBAAA;EAEA,sBAAA;EAEA,oBAAA;EACA,kBAAA;EACA,4BAAA;EAEA,2CAAA;EAEA,8BAAA;EACA,sBAAA;EACA,wBAAA;EAEA,0BAAA;EACA,wBAAA;EACA,yBAAA;EAEA,8BAAA;EACA,4BAAA;EAEA,yBAAA;EACA,8BAAA;EACA,4BAAA;EAEA,wBAAA;ACjBD;;ADoBA;EACC,SAAA;EACA,UAAA;EACA,sBAAA;ACjBD;;ADoBA;EACI;IACI,wBAAA;ECjBN;EDmBE;IACI,4BAAA;ECjBN;AACF;ADoBA;EACI,iCAAA;AClBJ;;ADsBA;EACC,WAAA;EACA,aAAA;EAEA,aAAA;EACA,sBAAA;EAEA,8EAAA;EAGA,mDAAA;EAEA,yEAAA;EACA,6CAAA;EAEA,oCAAA;ACzBD;AD2BC;EACC,aAAA;EACA,8BAAA;EAEA,wEAAA;EAEA,mEAAA;EACA,0CAAA;EAEA,+BAAA;EACA,0CAAA;EAEA,sCAAA;EACA,kCAAA;EACA,sCAAA;AC7BF;AD+BE;EACC,6BAAA;AC7BH;AD+BE;EACC,UAAA;AC7BH;ADgCE;EACC,4CAAA;AC9BH;ADkCC;EACC,gCAAA;AChCF;ADmCC;EACC,WAAA;EACA,YAAA;EAEA,gBAAA;EAEA,kBAAA;EAEA,wCAAA;EAEA,6BAAA;EACA,oCAAA;EACA,gCAAA;EAEA,+BAAA;EAEA,+DAAA;EACA,wCAAA;EAEA,UAAA;ACxCF;AD0CE;EACC,sCAAA;ACxCH;AD2CE;EAEC,cAAA;EAEA,OAAA;EAEA,aAAA;EACA,sBAAA;EAEA,mBAAA;EAEA,iBAAA;EACA,kBAAA;EAEA,kCAAA;EAEA,sCAAA;AChDH;ADmDG;EACC,oEAAA;EAEA,wCAAA;EAEA,wCAAA;EAEA,wBAAA;EAEA,+DAAA;EACA,wCAAA;ACrDJ;ADuDI;EACC,wCAAA;EACA,oCAAA;ACrDL;ADwDI;EACC,mBAAA;ACtDL;ADwDK;EACC,aAAA;ACtDN;ADwDM;EACC,uBAAA;EACA,iCAAA;EACA,iBAAA;ACtDP;ADyDM;EACC,oBAAA;EACA,kCAAA;EACA,0CAAA;EACA,4BAAA;EACA,gBAAA;ACvDP;AD2DK;EACC,kCAAA;ACzDN;AD2DM;EACC,6BAAA;EACA,kCAAA;ACzDP;AD8DK;EACC,8CAAA;EACA,mCAAA;EAEA,gFAAA;EAGA,8CAAA;EAEA,uBAAA;EAAA,kBAAA;EAEA,4DAAA;ACjEN","file":"taskList.css"}

View file

@ -1,240 +0,0 @@
:root {
--task-list-width: 100%;
--task-list-height: 100vh;
--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;
--task-count-margin-right: 0px;
--body-background-color: rgba(0, 0, 0, 0.7);
--body-border-width: 5px;
--body-border-color: white;
--body-border-radius: 5px;
--body-margin-top: 0px;
--number-of-lines: 1;
--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;
--gap-between-scrolls: 0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@keyframes scroll {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-100%);
}
}
.primary, .secondary {
animation: scroll linear infinite;
}
#main-container {
width: 100%;
height: 100vh;
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);
transition: opacity 0.5s ease-in-out;
.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-count {
margin-right: var(--task-count-margin-right);
}
}
.task-wrapper, .task-container {
transition: height 0.5s ease-out;
}
.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.secondary {
margin-top: var(--gap-between-scrolls);
}
.task-container {
// position: absolute;
width: inherit;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
margin-left: auto;
margin-right: auto;
margin-top: var(--body-margin-top);
transition: transform 0.4s ease-in-out;
.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);
font-size: var(--username-font-size);
}
.user-tasks {
counter-reset: item;
.todo {
display: flex;
&:before {
counter-increment: item;
content: counters(item, ".") ". ";
margin-right: 5px;
}
.content {
display: -webkit-box;
line-clamp: var(--number-of-lines);
-webkit-line-clamp: var(--number-of-lines); // change this to the number of lines you want
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.done {
color: var(--done-task-font-color);
.content {
text-decoration: line-through;
color: var(--done-task-font-color);
}
}
.focus {
background: var(--focus-task-background-color);
color: var(--focus-task-font-color);
padding: var(--focus-task-vertical-padding)
var(--focus-task-horizontal-padding);
border-radius: var(--focus-task-border-radius);
width:fit-content;
margin-left: calc(var(--focus-task-horizontal-padding) * -1);
}
}
}
}
}
}