sync from github

This commit is contained in:
minster586
2025-08-25 01:16:23 -04:00
parent 3c95ac2361
commit 9384c57f43
53 changed files with 6941 additions and 1347 deletions

View File

@@ -1,452 +1,499 @@
let streamerBotClient;
let streamerBotConnected = false;
let kickWebSocket = null;
let tikfinityWebSocket = null;
let speakerBotClient = null;
async function saveSettingsToLocalStorage() {
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
const colorfields = document.querySelectorAll("input[type=color]:not(.avoid)");
const selects = document.querySelectorAll("select:not(.avoid)");
const hiddenField = document.querySelector("textarea[name=youTubeCustomEmotes]:not(.avoid)");
/* -------------------------
Salvar configurações no localStorage
-------------------------- */
function saveSettingsToLocalStorage() {
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
const colorfields = document.querySelectorAll("input[type=color]:not(.avoid)");
const selects = document.querySelectorAll("select:not(.avoid)");
const ranges = document.querySelectorAll("input[type=range]:not(.avoid)");
const settings = {};
const ranges = document.querySelectorAll("input[type=range]:not(.avoid)");
checkboxes.forEach(cb => settings[cb.name] = cb.checked);
ranges.forEach(r => settings[r.name] = r.value);
textfields.forEach(tf => settings[tf.name] = tf.value);
numberfields.forEach(nf => settings[nf.name] = nf.value);
colorfields.forEach(cf => settings[cf.name] = cf.value);
selects.forEach(s => settings[s.name] = s.value);
const settings = {};
localStorage.setItem("chatrdWidgetSettings", JSON.stringify(settings));
checkboxes.forEach((checkbox) => {
settings[checkbox.name] = checkbox.checked;
});
ranges.forEach((range) => {
settings[range.name] = range.value;
});
textfields.forEach((textfield) => {
settings[textfield.name] = textfield.value;
});
numberfields.forEach((numberfield) => {
settings[numberfield.name] = numberfield.value;
});
colorfields.forEach((colorfield) => {
settings[colorfield.name] = colorfield.value;
});
selects.forEach((select) => {
settings[select.name] = select.value;
});
localStorage.setItem("chatWidgetSettings", JSON.stringify(settings));
if (streamerBotConnected == true) {
streamerBotClient.doAction(
{ name : "YouTube Custom Emotes" },
{
"chatrdytcustomemotes": JSON.stringify(hiddenField.value.trim()),
}
).then( (setglobals) => {
console.debug('Saving YouTube Emotes from Streamer.Bot', setglobals);
});
}
// Salva emotes no Streamer.bot
try {
const youtubeMemberEmotes = document.querySelector("textarea[name=youTubeCustomEmotes]:not(.avoid)");
youtubeSaveMemberEmotes(JSON.parse(youtubeMemberEmotes.value));
}
catch (err) {
console.error("[ChatRD] Emotes JSON inválido", err);
}
generateUrl();
}
/* -------------------------
Carregar configurações do localStorage
-------------------------- */
async function loadSettingsFromLocalStorage() {
const saved = localStorage.getItem("chatWidgetSettings");
if (!saved) return;
const saved = localStorage.getItem("chatrdWidgetSettings");
if (!saved) return;
const settings = JSON.parse(saved);
console.log(settings);
const settings = JSON.parse(saved);
Object.keys(settings).forEach((key) => {
const input = document.querySelector(`[name="${key}"]`);
if (input) {
if (input.type === "checkbox") {
input.checked = settings[key];
}
else {
input.value = settings[key];
}
}
});
Object.keys(settings).forEach(key => {
const input = document.querySelector(`[name="${key}"]`);
if (input) {
if (input.type === "checkbox") {
input.checked = settings[key];
} else {
input.value = settings[key];
}
}
});
document.querySelector('#font-value').textContent = Math.floor(document.querySelector('#font-slider').value * 100) + '%';
document.querySelector('#bg-opacity-value').textContent = Math.floor(document.querySelector('#bg-opacity-slider').value * 100) + '%';
document.querySelector('#font-value').textContent = Math.floor(document.querySelector('#font-slider').value * 100) + '%';
youtubeLoadMemberEmotes().then(settings => {
if (settings) {
const youtubeMemberEmotes = document.querySelector("textarea[name=youTubeCustomEmotes]:not(.avoid)");
console.log('[ChatRD][Settings] YouTube Member Emotes Loaded', settings);
youtubeMemberEmotes.value = JSON.stringify(settings);
populateEmoteList();
}
});
}
/* -------------------------
Configurar eventos para salvar mudanças
-------------------------- */
function pushChangeEvents() {
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
const colorfields = document.querySelectorAll("input[type=color]:not(.avoid)");
const selects = document.querySelectorAll("select:not(.avoid)");
const ranges = document.querySelectorAll("input[type=range]:not(.avoid)");
var streamerBotServerAddress = document.querySelector('input[type=text][name=streamerBotServerAddress]').value;
var streamerBotServerPort = document.querySelector('input[type=text][name=streamerBotServerPort]').value;
[...checkboxes, ...textfields, ...numberfields, ...colorfields, ...selects, ...ranges].forEach(el => {
el.addEventListener('change', saveSettingsToLocalStorage);
el.addEventListener('input', saveSettingsToLocalStorage);
});
streamerBotClient = new StreamerbotClient({
host: streamerBotServerAddress,
port: streamerBotServerPort,
onConnect: (data) => {
streamerBotConnected = true;
document.querySelector('#font-slider').addEventListener('input', function () {
document.querySelector('#font-value').textContent = Math.floor(this.value * 100) + '%';
});
document.querySelector('#memberemotesbstatus').classList.remove('offline');
document.querySelector('#memberemotesbstatus').classList.add('online');
document.querySelector('#memberemotesbstatus span').textContent = 'Streamer.Bot is Online!';
document.querySelector('#bg-opacity-slider').addEventListener('input', function () {
document.querySelector('#bg-opacity-value').textContent = Math.floor(this.value * 100) + '%';
});
}
streamerBotClient.getGlobals().then( (getglobals) => {
const settings = JSON.parse(getglobals.variables.chatrdytcustomemotes.value);
console.debug('Getting YouTube Emotes from Streamer.Bot', settings);
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
textarea.value = settings;
populateEmoteList();
});
},
onDisconnect: () => {
console.error('Streamer.bot Disconnected!');
streamerBotConnected = false;
document.querySelector('#memberemotesbstatus').classList.remove('online');
document.querySelector('#memberemotesbstatus').classList.add('offline');
document.querySelector('#memberemotesbstatus span').textContent = 'Streamer.Bot is Offline!';
}
});
/* -------------------------
Gerar URL de preview
-------------------------- */
function generateUrl() {
const outputField = document.getElementById("outputUrl");
outputField.value = '';
const baseUrlObj = new URL(window.location.href);
baseUrlObj.pathname = baseUrlObj.pathname.replace(/index\.html$/, "chat.html");
const baseUrl = baseUrlObj.toString();
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
const colorfields = document.querySelectorAll("input[type=color]:not(.avoid)");
const selects = document.querySelectorAll("select:not(.avoid)");
const ranges = document.querySelectorAll("input[type=range]:not(.avoid)");
const params = new URLSearchParams();
selects.forEach(s => params.set(s.name, s.value));
ranges.forEach(r => params.set(r.name, r.value));
checkboxes.forEach(cb => params.set(cb.name, cb.checked));
colorfields.forEach(cf => params.set(cf.name, cf.value));
textfields.forEach(tf => params.set(tf.name, tf.value));
numberfields.forEach(nf => params.set(nf.name, nf.value));
outputField.value = baseUrl + '?' + params.toString();
document.querySelector('#preview iframe').src = 'chat.html?' + params.toString();
}
/* -------------------------
Copiar URL para clipboard
-------------------------- */
function copyUrl() {
const output = document.getElementById("outputUrl");
const value = output.value;
const button = document.querySelector('.url-bar button');
const buttonDefaultText = 'Copy URL';
navigator.clipboard.writeText(value).then(() => {
button.textContent = 'ChatRD URL Copied!';
button.style.backgroundColor = "#00dd63";
setTimeout(() => {
button.textContent = buttonDefaultText;
button.removeAttribute('style');
}, 3000);
}).catch(err => {
console.error("Failed to copy: ", err);
});
}
/* -------------------------
Mostrar/esconder plataformas
-------------------------- */
function setupPlatformToggles() {
const platforms = document.querySelectorAll('.platform');
platforms.forEach(platform => {
const platformId = platform.id;
const toggleName = `show${capitalize(platformId)}`;
const toggle = platform.querySelector(`input[name="${toggleName}"]`);
const setupDiv = platform.querySelector('.setup');
if (toggle && setupDiv) {
// Removido: initializeTransitionStyles(setupDiv);
// Defina o overflow no CSS ou aqui, se preferir
setupDiv.style.overflow = 'hidden';
setupDiv.style.transition = 'max-height 0.4s ease, opacity 0.4s ease';
setVisible(setupDiv, toggle.checked);
toggle.removeEventListener('change', toggle._handler || (() => { }));
const handler = () => setVisible(setupDiv, toggle.checked);
toggle._handler = handler;
toggle.addEventListener('change', handler);
}
});
function setVisible(element, visible) {
if (visible) {
// Remove 'display: none' para que a altura possa ser calculada
element.style.display = 'block';
// Força o elemento a iniciar com altura e opacidade zero
element.style.maxHeight = '0px';
element.style.opacity = '0';
element.offsetHeight; // Força a renderização
// Inicia a transição para a altura real e opacidade completa
element.style.maxHeight = element.scrollHeight + 'px';
element.style.opacity = '1';
// Remove os estilos após a transição de abertura
element.addEventListener('transitionend', function handler() {
element.style.maxHeight = null;
element.style.opacity = null;
element.removeEventListener('transitionend', handler);
});
}
else {
// Define o maxHeight para a altura atual antes de iniciar a transição de fechamento
element.style.maxHeight = element.scrollHeight + 'px';
element.offsetHeight; // Força a renderização
// Inicia a transição para fechar o elemento
element.style.maxHeight = '0px';
element.style.opacity = '0';
// Esconde o elemento com 'display: none' após a transição
setTimeout(() => {
if (element.style.opacity === '0') {
element.style.display = 'none';
}
}, 400); // O tempo precisa ser o mesmo da transição (0.4s)
}
}
// A função initializeTransitionStyles() foi removida.
// O estilo de transição foi movido para a função principal para ser definido uma única vez.
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
}
/* -------------------------
Navegação no footer
-------------------------- */
function setupFooterNavBar() {
document.querySelectorAll('.nav-bar a').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
if (!targetId || !targetId.startsWith('#')) return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
const offset = 20;
const y = targetElement.getBoundingClientRect().top + window.scrollY - offset;
window.scrollTo({ top: y, behavior: 'smooth' });
}
});
});
}
/* -------------------------
Modal para adicionar emotes
-------------------------- */
function setupAddEmoteModal() {
const modal = document.getElementById("addEmoteModal");
const nameInput = document.getElementById("newEmoteName");
const urlInput = document.getElementById("newEmoteURL");
const confirmBtn = document.getElementById("confirmAddEmote");
const cancelBtn = document.getElementById("cancelAddEmote");
const addButton = document.querySelector("#addEmoteButton");
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
if (!modal || !addButton || !textarea) return;
// ESC global → aciona cancelBtn
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !modal.classList.contains("hidden")) {
cancelBtn.click();
}
});
// ENTER nos inputs → aciona confirmBtn
[nameInput, urlInput].forEach(input => {
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault(); // evita submit/form
confirmBtn.click();
}
});
});
addButton.onclick = (event) => {
event.preventDefault();
if (streamerBotConnected) {
nameInput.value = "";
urlInput.value = "";
modal.classList.remove("hidden");
nameInput.focus();
} else {
alert("Streamer.bot is Offline!");
}
};
cancelBtn.onclick = (e) => {
e.preventDefault();
modal.classList.add("hidden");
};
confirmBtn.onclick = (e) => {
e.preventDefault();
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (!name || !url) {
alert("Both fields are required.");
return;
}
let emotes;
try {
emotes = JSON.parse(textarea.value);
} catch (err) {
console.error("Invalid JSON", err);
alert("Emote data is invalid.");
return;
}
if (emotes[name]) {
alert(`Emote "${name}" already exists.`);
return;
}
emotes[name] = url;
textarea.value = JSON.stringify(emotes);
modal.classList.add("hidden");
populateEmoteList();
};
}
async function pushChangeEvents() {
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
const colorfields = document.querySelectorAll("input[type=color]:not(.avoid)");
const selects = document.querySelectorAll("select:not(.avoid)");
/* -------------------------
Lista de emotes
-------------------------- */
function populateEmoteList() {
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
const emoteList = document.querySelector("#youtube .emote-list");
if (!textarea || !emoteList) return;
const ranges = document.querySelectorAll("input[type=range]:not(.avoid)");
emoteList.querySelectorAll(".emote-item").forEach(item => {
if (item.querySelector("button")?.id !== "addEmoteButton") {
item.remove();
}
});
checkboxes.forEach((checkbox) => {
checkbox.addEventListener('change', () => {
generateUrl();
saveSettingsToLocalStorage();
});
});
textfields.forEach((textfield) => {
textfield.addEventListener('input', () => {
generateUrl();
saveSettingsToLocalStorage();
});
});
numberfields.forEach((numberfield) => {
numberfield.addEventListener('input', () => {
generateUrl();
saveSettingsToLocalStorage();
});
});
colorfields.forEach((colorfield) => {
colorfield.addEventListener('change', () => {
generateUrl();
saveSettingsToLocalStorage();
});
});
selects.forEach((select) => {
select.addEventListener('change', () => {
generateUrl();
saveSettingsToLocalStorage();
});
});
textfields.forEach((textfield) => {
textfield.addEventListener('input', () => {
generateUrl();
saveSettingsToLocalStorage();
});
});
let emotes;
try {
emotes = JSON.parse(textarea.value);
} catch (e) {
console.error("[ChatRD][Settings] Invalid JSON in YouTube Emotes textarea", e);
return;
}
ranges.forEach((range) => {
range.addEventListener('change', () => {
generateUrl();
saveSettingsToLocalStorage();
});
});
const addButtonSpan = emoteList.querySelector("#addEmoteButton")?.parentElement;
document.querySelector('#font-slider').addEventListener('input', function () {
document.querySelector('#font-value').textContent = Math.floor(this.value * 100) + '%';
});
for (const [emoteName, emoteUrl] of Object.entries(emotes)) {
const span = document.createElement("span");
span.classList.add("emote-item");
span.innerHTML = `
<img data-emote="${emoteName}" src="${emoteUrl}" alt="">
<em>${emoteName}</em>
<button class="delete"><i class="fa-solid fa-trash-can"></i></button>
`;
document.querySelector('#bg-opacity-slider').addEventListener('input', function () {
document.querySelector('#bg-opacity-value').textContent = this.value;
});
span.querySelector(".delete").addEventListener("click", () => {
if (confirm(`Are you sure you want to delete '${emoteName}'?`)) {
delete emotes[emoteName];
textarea.value = JSON.stringify(emotes);
populateEmoteList();
}
});
emoteList.insertBefore(span, addButtonSpan || null);
}
saveSettingsToLocalStorage();
}
/* -------------------------
Funções YouTube <-> Streamer.bot
-------------------------- */
function youtubeSaveMemberEmotes(data) {
if (!streamerBotClient) return;
const json = JSON.stringify(data);
streamerBotClient.doAction({ name: "[YouTube] Member Emotes" }, {
"chatrdytcustomemotes": json,
}).then((res) => {
console.debug('[ChatRD][Settings] Saving YouTube Member Emotes... ', res);
});
}
function youtubeLoadMemberEmotes() {
if (!streamerBotClient) return Promise.resolve(null);
return streamerBotClient.getGlobals().then((globals) => {
console.debug('[ChatRD][Settings] Loading Global Vars...', globals);
const emoteglobal = globals.variables?.chatrdytcustomemotes;
if (!emoteglobal) {
console.warn('[ChatRD][Settings] Global variable "chatrdytcustomemotes" not found.');
return null;
}
try {
return JSON.parse(emoteglobal.value);
} catch (e) {
console.error('[ChatRD][Settings] Failed to parse YouTube Member Emote JSON', e);
return null;
}
});
}
/* -------------------------
Conexão com Streamer.bot
-------------------------- */
function streamerBotConnect() {
const streamerBotStatus = document.getElementById('streamerBotStatus');
const streamerBotServerAddress = document.querySelector('input[type=text][name=streamerBotServerAddress]').value;
const streamerBotServerPort = document.querySelector('input[type=text][name=streamerBotServerPort]').value;
streamerBotClient = new StreamerbotClient({
host: streamerBotServerAddress,
port: streamerBotServerPort,
onConnect: () => {
console.debug(`[ChatRD][Settings] Connected to Streamer.bot successfully!`);
streamerBotConnected = true;
streamerBotStatus.classList.add('connected');
streamerBotStatus.querySelector('small').textContent = `Connected`;
loadSettingsFromLocalStorage();
generateUrl();
pushChangeEvents();
setupFooterNavBar();
setupAddEmoteModal();
setupPlatformToggles();
},
onDisconnect: () => {
streamerBotStatus.classList.remove('connected');
streamerBotStatus.querySelector('small').textContent = `Awaiting for connection`;
streamerBotConnected = false;
console.debug(`[ChatRD][Settings] Streamer.bot Disconnected!`);
}
});
}
async function generateUrl() {
document.getElementById("outputUrl").value = '';
async function speakerBotConnection() {
const speakerBotStatus = document.getElementById('speakerBotStatus');
var baseUrl = 'https://vortisrd.github.io/chatrd/chat.html';
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
const colorfields = document.querySelectorAll("input[type=color]:not(.avoid)");
const selects = document.querySelectorAll("select:not(.avoid)");
const speakerBotServerAddress = document.querySelector('input[type=text][name=speakerBotServerAddress]').value;
const speakerBotServerPort = document.querySelector('input[type=text][name=speakerBotServerPort]').value;
const speakerBotVoiceAlias = document.querySelector('input[type=text][name=speakerBotVoiceAlias]').value;
const ranges = document.querySelectorAll("input[type=range]:not(.avoid)");
const showSpeakerbot = document.querySelector('input[type=checkbox][name=showSpeakerbot]').checked;
const params = new URLSearchParams();
selects.forEach((select) => {
params.set(select.name, select.value);
});
ranges.forEach((range) => {
params.set(range.name, range.value);
});
checkboxes.forEach((checkbox) => {
params.set(checkbox.name, checkbox.checked);
});
colorfields.forEach((colorfield) => {
params.set(colorfield.name, colorfield.value);
});
textfields.forEach((textfield) => {
params.set(textfield.name, textfield.value);
});
numberfields.forEach((numberfield) => {
params.set(numberfield.name, numberfield.value);
});
if (!showSpeakerbot) {
// Se não é pra mostrar, desconecta caso esteja ativo
if (speakerBotClient && speakerBotClient.ws && speakerBotClient.ws.readyState !== WebSocket.CLOSED) {
console.log("[ChatRD][Settings] Disconnecting SpeakerBot...");
speakerBotClient.disconnect();
}
return;
}
document.getElementById("outputUrl").value = baseUrl + '?' + params.toString();
document.querySelector('#chat-preview iframe').src = 'chat.html?'+params.toString();
}
// Se já está conectado ou conectando, não cria outro
if (speakerBotClient && speakerBotClient.ws && speakerBotClient.ws.readyState !== WebSocket.CLOSED) {
console.log("[ChatRD][Settings] SpeakerBot WebSocket is already on!.");
return;
}
async function copyUrl() {
// Cria nova instância
speakerBotClient = new SpeakerBotClient({
host: speakerBotServerAddress,
port: speakerBotServerPort,
voiceAlias: speakerBotVoiceAlias,
const output = document.getElementById("outputUrl")
const value = output.value;
const button = document.querySelector('.url-bar button');
const buttonDefaulText = 'Copy URL';
onConnect: () => {
speakerBotStatus.classList.add('connected');
speakerBotStatus.querySelector('small').textContent = `Connected`;
},
navigator.clipboard.writeText(value)
.then(() => {
button.textContent = 'ChatRD URL Copied!';
button.style.backgroundColor = "#00dd63";
setTimeout(() => {
button.textContent = buttonDefaulText;
button.removeAttribute('style');
}, 3000);
})
.catch(err => {
console.error("Failed to copy: ", err);
});
}
async function setupAddEmoteModal() {
const modal = document.getElementById("addEmoteModal");
const nameInput = document.getElementById("newEmoteName");
const urlInput = document.getElementById("newEmoteURL");
const confirmBtn = document.getElementById("confirmAddEmote");
const cancelBtn = document.getElementById("cancelAddEmote");
const addButton = document.querySelector("#youtube .emote-item:last-child .add");
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
if (!modal || !addButton || !textarea) return;
// Show modal
addButton.onclick = () => {
if (streamerBotConnected == true) {
nameInput.value = "";
urlInput.value = "";
modal.classList.remove("hidden");
nameInput.focus();
}
else {
alert("Streamer.bot is Offline!");
return;
}
};
// Cancel
cancelBtn.onclick = () => {
modal.classList.add("hidden");
};
// Confirm
confirmBtn.onclick = () => {
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (!name || !url) {
alert("Both fields are required.");
return;
}
let emotes;
try {
emotes = JSON.parse(textarea.value);
} catch (err) {
console.error("Invalid JSON", err);
alert("Emote data is invalid.");
return;
}
if (emotes[name]) {
alert(`Emote "${name}" already exists.`);
return;
}
// Add and update
emotes[name] = url;
textarea.value = JSON.stringify(emotes, null, 4);
modal.classList.add("hidden");
populateEmoteList();
};
onDisconnect: () => {
speakerBotStatus.classList.remove('connected');
speakerBotStatus.querySelector('small').textContent = `Awaiting for connection`;
}
});
}
async function populateEmoteList() {
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
const emoteList = document.querySelector("#youtube .emote-list");
if (!textarea || !emoteList) return;
const addButtonSpan = emoteList.querySelector(".emote-item:last-child");
// Remove all emote items except the add button
emoteList.querySelectorAll(".emote-item").forEach(item => {
if (item !== addButtonSpan) {
item.remove();
}
});
let emotes;
try {
emotes = JSON.parse(textarea.value);
} catch (e) {
console.error("Invalid JSON in YouTube Emotes textarea", e);
return;
}
// Recreate each emote item
for (const [emoteName, emoteUrl] of Object.entries(emotes)) {
const span = document.createElement("span");
span.classList.add("emote-item");
span.innerHTML = `
<img data-emote="${emoteName}" src="${emoteUrl}" alt="">
<em>${emoteName}</em>
<button class="delete"><i class="fa-solid fa-trash-can"></i></button>
`;
// Add delete handler directly to the button
const deleteBtn = span.querySelector(".delete");
deleteBtn.addEventListener("click", () => {
if (confirm(`Are you sure you want to delete '${emoteName}'?`)) {
delete emotes[emoteName];
textarea.value = JSON.stringify(emotes, null, 4);
populateEmoteList(); // Re-render everything
}
});
emoteList.insertBefore(span, addButtonSpan);
}
setupAddEmoteModal();
generateUrl();
saveSettingsToLocalStorage();
}
const accordionButtons = document.querySelectorAll("button.accordion");
accordionButtons.forEach(button => {
button.addEventListener("click", () => {
const targetId = button.getAttribute("data-target");
const target = document.getElementById(targetId);
const icon = button.querySelector("i");
if (!target || !target.classList.contains("accordion-container")) return;
const isOpen = target.classList.contains("open");
// Fecha todos os outros accordions
document.querySelectorAll(".accordion-container.open").forEach(container => {
if (container !== target) {
container.classList.remove("open");
container.style.maxHeight = null;
const otherButton = document.querySelector(`button.accordion[data-target="${container.id}"]`);
if (otherButton) {
const otherIcon = otherButton.querySelector("i");
if (otherIcon) otherIcon.className = "fa-solid fa-chevron-down";
}
}
});
// Alterna o atual
if (!isOpen) {
target.classList.add("open");
target.style.maxHeight = target.scrollHeight + "px";
if (icon) icon.className = "fa-solid fa-chevron-up";
// Espera a animação terminar para scrollar
target.addEventListener("transitionend", function handler(e) {
if (e.propertyName === "max-height") {
target.removeEventListener("transitionend", handler);
const offset = target.getBoundingClientRect().top + window.scrollY - 60;
window.scrollTo({
top: offset,
behavior: "smooth"
});
}
});
}
else {
target.classList.remove("open");
target.style.maxHeight = null;
if (icon) icon.className = "fa-solid fa-chevron-down";
}
});
});
window.addEventListener('load', () => {
loadSettingsFromLocalStorage();
generateUrl();
pushChangeEvents();
populateEmoteList();
document.querySelectorAll('.nav-bar a').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
// Remove todas as classes dos links dentro da nav-bar
document.querySelectorAll('.nav-bar a').forEach(link => {
link.classList.remove('active');
});
this.classList.add('active');
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
const offset = 60; // ajusta 20px acima
const y = targetElement.getBoundingClientRect().top + window.scrollY - offset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
}
});
});
/* -------------------------
Inicialização
-------------------------- */
document.addEventListener('DOMContentLoaded', () => {
streamerBotConnect();
speakerBotConnection();
const speakerBotSwitcher = document.querySelector('input[type=checkbox][name=showSpeakerbot]');
speakerBotSwitcher.addEventListener('change', () => {
speakerBotConnection();
});
});