Compare commits

...

65 commits

Author SHA1 Message Date
4f81a40f6b Update README.md 2026-09-27 07:38:31 +00:00
liyunze
3ef3ddf1ba fixed focus task 2026-09-14 13:00:49 +08:00
liyunze
9b9e301076 organised js 2026-08-07 12:20:16 +10:00
liyunze
6efe723361 updated javascript codebase 2026-08-06 22:30:41 +10:00
liyunze
14612e281e update oauth expired modal 2026-05-31 19:26:33 +10:00
liyunze
3c9b8f4bc9 fixed shared chat bug 2026-01-06 20:36:37 +08:00
RythonDev
8a3bf1defe
Merge pull request #2 from liyunze-coding/liyunze-coding-patch-3
Update README.md
2025-06-21 23:37:03 +10:00
RythonDev
ced60491b4
Update README.md 2025-06-21 23:36:31 +10:00
liyunze
2fb68c28a5 Merge branch 'main' of https://github.com/liyunze-coding/multi-task-widget-infinity 2025-05-24 15:31:58 +10:00
liyunze
a3d6e00827 responds with capital letter 2025-05-24 15:31:54 +10:00
liyunze
9cf88aed56 removed points commands 2025-05-24 15:31:01 +10:00
liyunze
ff06093544 changed auth method, added webfont file 2025-02-20 22:04:19 +11:00
liyunze
5c94814a8c added clearedDone response 2025-01-31 10:05:26 +08:00
liyunze
8d40b96153 updated comfyJS 2025-01-30 16:02:47 +08:00
liyunze
478c4b5f74 Merge branch 'main' of https://github.com/liyunze-coding/multi-task-widget-infinity 2025-01-17 09:57:27 +08:00
liyunze
64bd4a713f removed case sensitivity, new auth method 2025-01-17 09:57:02 +08:00
RythonDev
82cdc9af28
Merge pull request #1 from liyunze-coding/liyunze-coding-patch-1
Update auth.js
2025-01-17 09:45:07 +08:00
RythonDev
884bd98a20
Update auth.js 2025-01-17 09:43:54 +08:00
liyunze
cae6b501dd added reply function 2025-01-12 21:29:37 +08:00
liyunze
9a1ed5c8a4 added extra scripts and extra styles 2024-12-17 18:13:09 +08:00
liyunze
b7909a3710 fixed existing task error reply 2024-11-22 21:42:35 +11:00
liyunze
17637fe750 fixed check command 2024-10-31 04:34:42 +11:00
liyunze
05c448dcc5 now = focus 2024-10-29 08:07:53 +11:00
liyunze
9baf6ef678 fixed done command 2024-10-25 20:00:47 +11:00
liyunze
81e3b5f5ba ver 3.3 2024-10-25 19:47:40 +11:00
liyunze
33cb61b10f fixed taskmaster 2024-10-17 08:09:11 +11:00
liyunze
574fbf3760 updated !list command 2024-10-16 22:07:00 +11:00
liyunze
80b541552b smooth infinite scrolling 2024-10-14 21:06:05 +11:00
liyunze
1761913dbb version 3.111 2024-10-13 22:35:42 +11:00
liyunze
16899adee3 version 3.11 2024-10-13 20:49:50 +11:00
liyunze
6611765c38 fixed enableLimit bug 2024-09-02 12:34:55 +10:00
liyunze
6c0f98abff added !tasks to add commands 2024-08-10 17:09:27 +10:00
liyunze
605b25a246 log, focused, completed and other new features 2024-08-10 17:04:37 +10:00
liyunze
36ef958eb6 hide when no tasks, edit focus task 2024-07-03 13:45:58 +10:00
liyunze
ba0d59b90e multi-lines per task feature 2024-06-08 09:16:41 +08:00
liyunze
d1ab11327b patched undone bug 2024-06-03 10:58:59 +08:00
liyunze
a6eaff7171 ver 2.33 2024-05-28 15:58:17 +08:00
liyunze
b40f670411 ver 2.32 2024-05-28 09:48:13 +08:00
liyunze
24d58867c9 version 2.3 2024-05-25 10:07:17 +08:00
liyunze
81c350cf00 bug fixes 2024-05-11 21:19:19 +08:00
liyunze
6a963d028c bug fixes 2024-05-11 14:36:07 +08:00
liyunze
deb9344950 many updates 2024-05-09 22:22:10 +08:00
liyunze
a5ea077f88 taskmaster feature 2024-05-04 22:35:04 +08:00
liyunze
36ca7ad5c0 added reset task master command 2024-05-04 10:28:34 +08:00
liyunze
6cd20763ae added task master command 2024-05-03 21:43:02 +08:00
liyunze
892674476e big update: indexedDB, defaultconfigs 2024-04-29 17:08:31 +08:00
liyunze
4909c87a6f fixes 2024-04-27 13:18:37 +08:00
liyunze
45c394881f added margin right to css 2024-01-17 13:35:29 +08:00
liyunze
a618891eb1 fixed response error, made sure completed task count does not exceed 2024-01-17 13:31:02 +08:00
liyunze
4631b1f6a3 BETA changed configs format, nowTask, score display 2024-01-17 12:59:58 +08:00
liyunze
8b7d1f8660 add points when complete task 2023-12-22 22:45:09 +08:00
liyunze
1890d9c02d leaderboard, sync points, modify points + count 2023-12-22 17:43:37 +08:00
liyunze
128e6eafe0 set test tasks to false 2023-12-13 20:05:52 +08:00
liyunze
4de7edf03f added JSDocs 2023-12-13 20:05:22 +08:00
liyunze
ef6c25e60a added JSDocs 2023-12-03 21:19:55 +08:00
liyunze
d68d2f8c97 fixed cleardone animation 2023-12-02 14:43:53 +08:00
liyunze
e0088dc1e3 changed Task(s) to Task in focused 2023-12-01 23:05:52 +08:00
liyunze
a372d393ce fixed admin delete command 2023-12-01 11:42:39 +08:00
liyunze
e264073c29 fixed rendering issues, undone task count 2023-12-01 10:14:20 +08:00
liyunze
3c86c9ccc5 added TaskSeparator config setting 2023-11-25 10:44:44 +08:00
liyunze
6dbf541022 modified configs 2023-11-24 10:43:20 +08:00
liyunze
0928900177 fixed unfocused response 2023-11-24 10:41:55 +08:00
liyunze
3b915558bc added more configs 2023-11-23 19:46:18 +08:00
liyunze
aa1e513477 New features
- !count
- !points
- !boardcount
- !focus
- automaticDoneIndex
2023-11-22 21:18:44 +08:00
liyunze
4ccbe0f50d fixed undone command 2023-11-16 22:07:58 +08:00
29 changed files with 4599 additions and 1759 deletions

5
.gitignore vendored
View file

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

202
README.md
View file

@ -1,203 +1,3 @@
# Multi-Task widget Infinity (PRIVATE) # Multi-Task widget Infinity
![Multi Task Widget](./images/preview.png "Multi-Task widget") ![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 // Authentication and channels - required
const channel = ""; // your channel const channel = ""; // your channel
const username = ""; // bot account const username = ""; // bot account
const oauth = ""; // should be oauth:xxxxxxxxxxxx from the bot account DO NOT INCLUDE oauth: const oauth = ""; // should be oauth:xxxxxxxxxxxx from the bot account
return { return {
channel, channel,

View file

@ -1,343 +1,419 @@
const configs = (function () { const configs = (function () {
"use strict"; "use strict";
// settings
const enableLimit = true; // true or false
const limit = 5; // integer
// animation
const scrollSpeed = 40; // milliseconds
const gapBetweenScrolls = 0; // px
// STYLES
// task list
const taskListBackgroundColor = "#000000"; // hex only
const taskListBackgroundOpacity = 0; // 0.0 - 1.0
const taskListBorderWidth = "0px"; // px
const taskListBorderColor = "#ffffff"; // hex or name
const taskListBorderRadius = "5px"; // px
const taskListHorizontalPadding = "0px"; // px
const taskListVerticalPadding = "0px"; // px
// header
const headerHorizontalPadding = "10px"; // px
const headerVerticalPadding = "10px"; // px
const headerBackgroundColor = "#000000"; // hex only
const headerBackgroundOpacity = 0.8; // 0.0 - 1.0
const headerBorderWidth = "2px"; // px
const headerBorderColor = "#ffffff"; // hex or name
const headerBorderRadius = "5px"; // px
const headerFontFamily = "Fredoka"; // font name
const headerGoogleFont = true; // true: use google font, false: use system font
const headerFontSize = "25px"; // px
const headerFontWeight = "bold"; // normal or bold or number
const headerFontColor = "#ffffff"; // hex or name
// body
const bodyBackgroundColor = "#000000"; // hex only
const bodyBackgroundOpacity = 0; // 0.0 - 1.0
const bodyBorderWidth = "0px"; // px
const bodyBorderColor = "#fff"; // hex or name
const bodyBorderRadius = "5px"; // px
// task
const lineHeight = 1.5; // number
const usernameFontWeight = "bold"; // normal or bold or number
const usernameColor = "#ffc0cb"; // hex or name or "" for twitch user color
const taskWidth = "100%";
const taskBackgroundColor = "#000000"; // hex only
const taskBackgroundOpacity = 0.7; // 0.0 - 1.0
const taskFontFamily = "Poppins"; // font name
const taskGoogleFont = true; // true: use google font, false: use system font
const taskFontSize = "20px"; // px
const taskFontColor = "#ffffff"; // hex or name
const taskBorderColor = "#000000"; // hex or name
const taskBorderWidth = "0px"; // px
const taskBorderRadius = "5px"; // px
const taskMarginBottom = "5px"; // px
const taskHorizontalPadding = "20px"; // px
const taskVerticalPadding = "10px"; // px
// Add task commands - please add commands in the exact format
const addTaskCommands = [
"!taska",
"!taskadd",
"!atask",
"!addtask",
"!task",
"!add",
"!todo",
];
// Edit task commands - please add commands in the exact format
const editTaskCommands = [
"!taske",
"!taskedit",
"!etask",
"!edittask",
"!edit",
];
// Delete task commands - please add commands in the exact format
const deleteTaskCommands = [
"!taskd",
"!taskdel",
"!taskdelete",
"!deltask",
"!deletetask",
"!taskr",
"!taskremove",
"!rtask",
"!removetask",
"!remove",
"!delete",
];
// Finish task commands - please add commands in the exact format
const finishTaskCommands = [
"!taskf",
"!taskfinish",
"!ftask",
"!finishtask",
"!taskd",
"!taskdone",
"!donetask",
"!dtask",
"!finish",
"!done",
"!finished",
];
// Unfinish task commands - please add commands in the exact format
const unfinishTaskCommands = [
"!tasku",
"!taskunfinish",
"!utask",
"!unfinishtask",
"!taskud",
"!taskundone",
"!undonetask",
"!undone",
"!unfinish",
"!unfinished",
];
// Check task commands - please add commands in the exact format
const checkCommands = [
"!taskc",
"!taskcheck",
"!ctask",
"!checktask",
"!mytask",
"!check",
"!mytasks",
];
// List tasks commands - please add commands in the exact format
const listCommands = ["!taskl", "!tasklist", "!listtasks", "!list"];
// Clear my done commands - please add commands in the exact format
const clearMyDoneCommands = ["!clearmydone"];
// Help commands - please add commands in the exact format
const helpCommands = [
"!taskh",
"!taskhelp",
"!htask",
"!helptask",
"!tasks",
"!help",
];
// Admin clear all except streamer - please add commands following the exact format
const adminClearNotStreamerCommands = [
"!clearnotstreamer",
"!aclearnotstreamer",
"!adminclearnotstreamer",
"!clearns",
];
// Admin delete - please add commands following the exact format
const adminDeleteCommands = [
"!taskadel",
"!adel",
"!adelete",
"!admindelete",
];
// Admin clear done - please add commands following the exact format
const adminClearDoneCommands = [
"!acleardone",
"!admincleardone",
"!cleardone",
];
const adminClearAllCommands = [
"!clearall",
"!allclear",
"!adminclearall",
"!adminallclear",
"!aclearall",
"!aclear",
];
// Responses
const taskAdded = 'The task(s) "{task}" has been added, {user}!';
const noTaskAdded =
"Looks like you already hit the limit of incomplete tasks, {user}";
const noTaskContent = "Try using !task the-task-you-are-working-on {user}";
const taskDeleted =
'Task(s) "{task}" has been deleted successfully, {user}';
const specifyTaskIndex = "Try specifying the index of the task {user}";
const duplicateTask =
"Looks like you already have the task '{message}' up there {user}!";
const clearTasksExceptBroadcaster =
"All tasks have been cleared except for the streamer's, {user}!";
const adminDeleteTasks = "All of {mentioned}'s tasks have been deleted";
const taskFinished = 'Good job on finishing the task "{task}", {user}!';
const allTasksFinished = "Good job on finishing all your tasks, {user}!";
const taskUnfinished =
'Task(s) "{task}" has been unmarked as done, {user}!';
const taskEdited =
"Task '{originalTask}' has been edited to '{task}' successfully, {user}";
const noTaskEdit = "Try doing !edit [index] [new task] {user}";
const noTask = "Looks like you don't have a task up there {user}";
const noTaskA = "Looks like there is no task from that user there {user}";
const notMod = "Permission denied, {user}; Mods only";
const clearedAll = "All tasks have been cleared!";
const clearedDone = "All completed tasks have been cleared!";
const clearedMyDone = "All your completed tasks have been cleared!";
const specifyUser = "Try specifying a user to delete their tasks {user}";
const help = `{user} Use the following commands to help you out - !task !edit !remove !done. For mods, you can do !adel @user. More commmands here: https://github.com/liyunze-coding/Chat-Task-Tic-Overlay/blob/main/MultiTask.md/`;
// to edit check task command, go to
// scripts/taskList.js
// function checkTasks(username)
const additionalCommands = {
"!botcred":
"{user} Ryan is the creator of this bot! You can find him on https://github.com/liyunze-coding or https://www.twitch.tv/RythonDev",
};
const titles = [
"!taskhelp",
"!task",
"!edit",
"!remove",
"!done",
"!botcred",
];
const settings = { const settings = {
enableLimit, replyInChat: true, // true or false
limit, enableLimit: false, // true or false
headerGoogleFont, limit: 10, // integer
taskGoogleFont, automaticDoneIndex: false, // true or false - Automatically assume first unfinished task is complete
}; taskName: "task", // string (singular)
pointsName: "points", // string
const styles = { pointsPerTask: 10, // integer
taskListBackgroundColor, taskSeparator: [";", ","], // array of strings
taskListBackgroundOpacity, openQuote: '"', // string
taskListBorderWidth, closeQuote: '"', // string
taskListBorderColor, testTasks: false, // true or false
taskListBorderRadius, headerGoogleFont: true, // true: use google font, false: use system font
taskListHorizontalPadding, taskGoogleFont: true, // true: use google font, false: use system font
taskListVerticalPadding, displayTaskCount: true, // true or false
headerHorizontalPadding, enableTaskMaster: true, // true or false
headerVerticalPadding, hideWhenNoTasks: false, // true or false
headerBackgroundColor, taskCharacterLimit: 15, // integer
headerBackgroundOpacity, taskCharacterLimitEnabled: false, // true or false
headerBorderWidth,
headerBorderColor,
headerBorderRadius,
headerFontSize,
headerFontWeight,
headerFontFamily,
headerFontColor,
bodyBackgroundColor,
bodyBackgroundOpacity,
bodyBorderWidth,
bodyBorderColor,
bodyBorderRadius,
taskWidth,
lineHeight,
usernameFontWeight,
usernameColor,
taskBackgroundColor,
taskBackgroundOpacity,
taskFontFamily,
taskFontSize,
taskFontColor,
taskBorderColor,
taskBorderWidth,
taskBorderRadius,
taskMarginBottom,
taskHorizontalPadding,
taskVerticalPadding,
}; };
const animation = { const animation = {
scrollSpeed, scrollSpeed: 40, // milliseconds
gapBetweenScrolls, gapBetweenScrolls: 0, // px
titles, titles: [
"!taskhelp",
"!task",
"!edit",
"!remove",
"!done",
"!undone",
"!botcred",
"!count",
"!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 = { const commands = {
addTaskCommands, // Add task commands - please add commands in the exact format
editTaskCommands, addTaskCommands: [
deleteTaskCommands, "!taska",
finishTaskCommands, "!taskadd",
unfinishTaskCommands, "!atask",
helpCommands, "!addtask",
checkCommands, "!task",
listCommands, "!add",
clearMyDoneCommands, "!todo",
adminDeleteCommands, "!a",
adminClearNotStreamerCommands, ],
adminClearDoneCommands,
adminClearAllCommands, // Edit task commands - please add commands in the exact format
additionalCommands, 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 = { const responses = {
taskAdded, // Responses related to task addition
noTaskAdded, taskAdded: "The {taskName}(s) {task} has been added, {user}!",
noTaskContent, noTaskAdded:
taskDeleted, "Looks like you already hit the limit of incomplete {taskName}s, {user}",
specifyTaskIndex, noTaskContent:
duplicateTask, "Try using !task the-{taskName}-you-are-working-on {user}",
taskFinished, duplicateTask:
allTasksFinished, "Looks like you already have the {taskName} {task} up there {user}!",
taskUnfinished, taskEdited:
taskEdited, "{taskName} {originalTask} has been edited to {task} successfully, {user}",
noTaskEdit, noTaskEdit: "Try doing !edit [index] [new task] {user}",
noTask, nowTask:
noTaskA, "{taskName} {task} is now the task you are working on, {user}!",
notMod, taskLogged: "Task(s) {task} has been logged, {user}!",
help, taskAlreadyCompleted:
clearTasksExceptBroadcaster, "Looks like you already completed that {taskName} {task} up there {user}!",
adminDeleteTasks,
clearedMyDone, // Grouped by task progression responses
clearedAll, taskNext:
clearedDone, "Good job on finishing the task {oldTask}! Now moving onto {newTask}, {user}!",
specifyUser, 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 { return {

View file

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

View file

@ -1,32 +1,49 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="./lib/webfont.js"></script>
<script src="./lib/comfy.min.js"></script>
<head> <script src="./auth.js"></script>
<meta charset="UTF-8" /> <script src="./configs.js"></script>
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <script defer src="./scripts/core/defaultConfigs.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script defer src="./scripts/core/constants.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js"></script> <script defer src="./scripts/core/functions.js"></script>
<script src="https://cdn.jsdelivr.net/npm/comfy.js@latest/dist/comfy.min.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 src="./auth.js"></script> <link rel="stylesheet" type="text/css" href="./styles/taskList.css" />
<script src="./configs.js"></script> <link
<script defer src="scripts/index.js"></script> rel="stylesheet"
<script defer src="scripts/taskList.js"></script> type="text/css"
href="./styles/extraStyles.css"
/>
<link rel="stylesheet" type="text/css" href="styles/taskList.css" /> <title>Task List</title>
</head>
<title>Task List</title> <body>
</head> <div id="main-container">
<div class="header">
<body> <div class="title">!taskhelp</div>
<div id="main-container"> <div class="task-count">0/0</div>
<div class="header"> </div>
<div class="title">!taskhelp</div> <div class="task-wrapper">
<div class="task-count">0/0</div> <div class="task-container primary">
</div> <!--
<div class="task-wrapper">
<div class="task-container primary">
<!--
<div class="task"> <div class="task">
<div class="username">RyanPython</div> <div class="username">RyanPython</div>
<ol class="user-tasks"> <ol class="user-tasks">
@ -35,10 +52,42 @@
</ol> </ol>
</div> </div>
--> -->
</div>
<div class="task-container secondary"></div>
</div> </div>
<div class="task-container secondary"></div>
</div> </div>
</div>
</body>
<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> </html>

1
lib/comfy.min.js vendored Normal file

File diff suppressed because one or more lines are too long

33
lib/webfont.js Normal file
View file

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

74
scripts/chat/index.js Normal file
View file

@ -0,0 +1,74 @@
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

@ -0,0 +1,668 @@
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
}
}

38
scripts/core/constants.js Normal file
View file

@ -0,0 +1,38 @@
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",
});

165
scripts/core/db.js Normal file
View file

@ -0,0 +1,165 @@
/*
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

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

18
scripts/core/functions.js Normal file
View file

@ -0,0 +1,18 @@
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]
);
}

64
scripts/core/utils.js Normal file
View file

@ -0,0 +1,64 @@
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}`;
}

View file

@ -1,225 +0,0 @@
const commands = configs.commands;
const responses = configs.responses;
let params = {};
function respond(template, params) {
ComfyJS.Say(
template
.replace("{user}", `@${params.user}`)
.replace("{message}", params.message)
.replace("{mentioned}", `@${params.mentioned}`)
.replace("{task}", params.task)
.replace("{originalTask}", params.originalTask)
);
}
function isMod(flags) {
return flags.broadcaster || flags.mod;
}
ComfyJS.onCommand = (user, command, message, flags, extra) => {
command = `!${command.toLowerCase()}`;
params = {
user: user,
message: message,
mentioned: "",
task: "",
originalTask: "",
};
if (commands.addTaskCommands.includes(command)) {
let addStatus = addTask(user, extra.userColor, message);
if (addStatus === 0) {
// limit has reached
respond(responses.noTaskAdded, params);
} else if (addStatus === 1) {
// duplicate task
respond(responses.duplicateTask, params);
} else if (addStatus === 2) {
// task has no content
respond(responses.noTaskContent, params);
} else {
let addResponse = responses.taskAdded;
params.task = addStatus.task;
if (addStatus.tasksFailedToAdd !== "") {
addResponse += ` | Failed to add task(s): "${addStatus.tasksFailedToAdd}"`;
}
// task added
respond(addResponse, params);
}
} else if (commands.editTaskCommands.includes(command)) {
let editStatus = editTask(user, message);
if (editStatus === 0) {
// no task
respond(responses.noTask, params);
} else if (editStatus === 1) {
// invalid input
respond(responses.noTaskEdit, params);
} else {
// task edited
params.task = editStatus[1];
params.originalTask = editStatus[0];
respond(responses.taskEdited, params);
}
} else if (commands.deleteTaskCommands.includes(command)) {
let removeStatus = removeTask(user, message);
if (removeStatus === 0) {
// no task
respond(responses.noTask, params);
} else if (removeStatus === 1) {
// invalid input
respond(responses.specifyTaskIndex, params);
} else {
// task deleted
let deletedResponse = responses.taskDeleted;
params.task = removeStatus.removedTasks;
if (removeStatus.failedTasks !== "") {
deletedResponse += ` | Failed to delete task(s): "${removeStatus.failedTasks}"`;
}
respond(deletedResponse, params);
}
} else if (commands.finishTaskCommands.includes(command)) {
if (message === "all") {
let finishAllStatus = markAllTasksAsDone(user);
if (finishAllStatus === 0) {
// user has no tasks
respond(responses.noTask, params);
} else {
// all tasks finished
let finishedResponse = responses.allTasksFinished;
respond(finishedResponse, params);
}
return;
}
let finishStatus = markTaskDone(user, message);
if (finishStatus === 0) {
// user has no tasks
respond(responses.noTask, params);
} else if (finishStatus === 1) {
// invalid input
respond(responses.specifyTaskIndex, params);
} else {
// task finished
let finishedResponse = responses.taskFinished;
params.task = finishStatus.markedTasks;
if (finishStatus.failedTasks !== "") {
finishedResponse += ` | Failed to finish task(s): "${finishStatus.failedTasks}"`;
}
respond(finishedResponse, params);
}
} else if (commands.unfinishTaskCommands.includes(command)) {
let unfinishStatus = markTaskUndone(user, message);
if (unfinishStatus === 0) {
// user has no tasks
respond(responses.noTask, params);
} else if (unfinishStatus === 1) {
// invalid input
respond(responses.specifyTaskIndex, params);
} else {
// task unfinished
let unfinishedResponse = responses.taskUnfinished;
params.task = unfinishStatus.markedTasks;
if (unfinishStatus.failedTasks !== "") {
unfinishedResponse += ` | Failed to unfinish task(s): "${unfinishStatus.failedTasks}"`;
}
respond(unfinishedResponse, params);
}
} else if (commands.checkCommands.includes(command)) {
if (message === "") {
let response = checkTasks(user);
if (response === 0) {
return respond(responses.noTask, params);
}
return ComfyJS.Say(response);
} else {
let mentioned = message.replace("@", "");
let response = checkTasks(mentioned);
if (response === 0) {
// no tasks
return respond(responses.noTaskA, params);
}
return ComfyJS.Say(response);
}
} else if (commands.adminDeleteCommands.includes(command)) {
if (!isMod(flags)) {
respond(responses.notMod, params);
return;
}
let mentioned = message.replace("@", "");
if (mentioned === "") {
respond(responses.specifyUser, params);
return;
}
clearUserTasks(mentioned);
params.mentioned = mentioned;
respond(responses.adminDeleteTasks, params);
return mentioned;
} else if (commands.adminClearDoneCommands.includes(command)) {
if (!isMod(flags)) {
respond(responses.notMod, params);
return;
} else {
clearAllDoneTasks();
respond(responses.clearedDone, params);
}
} else if (commands.adminClearAllCommands.includes(command)) {
if (!isMod(flags)) {
respond(responses.notMod, params);
return;
} else {
clearAllTasks();
respond(responses.clearedAll, params);
}
} else if (commands.clearMyDoneCommands.includes(command)) {
let response = clearOwnDoneTasks(user);
if (response === 0) {
// no tasks
respond(responses.noTask, params);
return;
}
respond(responses.clearedMyDone, params);
} else if (commands.adminClearNotStreamerCommands.includes(command)) {
clearAllExceptStreamer(auth.channel);
respond(responses.clearTasksExceptBroadcaster, params);
} else if (commands.helpCommands.includes(command)) {
respond(responses.help, params);
} else if (commands.listCommands.includes(command)) {
let response = listTasks(user);
if (response === 0) {
respond(responses.noTask, params);
return;
}
ComfyJS.Say(response);
} else if (commands.additionalCommands[command]) {
respond(commands.additionalCommands[command], params);
} else {
// command not found
}
};
ComfyJS.Init(auth.username, `oauth:${auth.oauth}`, [auth.channel]);

View file

@ -1,964 +0,0 @@
/*
DB structure:
- tasks
- username:
- todos (array)
- [{text, done}, {text, done}]
- done
- userColor
*/
const settings = configs.settings;
const styles = configs.styles;
const scrollSpeed = configs.animation.scrollSpeed;
let scrolling = false;
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);
}
function resetDB() {
localStorage.clear();
setupDB();
}
function setupDB() {
if (!localStorage.tasks) {
localStorage.setItem(`tasks`, "{}");
}
}
// 0: limit has reached
// 1: duplicate task
// 2: invalid input
// string (single or multiple tasks): success
function addTask(username, userColor, task) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username]) {
tasks[username] = {
todos: [],
done: [],
userColor: userColor,
};
}
if (
incompleteTasksCount(username) >= settings.limit &&
settings.enableLimit
) {
return 0;
}
if (
tasks[username].todos.find(
(t) => t.text.toLowerCase() === task.toLowerCase()
)
) {
return 1;
}
// if task is whitespace or empty
if (!task || !task.trim()) {
return 2;
}
let tasksFailedToAdd = [];
// if task has commas
if (task.includes(",")) {
let tasksToAdd = task.split(",").map((t) => t.trim());
if (
incompleteTasksCount(username) + tasksToAdd.length >
settings.limit &&
settings.enableLimit
) {
tasksFailedToAdd = tasksToAdd.slice(
settings.limit - incompleteTasksCount(username),
tasksToAdd.length
);
tasksToAdd = tasksToAdd.slice(
0,
settings.limit - incompleteTasksCount(username)
);
}
for (const t of tasksToAdd) {
tasks[username].todos.push({ text: t, done: false });
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
task: tasksToAdd.join('", "'),
tasksFailedToAdd: tasksFailedToAdd.join('", "') || "",
};
}
tasks[username].todos.push({ text: task, done: false });
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
task: task,
tasksFailedToAdd: "",
};
}
// 0: user has no tasks
// 1: invalid input
// removed task: success
function removeTask(username, task) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username] || tasks[username].todos.length === 0) {
return 0;
}
const incompleteTasks = tasks[username].todos.filter((t) => !t.done);
// match regex: integers separated by space e.g. '1 2 3 4'
if (task.match(/^(\d+ )*\d+$/)) {
// insert commas between integers
task = task.replace(/(\d+)/g, "$1,");
// remove trailing comma
task = task.slice(0, -1);
}
let removedTaskIndex = [];
let tasksRemoved = [];
let tasksFailedToRemove = [];
// check if there's a comma in the task (multiple tasks)
if (task.includes(",")) {
let tasksToRemove = task.split(",").map((t) => t.trim());
for (const t of tasksToRemove) {
let index = tasks[username].todos.findIndex(
(task) => task.text.toLowerCase() === t.toLowerCase()
);
if (index === -1) {
if (isInt(t)) {
index = parseInt(t) - 1; // ACTUAL INDEX
} else if (incompleteTasks.length === 1) {
index = tasks[username].todos.findIndex((t) => !t.done);
tasksRemoved.push(tasks[username].todos[index].text);
removedTaskIndex.push(index);
} else {
tasksFailedToRemove.push(t);
continue;
}
if (index < 0 || index > tasks[username].todos.length - 1) {
tasksFailedToRemove.push(t);
continue;
} else {
tasksRemoved.push(tasks[username].todos[index].text);
removedTaskIndex.push(index);
}
} else {
tasksRemoved.push(tasks[username].todos[index].text);
removedTaskIndex.push(index);
}
}
if (tasksRemoved.length === 0) {
return 1;
}
// sort removedTaskIndex in descending order
removedTaskIndex.sort((a, b) => b - a);
// remove tasks from tasks array
for (const index of removedTaskIndex) {
tasks[username].todos.splice(index, 1);
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
removedTasks: tasksRemoved.join('", "'),
failedTasks: tasksFailedToRemove.join('", "'),
};
} else {
let index = tasks[username].todos.findIndex(
(t) => t.text.toLowerCase() === task.toLowerCase()
);
if (index === -1) {
if (isInt(task)) {
index = parseInt(task) - 1; // ACTUAL INDEX
} else if (incompleteTasks.length === 1) {
index = tasks[username].todos.findIndex((t) => !t.done);
} else {
return 1;
}
if (index < 0 || index > tasks[username].todos.length - 1) {
return 1;
}
}
task = tasks[username].todos[index].text;
tasks[username].todos.splice(index, 1);
localStorage.setItem(`tasks`, JSON.stringify(tasks));
renderTaskListToDOM();
return {
removedTasks: task,
failedTasks: "",
};
}
}
// function to determine if string is integer
function isInt(value) {
return (
!isNaN(value) &&
parseInt(Number(value)) == value &&
!isNaN(parseInt(value, 10))
);
}
// 0: user has no tasks
// 1: success
function clearOwnDoneTasks(username) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username] || tasks[username].todos.length === 0) {
return 0;
}
let incompleteUserTasks = tasks[username].todos.filter((t) => !t.done);
// replace user's task with incomplete tasks
tasks[username].todos = incompleteUserTasks;
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return 1;
}
// 0: user has no tasks
// 1: invalid input
// task: succcess
function markTaskDone(username, task) {
const tasks = JSON.parse(localStorage.tasks);
// user does not have any tasks
if (!tasks[username] || tasks[username].todos.length === 0) {
return 0;
}
const incompleteTasks = tasks[username].todos.filter((t) => !t.done);
// match regex: integers separated by space e.g. '1 2 3 4'
if (task.match(/^(\d+ )*\d+$/)) {
// insert commas between integers
task = task.replace(/(\d+)/g, "$1,");
// remove trailing comma
task = task.slice(0, -1);
}
let tasksMarkedComplete = [];
let tasksFailedToComplete = [];
// check if there's a comma in the task (multiple tasks)
if (task.includes(",")) {
let tasksToMarkDone = task.split(",").map((t) => t.trim());
for (const t of tasksToMarkDone) {
let index = tasks[username].todos.findIndex(
(task) => task.text.toLowerCase() === t.toLowerCase()
);
if (index === -1) {
if (isInt(t)) {
index = parseInt(t) - 1; // ACTUAL INDEX
} else if (incompleteTasks.length === 1) {
index = tasks[username].todos.findIndex((t) => !t.done);
tasksMarkedComplete.push(tasks[username].todos[index].text);
} else {
tasksFailedToComplete.push(t);
continue;
}
if (index < 0 || index > tasks[username].todos.length - 1) {
tasksFailedToComplete.push(t);
continue;
} else {
tasksMarkedComplete.push(tasks[username].todos[index].text);
}
} else {
tasksMarkedComplete.push(tasks[username].todos[index].text);
}
tasks[username].todos[index].done = true;
}
if (tasksMarkedComplete.length === 0) {
return 1;
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
markedTasks: tasksMarkedComplete.join('", "'),
failedTasks: tasksFailedToComplete.join('", "'),
};
} else {
let index = tasks[username].todos.findIndex(
(t) => t.text.toLowerCase() === task.toLowerCase()
);
if (index === -1) {
if (isInt(task)) {
index = parseInt(task) - 1; // ACTUAL INDEX
} else if (incompleteTasks.length === 1) {
index = tasks[username].todos.findIndex((t) => !t.done);
} else {
return 1;
}
if (index < 0 || index > tasks[username].todos.length - 1) {
return 1;
}
}
task = tasks[username].todos[index].text;
tasks[username].todos[index].done = true;
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
markedTasks: task,
failedTasks: "",
};
}
}
function markAllTasksAsDone(username) {
const tasks = JSON.parse(localStorage.tasks);
// user does not have any tasks
if (!tasks[username] || tasks[username].todos.length === 0) {
return 0;
}
for (const task of tasks[username].todos) {
task.done = true;
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return 1;
}
function markTaskUndone(username, task) {
const tasks = JSON.parse(localStorage.tasks);
// user does not have any tasks
if (!tasks[username] || tasks[username].todos.length === 0) {
return 0;
}
const completedTasks = tasks[username].todos.filter((t) => t.done);
// match regex: integers separated by space e.g. '1 2 3 4'
if (task.match(/^(\d+ )*\d+$/)) {
// insert commas between integers
task = task.replace(/(\d+)/g, "$1,");
// remove trailing comma
task = task.slice(0, -1);
}
let tasksMarkedUndone = [];
let tasksFailedToMarkUndone = [];
// check if there's a comma in the task (multiple tasks)
if (task.includes(",")) {
let tasksToMarkUndone = task.split(",").map((t) => t.trim());
for (const t of tasksToMarkUndone) {
let index = tasks[username].todos.findIndex(
(task) => task.text.toLowerCase() === t.toLowerCase()
);
if (index === -1) {
// user input isn't the task name
if (isInt(t)) {
index = parseInt(t) - 1; // ACTUAL INDEX
} else {
tasksFailedToMarkUndone.push(t);
continue;
}
if (index < 0 || index > tasks[username].todos.length - 1) {
tasksFailedToMarkUndone.push(t);
continue;
} else {
tasksMarkedUndone.push(tasks[username].todos[index].text);
}
} else {
tasksMarkedUndone.push(tasks[username].todos[index].text);
}
tasks[username].todos[index].done = false;
}
if (tasksMarkedUndone.length === 0) {
return 1;
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
markedTasks: tasksMarkedUndone.join('", "'),
failedTasks: tasksFailedToMarkUndone.join('", "'),
};
} else {
let index = tasks[username].todos.findIndex(
(t) => t.text.toLowerCase() === task.toLowerCase()
);
if (index === -1) {
if (isInt(task)) {
index = parseInt(task) - 1; // ACTUAL INDEX
} else if (completedTasks.length === 1) {
index = tasks[username].todos.findIndex((t) => t.done);
} else {
return 1;
}
if (index < 0 || index > tasks[username].todos.length - 1) {
return 1;
}
tasks[username].todos[index].done = false;
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return {
markedTasks: tasks[username].todos[index].text,
failedTasks: "",
};
}
}
}
// 0: user has no tasks
// 1: invalid input
// [original task, new task]: succcess
function editTask(username, message) {
const tasks = JSON.parse(localStorage.tasks);
let noSpecifiedIndex = false;
if (!tasks[username] || tasks[username].todos.length === 0) {
return 0;
}
// example 'message': 1 new task
let index = parseInt(message.split(" ")[0]) - 1; // ACTUAL INDEX
let incompleteTaskCount = tasks[username].todos.filter(
(t) => !t.done
).length;
if (incompleteTaskCount === 1 && index !== 0) {
// find index of incomplete task
index = tasks[username].todos.findIndex((t) => !t.done);
noSpecifiedIndex = true;
}
if (isNaN(index)) {
return 1;
}
let newTask = message.split(" ").slice(1).join(" ");
if (noSpecifiedIndex) {
newTask = message;
}
// if newTask is empty or whitespace, return 1
if (!newTask || !newTask.trim()) {
return 1;
}
if (index < 0 || index > tasks[username].todos.length - 1) {
return 1;
}
let originalTask = tasks[username].todos[index].text;
tasks[username].todos[index].text = newTask;
localStorage.setItem(`tasks`, JSON.stringify(tasks));
if (!scrolling) {
renderTaskListToDOM();
}
return [originalTask, newTask];
}
// 0: user has no tasks
function checkTasks(username) {
const tasks = JSON.parse(localStorage.tasks);
// Go through keys of tasks, find match of lowercased username
username = Object.keys(tasks).find(
(user) => user.toLowerCase() === username.toLowerCase()
);
if (!tasks[username]) {
return 0;
}
// filter completed tasks
const incompleteTasks = tasks[username].todos.filter((t) => !t.done);
const completedTasks = tasks[username].todos.filter((t) => t.done);
// format incomplete tasks into string: 1. task 1 | 2. task 2 | 3. task 3...
let reply = `@${username} incomplete tasks (${incompleteTasks.length}) : `;
let taskIndex;
for (let i = 0; i < incompleteTasks.length; i++) {
// get index of task by task name
taskIndex =
tasks[username].todos.findIndex(
(t) =>
t.text.toLowerCase() ===
incompleteTasks[i].text.toLowerCase()
) + 1;
reply += `${taskIndex}. ${incompleteTasks[i].text} | `;
}
reply = reply.slice(0, -3);
// format completed tasks into string: 1. task 1 | 2. task 2 | 3. task 3...
// reply += `\n|| completed tasks (${completedTasks.length}) : `;
// for (let j = 0; j < completedTasks.length; j++) {
// taskIndex =
// tasks[username].todos.findIndex(
// (t) =>
// t.text.toLowerCase() ===
// completedTasks[j].text.toLowerCase()
// ) + 1;
// reply += `${taskIndex}. ${completedTasks[j].text} | `;
// }
// reply = reply.slice(0, -3);
return reply;
}
// 0: user has no tasks
function listTasks(username) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username]) {
return 0;
}
// filter completed tasks
const incompleteTasks = tasks[username].todos.filter((t) => !t.done);
if (incompleteTasks.length === 0) {
return 0;
}
// format incomplete tasks into string: 1. task 1 | 2. task 2 | 3. task 3...
let reply = `@${username} `;
let taskIndex;
for (let i = 0; i < incompleteTasks.length; i++) {
// get index of task by task name
taskIndex =
tasks[username].todos.findIndex(
(t) =>
t.text.toLowerCase() ===
incompleteTasks[i].text.toLowerCase()
) + 1;
reply += `${incompleteTasks[i].text}, `;
}
reply = reply.slice(0, -2);
return reply;
}
function incompleteTasksCount(username) {
const tasks = JSON.parse(localStorage.tasks);
if (!tasks[username]) {
return 0;
}
return tasks[username].todos.filter((t) => !t.done).length;
}
// 0: user has no tasks
function clearUserTasks(username) {
const tasks = JSON.parse(localStorage.tasks);
// find username where lowercase matches username
username = Object.keys(tasks).find(
(user) => user.toLowerCase() === username.toLowerCase()
);
if (!username) {
return 0;
}
tasks[username].todos = [];
localStorage.setItem(`tasks`, JSON.stringify(tasks));
}
function clearAllDoneTasks() {
const tasks = JSON.parse(localStorage.tasks);
for (const user in tasks) {
tasks[user].todos = tasks[user].todos.filter((t) => !t.done);
}
cancelAnimation();
checkToAnimate();
localStorage.setItem(`tasks`, JSON.stringify(tasks));
renderTaskListToDOM();
}
function clearAllTasks() {
resetDB();
cancelAnimation();
checkToAnimate();
renderTaskListToDOM();
}
function clearAllExceptStreamer(streamer) {
// clear all tasks except for broadcasters
const tasks = JSON.parse(localStorage.tasks);
for (const user in tasks) {
if (user.toLowerCase() !== streamer.toLowerCase()) {
tasks[user].todos = [];
}
}
localStorage.setItem(`tasks`, JSON.stringify(tasks));
clearAllDoneTasks();
cancelAnimation();
checkToAnimate();
renderTaskListToDOM();
}
function renderTaskListToDOM() {
const tasks = JSON.parse(localStorage.tasks);
const taskContainers = document.querySelectorAll(".task-container");
taskContainers.forEach((taskList) => {
taskList.innerHTML = "";
let totalTaskCount = 0;
let completedTasksCount = 0;
for (const user in tasks) {
const userTasks = tasks[user];
if (userTasks.todos.length === 0) {
continue;
}
const userColor = userTasks.userColor;
const taskDiv = document.createElement("div");
taskDiv.classList.add("task");
taskList.appendChild(taskDiv);
const usernameDiv = document.createElement("div");
usernameDiv.classList.add("username");
usernameDiv.innerText = user;
if (styles.usernameColor === "") {
usernameDiv.style.color = userColor;
} else {
usernameDiv.style.color = styles.usernameColor;
}
taskDiv.appendChild(usernameDiv);
const olListDiv = document.createElement("ol");
olListDiv.classList.add("user-tasks");
taskDiv.appendChild(olListDiv);
for (const task of userTasks.todos) {
const taskElement = document.createElement("li");
taskElement.classList.add("todo");
totalTaskCount++;
if (task.done) {
taskElement.classList.add("done");
completedTasksCount++;
}
taskElement.innerText = task.text;
olListDiv.appendChild(taskElement);
}
}
document.querySelector(
".task-count"
).innerText = `${completedTasksCount}/${totalTaskCount}`;
});
checkToAnimate();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// unit tests
async function tests1() {
let listOfStreamers = [
`cloudydayzzz`,
`berryspace`,
`MohFocus`,
`xeno_hiraeth`,
`euphie___`,
`unknownnie`,
`theyolotato`,
// `charliosaurus`,
// `jutstreams`,
// `mikewhatwhere`,
// `studypaws`,
// `pcc_lanezzz`,
// `workwithjandj`,
// `studylena`,
];
for (let i = 0; i < listOfStreamers.length; i++) {
addTask(
listOfStreamers[i],
"#ffc0cb",
`task 1, task 2, task 3, task 4, task 5`
);
await sleep(1000);
markTaskDone(listOfStreamers[i], `1, 2, 3, 4, 5`);
await sleep(1000);
}
}
async function checkToAnimate() {
// compare task container height and task wrapper height
let taskContainer = document.querySelector(".task-container");
let taskContainerHeight = taskContainer.scrollHeight;
let taskWrapper = document.querySelector(".task-wrapper");
let taskWrapperHeight = taskWrapper.clientHeight;
// scroll task wrapper up and down once
if (taskContainerHeight > taskWrapperHeight && !scrolling) {
if (!scrolling) {
document.querySelector(".secondary").style.display = "flex";
let finalHeight =
taskContainerHeight + configs.animation.gapBetweenScrolls;
let primaryKeyFrames = [
{ transform: `translateY(0)` },
{ transform: `translateY(-${finalHeight}px)` },
];
let secondaryKeyFrames = [
{ transform: `translateY(${finalHeight}px)` },
{ transform: `translateY(0)` },
];
let scrollingSpeed = (finalHeight / scrollSpeed) * 1000;
let options = {
duration: scrollingSpeed,
iterations: 1,
easing: "linear",
};
primaryAnimation = document
.querySelector(".primary")
.animate(primaryKeyFrames, options);
secondaryAnimation = document
.querySelector(".secondary")
.animate(secondaryKeyFrames, options);
primaryAnimation.play();
secondaryAnimation.play();
scrolling = true;
addAnimationListeners();
}
} else if (!scrolling) {
document.querySelector(".secondary").style.display = "none";
// cancel animations
if (primaryAnimation) {
primaryAnimation.cancel();
}
if (secondaryAnimation) {
secondaryAnimation.cancel();
}
scrolling = false;
}
}
function addAnimationListeners() {
if (primaryAnimation) {
primaryAnimation.addEventListener("finish", animationFinished);
primaryAnimation.addEventListener("cancel", animationFinished);
}
}
function animationFinished() {
scrolling = false;
renderTaskListToDOM();
checkToAnimate();
}
function cancelAnimation() {
if (primaryAnimation) {
primaryAnimation.cancel();
}
if (secondaryAnimation) {
secondaryAnimation.cancel();
}
scrolling = false;
console.log("animation cancelled");
}
(function () {
setupDB();
importStyles();
renderTaskListToDOM();
})();

402
scripts/tasks/counts.js Normal file
View file

@ -0,0 +1,402 @@
/**
* 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

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

1326
scripts/tasks/taskCore.js Normal file

File diff suppressed because it is too large Load diff

View file

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

27
scripts/testing/tests.js Normal file
View file

@ -0,0 +1,27 @@
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);
}
}

180
scripts/ui/renderer.js Normal file
View file

@ -0,0 +1,180 @@
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;
}

94
scripts/ui/styles.js Normal file
View file

@ -0,0 +1,94 @@
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);
}

23
scripts/ui/taskList.js Normal file
View file

@ -0,0 +1,23 @@
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);
});

4
styles/extraStyles.css Normal file
View file

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

View file

@ -17,10 +17,13 @@
--header-font-family: sans-serif; --header-font-family: sans-serif;
--header-font-color: white; --header-font-color: white;
--header-font-weight: bold; --header-font-weight: bold;
--task-count-margin-right: 0px;
--body-background-color: rgba(0, 0, 0, 0.7); --body-background-color: rgba(0, 0, 0, 0.7);
--body-border-width: 5px; --body-border-width: 5px;
--body-border-color: white; --body-border-color: white;
--body-border-radius: 5px; --body-border-radius: 5px;
--body-margin-top: 0px;
--number-of-lines: 1;
--line-height: 1.5; --line-height: 1.5;
--username-font-weight: bold; --username-font-weight: bold;
--task-background-color: rgba(0, 0, 0, 0.7); --task-background-color: rgba(0, 0, 0, 0.7);
@ -35,6 +38,7 @@
--task-margin-bottom: 5px; --task-margin-bottom: 5px;
--task-horizontal-padding: 5px; --task-horizontal-padding: 5px;
--task-vertical-padding: 5px; --task-vertical-padding: 5px;
--gap-between-scrolls: 0;
} }
* { * {
@ -43,8 +47,16 @@
box-sizing: border-box; box-sizing: border-box;
} }
body { @keyframes scroll {
background-color: gray; 0% {
transform: translateY(0);
}
100% {
transform: translateY(-100%);
}
}
.primary, .secondary {
animation: scroll linear infinite;
} }
#main-container { #main-container {
@ -56,6 +68,7 @@ body {
background-color: var(--task-list-background-color); background-color: var(--task-list-background-color);
border: solid var(--task-list-border-width) var(--task-list-border-color); border: solid var(--task-list-border-width) var(--task-list-border-color);
border-radius: var(--task-list-border-radius); border-radius: var(--task-list-border-radius);
transition: opacity 0.5s ease-in-out;
} }
#main-container .header { #main-container .header {
display: flex; display: flex;
@ -75,6 +88,12 @@ body {
#main-container .header .fade { #main-container .header .fade {
opacity: 0; 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 { #main-container .task-wrapper {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -89,8 +108,10 @@ body {
border-radius: var(--body-border-radius); border-radius: var(--body-border-radius);
z-index: 1; z-index: 1;
} }
#main-container .task-wrapper .task-container.secondary {
margin-top: var(--gap-between-scrolls);
}
#main-container .task-wrapper .task-container { #main-container .task-wrapper .task-container {
position: absolute;
width: inherit; width: inherit;
left: 0; left: 0;
display: flex; display: flex;
@ -98,6 +119,8 @@ body {
align-items: center; align-items: center;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-top: var(--body-margin-top);
transition: transform 0.4s ease-in-out;
} }
#main-container .task-wrapper .task-container .task { #main-container .task-wrapper .task-container .task {
padding: var(--task-vertical-padding) var(--task-horizontal-padding); padding: var(--task-vertical-padding) var(--task-horizontal-padding);
@ -109,15 +132,94 @@ body {
} }
#main-container .task-wrapper .task-container .task .username { #main-container .task-wrapper .task-container .task .username {
font-weight: var(--username-font-weight); font-weight: var(--username-font-weight);
font-size: var(--username-font-size);
} }
#main-container .task-wrapper .task-container .task .user-tasks { #main-container .task-wrapper .task-container .task .user-tasks {
list-style-position: inside; counter-reset: item;
} }
#main-container .task-wrapper .task-container .task .user-tasks .todo { #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; overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
#main-container .task-wrapper .task-container .task .user-tasks .done { #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; text-decoration: line-through;
color: var(--done-task-font-color);
}
#main-container .task-wrapper .task-container .task .user-tasks .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: -moz-fit-content;
width: fit-content;
margin-left: calc(var(--focus-task-horizontal-padding) * -1);
}/*# sourceMappingURL=taskList.css.map */ }/*# 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;
}

1
styles/taskList.css.map Normal file
View file

@ -0,0 +1 @@
{"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"}

240
styles/taskList.scss Normal file
View file

@ -0,0 +1,240 @@
: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);
}
}
}
}
}
}