mirror of
https://github.com/liyunze-coding/rython-task-bot-v2.git
synced 2026-09-22 07:24:27 +00:00
refresh, added emote support
This commit is contained in:
parent
168ef05777
commit
e6c58a1ec7
20 changed files with 1016 additions and 233 deletions
28
Code.cs
28
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 = "add", task = taskName, completed = completed, focused = focused }, null);
|
broadcast(new { mode = "refresh", 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 = "edit", index = index, task = newTask }, null);
|
broadcast(new { mode = "refresh", 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 = "focus", index = n }, null);
|
broadcast(new { mode = "refresh", 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 = "focus", index = n }, null);
|
broadcast(new { mode = "refresh", 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 = "focus", index = response.Data.Item1 }, null);
|
broadcast(new { mode = "refresh", index = response.Data.Item1 }, null);
|
||||||
return new Response<(int, string)>(true, response.Data, null);
|
return new Response<(int, string)>(true, response.Data, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -846,7 +846,7 @@ 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 = "done", index = focusedTaskIndex }, null);
|
Broadcast(new { mode = "refresh", index = focusedTaskIndex }, null);
|
||||||
operations.SaveIntoTasks(userTasks);
|
operations.SaveIntoTasks(userTasks);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Respond(BotResponses.NextSuccess(completedTaskName, focusResponse.Data.Item1 + 1, focusResponse.Data.Item2));
|
Respond(BotResponses.NextSuccess(completedTaskName, focusResponse.Data.Item1 + 1, focusResponse.Data.Item2));
|
||||||
|
|
@ -997,7 +997,7 @@ 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 = "remove", index = i }, null);
|
Broadcast(new { mode = "refresh", index = i }, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.SaveIntoTasks(userTasks);
|
operations.SaveIntoTasks(userTasks);
|
||||||
|
|
@ -1024,7 +1024,7 @@ public class CPHInline
|
||||||
operations.RemoveUser(key);
|
operations.RemoveUser(key);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Respond(BotResponses.AdminDeleteSuccess);
|
Respond(BotResponses.AdminDeleteSuccess);
|
||||||
Broadcast(new { mode = "admindelete", id = key }, null);
|
Broadcast(new { mode = "refresh", id = key }, null);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1081,7 +1081,7 @@ public class CPHInline
|
||||||
{
|
{
|
||||||
userTasks[i].Completed = true;
|
userTasks[i].Completed = true;
|
||||||
userTasks[i].Focused = false;
|
userTasks[i].Focused = false;
|
||||||
Broadcast(new { mode = "done", index = i }, null);
|
Broadcast(new { mode = "refresh", index = i }, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
IncrementDoneCount(taskIndices.Count);
|
IncrementDoneCount(taskIndices.Count);
|
||||||
|
|
@ -1096,7 +1096,7 @@ public class CPHInline
|
||||||
operations.Unfocus();
|
operations.Unfocus();
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Respond(BotResponses.Unfocused);
|
Respond(BotResponses.Unfocused);
|
||||||
Broadcast(new { mode = "unfocus" }, null);
|
Broadcast(new { mode = "refresh" }, null);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1144,7 +1144,7 @@ public class CPHInline
|
||||||
{
|
{
|
||||||
userTasks[i].Completed = false;
|
userTasks[i].Completed = false;
|
||||||
userTasks[i].Focused = false;
|
userTasks[i].Focused = false;
|
||||||
Broadcast(new { mode = "undone", index = i }, null);
|
Broadcast(new { mode = "refresh", index = i }, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.SaveIntoTasks(userTasks);
|
operations.SaveIntoTasks(userTasks);
|
||||||
|
|
@ -1169,7 +1169,7 @@ public class CPHInline
|
||||||
operations.ClearUserCompletedTasks(key);
|
operations.ClearUserCompletedTasks(key);
|
||||||
operations.Cleanup(false);
|
operations.Cleanup(false);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Broadcast(new { mode = "clearmydone" }, null);
|
Broadcast(new { mode = "refresh" }, null);
|
||||||
Respond(BotResponses.ClearMyDone);
|
Respond(BotResponses.ClearMyDone);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1179,7 +1179,7 @@ public class CPHInline
|
||||||
operations.ClearCompletedTasks();
|
operations.ClearCompletedTasks();
|
||||||
operations.Cleanup(false);
|
operations.Cleanup(false);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Broadcast(new { mode = "cleardone" }, null);
|
Broadcast(new { mode = "refresh" }, null);
|
||||||
Respond(BotResponses.ClearDone);
|
Respond(BotResponses.ClearDone);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1189,7 +1189,7 @@ public class CPHInline
|
||||||
operations.FilterToStreamers(GetStreamerUsernames());
|
operations.FilterToStreamers(GetStreamerUsernames());
|
||||||
operations.Cleanup(false);
|
operations.Cleanup(false);
|
||||||
SaveTasks();
|
SaveTasks();
|
||||||
Broadcast(new { mode = "clearns" }, null);
|
Broadcast(new { mode = "refresh" }, null);
|
||||||
Respond(BotResponses.ClearNotStreamer);
|
Respond(BotResponses.ClearNotStreamer);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
const configs = (function () {
|
const configs = (function () {
|
||||||
const streamerBotSettings = {
|
const streamerBotSettings = {
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: 8080,
|
port: 6968,
|
||||||
endpoint: "/",
|
endpoint: "/",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -17,18 +17,23 @@ const configs = (function () {
|
||||||
"!clearmydone",
|
"!clearmydone",
|
||||||
];
|
];
|
||||||
|
|
||||||
const twitchSettings = {
|
const userColorSettings = {
|
||||||
autoUserColor: true
|
autoUserColor: true,
|
||||||
}
|
};
|
||||||
|
|
||||||
const kickSettings = {
|
const emoteSettings = {
|
||||||
autouserColor: true
|
enabled: true,
|
||||||
}
|
channelName: "rythondev",
|
||||||
|
channelId: "248474026",
|
||||||
|
providers: ["7tv", "bttv", "ffz"],
|
||||||
|
size: "1x",
|
||||||
|
cacheHours: 24,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
streamerBotSettings,
|
streamerBotSettings,
|
||||||
commands,
|
commands,
|
||||||
twitchSettings,
|
userColorSettings,
|
||||||
kickSettings
|
emoteSettings,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<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="./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="./scripts/tasklist-view.js"></script>
|
<script defer src="./scripts/tasklist-view.js"></script>
|
||||||
<script defer src="./scripts/streamerbot.js"></script>
|
<script defer src="./scripts/streamerbot.js"></script>
|
||||||
<script defer src="./scripts/header-animation.js"></script>
|
<script defer src="./scripts/header-animation.js"></script>
|
||||||
|
|
|
||||||
273
widgets/horizontal/scripts/emotes.js
Normal file
273
widgets/horizontal/scripts/emotes.js
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,8 +21,9 @@ function getAllLocalstorage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
client.on("General.Custom", (data) => onCustom(data));
|
client.on("General.Custom", (data) => onCustom(data));
|
||||||
if (configs.twitchSettings.autoUserColor) {
|
if (configs.userColorSettings.autoUserColor) {
|
||||||
client.on("Twitch.ChatMessage", (data) => onTwitchFirstWord(data));
|
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
|
||||||
|
client.on("Kick.ChatMessage", (data) => onChatMessage(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
let taskList;
|
let taskList;
|
||||||
|
|
@ -57,12 +58,10 @@ function relativeLuminance({ r, g, b }) {
|
||||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTwitchFirstWord(data) {
|
async function onChatMessage(data) {
|
||||||
let userColor = data.data.user.color; // hex colour #FF69B4
|
let userColor = data.data.user.color; // hex colour #FF69B4
|
||||||
|
|
||||||
|
if (userColor != undefined || userColor != "undefined") {
|
||||||
|
|
||||||
if (userColor != undefined || userColor != 'undefined') {
|
|
||||||
// update localstorage
|
// update localstorage
|
||||||
// get id: platform-userID
|
// get id: platform-userID
|
||||||
let userId = data.data.user.id;
|
let userId = data.data.user.id;
|
||||||
|
|
@ -71,7 +70,7 @@ async function onTwitchFirstWord(data) {
|
||||||
localStorage.setItem(`${key}-color`, userColor);
|
localStorage.setItem(`${key}-color`, userColor);
|
||||||
userColors[`${key}-color`] = userColor;
|
userColors[`${key}-color`] = userColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,6 +134,22 @@ async function refresh() {
|
||||||
// LOAD TASK LIST
|
// LOAD TASK LIST
|
||||||
async function onConnect() {
|
async function onConnect() {
|
||||||
taskList = new TaskList(".task-panel");
|
taskList = new TaskList(".task-panel");
|
||||||
|
|
||||||
|
if (configs.emoteSettings.enabled && configs.emoteSettings.channelId) {
|
||||||
|
window.emoteManager = new EmoteManager({
|
||||||
|
channelName: configs.emoteSettings.channelName,
|
||||||
|
channelId: configs.emoteSettings.channelId,
|
||||||
|
providers: configs.emoteSettings.providers,
|
||||||
|
size: configs.emoteSettings.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.emoteManager.init().then(() => {
|
||||||
|
if (taskList) {
|
||||||
|
taskList.load(taskList.getData());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,59 +177,9 @@ function onCustom(payload) {
|
||||||
const username = data.username;
|
const username = data.username;
|
||||||
|
|
||||||
switch (body.mode) {
|
switch (body.mode) {
|
||||||
case "add":
|
case "refresh":
|
||||||
// body.task;
|
|
||||||
let taskPayload = {
|
|
||||||
text: body.task,
|
|
||||||
done: body.completed,
|
|
||||||
focused: body.focused,
|
|
||||||
};
|
|
||||||
|
|
||||||
let usernameColor =
|
|
||||||
userColors[id] ?? localStorage.getItem(`${id}-color`);
|
|
||||||
|
|
||||||
if (configs.twitchSettings.autoUserColor && usernameColor != undefined && usernameColor != null) {
|
|
||||||
taskPayload["color"] = usernameColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
taskList.addTask(id, taskPayload, username);
|
|
||||||
break;
|
|
||||||
case "focus":
|
|
||||||
taskList.focusTask(id, body.index, usernameColor);
|
|
||||||
break;
|
|
||||||
case "edit":
|
|
||||||
taskList.editTask(id, body.index, body.task, usernameColor);
|
|
||||||
break;
|
|
||||||
case "remove":
|
|
||||||
taskList.removeTask(id, body.index, usernameColor);
|
|
||||||
break;
|
|
||||||
case "done":
|
|
||||||
taskList.doneTask(id, body.index, usernameColor);
|
|
||||||
break;
|
|
||||||
case "admindelete":
|
|
||||||
taskList.removeSection(body.id);
|
|
||||||
break;
|
|
||||||
case "clearns":
|
|
||||||
refresh();
|
refresh();
|
||||||
break;
|
break;
|
||||||
case "cleardone":
|
|
||||||
taskList.cleardone();
|
|
||||||
break;
|
|
||||||
case "clearmydone":
|
|
||||||
taskList.clearmydone(id);
|
|
||||||
break;
|
|
||||||
case "clearall":
|
|
||||||
refresh();
|
|
||||||
break;
|
|
||||||
case "undone":
|
|
||||||
taskList.undoneTask(id, body.index);
|
|
||||||
break;
|
|
||||||
case "unfocus":
|
|
||||||
taskList.unfocusTask(id);
|
|
||||||
break;
|
|
||||||
case "admindelete":
|
|
||||||
taskList.removeSection(body.id);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,13 @@ class TaskList {
|
||||||
|
|
||||||
// ── dom helpers ────────────────────────────────────
|
// ── dom helpers ────────────────────────────────────
|
||||||
|
|
||||||
|
#parseTaskText(text) {
|
||||||
|
if (window.emoteManager && window.emoteManager.loaded) {
|
||||||
|
return window.emoteManager.parseText(text);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
#createTaskEl(task, index) {
|
#createTaskEl(task, index) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className =
|
div.className =
|
||||||
|
|
@ -314,7 +321,8 @@ class TaskList {
|
||||||
numberSpan.textContent = `${index + 1}.`;
|
numberSpan.textContent = `${index + 1}.`;
|
||||||
|
|
||||||
const textSpan = document.createElement("span");
|
const textSpan = document.createElement("span");
|
||||||
textSpan.textContent = task.text;
|
textSpan.className = "task-text";
|
||||||
|
textSpan.innerHTML = this.#parseTaskText(task.text);
|
||||||
|
|
||||||
div.replaceChildren(numberSpan, textSpan);
|
div.replaceChildren(numberSpan, textSpan);
|
||||||
return div;
|
return div;
|
||||||
|
|
@ -366,7 +374,8 @@ class TaskList {
|
||||||
}
|
}
|
||||||
if (el.dataset.text !== task.text) {
|
if (el.dataset.text !== task.text) {
|
||||||
el.dataset.text = task.text;
|
el.dataset.text = task.text;
|
||||||
el.querySelector("span:last-child").textContent = 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}.`;
|
el.querySelector(".task-number").textContent = `${i + 1}.`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,3 +141,14 @@ body {
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emote-img {
|
||||||
|
height: 1.4em;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
margin: -0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task.done .emote-img {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
const configs = (function () {
|
const configs = (function () {
|
||||||
const streamerBotSettings = {
|
const streamerBotSettings = {
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: 8080,
|
port: 6968,
|
||||||
endpoint: "/",
|
endpoint: "/",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -19,12 +19,17 @@ const configs = (function () {
|
||||||
"!clearmydone",
|
"!clearmydone",
|
||||||
];
|
];
|
||||||
|
|
||||||
const twitchSettings = {
|
const userColorSettings = {
|
||||||
autoUserColor: true,
|
autoUserColor: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const kickSettings = {
|
const emoteSettings = {
|
||||||
autouserColor: true,
|
enabled: true,
|
||||||
|
channelName: "rythondev",
|
||||||
|
channelId: "248474026",
|
||||||
|
providers: ["7tv", "bttv", "ffz"],
|
||||||
|
size: "1x",
|
||||||
|
cacheHours: 24,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -33,5 +38,6 @@ const configs = (function () {
|
||||||
commands,
|
commands,
|
||||||
twitchSettings,
|
twitchSettings,
|
||||||
kickSettings,
|
kickSettings,
|
||||||
|
emoteSettings,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<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="./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="./scripts/tasklist-view.js"></script>
|
<script defer src="./scripts/tasklist-view.js"></script>
|
||||||
<script defer src="./scripts/streamerbot.js"></script>
|
<script defer src="./scripts/streamerbot.js"></script>
|
||||||
<script defer src="./scripts/header-animation.js"></script>
|
<script defer src="./scripts/header-animation.js"></script>
|
||||||
|
|
|
||||||
273
widgets/streamer-only/scripts/emotes.js
Normal file
273
widgets/streamer-only/scripts/emotes.js
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,8 +21,9 @@ function getAllLocalstorage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
client.on("General.Custom", (data) => onCustom(data));
|
client.on("General.Custom", (data) => onCustom(data));
|
||||||
if (configs.twitchSettings.autoUserColor) {
|
if (configs.userColorSettings.autoUserColor) {
|
||||||
client.on("Twitch.ChatMessage", (data) => onTwitchFirstWord(data));
|
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
|
||||||
|
client.on("Kick.ChatMessage", (data) => onChatMessage(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
let taskList;
|
let taskList;
|
||||||
|
|
@ -57,7 +58,7 @@ function relativeLuminance({ r, g, b }) {
|
||||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTwitchFirstWord(data) {
|
async function onChatMessage(data) {
|
||||||
let userColor = data.data.user.color; // hex colour
|
let userColor = data.data.user.color; // hex colour
|
||||||
|
|
||||||
if (userColor != undefined || userColor != "undefined") {
|
if (userColor != undefined || userColor != "undefined") {
|
||||||
|
|
@ -134,6 +135,22 @@ async function refresh() {
|
||||||
// LOAD TASK LIST
|
// LOAD TASK LIST
|
||||||
async function onConnect() {
|
async function onConnect() {
|
||||||
taskList = new TaskList(".task-panel");
|
taskList = new TaskList(".task-panel");
|
||||||
|
|
||||||
|
if (configs.emoteSettings.enabled && configs.emoteSettings.channelId) {
|
||||||
|
window.emoteManager = new EmoteManager({
|
||||||
|
channelName: configs.emoteSettings.channelName,
|
||||||
|
channelId: configs.emoteSettings.channelId,
|
||||||
|
providers: configs.emoteSettings.providers,
|
||||||
|
size: configs.emoteSettings.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.emoteManager.init().then(() => {
|
||||||
|
if (taskList) {
|
||||||
|
taskList.load(taskList.getData());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,63 +178,9 @@ function onCustom(payload) {
|
||||||
const username = data.username;
|
const username = data.username;
|
||||||
|
|
||||||
switch (body.mode) {
|
switch (body.mode) {
|
||||||
case "add":
|
case "refresh":
|
||||||
// body.task;
|
|
||||||
let taskPayload = {
|
|
||||||
text: body.task,
|
|
||||||
done: body.completed,
|
|
||||||
focused: body.focused,
|
|
||||||
};
|
|
||||||
|
|
||||||
let usernameColor =
|
|
||||||
userColors[id] ?? localStorage.getItem(`${id}-color`);
|
|
||||||
|
|
||||||
if (
|
|
||||||
configs.twitchSettings.autoUserColor &&
|
|
||||||
usernameColor != undefined &&
|
|
||||||
usernameColor != null
|
|
||||||
) {
|
|
||||||
taskPayload["color"] = usernameColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
taskList.addTask(id, taskPayload, username);
|
|
||||||
break;
|
|
||||||
case "focus":
|
|
||||||
taskList.focusTask(id, body.index, usernameColor);
|
|
||||||
break;
|
|
||||||
case "edit":
|
|
||||||
taskList.editTask(id, body.index, body.task, usernameColor);
|
|
||||||
break;
|
|
||||||
case "remove":
|
|
||||||
taskList.removeTask(id, body.index, usernameColor);
|
|
||||||
break;
|
|
||||||
case "done":
|
|
||||||
taskList.doneTask(id, body.index, usernameColor);
|
|
||||||
break;
|
|
||||||
case "admindelete":
|
|
||||||
taskList.removeSection(body.id);
|
|
||||||
break;
|
|
||||||
case "clearns":
|
|
||||||
refresh();
|
refresh();
|
||||||
break;
|
break;
|
||||||
case "cleardone":
|
|
||||||
taskList.cleardone();
|
|
||||||
break;
|
|
||||||
case "clearmydone":
|
|
||||||
taskList.clearmydone(id);
|
|
||||||
break;
|
|
||||||
case "clearall":
|
|
||||||
refresh();
|
|
||||||
break;
|
|
||||||
case "undone":
|
|
||||||
taskList.undoneTask(id, body.index);
|
|
||||||
break;
|
|
||||||
case "unfocus":
|
|
||||||
taskList.unfocusTask(id);
|
|
||||||
break;
|
|
||||||
case "admindelete":
|
|
||||||
taskList.removeSection(body.id);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -279,6 +279,13 @@ class TaskList {
|
||||||
|
|
||||||
// ── dom helpers ────────────────────────────────────
|
// ── dom helpers ────────────────────────────────────
|
||||||
|
|
||||||
|
#parseTaskText(text) {
|
||||||
|
if (window.emoteManager && window.emoteManager.loaded) {
|
||||||
|
return window.emoteManager.parseText(text);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
#createTaskEl(task, index) {
|
#createTaskEl(task, index) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className =
|
div.className =
|
||||||
|
|
@ -291,7 +298,8 @@ class TaskList {
|
||||||
numberSpan.textContent = `${index + 1}.`;
|
numberSpan.textContent = `${index + 1}.`;
|
||||||
|
|
||||||
const textSpan = document.createElement("span");
|
const textSpan = document.createElement("span");
|
||||||
textSpan.textContent = task.text;
|
textSpan.className = "task-text";
|
||||||
|
textSpan.innerHTML = this.#parseTaskText(task.text);
|
||||||
|
|
||||||
div.replaceChildren(numberSpan, textSpan);
|
div.replaceChildren(numberSpan, textSpan);
|
||||||
return div;
|
return div;
|
||||||
|
|
@ -343,7 +351,8 @@ class TaskList {
|
||||||
}
|
}
|
||||||
if (el.dataset.text !== task.text) {
|
if (el.dataset.text !== task.text) {
|
||||||
el.dataset.text = task.text;
|
el.dataset.text = task.text;
|
||||||
el.querySelector("span:last-child").textContent = 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}.`;
|
el.querySelector(".task-number").textContent = `${i + 1}.`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,18 +19,18 @@
|
||||||
--focused-padding: 2px 4px; /* Spacing inside focused task highlight */
|
--focused-padding: 2px 4px; /* Spacing inside focused task highlight */
|
||||||
|
|
||||||
/* -- Font Sizes -- */
|
/* -- Font Sizes -- */
|
||||||
--title-font-size: 1.5rem; /* Size of the panel title */
|
--title-font-size: 1rem; /* Size of the panel title */
|
||||||
--section-title-font-size: 1.25rem; /* Size of section headings */
|
--section-title-font-size: 1rem; /* Size of section headings */
|
||||||
--header-font-size: 14px; /* Size of the header text */
|
--header-font-size: 1rem; /* Size of the header text */
|
||||||
--header-subtitle-font-size: 13px; /* Size of the header subtitle */
|
--header-subtitle-font-size: 1rem; /* Size of the header subtitle */
|
||||||
--task-font-size: 1.2rem; /* Size of task text */
|
--task-font-size: 1rem; /* Size of task text */
|
||||||
|
|
||||||
/* -- Borders & Dividers -- */
|
/* -- Borders & Dividers -- */
|
||||||
--header-border-color: #e0dbd5; /* Line below the header */
|
--header-border-color: #e0dbd5; /* Line below the header */
|
||||||
--section-border-color: white; /* Line between sections */
|
--section-border-color: white; /* Line between sections */
|
||||||
|
|
||||||
/* -- Spacing -- */
|
/* -- Spacing -- */
|
||||||
--header-padding: 16px 16px 12px; /* Spacing inside the header */
|
--header-padding: 10px 16px 5px; /* Spacing inside the header */
|
||||||
--section-padding: 10px 16px 6px; /* Spacing inside each section */
|
--section-padding: 10px 16px 6px; /* Spacing inside each section */
|
||||||
--task-gap: 6px; /* Gap between task items */
|
--task-gap: 6px; /* Gap between task items */
|
||||||
--task-padding: 3px 0; /* Spacing around each task */
|
--task-padding: 3px 0; /* Spacing around each task */
|
||||||
|
|
@ -104,7 +104,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title.has-user-color {
|
.section-title.has-user-color {
|
||||||
color: var(--user-color); /*auto user color*/
|
color: var(--user-color); /*auto user color*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.task.done,
|
.task.done,
|
||||||
|
|
@ -129,3 +129,14 @@ body {
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emote-img {
|
||||||
|
height: 1.4em;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
margin: -0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task.done .emote-img {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Widget - Rython Task Bot v2
|
# Widget - Rython Task Bot v2
|
||||||
|
|
||||||
## Instructions
|
Optional widget for [Rython Task Bot v2](https://github.com/liyunze-coding/rython-task-bot-v2)
|
||||||
|
|
||||||
### Websocket Server
|
## Instructions
|
||||||
|
|
||||||
Websocket server running is required for browser source to work.
|
Websocket server running is required for browser source to work.
|
||||||
|
|
||||||
|
|
@ -13,14 +13,6 @@ Websocket server running is required for browser source to work.
|
||||||
3. If port number already taken, change port number
|
3. If port number already taken, change port number
|
||||||
4. Ensure that [config.js](./config.js)'s websocket settings are in-sync with Streamer.Bot's
|
4. Ensure that [config.js](./config.js)'s websocket settings are in-sync with Streamer.Bot's
|
||||||
|
|
||||||
### OBS
|
|
||||||
|
|
||||||
1. Add a new browser source in your scene
|
|
||||||
2. Tick on "Local"
|
|
||||||
3. Navigate to the index.html file
|
|
||||||
4. Optionally customise the width and height
|
|
||||||
5. Click on "Ok"
|
|
||||||
|
|
||||||
## Style Customisation
|
## Style Customisation
|
||||||
|
|
||||||
Note: `styles/structure.css` is meant to be the bare structure to allow the task widget to be functional.
|
Note: `styles/structure.css` is meant to be the bare structure to allow the task widget to be functional.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
const configs = (function () {
|
const configs = (function () {
|
||||||
const streamerBotSettings = {
|
const streamerBotSettings = {
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: 8080,
|
port: 6968,
|
||||||
endpoint: "/",
|
endpoint: "/",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -17,18 +17,23 @@ const configs = (function () {
|
||||||
"!clearmydone",
|
"!clearmydone",
|
||||||
];
|
];
|
||||||
|
|
||||||
const twitchSettings = {
|
const userColorSettings = {
|
||||||
autoUserColor: true
|
autoUserColor: true,
|
||||||
}
|
};
|
||||||
|
|
||||||
const kickSettings = {
|
const emoteSettings = {
|
||||||
autouserColor: true
|
enabled: true,
|
||||||
}
|
channelName: "rythondev",
|
||||||
|
channelId: "248474026",
|
||||||
|
providers: ["7tv", "bttv", "ffz"],
|
||||||
|
size: "1x",
|
||||||
|
cacheHours: 24,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
streamerBotSettings,
|
streamerBotSettings,
|
||||||
commands,
|
commands,
|
||||||
twitchSettings,
|
userColorSettings,
|
||||||
kickSettings
|
emoteSettings,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<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="./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="./scripts/tasklist-view.js"></script>
|
<script defer src="./scripts/tasklist-view.js"></script>
|
||||||
<script defer src="./scripts/streamerbot.js"></script>
|
<script defer src="./scripts/streamerbot.js"></script>
|
||||||
<script defer src="./scripts/header-animation.js"></script>
|
<script defer src="./scripts/header-animation.js"></script>
|
||||||
|
|
|
||||||
273
widgets/vertical/scripts/emotes.js
Normal file
273
widgets/vertical/scripts/emotes.js
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,8 +21,9 @@ function getAllLocalstorage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
client.on("General.Custom", (data) => onCustom(data));
|
client.on("General.Custom", (data) => onCustom(data));
|
||||||
if (configs.twitchSettings.autoUserColor) {
|
if (configs.userColorSettings.autoUserColor) {
|
||||||
client.on("Twitch.ChatMessage", (data) => onTwitchFirstWord(data));
|
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
|
||||||
|
client.on("Kick.ChatMessage", (data) => onChatMessage(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
let taskList;
|
let taskList;
|
||||||
|
|
@ -57,12 +58,10 @@ function relativeLuminance({ r, g, b }) {
|
||||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTwitchFirstWord(data) {
|
async function onChatMessage(data) {
|
||||||
let userColor = data.data.user.color; // hex colour #FF69B4
|
let userColor = data.data.user.color; // hex colour #FF69B4
|
||||||
|
|
||||||
|
if (userColor != undefined || userColor != "undefined") {
|
||||||
|
|
||||||
if (userColor != undefined || userColor != 'undefined') {
|
|
||||||
// update localstorage
|
// update localstorage
|
||||||
// get id: platform-userID
|
// get id: platform-userID
|
||||||
let userId = data.data.user.id;
|
let userId = data.data.user.id;
|
||||||
|
|
@ -71,7 +70,7 @@ async function onTwitchFirstWord(data) {
|
||||||
localStorage.setItem(`${key}-color`, userColor);
|
localStorage.setItem(`${key}-color`, userColor);
|
||||||
userColors[`${key}-color`] = userColor;
|
userColors[`${key}-color`] = userColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,6 +134,22 @@ async function refresh() {
|
||||||
// LOAD TASK LIST
|
// LOAD TASK LIST
|
||||||
async function onConnect() {
|
async function onConnect() {
|
||||||
taskList = new TaskList(".task-panel");
|
taskList = new TaskList(".task-panel");
|
||||||
|
|
||||||
|
if (configs.emoteSettings.enabled && configs.emoteSettings.channelId) {
|
||||||
|
window.emoteManager = new EmoteManager({
|
||||||
|
channelName: configs.emoteSettings.channelName,
|
||||||
|
channelId: configs.emoteSettings.channelId,
|
||||||
|
providers: configs.emoteSettings.providers,
|
||||||
|
size: configs.emoteSettings.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.emoteManager.init().then(() => {
|
||||||
|
if (taskList) {
|
||||||
|
taskList.load(taskList.getData());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,59 +177,9 @@ function onCustom(payload) {
|
||||||
const username = data.username;
|
const username = data.username;
|
||||||
|
|
||||||
switch (body.mode) {
|
switch (body.mode) {
|
||||||
case "add":
|
case "refresh":
|
||||||
// body.task;
|
|
||||||
let taskPayload = {
|
|
||||||
text: body.task,
|
|
||||||
done: body.completed,
|
|
||||||
focused: body.focused,
|
|
||||||
};
|
|
||||||
|
|
||||||
let usernameColor =
|
|
||||||
userColors[id] ?? localStorage.getItem(`${id}-color`);
|
|
||||||
|
|
||||||
if (configs.twitchSettings.autoUserColor && usernameColor != undefined && usernameColor != null) {
|
|
||||||
taskPayload["color"] = usernameColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
taskList.addTask(id, taskPayload, username);
|
|
||||||
break;
|
|
||||||
case "focus":
|
|
||||||
taskList.focusTask(id, body.index, usernameColor);
|
|
||||||
break;
|
|
||||||
case "edit":
|
|
||||||
taskList.editTask(id, body.index, body.task, usernameColor);
|
|
||||||
break;
|
|
||||||
case "remove":
|
|
||||||
taskList.removeTask(id, body.index, usernameColor);
|
|
||||||
break;
|
|
||||||
case "done":
|
|
||||||
taskList.doneTask(id, body.index, usernameColor);
|
|
||||||
break;
|
|
||||||
case "admindelete":
|
|
||||||
taskList.removeSection(body.id);
|
|
||||||
break;
|
|
||||||
case "clearns":
|
|
||||||
refresh();
|
refresh();
|
||||||
break;
|
break;
|
||||||
case "cleardone":
|
|
||||||
taskList.cleardone();
|
|
||||||
break;
|
|
||||||
case "clearmydone":
|
|
||||||
taskList.clearmydone(id);
|
|
||||||
break;
|
|
||||||
case "clearall":
|
|
||||||
refresh();
|
|
||||||
break;
|
|
||||||
case "undone":
|
|
||||||
taskList.undoneTask(id, body.index);
|
|
||||||
break;
|
|
||||||
case "unfocus":
|
|
||||||
taskList.unfocusTask(id);
|
|
||||||
break;
|
|
||||||
case "admindelete":
|
|
||||||
taskList.removeSection(body.id);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,13 @@ class TaskList {
|
||||||
|
|
||||||
// ── dom helpers ────────────────────────────────────
|
// ── dom helpers ────────────────────────────────────
|
||||||
|
|
||||||
|
#parseTaskText(text) {
|
||||||
|
if (window.emoteManager && window.emoteManager.loaded) {
|
||||||
|
return window.emoteManager.parseText(text);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
#createTaskEl(task, index) {
|
#createTaskEl(task, index) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className =
|
div.className =
|
||||||
|
|
@ -314,7 +321,8 @@ class TaskList {
|
||||||
numberSpan.textContent = `${index + 1}.`;
|
numberSpan.textContent = `${index + 1}.`;
|
||||||
|
|
||||||
const textSpan = document.createElement("span");
|
const textSpan = document.createElement("span");
|
||||||
textSpan.textContent = task.text;
|
textSpan.className = "task-text";
|
||||||
|
textSpan.innerHTML = this.#parseTaskText(task.text);
|
||||||
|
|
||||||
div.replaceChildren(numberSpan, textSpan);
|
div.replaceChildren(numberSpan, textSpan);
|
||||||
return div;
|
return div;
|
||||||
|
|
@ -366,7 +374,8 @@ class TaskList {
|
||||||
}
|
}
|
||||||
if (el.dataset.text !== task.text) {
|
if (el.dataset.text !== task.text) {
|
||||||
el.dataset.text = task.text;
|
el.dataset.text = task.text;
|
||||||
el.querySelector("span:last-child").textContent = 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}.`;
|
el.querySelector(".task-number").textContent = `${i + 1}.`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,3 +129,14 @@ body {
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emote-img {
|
||||||
|
height: 1.4em;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
margin: -0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task.done .emote-img {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue