I managed to add the ability to change rate and volume of speech output without breaking it XD. Yes, I cheated with Claude.
code
// ==UserScript== // @name Online Status to Text // @namespace https://swervelord.dev // @version 1.0.0 // @description Replaces Torn profile online status icons with text and announces the status. // @author swervelord [3637232] // @license MIT // @match https://www.torn.com/profiles.php?XID* // @grant none // ==/UserScript== (function () { 'use strict'; var SPEECH_RATE = 1.3; // 1.0 = normal speed, 2.5 = faster, 0.5 = slower var SPEECH_VOLUME = 0.7; // 1.0 = full volume, 0.7 = quieter const STATUS_CLASSES = [ { className: 'user-status-16-Online', label: 'Online' }, { className: 'user-status-16-Away', label: 'Idle' }, { className: 'user-status-16-Offline', label: 'Offline' }, ]; let lastAnnouncedProfileStatus = ''; function addTextStatusStyles() { if (document.getElementById('online-status-to-text-styles')) { return; } const style.id = 'online-status-to-text-styles'; style.textContent = ` ul.basic-info li[data-online-status-to-text="true"] { background: none !important; background-image: none !important; color: #ffffff !important; display: inline-block !important; float: left !important; font-size: 13px !important; font-weight: 700 !important; height: auto !important; line-height: 16px !important; margin: 0 8px 0 0 !important; min-width: 0 !important; overflow: visible !important; padding: 0 !important; position: static !important; text-indent: 0 !important; text-transform: none !important; white-space: nowrap !important; width: auto !important; } `; document.head.appendChild(style); } function getProfileId(statusIcon) { const match = statusIcon.id && statusIcon.id.match(/profile-(d+)/); return match ? match[1] : location.search; } function getStatus(statusIcon) { return STATUS_CLASSES.find((status) => statusIcon.classList.contains(status.className)); } function speakStatus(label) { if (!('speechSynthesis' in window) || !('SpeechSynthesisUtterance' in window)) { return; } var utterance = new SpeechSynthesisUtterance(label); utterance.rate = SPEECH_RATE; utterance.volume = SPEECH_VOLUME; window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); } function replaceStatusIcon(statusIcon) { const status = getStatus(statusIcon); if (!status) { return; } if (statusIcon.textContent.trim() !== status.label) { statusIcon.textContent = status.label; } if (statusIcon.getAttribute('aria-label') !== status.label) { statusIcon.setAttribute('aria-label', status.label); } if (statusIcon.getAttribute('title') !== status.label) { statusIcon.setAttribute('title', status.label); } if (statusIcon.dataset.onlineStatusToText !== 'true') { statusIcon.dataset.onlineStatusToText = 'true'; statusIcon.style.backgroundImage = 'none'; statusIcon.style.width = 'auto'; statusIcon.style.height = 'auto'; statusIcon.style.minWidth = '0'; statusIcon.style.fontWeight = '700'; statusIcon.style.fontSize = '13px'; statusIcon.style.lineHeight = '16px'; statusIcon.style.textIndent = '0'; statusIcon.style.overflow = 'visible'; statusIcon.style.color = '#ffffff'; } const profileStatusKey = `${getProfileId(statusIcon)}:${status.label}`; if (profileStatusKey !== lastAnnouncedProfileStatus) { lastAnnouncedProfileStatus = profileStatusKey; speakStatus(status.label); } } function updateProfileStatus() { addTextStatusStyles(); document .querySelectorAll('ul.basic-info li[class*="user-status-16-"]') .forEach(replaceStatusIcon); } updateProfileStatus(); new MutationObserver(updateProfileStatus).observe(document.documentElement, { childList: true, subtree: true, }); })();