import { writable, get } from 'svelte/store'; import { browser } from '$app/environment'; // TTS enabled state (default: false) export const ttsEnabled = writable(false); // TTS settings export const ttsSettings = writable({ voice: null, // SpeechSynthesisVoice object voiceName: '', // Voice name for persistence rate: 1, // 0.5 - 2 pitch: 1, // 0.5 - 2 volume: 1 // 0 - 1 }); // User filter: 'all' | 'guests' | 'registered' export const ttsUserFilter = writable('all'); // Set of muted user IDs export const ttsMutedUsers = writable(new Set()); // Available voices from browser export const availableVoices = writable([]); // Current realm ID for per-realm settings let currentRealmId = null; // Initialize voices from browser export function initVoices() { if (!browser || !window.speechSynthesis) return; const loadVoices = () => { const voices = window.speechSynthesis.getVoices(); availableVoices.set(voices); // Restore saved voice if available const settings = get(ttsSettings); if (settings.voiceName && voices.length > 0) { const savedVoice = voices.find(v => v.name === settings.voiceName); if (savedVoice) { ttsSettings.update(s => ({ ...s, voice: savedVoice })); } } }; // Chrome needs this event, Firefox loads immediately if (window.speechSynthesis.onvoiceschanged !== undefined) { window.speechSynthesis.onvoiceschanged = loadVoices; } loadVoices(); } // Set voice by name export function setVoice(voiceName) { const voices = get(availableVoices); const voice = voices.find(v => v.name === voiceName); if (voice) { ttsSettings.update(s => ({ ...s, voice, voiceName })); saveSettings(); } } // Check if message should be spoken function shouldSpeak(message) { if (!get(ttsEnabled)) return false; // Check if user is muted const mutedUsers = get(ttsMutedUsers); if (mutedUsers.has(message.userId)) return false; // Check user filter const filter = get(ttsUserFilter); if (filter === 'guests' && !message.isGuest) return false; if (filter === 'registered' && message.isGuest) return false; return true; } // Strip HTML/markdown and clean text for TTS function cleanTextForTTS(text) { if (!text) return ''; // Remove HTML tags let clean = text.replace(/<[^>]*>/g, ''); // Remove markdown formatting clean = clean.replace(/\*\*(.+?)\*\*/g, '$1'); // bold clean = clean.replace(/\*(.+?)\*/g, '$1'); // italic clean = clean.replace(/__(.+?)__/g, '$1'); // underline clean = clean.replace(/~~(.+?)~~/g, '$1'); // strikethrough clean = clean.replace(/`(.+?)`/g, '$1'); // code // Remove URLs clean = clean.replace(/https?:\/\/\S+/g, 'link'); // Limit length to prevent very long TTS if (clean.length > 200) { clean = clean.substring(0, 200) + '...'; } return clean.trim(); } // Speak a message export function speakMessage(message) { if (!browser || !window.speechSynthesis) return; if (!shouldSpeak(message)) return; const settings = get(ttsSettings); const text = cleanTextForTTS(message.content); if (!text) return; // Add username prefix const fullText = `${message.username} says: ${text}`; const utterance = new SpeechSynthesisUtterance(fullText); if (settings.voice) { utterance.voice = settings.voice; } utterance.rate = settings.rate; utterance.pitch = settings.pitch; utterance.volume = settings.volume; window.speechSynthesis.speak(utterance); } // Toggle mute for a user export function toggleUserMute(userId, username) { ttsMutedUsers.update(muted => { const newMuted = new Set(muted); if (newMuted.has(userId)) { newMuted.delete(userId); } else { newMuted.add(userId); } return newMuted; }); saveSettings(); } // Check if user is muted export function isUserMuted(userId) { return get(ttsMutedUsers).has(userId); } // Clear all muted users export function clearMutedUsers() { ttsMutedUsers.set(new Set()); saveSettings(); } // Save settings to localStorage function saveSettings() { if (!browser || !currentRealmId) return; const settings = get(ttsSettings); const data = { enabled: get(ttsEnabled), voiceName: settings.voiceName, rate: settings.rate, pitch: settings.pitch, volume: settings.volume, userFilter: get(ttsUserFilter), mutedUsers: Array.from(get(ttsMutedUsers)) }; localStorage.setItem(`tts_settings_${currentRealmId}`, JSON.stringify(data)); } // Load settings from localStorage export function loadSettings(realmId) { currentRealmId = realmId; if (!browser || !realmId) return; const saved = localStorage.getItem(`tts_settings_${realmId}`); if (!saved) { // Reset to defaults for new realm ttsEnabled.set(false); ttsUserFilter.set('all'); ttsMutedUsers.set(new Set()); return; } try { const data = JSON.parse(saved); ttsEnabled.set(data.enabled || false); ttsUserFilter.set(data.userFilter || 'all'); ttsMutedUsers.set(new Set(data.mutedUsers || [])); ttsSettings.update(s => ({ ...s, voiceName: data.voiceName || '', rate: data.rate ?? 1, pitch: data.pitch ?? 1, volume: data.volume ?? 1 })); // Restore voice object if voices are loaded if (data.voiceName) { const voices = get(availableVoices); const voice = voices.find(v => v.name === data.voiceName); if (voice) { ttsSettings.update(s => ({ ...s, voice })); } } } catch (e) { console.error('Failed to load TTS settings:', e); } } // Subscribe to store changes to auto-save if (browser) { ttsEnabled.subscribe(() => saveSettings()); ttsSettings.subscribe(() => saveSettings()); ttsUserFilter.subscribe(() => saveSettings()); }