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;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -481,7 +481,7 @@ public class TaskOperations
|
|||
string oldName = userTasks[index].Name;
|
||||
userTasks[index].Name = newTask;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -502,7 +502,7 @@ public class TaskOperations
|
|||
UnfocusAll(tasks);
|
||||
tasks[n].Focused = true;
|
||||
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);
|
||||
}
|
||||
else if (indexByName > -1)
|
||||
|
|
@ -513,7 +513,7 @@ public class TaskOperations
|
|||
UnfocusAll(tasks);
|
||||
tasks[n].Focused = true;
|
||||
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);
|
||||
}
|
||||
else
|
||||
|
|
@ -521,7 +521,7 @@ public class TaskOperations
|
|||
var response = AddTask(rawInput, false, true);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -846,7 +846,7 @@ public class CPHInline
|
|||
string completedTaskName = userTasks[focusedTaskIndex].Name;
|
||||
userTasks[focusedTaskIndex].Completed = true;
|
||||
userTasks[focusedTaskIndex].Focused = false;
|
||||
Broadcast(new { mode = "done", index = focusedTaskIndex }, null);
|
||||
Broadcast(new { mode = "refresh", index = focusedTaskIndex }, null);
|
||||
operations.SaveIntoTasks(userTasks);
|
||||
SaveTasks();
|
||||
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))
|
||||
{
|
||||
userTasks.RemoveAt(i);
|
||||
Broadcast(new { mode = "remove", index = i }, null);
|
||||
Broadcast(new { mode = "refresh", index = i }, null);
|
||||
}
|
||||
|
||||
operations.SaveIntoTasks(userTasks);
|
||||
|
|
@ -1024,7 +1024,7 @@ public class CPHInline
|
|||
operations.RemoveUser(key);
|
||||
SaveTasks();
|
||||
Respond(BotResponses.AdminDeleteSuccess);
|
||||
Broadcast(new { mode = "admindelete", id = key }, null);
|
||||
Broadcast(new { mode = "refresh", id = key }, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1081,7 +1081,7 @@ public class CPHInline
|
|||
{
|
||||
userTasks[i].Completed = true;
|
||||
userTasks[i].Focused = false;
|
||||
Broadcast(new { mode = "done", index = i }, null);
|
||||
Broadcast(new { mode = "refresh", index = i }, null);
|
||||
}
|
||||
|
||||
IncrementDoneCount(taskIndices.Count);
|
||||
|
|
@ -1096,7 +1096,7 @@ public class CPHInline
|
|||
operations.Unfocus();
|
||||
SaveTasks();
|
||||
Respond(BotResponses.Unfocused);
|
||||
Broadcast(new { mode = "unfocus" }, null);
|
||||
Broadcast(new { mode = "refresh" }, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1144,7 +1144,7 @@ public class CPHInline
|
|||
{
|
||||
userTasks[i].Completed = false;
|
||||
userTasks[i].Focused = false;
|
||||
Broadcast(new { mode = "undone", index = i }, null);
|
||||
Broadcast(new { mode = "refresh", index = i }, null);
|
||||
}
|
||||
|
||||
operations.SaveIntoTasks(userTasks);
|
||||
|
|
@ -1169,7 +1169,7 @@ public class CPHInline
|
|||
operations.ClearUserCompletedTasks(key);
|
||||
operations.Cleanup(false);
|
||||
SaveTasks();
|
||||
Broadcast(new { mode = "clearmydone" }, null);
|
||||
Broadcast(new { mode = "refresh" }, null);
|
||||
Respond(BotResponses.ClearMyDone);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1179,7 +1179,7 @@ public class CPHInline
|
|||
operations.ClearCompletedTasks();
|
||||
operations.Cleanup(false);
|
||||
SaveTasks();
|
||||
Broadcast(new { mode = "cleardone" }, null);
|
||||
Broadcast(new { mode = "refresh" }, null);
|
||||
Respond(BotResponses.ClearDone);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1189,7 +1189,7 @@ public class CPHInline
|
|||
operations.FilterToStreamers(GetStreamerUsernames());
|
||||
operations.Cleanup(false);
|
||||
SaveTasks();
|
||||
Broadcast(new { mode = "clearns" }, null);
|
||||
Broadcast(new { mode = "refresh" }, null);
|
||||
Respond(BotResponses.ClearNotStreamer);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const configs = (function () {
|
||||
const streamerBotSettings = {
|
||||
host: "127.0.0.1",
|
||||
port: 8080,
|
||||
port: 6968,
|
||||
endpoint: "/",
|
||||
};
|
||||
|
||||
|
|
@ -17,18 +17,23 @@ const configs = (function () {
|
|||
"!clearmydone",
|
||||
];
|
||||
|
||||
const twitchSettings = {
|
||||
autoUserColor: true
|
||||
}
|
||||
const userColorSettings = {
|
||||
autoUserColor: true,
|
||||
};
|
||||
|
||||
const kickSettings = {
|
||||
autouserColor: true
|
||||
}
|
||||
const emoteSettings = {
|
||||
enabled: true,
|
||||
channelName: "rythondev",
|
||||
channelId: "248474026",
|
||||
providers: ["7tv", "bttv", "ffz"],
|
||||
size: "1x",
|
||||
cacheHours: 24,
|
||||
};
|
||||
|
||||
return {
|
||||
streamerBotSettings,
|
||||
commands,
|
||||
twitchSettings,
|
||||
kickSettings
|
||||
userColorSettings,
|
||||
emoteSettings,
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script src="./lib/streamerbot-client.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/streamerbot.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));
|
||||
if (configs.twitchSettings.autoUserColor) {
|
||||
client.on("Twitch.ChatMessage", (data) => onTwitchFirstWord(data));
|
||||
if (configs.userColorSettings.autoUserColor) {
|
||||
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
|
||||
client.on("Kick.ChatMessage", (data) => onChatMessage(data));
|
||||
}
|
||||
|
||||
let taskList;
|
||||
|
|
@ -57,12 +58,10 @@ function relativeLuminance({ r, g, 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
|
||||
|
||||
|
||||
|
||||
if (userColor != undefined || userColor != 'undefined') {
|
||||
if (userColor != undefined || userColor != "undefined") {
|
||||
// update localstorage
|
||||
// get id: platform-userID
|
||||
let userId = data.data.user.id;
|
||||
|
|
@ -71,7 +70,7 @@ async function onTwitchFirstWord(data) {
|
|||
localStorage.setItem(`${key}-color`, userColor);
|
||||
userColors[`${key}-color`] = userColor;
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +134,22 @@ async function refresh() {
|
|||
// LOAD TASK LIST
|
||||
async function onConnect() {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -162,59 +177,9 @@ function onCustom(payload) {
|
|||
const username = data.username;
|
||||
|
||||
switch (body.mode) {
|
||||
case "add":
|
||||
// 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":
|
||||
case "refresh":
|
||||
refresh();
|
||||
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:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,6 +302,13 @@ class TaskList {
|
|||
|
||||
// ── 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 =
|
||||
|
|
@ -314,7 +321,8 @@ class TaskList {
|
|||
numberSpan.textContent = `${index + 1}.`;
|
||||
|
||||
const textSpan = document.createElement("span");
|
||||
textSpan.textContent = task.text;
|
||||
textSpan.className = "task-text";
|
||||
textSpan.innerHTML = this.#parseTaskText(task.text);
|
||||
|
||||
div.replaceChildren(numberSpan, textSpan);
|
||||
return div;
|
||||
|
|
@ -366,7 +374,8 @@ class TaskList {
|
|||
}
|
||||
if (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}.`;
|
||||
|
||||
|
|
|
|||
|
|
@ -141,3 +141,14 @@ body {
|
|||
min-width: 16px;
|
||||
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 streamerBotSettings = {
|
||||
host: "127.0.0.1",
|
||||
port: 8080,
|
||||
port: 6968,
|
||||
endpoint: "/",
|
||||
};
|
||||
|
||||
|
|
@ -19,12 +19,17 @@ const configs = (function () {
|
|||
"!clearmydone",
|
||||
];
|
||||
|
||||
const twitchSettings = {
|
||||
const userColorSettings = {
|
||||
autoUserColor: true,
|
||||
};
|
||||
|
||||
const kickSettings = {
|
||||
autouserColor: true,
|
||||
const emoteSettings = {
|
||||
enabled: true,
|
||||
channelName: "rythondev",
|
||||
channelId: "248474026",
|
||||
providers: ["7tv", "bttv", "ffz"],
|
||||
size: "1x",
|
||||
cacheHours: 24,
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
@ -33,5 +38,6 @@ const configs = (function () {
|
|||
commands,
|
||||
twitchSettings,
|
||||
kickSettings,
|
||||
emoteSettings,
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script src="./lib/streamerbot-client.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/streamerbot.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));
|
||||
if (configs.twitchSettings.autoUserColor) {
|
||||
client.on("Twitch.ChatMessage", (data) => onTwitchFirstWord(data));
|
||||
if (configs.userColorSettings.autoUserColor) {
|
||||
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
|
||||
client.on("Kick.ChatMessage", (data) => onChatMessage(data));
|
||||
}
|
||||
|
||||
let taskList;
|
||||
|
|
@ -57,7 +58,7 @@ function relativeLuminance({ r, g, 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
|
||||
|
||||
if (userColor != undefined || userColor != "undefined") {
|
||||
|
|
@ -134,6 +135,22 @@ async function refresh() {
|
|||
// LOAD TASK LIST
|
||||
async function onConnect() {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -161,63 +178,9 @@ function onCustom(payload) {
|
|||
const username = data.username;
|
||||
|
||||
switch (body.mode) {
|
||||
case "add":
|
||||
// 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":
|
||||
case "refresh":
|
||||
refresh();
|
||||
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:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -279,6 +279,13 @@ class TaskList {
|
|||
|
||||
// ── 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 =
|
||||
|
|
@ -291,7 +298,8 @@ class TaskList {
|
|||
numberSpan.textContent = `${index + 1}.`;
|
||||
|
||||
const textSpan = document.createElement("span");
|
||||
textSpan.textContent = task.text;
|
||||
textSpan.className = "task-text";
|
||||
textSpan.innerHTML = this.#parseTaskText(task.text);
|
||||
|
||||
div.replaceChildren(numberSpan, textSpan);
|
||||
return div;
|
||||
|
|
@ -343,7 +351,8 @@ class TaskList {
|
|||
}
|
||||
if (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}.`;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,18 +19,18 @@
|
|||
--focused-padding: 2px 4px; /* Spacing inside focused task highlight */
|
||||
|
||||
/* -- Font Sizes -- */
|
||||
--title-font-size: 1.5rem; /* Size of the panel title */
|
||||
--section-title-font-size: 1.25rem; /* Size of section headings */
|
||||
--header-font-size: 14px; /* Size of the header text */
|
||||
--header-subtitle-font-size: 13px; /* Size of the header subtitle */
|
||||
--task-font-size: 1.2rem; /* Size of task text */
|
||||
--title-font-size: 1rem; /* Size of the panel title */
|
||||
--section-title-font-size: 1rem; /* Size of section headings */
|
||||
--header-font-size: 1rem; /* Size of the header text */
|
||||
--header-subtitle-font-size: 1rem; /* Size of the header subtitle */
|
||||
--task-font-size: 1rem; /* Size of task text */
|
||||
|
||||
/* -- Borders & Dividers -- */
|
||||
--header-border-color: #e0dbd5; /* Line below the header */
|
||||
--section-border-color: white; /* Line between sections */
|
||||
|
||||
/* -- 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 */
|
||||
--task-gap: 6px; /* Gap between task items */
|
||||
--task-padding: 3px 0; /* Spacing around each task */
|
||||
|
|
@ -104,7 +104,7 @@ body {
|
|||
}
|
||||
|
||||
.section-title.has-user-color {
|
||||
color: var(--user-color); /*auto user color*/
|
||||
color: var(--user-color); /*auto user color*/
|
||||
}
|
||||
|
||||
.task.done,
|
||||
|
|
@ -129,3 +129,14 @@ body {
|
|||
min-width: 16px;
|
||||
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
|
||||
|
||||
## 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.
|
||||
|
||||
|
|
@ -13,14 +13,6 @@ Websocket server running is required for browser source to work.
|
|||
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
|
||||
|
||||
### 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
|
||||
|
||||
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 streamerBotSettings = {
|
||||
host: "127.0.0.1",
|
||||
port: 8080,
|
||||
port: 6968,
|
||||
endpoint: "/",
|
||||
};
|
||||
|
||||
|
|
@ -17,18 +17,23 @@ const configs = (function () {
|
|||
"!clearmydone",
|
||||
];
|
||||
|
||||
const twitchSettings = {
|
||||
autoUserColor: true
|
||||
}
|
||||
const userColorSettings = {
|
||||
autoUserColor: true,
|
||||
};
|
||||
|
||||
const kickSettings = {
|
||||
autouserColor: true
|
||||
}
|
||||
const emoteSettings = {
|
||||
enabled: true,
|
||||
channelName: "rythondev",
|
||||
channelId: "248474026",
|
||||
providers: ["7tv", "bttv", "ffz"],
|
||||
size: "1x",
|
||||
cacheHours: 24,
|
||||
};
|
||||
|
||||
return {
|
||||
streamerBotSettings,
|
||||
commands,
|
||||
twitchSettings,
|
||||
kickSettings
|
||||
userColorSettings,
|
||||
emoteSettings,
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script src="./lib/streamerbot-client.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/streamerbot.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));
|
||||
if (configs.twitchSettings.autoUserColor) {
|
||||
client.on("Twitch.ChatMessage", (data) => onTwitchFirstWord(data));
|
||||
if (configs.userColorSettings.autoUserColor) {
|
||||
client.on("Twitch.ChatMessage", (data) => onChatMessage(data));
|
||||
client.on("Kick.ChatMessage", (data) => onChatMessage(data));
|
||||
}
|
||||
|
||||
let taskList;
|
||||
|
|
@ -57,12 +58,10 @@ function relativeLuminance({ r, g, 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
|
||||
|
||||
|
||||
|
||||
if (userColor != undefined || userColor != 'undefined') {
|
||||
if (userColor != undefined || userColor != "undefined") {
|
||||
// update localstorage
|
||||
// get id: platform-userID
|
||||
let userId = data.data.user.id;
|
||||
|
|
@ -71,7 +70,7 @@ async function onTwitchFirstWord(data) {
|
|||
localStorage.setItem(`${key}-color`, userColor);
|
||||
userColors[`${key}-color`] = userColor;
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +134,22 @@ async function refresh() {
|
|||
// LOAD TASK LIST
|
||||
async function onConnect() {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -162,59 +177,9 @@ function onCustom(payload) {
|
|||
const username = data.username;
|
||||
|
||||
switch (body.mode) {
|
||||
case "add":
|
||||
// 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":
|
||||
case "refresh":
|
||||
refresh();
|
||||
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:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,6 +302,13 @@ class TaskList {
|
|||
|
||||
// ── 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 =
|
||||
|
|
@ -314,7 +321,8 @@ class TaskList {
|
|||
numberSpan.textContent = `${index + 1}.`;
|
||||
|
||||
const textSpan = document.createElement("span");
|
||||
textSpan.textContent = task.text;
|
||||
textSpan.className = "task-text";
|
||||
textSpan.innerHTML = this.#parseTaskText(task.text);
|
||||
|
||||
div.replaceChildren(numberSpan, textSpan);
|
||||
return div;
|
||||
|
|
@ -366,7 +374,8 @@ class TaskList {
|
|||
}
|
||||
if (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}.`;
|
||||
|
||||
|
|
|
|||
|
|
@ -129,3 +129,14 @@ body {
|
|||
min-width: 16px;
|
||||
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