This commit is contained in:
retoor 2025-12-31 10:31:07 +01:00
parent 74074093dc
commit 6099fc651c
5 changed files with 231 additions and 68 deletions

View File

@ -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) => {

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -1,70 +1,241 @@
// retoor <retoor@molodetz.nl>
class SnekSpeaker extends HTMLElement {
_enabled = false
import { app } from "./app.js";
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 = `<slot></slot>`;
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;
}
`;
this._utterance = new SpeechSynthesisUtterance();
this._selectVoice();
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();
}
toggle() {
if (window.speechSynthesis.speaking) {
window.speechSynthesis.pause();
_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 {
window.speechSynthesis.resume();
this._enable();
}
}
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];
_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 (this._messageHandler) {
app.removeEventListener('channel-message', this._messageHandler);
this._messageHandler = null;
}
console.log('TTS: Disabled');
}
_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;
this._utterance.text = text;
window.speechSynthesis.speak(this._utterance);
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);

View File

@ -237,15 +237,10 @@ 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");
}
}
}
}
messagesContainer.upsertMessage(data)
app.rpc.markAsRead(channelUid);