diff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js index e158dcc..a01fd4d 100644 --- a/src/snek/static/chat-input.js +++ b/src/snek/static/chat-input.js @@ -380,12 +380,12 @@ textToLeetAdvanced(text) { this.appendChild(this.textarea); - this.snekSpeaker = document.createElement("snek-speaker"); - this.appendChild(this.snekSpeaker); - this.sttButton = document.createElement("stt-button"); this.appendChild(this.sttButton); + this.ttsButton = document.createElement("tts-button"); + this.appendChild(this.ttsButton); + this.uploadButton = document.createElement("upload-button"); this.uploadButton.setAttribute("channel", this.channelUid); this.uploadButton.addEventListener("upload", (e) => { diff --git a/src/snek/static/editor.js b/src/snek/static/editor.js index eb3f084..99b2761 100644 --- a/src/snek/static/editor.js +++ b/src/snek/static/editor.js @@ -268,8 +268,7 @@ Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`; } goToLine(lineNum) { - const lines = this.editor.innerText.split(' -'); + const lines = this.editor.innerText.split('\n'); if (lineNum < 0 || lineNum >= lines.length) return; let offset = 0; diff --git a/src/snek/static/push.js b/src/snek/static/push.js index 709cf03..1e4f04a 100644 --- a/src/snek/static/push.js +++ b/src/snek/static/push.js @@ -38,9 +38,7 @@ export const registerServiceWorker = async (silent = false) => { } catch (error) { console.error("Error registering service worker:", error); if (!silent) { - alert("Registering push notifications failed. Please check your browser settings and try again. - -" + error); + alert("Registering push notifications failed. Please check your browser settings and try again.\n\n" + error); } } } diff --git a/src/snek/static/tts.js b/src/snek/static/tts.js index abb86c1..6a0afb7 100644 --- a/src/snek/static/tts.js +++ b/src/snek/static/tts.js @@ -1,70 +1,241 @@ // retoor -class SnekSpeaker extends HTMLElement { - - _enabled = false +import { app } from "./app.js"; - constructor() { +class TTSButton extends HTMLElement { + constructor() { super(); this.attachShadow({ mode: 'open' }); + this._enabled = false; + this._synthesis = window.speechSynthesis; + this._voices = []; + this._selectedVoice = null; + this._messageHandler = null; + this._spokenMessages = new Set(); - // Optionally show something in the DOM - this.shadowRoot.innerHTML = ``; - - this._utterance = new SpeechSynthesisUtterance(); - this._selectVoice(); - } - toggle() { - if (window.speechSynthesis.speaking) { - window.speechSynthesis.pause(); - } else { - window.speechSynthesis.resume(); - } - } - stop() { - window.speechSynthesis.cancel(); - } - disable() { - this._enabled = false - } - enable() { - this._enabled = true - } - set enabled(val) { - if (val) { - this.enable() - } else { - this.disable() - } - } - get enabled() { - return this._enabled - } - _selectVoice() { - const updateVoice = () => { - const voices = window.speechSynthesis.getVoices(); - const maleEnglishVoices = voices.filter(voice => - voice.lang.startsWith('en') && voice.name.toLowerCase().includes('male') - ); - if (maleEnglishVoices.length > 0) { - this._utterance.voice = maleEnglishVoices[0]; + const style = document.createElement('style'); + style.textContent = ` + :host { + display: inline-block; } + .tts-container { + position: relative; + } + .tts-button { + display: flex; + align-items: center; + justify-content: center; + padding: 10px 20px; + margin-left: 10px; + background-color: #000000; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + position: relative; + overflow: hidden; + transition: background-color 0.2s ease, transform 0.1s ease; + } + .tts-button:hover { + background-color: #222222; + } + .tts-button:active { + transform: scale(0.95); + } + :host([enabled]) .tts-button { + background-color: #2e7d32; + animation: pulse 1.5s ease-in-out infinite; + } + @keyframes pulse { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(46, 125, 50, 0.6); + } + 50% { + box-shadow: 0 0 0 8px rgba(46, 125, 50, 0); + } + } + .tts-button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + `; + + const container = document.createElement('div'); + container.className = 'tts-container'; + + this._button = document.createElement('button'); + this._button.className = 'tts-button'; + this._button.setAttribute('aria-label', 'Text to speech'); + this._button.innerHTML = '🔊'; + this._button.addEventListener('click', () => this._toggle()); + + container.appendChild(this._button); + this.shadowRoot.append(style, container); + + this._initSynthesis(); + } + + _initSynthesis() { + if (!this._synthesis) { + console.warn('TTS: Speech synthesis not supported'); + this._button.disabled = true; + this._button.title = 'Text-to-speech not supported'; + return; + } + + this._loadVoices(); + if (this._synthesis.onvoiceschanged !== undefined) { + this._synthesis.onvoiceschanged = () => this._loadVoices(); + } + } + + _loadVoices() { + this._voices = this._synthesis.getVoices(); + if (this._voices.length === 0) { + console.log('TTS: No voices available yet'); + return; + } + + console.log('TTS: Loaded', this._voices.length, 'voices'); + + const lang = navigator.language || 'en-US'; + const langPrefix = lang.split('-')[0]; + + this._selectedVoice = this._voices.find(v => v.lang === lang); + if (!this._selectedVoice) { + this._selectedVoice = this._voices.find(v => v.lang.startsWith(langPrefix)); + } + if (!this._selectedVoice) { + this._selectedVoice = this._voices.find(v => v.lang.startsWith('en')); + } + if (!this._selectedVoice) { + this._selectedVoice = this._voices[0]; + } + + console.log('TTS: Selected voice:', this._selectedVoice?.name, this._selectedVoice?.lang); + } + + _toggle() { + if (this._enabled) { + this._disable(); + } else { + this._enable(); + } + } + + _enable() { + if (this._voices.length === 0) { + this._loadVoices(); + } + + this._enabled = true; + this.setAttribute('enabled', ''); + this._spokenMessages.clear(); + + this._messageHandler = (data) => { + this._handleMessage(data); }; - updateVoice(); - // Some browsers load voices asynchronously - window.speechSynthesis.onvoiceschanged = updateVoice; + app.addEventListener('channel-message', this._messageHandler); + console.log('TTS: Enabled, listening for channel-message events'); } - speak(text) { - if(!this._enabled) return + _disable() { + this._enabled = false; + this.removeAttribute('enabled'); + this._synthesis.cancel(); - if (!text) return; + if (this._messageHandler) { + app.removeEventListener('channel-message', this._messageHandler); + this._messageHandler = null; + } + console.log('TTS: Disabled'); + } - this._utterance.text = text; - window.speechSynthesis.speak(this._utterance); + _handleMessage(data) { + if (!this._enabled) return; + if (!data) { + console.log('TTS: No data in message'); + return; + } + + console.log('TTS: Received message:', data.uid, 'is_final:', data.is_final); + + if (!data.is_final) { + console.log('TTS: Skipping non-final message'); + return; + } + + if (this._spokenMessages.has(data.uid)) { + console.log('TTS: Already spoken this message'); + return; + } + + const currentUser = app.user; + if (currentUser && (data.user_uid === currentUser.uid || data.username === currentUser.username)) { + console.log('TTS: Skipping own message'); + return; + } + + const text = this._extractText(data.message); + if (!text) { + console.log('TTS: No text to speak'); + return; + } + + this._spokenMessages.add(data.uid); + + if (this._spokenMessages.size > 100) { + const first = this._spokenMessages.values().next().value; + this._spokenMessages.delete(first); + } + + console.log('TTS: Speaking:', text.substring(0, 50) + '...'); + this._speak(text); + } + + _extractText(message) { + if (!message) return ''; + + let text = message + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/https?:\/\/[^\s]+/g, '') + .replace(/[*_~`#]/g, '') + .replace(/\n+/g, ' ') + .trim(); + + return text; + } + + _speak(text) { + if (!text) return; + if (!this._synthesis) return; + + if (this._voices.length === 0) { + this._loadVoices(); + } + + const utterance = new SpeechSynthesisUtterance(text); + + if (this._selectedVoice) { + utterance.voice = this._selectedVoice; + } + + utterance.rate = 1.0; + utterance.pitch = 1.0; + utterance.volume = 1.0; + + utterance.onstart = () => console.log('TTS: Started speaking'); + utterance.onend = () => console.log('TTS: Finished speaking'); + utterance.onerror = (e) => console.error('TTS: Error:', e.error); + + this._synthesis.speak(utterance); + } + + disconnectedCallback() { + this._disable(); } } -// Define the element -customElements.define('snek-speaker', SnekSpeaker); +customElements.define('tts-button', TTSButton); diff --git a/src/snek/templates/web.html b/src/snek/templates/web.html index 816cc31..e25d982 100644 --- a/src/snek/templates/web.html +++ b/src/snek/templates/web.html @@ -237,12 +237,7 @@ app.addEventListener("channel-message", (data) => { app.playSound("mention"); } else if (!isMentionForSomeoneElse(data.message)) { if(data.is_final){ - const speaker = document.querySelector("snek-speaker"); - if(speaker.enabled){ - speaker.speak(data.message); - }else{ - app.playSound("message"); - } + app.playSound("message"); } } }