mirror of
https://github.com/liyunze-coding/rython-task-bot-v2.git
synced 2026-09-22 07:24:27 +00:00
fixed sync issues
This commit is contained in:
parent
a4d75c930f
commit
c57bfd75fb
21 changed files with 101 additions and 1622 deletions
37
Code.cs
37
Code.cs
|
|
@ -464,7 +464,7 @@ public class TaskOperations
|
||||||
}
|
}
|
||||||
|
|
||||||
int newIndex = taskData[key].Tasks.Count - 1;
|
int newIndex = taskData[key].Tasks.Count - 1;
|
||||||
broadcast(new { mode = "refresh", task = taskName, completed = completed, focused = focused }, null);
|
broadcast(new { mode = "add", task = taskName, completed = completed, focused = focused }, null);
|
||||||
return new Response<(int, string)>(true, (newIndex, taskName), null);
|
return new Response<(int, string)>(true, (newIndex, taskName), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -481,7 +481,7 @@ public class TaskOperations
|
||||||
string oldName = userTasks[index].Name;
|
string oldName = userTasks[index].Name;
|
||||||
userTasks[index].Name = newTask;
|
userTasks[index].Name = newTask;
|
||||||
SaveIntoTasks(userTasks);
|
SaveIntoTasks(userTasks);
|
||||||
broadcast(new { mode = "refresh", index = index, task = newTask }, null);
|
broadcast(new { mode = "edit", index = index, task = newTask }, null);
|
||||||
return new Response<(string, string)>(true, (oldName, newTask), null);
|
return new Response<(string, string)>(true, (oldName, newTask), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -502,7 +502,7 @@ public class TaskOperations
|
||||||
UnfocusAll(tasks);
|
UnfocusAll(tasks);
|
||||||
tasks[n].Focused = true;
|
tasks[n].Focused = true;
|
||||||
SaveIntoTasks(tasks);
|
SaveIntoTasks(tasks);
|
||||||
broadcast(new { mode = "refresh", index = n }, null);
|
broadcast(new { mode = "focus", index = n }, null);
|
||||||
return new Response<(int, string)>(true, (n, tasks[n].Name), null);
|
return new Response<(int, string)>(true, (n, tasks[n].Name), null);
|
||||||
}
|
}
|
||||||
else if (indexByName > -1)
|
else if (indexByName > -1)
|
||||||
|
|
@ -513,7 +513,7 @@ public class TaskOperations
|
||||||
UnfocusAll(tasks);
|
UnfocusAll(tasks);
|
||||||
tasks[n].Focused = true;
|
tasks[n].Focused = true;
|
||||||
SaveIntoTasks(tasks);
|
SaveIntoTasks(tasks);
|
||||||
broadcast(new { mode = "refresh", index = n }, null);
|
broadcast(new { mode = "focus", index = n }, null);
|
||||||
return new Response<(int, string)>(true, (n, tasks[n].Name), null);
|
return new Response<(int, string)>(true, (n, tasks[n].Name), null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -521,7 +521,7 @@ public class TaskOperations
|
||||||
var response = AddTask(rawInput, false, true);
|
var response = AddTask(rawInput, false, true);
|
||||||
if (response.Success)
|
if (response.Success)
|
||||||
{
|
{
|
||||||
broadcast(new { mode = "refresh", index = response.Data.Item1 }, null);
|
broadcast(new { mode = "focus", index = response.Data.Item1 }, null);
|
||||||
return new Response<(int, string)>(true, response.Data, null);
|
return new Response<(int, string)>(true, response.Data, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -846,9 +846,9 @@ public class CPHInline
|
||||||
string completedTaskName = userTasks[focusedTaskIndex].Name;
|
string completedTaskName = userTasks[focusedTaskIndex].Name;
|
||||||
userTasks[focusedTaskIndex].Completed = true;
|
userTasks[focusedTaskIndex].Completed = true;
|
||||||
userTasks[focusedTaskIndex].Focused = false;
|
userTasks[focusedTaskIndex].Focused = false;
|
||||||
Broadcast(new { mode = "refresh", index = focusedTaskIndex }, null);
|
|
||||||
operations.SaveIntoTasks(userTasks);
|
operations.SaveIntoTasks(userTasks);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
|
Broadcast(new { mode = "done", index = focusedTaskIndex }, null);
|
||||||
Respond(BotResponses.NextSuccess(completedTaskName, focusResponse.Data.Item1 + 1, focusResponse.Data.Item2));
|
Respond(BotResponses.NextSuccess(completedTaskName, focusResponse.Data.Item1 + 1, focusResponse.Data.Item2));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -997,12 +997,15 @@ public class CPHInline
|
||||||
foreach (int i in taskIndices.OrderByDescending(n => n))
|
foreach (int i in taskIndices.OrderByDescending(n => n))
|
||||||
{
|
{
|
||||||
userTasks.RemoveAt(i);
|
userTasks.RemoveAt(i);
|
||||||
Broadcast(new { mode = "refresh", index = i }, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.SaveIntoTasks(userTasks);
|
operations.SaveIntoTasks(userTasks);
|
||||||
operations.Cleanup(true);
|
operations.Cleanup(true);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
|
foreach (int i in taskIndices.OrderByDescending(n => n))
|
||||||
|
{
|
||||||
|
Broadcast(new { mode = "remove", index = i }, null);
|
||||||
|
}
|
||||||
Respond(MessageBuilder.BuildRemoveMessage(tasksRemoved, tasksFailedToRemove, allTasks));
|
Respond(MessageBuilder.BuildRemoveMessage(tasksRemoved, tasksFailedToRemove, allTasks));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1024,7 +1027,7 @@ public class CPHInline
|
||||||
operations.RemoveUser(key);
|
operations.RemoveUser(key);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Respond(BotResponses.AdminDeleteSuccess);
|
Respond(BotResponses.AdminDeleteSuccess);
|
||||||
Broadcast(new { mode = "refresh", id = key }, null);
|
Broadcast(new { mode = "admindelete", id = key }, null);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1081,12 +1084,15 @@ public class CPHInline
|
||||||
{
|
{
|
||||||
userTasks[i].Completed = true;
|
userTasks[i].Completed = true;
|
||||||
userTasks[i].Focused = false;
|
userTasks[i].Focused = false;
|
||||||
Broadcast(new { mode = "refresh", index = i }, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IncrementDoneCount(taskIndices.Count);
|
IncrementDoneCount(taskIndices.Count);
|
||||||
operations.SaveIntoTasks(userTasks);
|
operations.SaveIntoTasks(userTasks);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
|
foreach (int i in taskIndices)
|
||||||
|
{
|
||||||
|
Broadcast(new { mode = "done", index = i }, null);
|
||||||
|
}
|
||||||
Respond(MessageBuilder.BuildCompletedMessage(tasksCompleted, tasksFailedToComplete, allTasks));
|
Respond(MessageBuilder.BuildCompletedMessage(tasksCompleted, tasksFailedToComplete, allTasks));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1096,7 +1102,7 @@ public class CPHInline
|
||||||
operations.Unfocus();
|
operations.Unfocus();
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Respond(BotResponses.Unfocused);
|
Respond(BotResponses.Unfocused);
|
||||||
Broadcast(new { mode = "refresh" }, null);
|
Broadcast(new { mode = "unfocus" }, null);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1144,11 +1150,14 @@ public class CPHInline
|
||||||
{
|
{
|
||||||
userTasks[i].Completed = false;
|
userTasks[i].Completed = false;
|
||||||
userTasks[i].Focused = false;
|
userTasks[i].Focused = false;
|
||||||
Broadcast(new { mode = "refresh", index = i }, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.SaveIntoTasks(userTasks);
|
operations.SaveIntoTasks(userTasks);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
|
foreach (int i in taskIndices)
|
||||||
|
{
|
||||||
|
Broadcast(new { mode = "undone", index = i }, null);
|
||||||
|
}
|
||||||
Respond(MessageBuilder.BuildUndoneMessage(tasksCompleted, tasksFailedToComplete));
|
Respond(MessageBuilder.BuildUndoneMessage(tasksCompleted, tasksFailedToComplete));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1169,7 +1178,7 @@ public class CPHInline
|
||||||
operations.ClearUserCompletedTasks(key);
|
operations.ClearUserCompletedTasks(key);
|
||||||
operations.Cleanup(false);
|
operations.Cleanup(false);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Broadcast(new { mode = "refresh" }, null);
|
Broadcast(new { mode = "clearmydone" }, null);
|
||||||
Respond(BotResponses.ClearMyDone);
|
Respond(BotResponses.ClearMyDone);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1179,7 +1188,7 @@ public class CPHInline
|
||||||
operations.ClearCompletedTasks();
|
operations.ClearCompletedTasks();
|
||||||
operations.Cleanup(false);
|
operations.Cleanup(false);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Broadcast(new { mode = "refresh" }, null);
|
Broadcast(new { mode = "cleardone" }, null);
|
||||||
Respond(BotResponses.ClearDone);
|
Respond(BotResponses.ClearDone);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1189,7 +1198,7 @@ public class CPHInline
|
||||||
operations.FilterToStreamers(GetStreamerUsernames());
|
operations.FilterToStreamers(GetStreamerUsernames());
|
||||||
operations.Cleanup(false);
|
operations.Cleanup(false);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Broadcast(new { mode = "refresh" }, null);
|
Broadcast(new { mode = "clearns" }, null);
|
||||||
Respond(BotResponses.ClearNotStreamer);
|
Respond(BotResponses.ClearNotStreamer);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
Websocket server running is required for browser source to work.
|
Websocket server running is required for browser source to work.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
|
|
||||||
1. Streamer.Bot -> Servers/Clients -> Websocket Server
|
1. Streamer.Bot -> Servers/Clients -> Websocket Server
|
||||||
2. Auto Start: `ON`; Click on `Start Server`
|
2. Auto Start: `ON`; Click on `Start Server`
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="./lib/streamerbot-client.js"></script>
|
<script src="../shared/lib/streamerbot-client.js"></script>
|
||||||
<script defer src="./config.js"></script>
|
<script defer src="./config.js"></script>
|
||||||
<script defer src="./scripts/emotes.js"></script>
|
<script defer src="../shared/scripts/emotes.js"></script>
|
||||||
<script defer src="./scripts/tasklist-view.js"></script>
|
<script defer src="../shared/scripts/tasklist-view.js"></script>
|
||||||
<script defer src="./scripts/streamerbot.js"></script>
|
<script defer src="../shared/scripts/streamerbot.js"></script>
|
||||||
<script defer src="./scripts/header-animation.js"></script>
|
<script defer src="./scripts/header-animation.js"></script>
|
||||||
<link rel="stylesheet" href="./styles/style.css" />
|
<link rel="stylesheet" href="./styles/style.css" />
|
||||||
<link rel="stylesheet" href="./styles/structure.css" />
|
<link rel="stylesheet" href="./styles/structure.css" />
|
||||||
|
|
|
||||||
|
|
@ -1,273 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
class EmoteManager {
|
|
||||||
#emotes = new Map();
|
|
||||||
#channelName = "";
|
|
||||||
#channelId = "";
|
|
||||||
#providers = ["7tv", "bttv", "ffz"];
|
|
||||||
#size = "1x";
|
|
||||||
#loaded = false;
|
|
||||||
#cacheTTL = 24 * 60 * 60 * 1000;
|
|
||||||
#CACHE_KEY = "emote-cache-v1";
|
|
||||||
#pattern = null;
|
|
||||||
#patternSize = 0;
|
|
||||||
|
|
||||||
constructor(config = {}) {
|
|
||||||
this.#channelName = config.channelName;
|
|
||||||
this.#channelId = config.channelId;
|
|
||||||
if (config.providers) this.#providers = config.providers;
|
|
||||||
if (config.size) this.#size = config.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
get loaded() {
|
|
||||||
return this.#loaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
get emoteCount() {
|
|
||||||
return this.#emotes.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
get providers() {
|
|
||||||
return [...this.#providers];
|
|
||||||
}
|
|
||||||
|
|
||||||
getEmote(name) {
|
|
||||||
return this.#emotes.get(name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
#buildPattern() {
|
|
||||||
if (this.#emotes.size === 0) {
|
|
||||||
this.#pattern = null;
|
|
||||||
this.#patternSize = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#pattern && this.#emotes.size === this.#patternSize) return;
|
|
||||||
|
|
||||||
const sorted = [...this.#emotes.keys()].sort(
|
|
||||||
(a, b) => b.length - a.length,
|
|
||||||
);
|
|
||||||
const escaped = sorted.map((n) =>
|
|
||||||
n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
||||||
);
|
|
||||||
this.#pattern = new RegExp(
|
|
||||||
`(?<![\\w])(?:${escaped.join("|")})(?![\\w])`,
|
|
||||||
"g",
|
|
||||||
);
|
|
||||||
this.#patternSize = this.#emotes.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseText(text) {
|
|
||||||
if (!text || !this.#loaded || this.#emotes.size === 0) return text;
|
|
||||||
|
|
||||||
this.#buildPattern();
|
|
||||||
if (!this.#pattern) return text;
|
|
||||||
|
|
||||||
return text.replace(this.#pattern, (match) => {
|
|
||||||
const emote = this.#emotes.get(match);
|
|
||||||
if (!emote) return match;
|
|
||||||
console.log(emote);
|
|
||||||
return `<img src="${emote.url}" alt="${match}" title="${match} (${emote.provider})" class="emote-img" />`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
const cached = this.#loadCache();
|
|
||||||
if (cached) {
|
|
||||||
this.#emotes = new Map(Object.entries(cached.emotes));
|
|
||||||
this.#loaded = true;
|
|
||||||
this.#patternSize = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.#fetchAll();
|
|
||||||
this.#loaded = true;
|
|
||||||
this.#patternSize = 0;
|
|
||||||
this.#saveCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
async forceRefresh() {
|
|
||||||
this.#emotes.clear();
|
|
||||||
this.#patternSize = 0;
|
|
||||||
await this.#fetchAll();
|
|
||||||
this.#loaded = true;
|
|
||||||
this.#patternSize = 0;
|
|
||||||
this.#saveCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
async #fetchAll() {
|
|
||||||
const fetchers = [];
|
|
||||||
if (this.#providers.includes("7tv")) fetchers.push(this.#fetch7TV());
|
|
||||||
if (this.#providers.includes("bttv")) fetchers.push(this.#fetchBTTV());
|
|
||||||
if (this.#providers.includes("ffz")) fetchers.push(this.#fetchFFZ());
|
|
||||||
await Promise.allSettled(fetchers);
|
|
||||||
}
|
|
||||||
|
|
||||||
async #fetch7TV() {
|
|
||||||
try {
|
|
||||||
const globalResp = await fetch(
|
|
||||||
"https://7tv.io/v3/emote-sets/global",
|
|
||||||
);
|
|
||||||
if (!globalResp.ok) return;
|
|
||||||
const globalData = await globalResp.json();
|
|
||||||
if (globalData.emotes) {
|
|
||||||
for (const emote of globalData.emotes) {
|
|
||||||
this.#add7TVEmote(emote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#channelId) {
|
|
||||||
const userResp = await fetch(
|
|
||||||
`https://7tv.io/v3/users/twitch/${this.#channelId}`,
|
|
||||||
);
|
|
||||||
if (!userResp.ok) return;
|
|
||||||
const userData = await userResp.json();
|
|
||||||
if (userData.emote_set?.emotes) {
|
|
||||||
for (const emote of userData.emote_set.emotes) {
|
|
||||||
this.#add7TVEmote(emote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("7TV emote fetch failed:", e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#add7TVEmote(emote) {
|
|
||||||
const entry = emote.data || emote;
|
|
||||||
const id = entry.id || emote.id;
|
|
||||||
const name = entry.name || emote.name;
|
|
||||||
if (!id || !name) return;
|
|
||||||
const nameLower = name;
|
|
||||||
const host = entry.host.url;
|
|
||||||
this.#emotes.set(nameLower, {
|
|
||||||
url: `https:${host}/${this.#size}.webp`,
|
|
||||||
provider: "7tv",
|
|
||||||
animated: entry.animated || false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async #fetchBTTV() {
|
|
||||||
try {
|
|
||||||
const globalResp = await fetch(
|
|
||||||
"https://api.betterttv.net/3/cached/emotes/global",
|
|
||||||
);
|
|
||||||
if (!globalResp.ok) return;
|
|
||||||
const globalData = await globalResp.json();
|
|
||||||
if (Array.isArray(globalData)) {
|
|
||||||
for (const emote of globalData) {
|
|
||||||
this.#emotes.set(emote.code, {
|
|
||||||
url: `https://cdn.betterttv.net/emote/${emote.id}/${this.#size}`,
|
|
||||||
provider: "bttv",
|
|
||||||
animated: emote.imageType === "gif",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#channelId) {
|
|
||||||
const userResp = await fetch(
|
|
||||||
`https://api.betterttv.net/3/cached/users/twitch/${this.#channelId}`,
|
|
||||||
);
|
|
||||||
if (!userResp.ok) return;
|
|
||||||
const userData = await userResp.json();
|
|
||||||
const allEmotes = [
|
|
||||||
...(userData.channelEmotes || []),
|
|
||||||
...(userData.sharedEmotes || []),
|
|
||||||
];
|
|
||||||
for (const emote of allEmotes) {
|
|
||||||
this.#emotes.set(emote.code, {
|
|
||||||
url: `https://cdn.betterttv.net/emote/${emote.id}/${this.#size}`,
|
|
||||||
provider: "bttv",
|
|
||||||
animated: emote.imageType === "gif",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("BTTV emote fetch failed:", e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async #fetchFFZ() {
|
|
||||||
try {
|
|
||||||
const globalResp = await fetch(
|
|
||||||
"https://api.frankerfacez.com/v1/set/global",
|
|
||||||
);
|
|
||||||
if (!globalResp.ok) return;
|
|
||||||
const globalData = await globalResp.json();
|
|
||||||
if (globalData.sets) {
|
|
||||||
for (const setId in globalData.sets) {
|
|
||||||
const set = globalData.sets[setId];
|
|
||||||
for (const emote of set.emoticons || []) {
|
|
||||||
if (emote.modifier) continue;
|
|
||||||
this.#emotes.set(emote.name, {
|
|
||||||
url: emote.urls[this.#toFFZSize(this.#size)],
|
|
||||||
provider: "ffz",
|
|
||||||
animated: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ffzId = this.#channelName || this.#channelId;
|
|
||||||
if (ffzId) {
|
|
||||||
const roomUrl = this.#channelId
|
|
||||||
? `https://api.frankerfacez.com/v1/room/id/${this.#channelId}`
|
|
||||||
: `https://api.frankerfacez.com/v1/room/${this.#channelName}`;
|
|
||||||
const roomResp = await fetch(roomUrl);
|
|
||||||
if (!roomResp.ok) return;
|
|
||||||
const roomData = await roomResp.json();
|
|
||||||
if (roomData.sets) {
|
|
||||||
for (const setId in roomData.sets) {
|
|
||||||
const set = roomData.sets[setId];
|
|
||||||
for (const emote of set.emoticons || []) {
|
|
||||||
if (emote.modifier) continue;
|
|
||||||
this.#emotes.set(emote.name, {
|
|
||||||
url: emote.urls[this.#toFFZSize(this.#size)],
|
|
||||||
provider: "ffz",
|
|
||||||
animated: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("FFZ emote fetch failed:", e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#toFFZSize(size) {
|
|
||||||
switch (size) {
|
|
||||||
case "3x":
|
|
||||||
case "4x":
|
|
||||||
return "4";
|
|
||||||
case "2x":
|
|
||||||
return "2";
|
|
||||||
default:
|
|
||||||
return "1";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#saveCache() {
|
|
||||||
try {
|
|
||||||
const cache = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
channelId: this.#channelId,
|
|
||||||
emotes: Object.fromEntries(this.#emotes),
|
|
||||||
};
|
|
||||||
localStorage.setItem(this.#CACHE_KEY, JSON.stringify(cache));
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#loadCache() {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(this.#CACHE_KEY);
|
|
||||||
if (!raw) return null;
|
|
||||||
const cache = JSON.parse(raw);
|
|
||||||
if (cache.channelId !== this.#channelId) return null;
|
|
||||||
if (Date.now() - cache.timestamp > this.#cacheTTL) return null;
|
|
||||||
return cache;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
// STREAMER.BOT SETTINGS
|
|
||||||
|
|
||||||
const client = new StreamerbotClient({
|
|
||||||
host: configs.streamerBotSettings.host,
|
|
||||||
port: configs.streamerBotSettings.port,
|
|
||||||
endpoint: configs.streamerBotSettings.endpoint,
|
|
||||||
onConnect: onConnect,
|
|
||||||
onDisconnect: onDisconnect,
|
|
||||||
onError: onError,
|
|
||||||
});
|
|
||||||
|
|
||||||
function getAllLocalstorage() {
|
|
||||||
const allLocalStorage = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
const key = localStorage.key(i);
|
|
||||||
allLocalStorage[key] = localStorage.getItem(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(allLocalStorage);
|
|
||||||
}
|
|
||||||
|
|
||||||
client.on("General.Custom", (data) => onCustom(data));
|
|
||||||
if (configs.userColorSettings.autoUserColor) {
|
|
||||||
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
|
|
||||||
client.on("Kick.ChatMessage", (data) => onChatMessage(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
let taskList;
|
|
||||||
let userColors = {};
|
|
||||||
|
|
||||||
function parseHexColor(hex) {
|
|
||||||
if (typeof hex !== "string") return null;
|
|
||||||
const s = hex.trim();
|
|
||||||
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
|
|
||||||
if (!m) return null;
|
|
||||||
let h = m[1].toLowerCase();
|
|
||||||
if (h.length === 3) {
|
|
||||||
h = h
|
|
||||||
.split("")
|
|
||||||
.map((c) => c + c)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
const r = parseInt(h.slice(0, 2), 16);
|
|
||||||
const g = parseInt(h.slice(2, 4), 16);
|
|
||||||
const b = parseInt(h.slice(4, 6), 16);
|
|
||||||
return { r, g, b };
|
|
||||||
}
|
|
||||||
|
|
||||||
function relativeLuminance({ r, g, b }) {
|
|
||||||
const toLinear = (v) => {
|
|
||||||
const s = v / 255;
|
|
||||||
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
||||||
};
|
|
||||||
const R = toLinear(r);
|
|
||||||
const G = toLinear(g);
|
|
||||||
const B = toLinear(b);
|
|
||||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onChatMessage(data) {
|
|
||||||
let userColor = data.data.user.color; // hex colour #FF69B4
|
|
||||||
|
|
||||||
if (userColor != undefined || userColor != "undefined") {
|
|
||||||
// update localstorage
|
|
||||||
// get id: platform-userID
|
|
||||||
let userId = data.data.user.id;
|
|
||||||
let key = `twitch-${userId}`;
|
|
||||||
|
|
||||||
localStorage.setItem(`${key}-color`, userColor);
|
|
||||||
userColors[`${key}-color`] = userColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDisconnect() {
|
|
||||||
showConnectionError("Connection Failed: Unable to connect to Streamer.bot");
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(err) {
|
|
||||||
showConnectionError(
|
|
||||||
"Connection Failed: " + (err?.message || "Unknown error"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showConnectionError(message) {
|
|
||||||
// Remove existing popup if any
|
|
||||||
const existing = document.getElementById("connection-error");
|
|
||||||
if (existing) existing.remove();
|
|
||||||
|
|
||||||
const popup = document.createElement("div");
|
|
||||||
popup.id = "connection-error";
|
|
||||||
popup.textContent = message;
|
|
||||||
Object.assign(popup.style, {
|
|
||||||
position: "fixed",
|
|
||||||
top: "20px",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
background: "#e53935",
|
|
||||||
color: "#fff",
|
|
||||||
padding: "12px 24px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
fontFamily: "'Fredoka', sans-serif",
|
|
||||||
fontSize: "1.1rem",
|
|
||||||
fontWeight: "700",
|
|
||||||
zIndex: "9999",
|
|
||||||
boxShadow: "0 4px 12px rgba(0,0,0,0.4)",
|
|
||||||
textAlign: "center",
|
|
||||||
});
|
|
||||||
document.body.appendChild(popup);
|
|
||||||
|
|
||||||
// Auto-dismiss after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
popup.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
||||||
duration: 300,
|
|
||||||
fill: "forwards",
|
|
||||||
}).onfinish = () => popup.remove();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
const response = await client.getGlobal("rython-task-bot", true);
|
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
if (response.status !== "ok" || !response.variable?.value) return;
|
|
||||||
|
|
||||||
const users = JSON.parse(response.variable.value);
|
|
||||||
const sections = transformToSections(users);
|
|
||||||
|
|
||||||
taskList.load(sections);
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOAD TASK LIST
|
|
||||||
async function onConnect() {
|
|
||||||
taskList = new TaskList(".task-panel");
|
|
||||||
|
|
||||||
if (configs.emoteSettings.enabled) {
|
|
||||||
let broadcaster = await client.getBroadcaster();
|
|
||||||
|
|
||||||
console.log(broadcaster);
|
|
||||||
|
|
||||||
let broadcasterName =
|
|
||||||
broadcaster.platforms.twitch?.broadcastUser ??
|
|
||||||
broadcaster.platforms.youtube?.broadcastUser ??
|
|
||||||
broadcaster.platforms.kick?.broadcastUser;
|
|
||||||
|
|
||||||
let broadcasterId =
|
|
||||||
broadcaster.platforms.twitch?.broadcastUserId ??
|
|
||||||
broadcaster.platforms.youtube?.broadcastUserId ??
|
|
||||||
broadcaster.platforms.kick?.broadcastUserId;
|
|
||||||
|
|
||||||
window.emoteManager = new EmoteManager({
|
|
||||||
channelName: broadcasterName,
|
|
||||||
channelId: broadcasterId,
|
|
||||||
providers: configs.emoteSettings.providers,
|
|
||||||
size: configs.emoteSettings.size,
|
|
||||||
});
|
|
||||||
|
|
||||||
window.emoteManager.init().then(() => {
|
|
||||||
if (taskList) {
|
|
||||||
taskList.load(taskList.getData());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function transformToSections(users) {
|
|
||||||
return Object.entries(users).map(([userId, userData]) => ({
|
|
||||||
id: userId,
|
|
||||||
title: userData.Username,
|
|
||||||
tasks: userData.Tasks.map((task) => ({
|
|
||||||
text: task.Name,
|
|
||||||
done: task.Completed,
|
|
||||||
focused: task.Focused,
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update task list action by action
|
|
||||||
function onCustom(payload) {
|
|
||||||
const data = payload.data;
|
|
||||||
if (!data.source && data.source != "rython-task-bot") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!taskList) return;
|
|
||||||
const body = data.body;
|
|
||||||
const id = data.id;
|
|
||||||
const username = data.username;
|
|
||||||
|
|
||||||
switch (body.mode) {
|
|
||||||
case "refresh":
|
|
||||||
refresh();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,591 +0,0 @@
|
||||||
// Task list view
|
|
||||||
|
|
||||||
class TaskList {
|
|
||||||
#data = [];
|
|
||||||
#firstRender = true;
|
|
||||||
#pendingRemovals = 0;
|
|
||||||
|
|
||||||
#scroll = {
|
|
||||||
offset: 0,
|
|
||||||
contentW: 0,
|
|
||||||
viewportW: 0,
|
|
||||||
speed: 50,
|
|
||||||
raf: null,
|
|
||||||
lastT: 0,
|
|
||||||
active: false,
|
|
||||||
wantStop: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
#els = {};
|
|
||||||
|
|
||||||
#ANIM_DURATION = 400;
|
|
||||||
#ANIM_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
||||||
|
|
||||||
#parseHexColor(hex) {
|
|
||||||
if (typeof hex !== "string") return null;
|
|
||||||
const s = hex.trim();
|
|
||||||
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
|
|
||||||
if (!m) return null;
|
|
||||||
let h = m[1].toLowerCase();
|
|
||||||
if (h.length === 3) {
|
|
||||||
h = h
|
|
||||||
.split("")
|
|
||||||
.map((c) => c + c)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
const r = parseInt(h.slice(0, 2), 16);
|
|
||||||
const g = parseInt(h.slice(2, 4), 16);
|
|
||||||
const b = parseInt(h.slice(4, 6), 16);
|
|
||||||
return { r, g, b };
|
|
||||||
}
|
|
||||||
|
|
||||||
#relativeLuminance({ r, g, b }) {
|
|
||||||
const toLinear = (v) => {
|
|
||||||
const s = v / 255;
|
|
||||||
return s <= 0.04045
|
|
||||||
? s / 12.92
|
|
||||||
: Math.pow((s + 0.055) / 1.055, 2.4);
|
|
||||||
};
|
|
||||||
const R = toLinear(r);
|
|
||||||
const G = toLinear(g);
|
|
||||||
const B = toLinear(b);
|
|
||||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(container) {
|
|
||||||
const el =
|
|
||||||
typeof container === "string"
|
|
||||||
? document.querySelector(container)
|
|
||||||
: container;
|
|
||||||
|
|
||||||
this.#els = {
|
|
||||||
track: el.querySelector(".scroll-track"),
|
|
||||||
content: el.querySelector(".scroll-content"),
|
|
||||||
viewport: el.querySelector(".scroll-viewport"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── public API ─────────────────────────────────────
|
|
||||||
|
|
||||||
load(sections) {
|
|
||||||
this.#data = structuredClone(sections);
|
|
||||||
this.#render();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
addSection(section) {
|
|
||||||
this.#data.push(structuredClone(section));
|
|
||||||
this.#render();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeSection(id) {
|
|
||||||
const i = this.#data.findIndex((s) => s.id === id);
|
|
||||||
if (i === -1) return this;
|
|
||||||
this.#data.splice(i, 1);
|
|
||||||
this.#render();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSection(id, updater) {
|
|
||||||
const section = this.#data.find((s) => s.id === id);
|
|
||||||
if (!section) return this;
|
|
||||||
updater(section);
|
|
||||||
this.#render();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
addTask(sectionId, task, sectionTitle) {
|
|
||||||
if (!this.#data.find((s) => s.id === sectionId)) {
|
|
||||||
this.addSection({
|
|
||||||
id: sectionId,
|
|
||||||
title: sectionTitle || sectionId,
|
|
||||||
tasks: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.updateSection(sectionId, (s) => {
|
|
||||||
s.tasks.push(structuredClone(task));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTask(sectionId, taskIndex) {
|
|
||||||
this.updateSection(sectionId, (s) => {
|
|
||||||
s.tasks.splice(taskIndex, 1);
|
|
||||||
});
|
|
||||||
this.#data = this.#data.filter((s) => s.tasks.length > 0);
|
|
||||||
this.#render();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
editTask(sectionId, taskIndex, newText, userColor) {
|
|
||||||
return this.updateSection(sectionId, (s) => {
|
|
||||||
if (s.tasks[taskIndex]) {
|
|
||||||
s.tasks[taskIndex].text = newText;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
doneTask(sectionId, taskIndex) {
|
|
||||||
return this.updateSection(sectionId, (s) => {
|
|
||||||
if (s.tasks[taskIndex]) {
|
|
||||||
s.tasks[taskIndex].done = true;
|
|
||||||
s.tasks[taskIndex].focused = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
undoneTask(sectionId, taskIndex) {
|
|
||||||
return this.updateSection(sectionId, (s) => {
|
|
||||||
if (s.tasks[taskIndex]) {
|
|
||||||
s.tasks[taskIndex].done = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
focusTask(sectionId, taskIndex) {
|
|
||||||
// unfocus any previously focused task
|
|
||||||
this.unfocusTask(sectionId);
|
|
||||||
|
|
||||||
return this.updateSection(sectionId, (s) => {
|
|
||||||
if (s.tasks[taskIndex]) {
|
|
||||||
s.tasks[taskIndex].focused = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unfocusTask(sectionId) {
|
|
||||||
return this.updateSection(sectionId, (s) => {
|
|
||||||
for (let i = 0; i < s.tasks.length; i++) {
|
|
||||||
s.tasks[i].focused = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
clearmydone(sectionId) {
|
|
||||||
this.updateSection(sectionId, (s) => {
|
|
||||||
s.tasks = s.tasks.filter((t) => !t.done);
|
|
||||||
});
|
|
||||||
this.#data = this.#data.filter((s) => s.tasks.length > 0);
|
|
||||||
this.#render();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
cleardone() {
|
|
||||||
this.#data.forEach((s) => {
|
|
||||||
s.tasks = s.tasks.filter((t) => !t.done);
|
|
||||||
});
|
|
||||||
this.#data = this.#data.filter((s) => s.tasks.length > 0);
|
|
||||||
this.#render();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
getData() {
|
|
||||||
return structuredClone(this.#data);
|
|
||||||
}
|
|
||||||
|
|
||||||
get sectionCount() {
|
|
||||||
return this.#data.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
get taskCount() {
|
|
||||||
return this.#data.reduce((sum, s) => sum + s.tasks.length, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
get doneCount() {
|
|
||||||
return this.#data.reduce(
|
|
||||||
(sum, s) => sum + s.tasks.filter((t) => t.done).length,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.#forceStop();
|
|
||||||
this.#els.content.innerHTML = "";
|
|
||||||
this.#data = [];
|
|
||||||
this.#firstRender = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── render ─────────────────────────────────────────
|
|
||||||
|
|
||||||
#render() {
|
|
||||||
if (this.#firstRender) {
|
|
||||||
this.#data.forEach((s) =>
|
|
||||||
this.#els.content.appendChild(this.#createSectionEl(s)),
|
|
||||||
);
|
|
||||||
this.#firstRender = false;
|
|
||||||
} else {
|
|
||||||
this.#patchContainer(this.#els.content, this.#data, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#pendingRemovals === 0) {
|
|
||||||
this.#syncScroll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── animations ─────────────────────────────────────
|
|
||||||
|
|
||||||
#animateIn(el) {
|
|
||||||
el.style.overflow = "hidden";
|
|
||||||
|
|
||||||
const w = el.scrollWidth;
|
|
||||||
el.style.width = "0px";
|
|
||||||
el.style.opacity = "0";
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const anim = el.animate(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
width: "0px",
|
|
||||||
opacity: 0,
|
|
||||||
transform: "translateX(-8px)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
width: w + "px",
|
|
||||||
opacity: 1,
|
|
||||||
transform: "translateX(0)",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
duration: this.#ANIM_DURATION,
|
|
||||||
easing: this.#ANIM_EASING,
|
|
||||||
fill: "forwards",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
anim.onfinish = () => {
|
|
||||||
el.style.width = "";
|
|
||||||
el.style.opacity = "";
|
|
||||||
el.style.overflow = "";
|
|
||||||
el.style.transform = "";
|
|
||||||
anim.cancel();
|
|
||||||
this.#syncScroll();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#animateRemove(el) {
|
|
||||||
this.#pendingRemovals++;
|
|
||||||
const w = el.scrollWidth;
|
|
||||||
|
|
||||||
const anim = el.animate(
|
|
||||||
[
|
|
||||||
{ width: w + "px", opacity: 1, transform: "translateX(0)" },
|
|
||||||
{ width: "0px", opacity: 0, transform: "translateX(-8px)" },
|
|
||||||
],
|
|
||||||
{
|
|
||||||
duration: this.#ANIM_DURATION,
|
|
||||||
easing: this.#ANIM_EASING,
|
|
||||||
fill: "forwards",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
anim.onfinish = () => {
|
|
||||||
el.remove();
|
|
||||||
this.#pendingRemovals--;
|
|
||||||
|
|
||||||
if (this.#pendingRemovals === 0) {
|
|
||||||
this.#syncScroll();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#animateStrikethrough(el) {
|
|
||||||
el.animate(
|
|
||||||
[
|
|
||||||
{ opacity: 0.5, transform: "scale(0.98)" },
|
|
||||||
{ opacity: 1, transform: "scale(1)" },
|
|
||||||
],
|
|
||||||
{ duration: 250, easing: "ease-out" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── dom helpers ────────────────────────────────────
|
|
||||||
|
|
||||||
#parseTaskText(text) {
|
|
||||||
if (window.emoteManager && window.emoteManager.loaded) {
|
|
||||||
return window.emoteManager.parseText(text);
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createTaskEl(task, index) {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.className =
|
|
||||||
"task" +
|
|
||||||
(task.done ? " done" : "") +
|
|
||||||
(task.focused ? " focused" : "");
|
|
||||||
div.dataset.text = task.text;
|
|
||||||
const numberSpan = document.createElement("span");
|
|
||||||
numberSpan.className = "task-number";
|
|
||||||
numberSpan.textContent = `${index + 1}.`;
|
|
||||||
|
|
||||||
const textSpan = document.createElement("span");
|
|
||||||
textSpan.className = "task-text";
|
|
||||||
textSpan.innerHTML = this.#parseTaskText(task.text);
|
|
||||||
|
|
||||||
div.replaceChildren(numberSpan, textSpan);
|
|
||||||
return div;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createSectionEl(section) {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.className = "section";
|
|
||||||
div.dataset.key = section.id;
|
|
||||||
if (section.title) {
|
|
||||||
const t = document.createElement("div");
|
|
||||||
t.className = "section-title";
|
|
||||||
t.textContent = section.title;
|
|
||||||
let color =
|
|
||||||
section.color ?? localStorage.getItem(`${section.id}-color`);
|
|
||||||
if (
|
|
||||||
configs.userColorSettings.autoUserColor &&
|
|
||||||
color != undefined &&
|
|
||||||
color != "undefined" &&
|
|
||||||
color != null
|
|
||||||
) {
|
|
||||||
t.style.setProperty("--user-color", color);
|
|
||||||
t.classList.add("has-user-color");
|
|
||||||
}
|
|
||||||
|
|
||||||
div.appendChild(t);
|
|
||||||
}
|
|
||||||
section.tasks.forEach((task, i) =>
|
|
||||||
div.appendChild(this.#createTaskEl(task, i)),
|
|
||||||
);
|
|
||||||
return div;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── patching ───────────────────────────────────────
|
|
||||||
|
|
||||||
#patchTasks(sectionEl, tasks, animate) {
|
|
||||||
const existing = [...sectionEl.querySelectorAll(":scope > .task")];
|
|
||||||
|
|
||||||
tasks.forEach((task, i) => {
|
|
||||||
if (i < existing.length) {
|
|
||||||
const el = existing[i];
|
|
||||||
const wantClass =
|
|
||||||
"task" +
|
|
||||||
(task.done ? " done" : "") +
|
|
||||||
(task.focused ? " focused" : "");
|
|
||||||
if (el.className !== wantClass) {
|
|
||||||
el.className = wantClass;
|
|
||||||
if (animate) this.#animateStrikethrough(el);
|
|
||||||
}
|
|
||||||
if (el.dataset.text !== task.text) {
|
|
||||||
el.dataset.text = task.text;
|
|
||||||
const textEl =
|
|
||||||
el.querySelector(".task-text") ||
|
|
||||||
el.querySelector("span:last-child");
|
|
||||||
textEl.innerHTML = this.#parseTaskText(task.text);
|
|
||||||
}
|
|
||||||
el.querySelector(".task-number").textContent = `${i + 1}.`;
|
|
||||||
|
|
||||||
// Update color if changed
|
|
||||||
if (task.color != undefined && task.color != "undefined") {
|
|
||||||
el.style.setProperty("--user-color", task.color);
|
|
||||||
el.classList.add("has-user-color");
|
|
||||||
} else {
|
|
||||||
el.style.removeProperty("--user-color");
|
|
||||||
el.classList.remove("has-user-color");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const newEl = this.#createTaskEl(task, i);
|
|
||||||
sectionEl.appendChild(newEl);
|
|
||||||
if (animate) this.#animateIn(newEl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = existing.length - 1; i >= tasks.length; i--) {
|
|
||||||
if (animate) {
|
|
||||||
this.#animateRemove(existing[i]);
|
|
||||||
} else {
|
|
||||||
existing[i].remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#patchContainer(container, sections, animate) {
|
|
||||||
const oldMap = new Map();
|
|
||||||
container.querySelectorAll(":scope > .section").forEach((el) => {
|
|
||||||
oldMap.set(el.dataset.key, el);
|
|
||||||
});
|
|
||||||
|
|
||||||
let cursor = container.firstElementChild;
|
|
||||||
|
|
||||||
sections.forEach((section) => {
|
|
||||||
const key = section.id;
|
|
||||||
const existing = oldMap.get(key);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
this.#patchTasks(existing, section.tasks, animate);
|
|
||||||
if (existing !== cursor) {
|
|
||||||
container.insertBefore(existing, cursor);
|
|
||||||
} else {
|
|
||||||
cursor = cursor.nextElementSibling;
|
|
||||||
}
|
|
||||||
oldMap.delete(key);
|
|
||||||
} else {
|
|
||||||
const newEl = this.#createSectionEl(section);
|
|
||||||
container.insertBefore(newEl, cursor);
|
|
||||||
if (animate) this.#animateIn(newEl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
oldMap.forEach((el) => {
|
|
||||||
if (animate) {
|
|
||||||
this.#animateRemove(el);
|
|
||||||
} else {
|
|
||||||
el.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── scroll ─────────────────────────────────────────
|
|
||||||
|
|
||||||
#tick = (now) => {
|
|
||||||
const s = this.#scroll;
|
|
||||||
const dt = (now - s.lastT) / 1000;
|
|
||||||
s.lastT = now;
|
|
||||||
|
|
||||||
if (dt < 0.2) {
|
|
||||||
s.contentW = this.#els.content.scrollWidth;
|
|
||||||
s.offset += s.speed * dt;
|
|
||||||
|
|
||||||
if (s.contentW > 0 && s.offset >= s.contentW) {
|
|
||||||
s.offset -= s.contentW;
|
|
||||||
|
|
||||||
if (s.wantStop) {
|
|
||||||
s.wantStop = false;
|
|
||||||
this.#forceStop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#els.track.style.transform = `translateX(${-s.offset}px)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
s.raf = requestAnimationFrame(this.#tick);
|
|
||||||
};
|
|
||||||
|
|
||||||
#startScroll() {
|
|
||||||
const s = this.#scroll;
|
|
||||||
s.wantStop = false;
|
|
||||||
if (s.active) return;
|
|
||||||
s.active = true;
|
|
||||||
s.lastT = performance.now();
|
|
||||||
s.raf = requestAnimationFrame(this.#tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
#requestStop() {
|
|
||||||
if (!this.#scroll.active) return;
|
|
||||||
this.#scroll.wantStop = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#forceStop() {
|
|
||||||
const s = this.#scroll;
|
|
||||||
s.active = false;
|
|
||||||
s.wantStop = false;
|
|
||||||
cancelAnimationFrame(s.raf);
|
|
||||||
s.raf = null;
|
|
||||||
s.offset = 0;
|
|
||||||
this.#els.track.style.transform = "";
|
|
||||||
|
|
||||||
const clone = this.#els.track.querySelector(".scroll-clone");
|
|
||||||
if (clone) clone.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
#syncScroll() {
|
|
||||||
const s = this.#scroll;
|
|
||||||
s.contentW = this.#els.content.scrollWidth;
|
|
||||||
s.viewportW = this.#els.viewport.clientWidth;
|
|
||||||
const needs = s.contentW > s.viewportW;
|
|
||||||
|
|
||||||
let clone = this.#els.track.querySelector(".scroll-clone");
|
|
||||||
|
|
||||||
if (needs) {
|
|
||||||
if (!clone) {
|
|
||||||
clone = this.#els.content.cloneNode(true);
|
|
||||||
clone.id = "";
|
|
||||||
clone.classList.add("scroll-clone");
|
|
||||||
this.#els.track.appendChild(clone);
|
|
||||||
} else {
|
|
||||||
this.#patchContainer(clone, this.#data, false);
|
|
||||||
}
|
|
||||||
if (s.offset >= s.contentW) {
|
|
||||||
s.offset %= s.contentW;
|
|
||||||
}
|
|
||||||
this.#startScroll();
|
|
||||||
} else {
|
|
||||||
if (s.active) {
|
|
||||||
if (clone) this.#patchContainer(clone, this.#data, false);
|
|
||||||
this.#requestStop();
|
|
||||||
} else {
|
|
||||||
if (clone) clone.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── usage ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
// const list2 = new TaskList(".task-panel");
|
|
||||||
|
|
||||||
// list2.load([
|
|
||||||
// {
|
|
||||||
// title: "someone",
|
|
||||||
// tasks: [
|
|
||||||
// { text: "dinner" },
|
|
||||||
// { text: "watch bad bunny halftime show" },
|
|
||||||
// { text: "tie tf down", done: true },
|
|
||||||
// { text: "boy dinner", done: true },
|
|
||||||
// { text: "3 hour SOOP" },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "Cape_Codder",
|
|
||||||
// tasks: [
|
|
||||||
// { text: "Part 2", done: true },
|
|
||||||
// { text: "Ch 1-2", done: true },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "sunflawer",
|
|
||||||
// tasks: [
|
|
||||||
// { text: "fix walls from home office" },
|
|
||||||
// { text: "sand walls zzzsdkjhfsk", done: true },
|
|
||||||
// { text: "meal prep" },
|
|
||||||
// { text: "buy wrist brace maybe" },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "extra_user1",
|
|
||||||
// tasks: [
|
|
||||||
// { text: "go grocery shopping" },
|
|
||||||
// { text: "clean the kitchen", done: true },
|
|
||||||
// { text: "respond to emails" },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
// demo
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// list2.addTask("someone2", { text: "new task just dropped" });
|
|
||||||
// }, 1000);
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// list2.;
|
|
||||||
// }, 3000);
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// list.toggleTask("sunflawer", 0);
|
|
||||||
// }, 10000);
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// list.removeSection("Cape_Codder");
|
|
||||||
// }, 12000);
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// list.removeTask("extra_user1", 1);
|
|
||||||
// }, 14000);
|
|
||||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
|
@ -9,17 +9,6 @@ const client = new StreamerbotClient({
|
||||||
onError: onError,
|
onError: onError,
|
||||||
});
|
});
|
||||||
|
|
||||||
function getAllLocalstorage() {
|
|
||||||
const allLocalStorage = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
const key = localStorage.key(i);
|
|
||||||
allLocalStorage[key] = localStorage.getItem(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(allLocalStorage);
|
|
||||||
}
|
|
||||||
|
|
||||||
client.on("General.Custom", (data) => onCustom(data));
|
client.on("General.Custom", (data) => onCustom(data));
|
||||||
if (configs.userColorSettings.autoUserColor) {
|
if (configs.userColorSettings.autoUserColor) {
|
||||||
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
|
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
|
||||||
|
|
@ -29,49 +18,35 @@ if (configs.userColorSettings.autoUserColor) {
|
||||||
let taskList;
|
let taskList;
|
||||||
let userColors = {};
|
let userColors = {};
|
||||||
|
|
||||||
function parseHexColor(hex) {
|
function getUserColor(sectionId) {
|
||||||
if (typeof hex !== "string") return null;
|
return (
|
||||||
const s = hex.trim();
|
userColors[`${sectionId}-color`] ??
|
||||||
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
|
localStorage.getItem(`${sectionId}-color`)
|
||||||
if (!m) return null;
|
);
|
||||||
let h = m[1].toLowerCase();
|
|
||||||
if (h.length === 3) {
|
|
||||||
h = h
|
|
||||||
.split("")
|
|
||||||
.map((c) => c + c)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
const r = parseInt(h.slice(0, 2), 16);
|
|
||||||
const g = parseInt(h.slice(2, 4), 16);
|
|
||||||
const b = parseInt(h.slice(4, 6), 16);
|
|
||||||
return { r, g, b };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function relativeLuminance({ r, g, b }) {
|
function taskColorFromSettings(sectionId) {
|
||||||
const toLinear = (v) => {
|
const usernameColor = getUserColor(sectionId);
|
||||||
const s = v / 255;
|
if (
|
||||||
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
configs.userColorSettings.autoUserColor &&
|
||||||
};
|
usernameColor != undefined &&
|
||||||
const R = toLinear(r);
|
usernameColor != null
|
||||||
const G = toLinear(g);
|
) {
|
||||||
const B = toLinear(b);
|
return usernameColor;
|
||||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onChatMessage(data) {
|
async function onChatMessage(data) {
|
||||||
let userColor = data.data.user.color; // hex colour #FF69B4
|
const userColor = data.data.user.color;
|
||||||
|
|
||||||
if (userColor != undefined || userColor != "undefined") {
|
if (userColor != undefined && userColor != "undefined") {
|
||||||
// update localstorage
|
const userId = data.data.user.id;
|
||||||
// get id: platform-userID
|
const key = `twitch-${userId}`;
|
||||||
let userId = data.data.user.id;
|
|
||||||
let key = `twitch-${userId}`;
|
|
||||||
|
|
||||||
localStorage.setItem(`${key}-color`, userColor);
|
localStorage.setItem(`${key}-color`, userColor);
|
||||||
userColors[`${key}-color`] = userColor;
|
userColors[`${key}-color`] = userColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDisconnect() {
|
function onDisconnect() {
|
||||||
|
|
@ -85,7 +60,6 @@ function onError(err) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showConnectionError(message) {
|
function showConnectionError(message) {
|
||||||
// Remove existing popup if any
|
|
||||||
const existing = document.getElementById("connection-error");
|
const existing = document.getElementById("connection-error");
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
|
@ -110,7 +84,6 @@ function showConnectionError(message) {
|
||||||
});
|
});
|
||||||
document.body.appendChild(popup);
|
document.body.appendChild(popup);
|
||||||
|
|
||||||
// Auto-dismiss after 5 seconds
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
popup.animate([{ opacity: 1 }, { opacity: 0 }], {
|
popup.animate([{ opacity: 1 }, { opacity: 0 }], {
|
||||||
duration: 300,
|
duration: 300,
|
||||||
|
|
@ -121,7 +94,6 @@ function showConnectionError(message) {
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
const response = await client.getGlobal("rython-task-bot", true);
|
const response = await client.getGlobal("rython-task-bot", true);
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
if (response.status !== "ok" || !response.variable?.value) return;
|
if (response.status !== "ok" || !response.variable?.value) return;
|
||||||
|
|
||||||
|
|
@ -131,19 +103,18 @@ async function refresh() {
|
||||||
taskList.load(sections);
|
taskList.load(sections);
|
||||||
}
|
}
|
||||||
|
|
||||||
// LOAD TASK LIST
|
|
||||||
async function onConnect() {
|
async function onConnect() {
|
||||||
taskList = new TaskList(".task-panel");
|
taskList = new TaskList(".task-panel");
|
||||||
|
|
||||||
if (configs.emoteSettings.enabled) {
|
if (configs.emoteSettings.enabled) {
|
||||||
let broadcaster = await client.getBroadcaster();
|
const broadcaster = await client.getBroadcaster();
|
||||||
|
|
||||||
let broadcasterName =
|
const broadcasterName =
|
||||||
broadcaster.platforms.twitch?.broadcastUser ??
|
broadcaster.platforms.twitch?.broadcastUser ??
|
||||||
broadcaster.platforms.youtube?.broadcastUser ??
|
broadcaster.platforms.youtube?.broadcastUser ??
|
||||||
broadcaster.platforms.kick?.broadcastUser;
|
broadcaster.platforms.kick?.broadcastUser;
|
||||||
|
|
||||||
let broadcasterId =
|
const broadcasterId =
|
||||||
broadcaster.platforms.twitch?.broadcastUserId ??
|
broadcaster.platforms.twitch?.broadcastUserId ??
|
||||||
broadcaster.platforms.youtube?.broadcastUserId ??
|
broadcaster.platforms.youtube?.broadcastUserId ??
|
||||||
broadcaster.platforms.kick?.broadcastUserId;
|
broadcaster.platforms.kick?.broadcastUserId;
|
||||||
|
|
@ -177,19 +148,58 @@ function transformToSections(users) {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update task list action by action
|
|
||||||
function onCustom(payload) {
|
function onCustom(payload) {
|
||||||
const data = payload.data;
|
const data = payload.data;
|
||||||
if (!data.source && data.source != "rython-task-bot") {
|
if (data.source !== "rython-task-bot") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!taskList) return;
|
if (!taskList) return;
|
||||||
|
|
||||||
const body = data.body;
|
const body = data.body;
|
||||||
const id = data.id;
|
const id = data.id;
|
||||||
const username = data.username;
|
const username = data.username;
|
||||||
|
|
||||||
switch (body.mode) {
|
switch (body.mode) {
|
||||||
case "refresh":
|
case "add": {
|
||||||
|
const taskPayload = {
|
||||||
|
text: body.task,
|
||||||
|
done: body.completed,
|
||||||
|
focused: body.focused,
|
||||||
|
};
|
||||||
|
const color = taskColorFromSettings(id);
|
||||||
|
if (color) taskPayload.color = color;
|
||||||
|
taskList.addTask(id, taskPayload, username);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "focus":
|
||||||
|
taskList.focusTask(id, body.index);
|
||||||
|
break;
|
||||||
|
case "edit":
|
||||||
|
taskList.editTask(id, body.index, body.task, taskColorFromSettings(id));
|
||||||
|
break;
|
||||||
|
case "remove":
|
||||||
|
taskList.removeTask(id, body.index);
|
||||||
|
break;
|
||||||
|
case "done":
|
||||||
|
taskList.doneTask(id, body.index);
|
||||||
|
break;
|
||||||
|
case "undone":
|
||||||
|
taskList.undoneTask(id, body.index);
|
||||||
|
break;
|
||||||
|
case "unfocus":
|
||||||
|
taskList.unfocusTask(id);
|
||||||
|
break;
|
||||||
|
case "admindelete":
|
||||||
|
taskList.removeSection(body.id);
|
||||||
|
break;
|
||||||
|
case "clearmydone":
|
||||||
|
taskList.clearmydone(id);
|
||||||
|
break;
|
||||||
|
case "cleardone":
|
||||||
|
taskList.cleardone();
|
||||||
|
break;
|
||||||
|
case "clearall":
|
||||||
|
case "clearns":
|
||||||
refresh();
|
refresh();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
Websocket server running is required for browser source to work.
|
Websocket server running is required for browser source to work.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
|
|
||||||
1. Streamer.Bot -> Servers/Clients -> Websocket Server
|
1. Streamer.Bot -> Servers/Clients -> Websocket Server
|
||||||
2. Auto Start: `ON`; Click on `Start Server`
|
2. Auto Start: `ON`; Click on `Start Server`
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
|
|
@ -3,11 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="./lib/streamerbot-client.js"></script>
|
<script src="../shared/lib/streamerbot-client.js"></script>
|
||||||
<script defer src="./config.js"></script>
|
<script defer src="./config.js"></script>
|
||||||
<script defer src="./scripts/emotes.js"></script>
|
<script defer src="../shared/scripts/emotes.js"></script>
|
||||||
<script defer src="./scripts/tasklist-view.js"></script>
|
<script defer src="./scripts/tasklist-view.js"></script>
|
||||||
<script defer src="./scripts/streamerbot.js"></script>
|
<script defer src="../shared/scripts/streamerbot.js"></script>
|
||||||
<script defer src="./scripts/header-animation.js"></script>
|
<script defer src="./scripts/header-animation.js"></script>
|
||||||
<link rel="stylesheet" href="./styles/style.css" />
|
<link rel="stylesheet" href="./styles/style.css" />
|
||||||
<link rel="stylesheet" href="./styles/structure.css" />
|
<link rel="stylesheet" href="./styles/structure.css" />
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,273 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
class EmoteManager {
|
|
||||||
#emotes = new Map();
|
|
||||||
#channelName = "";
|
|
||||||
#channelId = "";
|
|
||||||
#providers = ["7tv", "bttv", "ffz"];
|
|
||||||
#size = "1x";
|
|
||||||
#loaded = false;
|
|
||||||
#cacheTTL = 24 * 60 * 60 * 1000;
|
|
||||||
#CACHE_KEY = "emote-cache-v1";
|
|
||||||
#pattern = null;
|
|
||||||
#patternSize = 0;
|
|
||||||
|
|
||||||
constructor(config = {}) {
|
|
||||||
this.#channelName = config.channelName;
|
|
||||||
this.#channelId = config.channelId;
|
|
||||||
if (config.providers) this.#providers = config.providers;
|
|
||||||
if (config.size) this.#size = config.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
get loaded() {
|
|
||||||
return this.#loaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
get emoteCount() {
|
|
||||||
return this.#emotes.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
get providers() {
|
|
||||||
return [...this.#providers];
|
|
||||||
}
|
|
||||||
|
|
||||||
getEmote(name) {
|
|
||||||
return this.#emotes.get(name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
#buildPattern() {
|
|
||||||
if (this.#emotes.size === 0) {
|
|
||||||
this.#pattern = null;
|
|
||||||
this.#patternSize = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#pattern && this.#emotes.size === this.#patternSize) return;
|
|
||||||
|
|
||||||
const sorted = [...this.#emotes.keys()].sort(
|
|
||||||
(a, b) => b.length - a.length,
|
|
||||||
);
|
|
||||||
const escaped = sorted.map((n) =>
|
|
||||||
n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
||||||
);
|
|
||||||
this.#pattern = new RegExp(
|
|
||||||
`(?<![\\w])(?:${escaped.join("|")})(?![\\w])`,
|
|
||||||
"g",
|
|
||||||
);
|
|
||||||
this.#patternSize = this.#emotes.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseText(text) {
|
|
||||||
if (!text || !this.#loaded || this.#emotes.size === 0) return text;
|
|
||||||
|
|
||||||
this.#buildPattern();
|
|
||||||
if (!this.#pattern) return text;
|
|
||||||
|
|
||||||
return text.replace(this.#pattern, (match) => {
|
|
||||||
const emote = this.#emotes.get(match);
|
|
||||||
if (!emote) return match;
|
|
||||||
console.log(emote);
|
|
||||||
return `<img src="${emote.url}" alt="${match}" title="${match} (${emote.provider})" class="emote-img" />`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
const cached = this.#loadCache();
|
|
||||||
if (cached) {
|
|
||||||
this.#emotes = new Map(Object.entries(cached.emotes));
|
|
||||||
this.#loaded = true;
|
|
||||||
this.#patternSize = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.#fetchAll();
|
|
||||||
this.#loaded = true;
|
|
||||||
this.#patternSize = 0;
|
|
||||||
this.#saveCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
async forceRefresh() {
|
|
||||||
this.#emotes.clear();
|
|
||||||
this.#patternSize = 0;
|
|
||||||
await this.#fetchAll();
|
|
||||||
this.#loaded = true;
|
|
||||||
this.#patternSize = 0;
|
|
||||||
this.#saveCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
async #fetchAll() {
|
|
||||||
const fetchers = [];
|
|
||||||
if (this.#providers.includes("7tv")) fetchers.push(this.#fetch7TV());
|
|
||||||
if (this.#providers.includes("bttv")) fetchers.push(this.#fetchBTTV());
|
|
||||||
if (this.#providers.includes("ffz")) fetchers.push(this.#fetchFFZ());
|
|
||||||
await Promise.allSettled(fetchers);
|
|
||||||
}
|
|
||||||
|
|
||||||
async #fetch7TV() {
|
|
||||||
try {
|
|
||||||
const globalResp = await fetch(
|
|
||||||
"https://7tv.io/v3/emote-sets/global",
|
|
||||||
);
|
|
||||||
if (!globalResp.ok) return;
|
|
||||||
const globalData = await globalResp.json();
|
|
||||||
if (globalData.emotes) {
|
|
||||||
for (const emote of globalData.emotes) {
|
|
||||||
this.#add7TVEmote(emote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#channelId) {
|
|
||||||
const userResp = await fetch(
|
|
||||||
`https://7tv.io/v3/users/twitch/${this.#channelId}`,
|
|
||||||
);
|
|
||||||
if (!userResp.ok) return;
|
|
||||||
const userData = await userResp.json();
|
|
||||||
if (userData.emote_set?.emotes) {
|
|
||||||
for (const emote of userData.emote_set.emotes) {
|
|
||||||
this.#add7TVEmote(emote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("7TV emote fetch failed:", e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#add7TVEmote(emote) {
|
|
||||||
const entry = emote.data || emote;
|
|
||||||
const id = entry.id || emote.id;
|
|
||||||
const name = entry.name || emote.name;
|
|
||||||
if (!id || !name) return;
|
|
||||||
const nameLower = name;
|
|
||||||
const host = entry.host.url;
|
|
||||||
this.#emotes.set(nameLower, {
|
|
||||||
url: `https:${host}/${this.#size}.webp`,
|
|
||||||
provider: "7tv",
|
|
||||||
animated: entry.animated || false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async #fetchBTTV() {
|
|
||||||
try {
|
|
||||||
const globalResp = await fetch(
|
|
||||||
"https://api.betterttv.net/3/cached/emotes/global",
|
|
||||||
);
|
|
||||||
if (!globalResp.ok) return;
|
|
||||||
const globalData = await globalResp.json();
|
|
||||||
if (Array.isArray(globalData)) {
|
|
||||||
for (const emote of globalData) {
|
|
||||||
this.#emotes.set(emote.code, {
|
|
||||||
url: `https://cdn.betterttv.net/emote/${emote.id}/${this.#size}`,
|
|
||||||
provider: "bttv",
|
|
||||||
animated: emote.imageType === "gif",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#channelId) {
|
|
||||||
const userResp = await fetch(
|
|
||||||
`https://api.betterttv.net/3/cached/users/twitch/${this.#channelId}`,
|
|
||||||
);
|
|
||||||
if (!userResp.ok) return;
|
|
||||||
const userData = await userResp.json();
|
|
||||||
const allEmotes = [
|
|
||||||
...(userData.channelEmotes || []),
|
|
||||||
...(userData.sharedEmotes || []),
|
|
||||||
];
|
|
||||||
for (const emote of allEmotes) {
|
|
||||||
this.#emotes.set(emote.code, {
|
|
||||||
url: `https://cdn.betterttv.net/emote/${emote.id}/${this.#size}`,
|
|
||||||
provider: "bttv",
|
|
||||||
animated: emote.imageType === "gif",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("BTTV emote fetch failed:", e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async #fetchFFZ() {
|
|
||||||
try {
|
|
||||||
const globalResp = await fetch(
|
|
||||||
"https://api.frankerfacez.com/v1/set/global",
|
|
||||||
);
|
|
||||||
if (!globalResp.ok) return;
|
|
||||||
const globalData = await globalResp.json();
|
|
||||||
if (globalData.sets) {
|
|
||||||
for (const setId in globalData.sets) {
|
|
||||||
const set = globalData.sets[setId];
|
|
||||||
for (const emote of set.emoticons || []) {
|
|
||||||
if (emote.modifier) continue;
|
|
||||||
this.#emotes.set(emote.name, {
|
|
||||||
url: emote.urls[this.#toFFZSize(this.#size)],
|
|
||||||
provider: "ffz",
|
|
||||||
animated: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ffzId = this.#channelName || this.#channelId;
|
|
||||||
if (ffzId) {
|
|
||||||
const roomUrl = this.#channelId
|
|
||||||
? `https://api.frankerfacez.com/v1/room/id/${this.#channelId}`
|
|
||||||
: `https://api.frankerfacez.com/v1/room/${this.#channelName}`;
|
|
||||||
const roomResp = await fetch(roomUrl);
|
|
||||||
if (!roomResp.ok) return;
|
|
||||||
const roomData = await roomResp.json();
|
|
||||||
if (roomData.sets) {
|
|
||||||
for (const setId in roomData.sets) {
|
|
||||||
const set = roomData.sets[setId];
|
|
||||||
for (const emote of set.emoticons || []) {
|
|
||||||
if (emote.modifier) continue;
|
|
||||||
this.#emotes.set(emote.name, {
|
|
||||||
url: emote.urls[this.#toFFZSize(this.#size)],
|
|
||||||
provider: "ffz",
|
|
||||||
animated: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("FFZ emote fetch failed:", e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#toFFZSize(size) {
|
|
||||||
switch (size) {
|
|
||||||
case "3x":
|
|
||||||
case "4x":
|
|
||||||
return "4";
|
|
||||||
case "2x":
|
|
||||||
return "2";
|
|
||||||
default:
|
|
||||||
return "1";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#saveCache() {
|
|
||||||
try {
|
|
||||||
const cache = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
channelId: this.#channelId,
|
|
||||||
emotes: Object.fromEntries(this.#emotes),
|
|
||||||
};
|
|
||||||
localStorage.setItem(this.#CACHE_KEY, JSON.stringify(cache));
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#loadCache() {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(this.#CACHE_KEY);
|
|
||||||
if (!raw) return null;
|
|
||||||
const cache = JSON.parse(raw);
|
|
||||||
if (cache.channelId !== this.#channelId) return null;
|
|
||||||
if (Date.now() - cache.timestamp > this.#cacheTTL) return null;
|
|
||||||
return cache;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
// STREAMER.BOT SETTINGS
|
|
||||||
|
|
||||||
const client = new StreamerbotClient({
|
|
||||||
host: configs.streamerBotSettings.host,
|
|
||||||
port: configs.streamerBotSettings.port,
|
|
||||||
endpoint: configs.streamerBotSettings.endpoint,
|
|
||||||
onConnect: onConnect,
|
|
||||||
onDisconnect: onDisconnect,
|
|
||||||
onError: onError,
|
|
||||||
});
|
|
||||||
|
|
||||||
function getAllLocalstorage() {
|
|
||||||
const allLocalStorage = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
const key = localStorage.key(i);
|
|
||||||
allLocalStorage[key] = localStorage.getItem(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(allLocalStorage);
|
|
||||||
}
|
|
||||||
|
|
||||||
client.on("General.Custom", (data) => onCustom(data));
|
|
||||||
if (configs.userColorSettings.autoUserColor) {
|
|
||||||
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
|
|
||||||
client.on("Kick.ChatMessage", (data) => onChatMessage(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
let taskList;
|
|
||||||
let userColors = {};
|
|
||||||
|
|
||||||
function parseHexColor(hex) {
|
|
||||||
if (typeof hex !== "string") return null;
|
|
||||||
const s = hex.trim();
|
|
||||||
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
|
|
||||||
if (!m) return null;
|
|
||||||
let h = m[1].toLowerCase();
|
|
||||||
if (h.length === 3) {
|
|
||||||
h = h
|
|
||||||
.split("")
|
|
||||||
.map((c) => c + c)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
const r = parseInt(h.slice(0, 2), 16);
|
|
||||||
const g = parseInt(h.slice(2, 4), 16);
|
|
||||||
const b = parseInt(h.slice(4, 6), 16);
|
|
||||||
return { r, g, b };
|
|
||||||
}
|
|
||||||
|
|
||||||
function relativeLuminance({ r, g, b }) {
|
|
||||||
const toLinear = (v) => {
|
|
||||||
const s = v / 255;
|
|
||||||
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
||||||
};
|
|
||||||
const R = toLinear(r);
|
|
||||||
const G = toLinear(g);
|
|
||||||
const B = toLinear(b);
|
|
||||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onChatMessage(data) {
|
|
||||||
let userColor = data.data.user.color; // hex colour
|
|
||||||
|
|
||||||
if (userColor != undefined || userColor != "undefined") {
|
|
||||||
// update localstorage
|
|
||||||
// get id: platform-userID
|
|
||||||
let userId = data.data.user.id;
|
|
||||||
let key = `twitch-${userId}`;
|
|
||||||
|
|
||||||
localStorage.setItem(`${key}-color`, userColor);
|
|
||||||
userColors[`${key}-color`] = userColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDisconnect() {
|
|
||||||
showConnectionError("Connection Failed: Unable to connect to Streamer.bot");
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(err) {
|
|
||||||
showConnectionError(
|
|
||||||
"Connection Failed: " + (err?.message || "Unknown error"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showConnectionError(message) {
|
|
||||||
// Remove existing popup if any
|
|
||||||
const existing = document.getElementById("connection-error");
|
|
||||||
if (existing) existing.remove();
|
|
||||||
|
|
||||||
const popup = document.createElement("div");
|
|
||||||
popup.id = "connection-error";
|
|
||||||
popup.textContent = message;
|
|
||||||
Object.assign(popup.style, {
|
|
||||||
position: "fixed",
|
|
||||||
top: "20px",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
background: "#e53935",
|
|
||||||
color: "#fff",
|
|
||||||
padding: "12px 24px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
fontFamily: "'Fredoka', sans-serif",
|
|
||||||
fontSize: "1.1rem",
|
|
||||||
fontWeight: "700",
|
|
||||||
zIndex: "9999",
|
|
||||||
boxShadow: "0 4px 12px rgba(0,0,0,0.4)",
|
|
||||||
textAlign: "center",
|
|
||||||
});
|
|
||||||
document.body.appendChild(popup);
|
|
||||||
|
|
||||||
// Auto-dismiss after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
popup.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
||||||
duration: 300,
|
|
||||||
fill: "forwards",
|
|
||||||
}).onfinish = () => popup.remove();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
const response = await client.getGlobal("rython-task-bot", true);
|
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
if (response.status !== "ok" || !response.variable?.value) return;
|
|
||||||
|
|
||||||
const users = JSON.parse(response.variable.value);
|
|
||||||
console.log(users);
|
|
||||||
const sections = transformToSections(users);
|
|
||||||
|
|
||||||
taskList.load(sections);
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOAD TASK LIST
|
|
||||||
async function onConnect() {
|
|
||||||
taskList = new TaskList(".task-panel");
|
|
||||||
|
|
||||||
if (configs.emoteSettings.enabled) {
|
|
||||||
let broadcaster = await client.getBroadcaster();
|
|
||||||
|
|
||||||
console.log(broadcaster);
|
|
||||||
|
|
||||||
let broadcasterName =
|
|
||||||
broadcaster.platforms.twitch?.broadcastUser ??
|
|
||||||
broadcaster.platforms.youtube?.broadcastUser ??
|
|
||||||
broadcaster.platforms.kick?.broadcastUser;
|
|
||||||
|
|
||||||
let broadcasterId =
|
|
||||||
broadcaster.platforms.twitch?.broadcastUserId ??
|
|
||||||
broadcaster.platforms.youtube?.broadcastUserId ??
|
|
||||||
broadcaster.platforms.kick?.broadcastUserId;
|
|
||||||
|
|
||||||
window.emoteManager = new EmoteManager({
|
|
||||||
channelName: broadcasterName,
|
|
||||||
channelId: broadcasterId,
|
|
||||||
providers: configs.emoteSettings.providers,
|
|
||||||
size: configs.emoteSettings.size,
|
|
||||||
});
|
|
||||||
|
|
||||||
window.emoteManager.init().then(() => {
|
|
||||||
if (taskList) {
|
|
||||||
taskList.load(taskList.getData());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function transformToSections(users) {
|
|
||||||
return Object.entries(users).map(([userId, userData]) => ({
|
|
||||||
id: userId,
|
|
||||||
title: userData.Username,
|
|
||||||
tasks: userData.Tasks.map((task) => ({
|
|
||||||
text: task.Name,
|
|
||||||
done: task.Completed,
|
|
||||||
focused: task.Focused,
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update task list action by action
|
|
||||||
function onCustom(payload) {
|
|
||||||
const data = payload.data;
|
|
||||||
if (!data.source && data.source != "rython-task-bot") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!taskList) return;
|
|
||||||
const body = data.body;
|
|
||||||
const id = data.id;
|
|
||||||
const username = data.username;
|
|
||||||
|
|
||||||
switch (body.mode) {
|
|
||||||
case "refresh":
|
|
||||||
refresh();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,7 @@ Optional widget for [Rython Task Bot v2](https://github.com/liyunze-coding/rytho
|
||||||
|
|
||||||
Websocket server running is required for browser source to work.
|
Websocket server running is required for browser source to work.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
|
|
||||||
1. Streamer.Bot -> Servers/Clients -> Websocket Server
|
1. Streamer.Bot -> Servers/Clients -> Websocket Server
|
||||||
2. Auto Start: `ON`; Click on `Start Server`
|
2. Auto Start: `ON`; Click on `Start Server`
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
|
|
@ -3,11 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="./lib/streamerbot-client.js"></script>
|
<script src="../shared/lib/streamerbot-client.js"></script>
|
||||||
<script defer src="./config.js"></script>
|
<script defer src="./config.js"></script>
|
||||||
<script defer src="./scripts/emotes.js"></script>
|
<script defer src="../shared/scripts/emotes.js"></script>
|
||||||
<script defer src="./scripts/tasklist-view.js"></script>
|
<script defer src="../shared/scripts/tasklist-view.js"></script>
|
||||||
<script defer src="./scripts/streamerbot.js"></script>
|
<script defer src="../shared/scripts/streamerbot.js"></script>
|
||||||
<script defer src="./scripts/header-animation.js"></script>
|
<script defer src="./scripts/header-animation.js"></script>
|
||||||
<link rel="stylesheet" href="./styles/style.css" />
|
<link rel="stylesheet" href="./styles/style.css" />
|
||||||
<link rel="stylesheet" href="./styles/structure.css" />
|
<link rel="stylesheet" href="./styles/structure.css" />
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue