500 lines
19 KiB
JavaScript
500 lines
19 KiB
JavaScript
let streamerBotConnected = false;
|
|
let kickWebSocket = null;
|
|
let tikfinityWebSocket = null;
|
|
let speakerBotClient = null;
|
|
|
|
/* -------------------------
|
|
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 = {};
|
|
|
|
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);
|
|
|
|
localStorage.setItem("chatrdWidgetSettings", JSON.stringify(settings));
|
|
|
|
// 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("chatrdWidgetSettings");
|
|
if (!saved) return;
|
|
|
|
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];
|
|
}
|
|
}
|
|
});
|
|
|
|
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) + '%';
|
|
|
|
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)");
|
|
|
|
[...checkboxes, ...textfields, ...numberfields, ...colorfields, ...selects, ...ranges].forEach(el => {
|
|
el.addEventListener('change', saveSettingsToLocalStorage);
|
|
el.addEventListener('input', saveSettingsToLocalStorage);
|
|
});
|
|
|
|
document.querySelector('#font-slider').addEventListener('input', function () {
|
|
document.querySelector('#font-value').textContent = Math.floor(this.value * 100) + '%';
|
|
});
|
|
|
|
document.querySelector('#bg-opacity-slider').addEventListener('input', function () {
|
|
document.querySelector('#bg-opacity-value').textContent = Math.floor(this.value * 100) + '%';
|
|
});
|
|
}
|
|
|
|
/* -------------------------
|
|
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();
|
|
};
|
|
}
|
|
|
|
|
|
/* -------------------------
|
|
Lista de emotes
|
|
-------------------------- */
|
|
function populateEmoteList() {
|
|
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
|
|
const emoteList = document.querySelector("#youtube .emote-list");
|
|
if (!textarea || !emoteList) return;
|
|
|
|
emoteList.querySelectorAll(".emote-item").forEach(item => {
|
|
if (item.querySelector("button")?.id !== "addEmoteButton") {
|
|
item.remove();
|
|
}
|
|
});
|
|
|
|
let emotes;
|
|
try {
|
|
emotes = JSON.parse(textarea.value);
|
|
} catch (e) {
|
|
console.error("[ChatRD][Settings] Invalid JSON in YouTube Emotes textarea", e);
|
|
return;
|
|
}
|
|
|
|
const addButtonSpan = emoteList.querySelector("#addEmoteButton")?.parentElement;
|
|
|
|
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>
|
|
`;
|
|
|
|
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 speakerBotConnection() {
|
|
const speakerBotStatus = document.getElementById('speakerBotStatus');
|
|
|
|
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 showSpeakerbot = document.querySelector('input[type=checkbox][name=showSpeakerbot]').checked;
|
|
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Cria nova instância
|
|
speakerBotClient = new SpeakerBotClient({
|
|
host: speakerBotServerAddress,
|
|
port: speakerBotServerPort,
|
|
voiceAlias: speakerBotVoiceAlias,
|
|
|
|
onConnect: () => {
|
|
speakerBotStatus.classList.add('connected');
|
|
speakerBotStatus.querySelector('small').textContent = `Connected`;
|
|
},
|
|
|
|
onDisconnect: () => {
|
|
speakerBotStatus.classList.remove('connected');
|
|
speakerBotStatus.querySelector('small').textContent = `Awaiting for connection`;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
/* -------------------------
|
|
Inicialização
|
|
-------------------------- */
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
streamerBotConnect();
|
|
speakerBotConnection();
|
|
|
|
const speakerBotSwitcher = document.querySelector('input[type=checkbox][name=showSpeakerbot]');
|
|
speakerBotSwitcher.addEventListener('change', () => {
|
|
speakerBotConnection();
|
|
});
|
|
});
|