mirror of
https://github.com/liyunze-coding/rython-task-bot-v2.git
synced 2026-09-22 15:43:57 +00:00
modified shared scripts
This commit is contained in:
parent
c57bfd75fb
commit
abb3f5f7a0
8 changed files with 1142 additions and 5 deletions
|
|
@ -5,8 +5,8 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="../shared/lib/streamerbot-client.js"></script>
|
<script src="../shared/lib/streamerbot-client.js"></script>
|
||||||
<script defer src="./config.js"></script>
|
<script defer src="./config.js"></script>
|
||||||
<script defer src="../shared/scripts/emotes.js"></script>
|
<script defer src="./scripts/emotes.js"></script>
|
||||||
<script defer src="../shared/scripts/tasklist-view.js"></script>
|
<script defer src="./scripts/tasklist-view.js"></script>
|
||||||
<script defer src="../shared/scripts/streamerbot.js"></script>
|
<script defer src="../shared/scripts/streamerbot.js"></script>
|
||||||
<script defer src="./scripts/header-animation.js"></script>
|
<script defer src="./scripts/header-animation.js"></script>
|
||||||
<link rel="stylesheet" href="./styles/style.css" />
|
<link rel="stylesheet" href="./styles/style.css" />
|
||||||
|
|
|
||||||
|
|
@ -5,7 +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="../shared/lib/streamerbot-client.js"></script>
|
<script src="../shared/lib/streamerbot-client.js"></script>
|
||||||
<script defer src="./config.js"></script>
|
<script defer src="./config.js"></script>
|
||||||
<script defer src="../shared/scripts/emotes.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="../shared/scripts/streamerbot.js"></script>
|
<script defer src="../shared/scripts/streamerbot.js"></script>
|
||||||
<script defer src="./scripts/header-animation.js"></script>
|
<script defer src="./scripts/header-animation.js"></script>
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="../shared/lib/streamerbot-client.js"></script>
|
<script src="../shared/lib/streamerbot-client.js"></script>
|
||||||
<script defer src="./config.js"></script>
|
<script defer src="./config.js"></script>
|
||||||
<script defer src="../shared/scripts/emotes.js"></script>
|
<script defer src="./scripts/emotes.js"></script>
|
||||||
<script defer src="../shared/scripts/tasklist-view.js"></script>
|
<script defer src="./scripts/tasklist-view.js"></script>
|
||||||
<script defer src="../shared/scripts/streamerbot.js"></script>
|
<script defer src="../shared/scripts/streamerbot.js"></script>
|
||||||
<script defer src="./scripts/header-animation.js"></script>
|
<script defer src="./scripts/header-animation.js"></script>
|
||||||
<link rel="stylesheet" href="./styles/style.css" />
|
<link rel="stylesheet" href="./styles/style.css" />
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
591
widgets/vertical/scripts/tasklist-view.js
Normal file
591
widgets/vertical/scripts/tasklist-view.js
Normal file
|
|
@ -0,0 +1,591 @@
|
||||||
|
// Task list view
|
||||||
|
|
||||||
|
class TaskList {
|
||||||
|
#data = [];
|
||||||
|
#firstRender = true;
|
||||||
|
#pendingRemovals = 0;
|
||||||
|
|
||||||
|
#scroll = {
|
||||||
|
offset: 0,
|
||||||
|
contentH: 0,
|
||||||
|
viewportH: 0,
|
||||||
|
speed: 50,
|
||||||
|
raf: null,
|
||||||
|
lastT: 0,
|
||||||
|
active: false,
|
||||||
|
wantStop: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
#els = {};
|
||||||
|
|
||||||
|
#ANIM_DURATION = 400;
|
||||||
|
#ANIM_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
|
||||||
|
|
||||||
|
#parseHexColor(hex) {
|
||||||
|
if (typeof hex !== "string") return null;
|
||||||
|
const s = hex.trim();
|
||||||
|
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
|
||||||
|
if (!m) return null;
|
||||||
|
let h = m[1].toLowerCase();
|
||||||
|
if (h.length === 3) {
|
||||||
|
h = h
|
||||||
|
.split("")
|
||||||
|
.map((c) => c + c)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
const r = parseInt(h.slice(0, 2), 16);
|
||||||
|
const g = parseInt(h.slice(2, 4), 16);
|
||||||
|
const b = parseInt(h.slice(4, 6), 16);
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
|
||||||
|
#relativeLuminance({ r, g, b }) {
|
||||||
|
const toLinear = (v) => {
|
||||||
|
const s = v / 255;
|
||||||
|
return s <= 0.04045
|
||||||
|
? s / 12.92
|
||||||
|
: Math.pow((s + 0.055) / 1.055, 2.4);
|
||||||
|
};
|
||||||
|
const R = toLinear(r);
|
||||||
|
const G = toLinear(g);
|
||||||
|
const B = toLinear(b);
|
||||||
|
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(container) {
|
||||||
|
const el =
|
||||||
|
typeof container === "string"
|
||||||
|
? document.querySelector(container)
|
||||||
|
: container;
|
||||||
|
|
||||||
|
this.#els = {
|
||||||
|
track: el.querySelector(".scroll-track"),
|
||||||
|
content: el.querySelector(".scroll-content"),
|
||||||
|
viewport: el.querySelector(".scroll-viewport"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── public API ─────────────────────────────────────
|
||||||
|
|
||||||
|
load(sections) {
|
||||||
|
this.#data = structuredClone(sections);
|
||||||
|
this.#render();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addSection(section) {
|
||||||
|
this.#data.push(structuredClone(section));
|
||||||
|
this.#render();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSection(id) {
|
||||||
|
const i = this.#data.findIndex((s) => s.id === id);
|
||||||
|
if (i === -1) return this;
|
||||||
|
this.#data.splice(i, 1);
|
||||||
|
this.#render();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSection(id, updater) {
|
||||||
|
const section = this.#data.find((s) => s.id === id);
|
||||||
|
if (!section) return this;
|
||||||
|
updater(section);
|
||||||
|
this.#render();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTask(sectionId, task, sectionTitle) {
|
||||||
|
if (!this.#data.find((s) => s.id === sectionId)) {
|
||||||
|
this.addSection({
|
||||||
|
id: sectionId,
|
||||||
|
title: sectionTitle || sectionId,
|
||||||
|
tasks: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.updateSection(sectionId, (s) => {
|
||||||
|
s.tasks.push(structuredClone(task));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTask(sectionId, taskIndex) {
|
||||||
|
this.updateSection(sectionId, (s) => {
|
||||||
|
s.tasks.splice(taskIndex, 1);
|
||||||
|
});
|
||||||
|
this.#data = this.#data.filter((s) => s.tasks.length > 0);
|
||||||
|
this.#render();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
editTask(sectionId, taskIndex, newText, userColor) {
|
||||||
|
return this.updateSection(sectionId, (s) => {
|
||||||
|
if (s.tasks[taskIndex]) {
|
||||||
|
s.tasks[taskIndex].text = newText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
doneTask(sectionId, taskIndex) {
|
||||||
|
return this.updateSection(sectionId, (s) => {
|
||||||
|
if (s.tasks[taskIndex]) {
|
||||||
|
s.tasks[taskIndex].done = true;
|
||||||
|
s.tasks[taskIndex].focused = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
undoneTask(sectionId, taskIndex) {
|
||||||
|
return this.updateSection(sectionId, (s) => {
|
||||||
|
if (s.tasks[taskIndex]) {
|
||||||
|
s.tasks[taskIndex].done = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
focusTask(sectionId, taskIndex) {
|
||||||
|
// unfocus any previously focused task
|
||||||
|
this.unfocusTask(sectionId);
|
||||||
|
|
||||||
|
return this.updateSection(sectionId, (s) => {
|
||||||
|
if (s.tasks[taskIndex]) {
|
||||||
|
s.tasks[taskIndex].focused = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unfocusTask(sectionId) {
|
||||||
|
return this.updateSection(sectionId, (s) => {
|
||||||
|
for (let i = 0; i < s.tasks.length; i++) {
|
||||||
|
s.tasks[i].focused = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearmydone(sectionId) {
|
||||||
|
this.updateSection(sectionId, (s) => {
|
||||||
|
s.tasks = s.tasks.filter((t) => !t.done);
|
||||||
|
});
|
||||||
|
this.#data = this.#data.filter((s) => s.tasks.length > 0);
|
||||||
|
this.#render();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleardone() {
|
||||||
|
this.#data.forEach((s) => {
|
||||||
|
s.tasks = s.tasks.filter((t) => !t.done);
|
||||||
|
});
|
||||||
|
this.#data = this.#data.filter((s) => s.tasks.length > 0);
|
||||||
|
this.#render();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
return structuredClone(this.#data);
|
||||||
|
}
|
||||||
|
|
||||||
|
get sectionCount() {
|
||||||
|
return this.#data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get taskCount() {
|
||||||
|
return this.#data.reduce((sum, s) => sum + s.tasks.length, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get doneCount() {
|
||||||
|
return this.#data.reduce(
|
||||||
|
(sum, s) => sum + s.tasks.filter((t) => t.done).length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.#forceStop();
|
||||||
|
this.#els.content.innerHTML = "";
|
||||||
|
this.#data = [];
|
||||||
|
this.#firstRender = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── render ─────────────────────────────────────────
|
||||||
|
|
||||||
|
#render() {
|
||||||
|
if (this.#firstRender) {
|
||||||
|
this.#data.forEach((s) =>
|
||||||
|
this.#els.content.appendChild(this.#createSectionEl(s)),
|
||||||
|
);
|
||||||
|
this.#firstRender = false;
|
||||||
|
} else {
|
||||||
|
this.#patchContainer(this.#els.content, this.#data, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#pendingRemovals === 0) {
|
||||||
|
this.#syncScroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── animations ─────────────────────────────────────
|
||||||
|
|
||||||
|
#animateIn(el) {
|
||||||
|
el.style.overflow = "hidden";
|
||||||
|
|
||||||
|
const h = el.scrollHeight;
|
||||||
|
el.style.height = "0px";
|
||||||
|
el.style.opacity = "0";
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const anim = el.animate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
height: "0px",
|
||||||
|
opacity: 0,
|
||||||
|
transform: "translateX(-8px)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
height: h + "px",
|
||||||
|
opacity: 1,
|
||||||
|
transform: "translateX(0)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: this.#ANIM_DURATION,
|
||||||
|
easing: this.#ANIM_EASING,
|
||||||
|
fill: "forwards",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
anim.onfinish = () => {
|
||||||
|
el.style.height = "";
|
||||||
|
el.style.opacity = "";
|
||||||
|
el.style.overflow = "";
|
||||||
|
el.style.transform = "";
|
||||||
|
anim.cancel();
|
||||||
|
this.#syncScroll();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#animateRemove(el) {
|
||||||
|
this.#pendingRemovals++;
|
||||||
|
const h = el.scrollHeight;
|
||||||
|
|
||||||
|
const anim = el.animate(
|
||||||
|
[
|
||||||
|
{ height: h + "px", opacity: 1, transform: "translateX(0)" },
|
||||||
|
{ height: "0px", opacity: 0, transform: "translateX(-8px)" },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: this.#ANIM_DURATION,
|
||||||
|
easing: this.#ANIM_EASING,
|
||||||
|
fill: "forwards",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
anim.onfinish = () => {
|
||||||
|
el.remove();
|
||||||
|
this.#pendingRemovals--;
|
||||||
|
|
||||||
|
if (this.#pendingRemovals === 0) {
|
||||||
|
this.#syncScroll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#animateStrikethrough(el) {
|
||||||
|
el.animate(
|
||||||
|
[
|
||||||
|
{ opacity: 0.5, transform: "scale(0.98)" },
|
||||||
|
{ opacity: 1, transform: "scale(1)" },
|
||||||
|
],
|
||||||
|
{ duration: 250, easing: "ease-out" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── dom helpers ────────────────────────────────────
|
||||||
|
|
||||||
|
#parseTaskText(text) {
|
||||||
|
if (window.emoteManager && window.emoteManager.loaded) {
|
||||||
|
return window.emoteManager.parseText(text);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createTaskEl(task, index) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className =
|
||||||
|
"task" +
|
||||||
|
(task.done ? " done" : "") +
|
||||||
|
(task.focused ? " focused" : "");
|
||||||
|
div.dataset.text = task.text;
|
||||||
|
const numberSpan = document.createElement("span");
|
||||||
|
numberSpan.className = "task-number";
|
||||||
|
numberSpan.textContent = `${index + 1}.`;
|
||||||
|
|
||||||
|
const textSpan = document.createElement("span");
|
||||||
|
textSpan.className = "task-text";
|
||||||
|
textSpan.innerHTML = this.#parseTaskText(task.text);
|
||||||
|
|
||||||
|
div.replaceChildren(numberSpan, textSpan);
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createSectionEl(section) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "section";
|
||||||
|
div.dataset.key = section.id;
|
||||||
|
if (section.title) {
|
||||||
|
const t = document.createElement("div");
|
||||||
|
t.className = "section-title";
|
||||||
|
t.textContent = section.title;
|
||||||
|
let color =
|
||||||
|
section.color ?? localStorage.getItem(`${section.id}-color`);
|
||||||
|
if (
|
||||||
|
configs.userColorSettings.autoUserColor &&
|
||||||
|
color != undefined &&
|
||||||
|
color != "undefined" &&
|
||||||
|
color != null
|
||||||
|
) {
|
||||||
|
t.style.setProperty("--user-color", color);
|
||||||
|
t.classList.add("has-user-color");
|
||||||
|
}
|
||||||
|
|
||||||
|
div.appendChild(t);
|
||||||
|
}
|
||||||
|
section.tasks.forEach((task, i) =>
|
||||||
|
div.appendChild(this.#createTaskEl(task, i)),
|
||||||
|
);
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── patching ───────────────────────────────────────
|
||||||
|
|
||||||
|
#patchTasks(sectionEl, tasks, animate) {
|
||||||
|
const existing = [...sectionEl.querySelectorAll(":scope > .task")];
|
||||||
|
|
||||||
|
tasks.forEach((task, i) => {
|
||||||
|
if (i < existing.length) {
|
||||||
|
const el = existing[i];
|
||||||
|
const wantClass =
|
||||||
|
"task" +
|
||||||
|
(task.done ? " done" : "") +
|
||||||
|
(task.focused ? " focused" : "");
|
||||||
|
if (el.className !== wantClass) {
|
||||||
|
el.className = wantClass;
|
||||||
|
if (animate) this.#animateStrikethrough(el);
|
||||||
|
}
|
||||||
|
if (el.dataset.text !== task.text) {
|
||||||
|
el.dataset.text = task.text;
|
||||||
|
const textEl =
|
||||||
|
el.querySelector(".task-text") ||
|
||||||
|
el.querySelector("span:last-child");
|
||||||
|
textEl.innerHTML = this.#parseTaskText(task.text);
|
||||||
|
}
|
||||||
|
el.querySelector(".task-number").textContent = `${i + 1}.`;
|
||||||
|
|
||||||
|
// Update color if changed
|
||||||
|
if (task.color != undefined && task.color != "undefined") {
|
||||||
|
el.style.setProperty("--user-color", task.color);
|
||||||
|
el.classList.add("has-user-color");
|
||||||
|
} else {
|
||||||
|
el.style.removeProperty("--user-color");
|
||||||
|
el.classList.remove("has-user-color");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newEl = this.#createTaskEl(task, i);
|
||||||
|
sectionEl.appendChild(newEl);
|
||||||
|
if (animate) this.#animateIn(newEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = existing.length - 1; i >= tasks.length; i--) {
|
||||||
|
if (animate) {
|
||||||
|
this.#animateRemove(existing[i]);
|
||||||
|
} else {
|
||||||
|
existing[i].remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#patchContainer(container, sections, animate) {
|
||||||
|
const oldMap = new Map();
|
||||||
|
container.querySelectorAll(":scope > .section").forEach((el) => {
|
||||||
|
oldMap.set(el.dataset.key, el);
|
||||||
|
});
|
||||||
|
|
||||||
|
let cursor = container.firstElementChild;
|
||||||
|
|
||||||
|
sections.forEach((section) => {
|
||||||
|
const key = section.id;
|
||||||
|
const existing = oldMap.get(key);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
this.#patchTasks(existing, section.tasks, animate);
|
||||||
|
if (existing !== cursor) {
|
||||||
|
container.insertBefore(existing, cursor);
|
||||||
|
} else {
|
||||||
|
cursor = cursor.nextElementSibling;
|
||||||
|
}
|
||||||
|
oldMap.delete(key);
|
||||||
|
} else {
|
||||||
|
const newEl = this.#createSectionEl(section);
|
||||||
|
container.insertBefore(newEl, cursor);
|
||||||
|
if (animate) this.#animateIn(newEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
oldMap.forEach((el) => {
|
||||||
|
if (animate) {
|
||||||
|
this.#animateRemove(el);
|
||||||
|
} else {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── scroll ─────────────────────────────────────────
|
||||||
|
|
||||||
|
#tick = (now) => {
|
||||||
|
const s = this.#scroll;
|
||||||
|
const dt = (now - s.lastT) / 1000;
|
||||||
|
s.lastT = now;
|
||||||
|
|
||||||
|
if (dt < 0.2) {
|
||||||
|
s.contentH = this.#els.content.scrollHeight;
|
||||||
|
s.offset += s.speed * dt;
|
||||||
|
|
||||||
|
if (s.contentH > 0 && s.offset >= s.contentH) {
|
||||||
|
s.offset -= s.contentH;
|
||||||
|
|
||||||
|
if (s.wantStop) {
|
||||||
|
s.wantStop = false;
|
||||||
|
this.#forceStop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#els.track.style.transform = `translateY(${-s.offset}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
s.raf = requestAnimationFrame(this.#tick);
|
||||||
|
};
|
||||||
|
|
||||||
|
#startScroll() {
|
||||||
|
const s = this.#scroll;
|
||||||
|
s.wantStop = false;
|
||||||
|
if (s.active) return;
|
||||||
|
s.active = true;
|
||||||
|
s.lastT = performance.now();
|
||||||
|
s.raf = requestAnimationFrame(this.#tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
#requestStop() {
|
||||||
|
if (!this.#scroll.active) return;
|
||||||
|
this.#scroll.wantStop = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forceStop() {
|
||||||
|
const s = this.#scroll;
|
||||||
|
s.active = false;
|
||||||
|
s.wantStop = false;
|
||||||
|
cancelAnimationFrame(s.raf);
|
||||||
|
s.raf = null;
|
||||||
|
s.offset = 0;
|
||||||
|
this.#els.track.style.transform = "";
|
||||||
|
|
||||||
|
const clone = this.#els.track.querySelector(".scroll-clone");
|
||||||
|
if (clone) clone.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
#syncScroll() {
|
||||||
|
const s = this.#scroll;
|
||||||
|
s.contentH = this.#els.content.scrollHeight;
|
||||||
|
s.viewportH = this.#els.viewport.clientHeight;
|
||||||
|
const needs = s.contentH > s.viewportH;
|
||||||
|
|
||||||
|
let clone = this.#els.track.querySelector(".scroll-clone");
|
||||||
|
|
||||||
|
if (needs) {
|
||||||
|
if (!clone) {
|
||||||
|
clone = this.#els.content.cloneNode(true);
|
||||||
|
clone.id = "";
|
||||||
|
clone.classList.add("scroll-clone");
|
||||||
|
this.#els.track.appendChild(clone);
|
||||||
|
} else {
|
||||||
|
this.#patchContainer(clone, this.#data, false);
|
||||||
|
}
|
||||||
|
if (s.offset >= s.contentH) {
|
||||||
|
s.offset %= s.contentH;
|
||||||
|
}
|
||||||
|
this.#startScroll();
|
||||||
|
} else {
|
||||||
|
if (s.active) {
|
||||||
|
if (clone) this.#patchContainer(clone, this.#data, false);
|
||||||
|
this.#requestStop();
|
||||||
|
} else {
|
||||||
|
if (clone) clone.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── usage ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
// const list2 = new TaskList(".task-panel");
|
||||||
|
|
||||||
|
// list2.load([
|
||||||
|
// {
|
||||||
|
// title: "someone",
|
||||||
|
// tasks: [
|
||||||
|
// { text: "dinner" },
|
||||||
|
// { text: "watch bad bunny halftime show" },
|
||||||
|
// { text: "tie tf down", done: true },
|
||||||
|
// { text: "boy dinner", done: true },
|
||||||
|
// { text: "3 hour SOOP" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Cape_Codder",
|
||||||
|
// tasks: [
|
||||||
|
// { text: "Part 2", done: true },
|
||||||
|
// { text: "Ch 1-2", done: true },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "sunflawer",
|
||||||
|
// tasks: [
|
||||||
|
// { text: "fix walls from home office" },
|
||||||
|
// { text: "sand walls zzzsdkjhfsk", done: true },
|
||||||
|
// { text: "meal prep" },
|
||||||
|
// { text: "buy wrist brace maybe" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "extra_user1",
|
||||||
|
// tasks: [
|
||||||
|
// { text: "go grocery shopping" },
|
||||||
|
// { text: "clean the kitchen", done: true },
|
||||||
|
// { text: "respond to emails" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// demo
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// list2.addTask("someone2", { text: "new task just dropped" });
|
||||||
|
// }, 1000);
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// list2.;
|
||||||
|
// }, 3000);
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// list.toggleTask("sunflawer", 0);
|
||||||
|
// }, 10000);
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// list.removeSection("Cape_Codder");
|
||||||
|
// }, 12000);
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// list.removeTask("extra_user1", 1);
|
||||||
|
// }, 14000);
|
||||||
Loading…
Reference in a new issue