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 = ` ${emoteName} `; 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(); }); });