1/* =========================== Daem Accessibility Injector - No iframe (widget) - Injects DOM + CSS + JS =========================== */ 2// Capture script base URL immediately (document.currentScript only works at parse time) 3const _daemScriptBase = (() => 4{ 5 const s = document.currentScript; 6 if (!s || !s.src) return ''; 7 return s.src.substring(0, s.src.lastIndexOf('/') + 1); 8})(); 9 10const plugHostingUrl2020 = "https://mcit.gov.eg"; // For Using Here And applyChange.js 11const youTubeVideoLink = "https://www.youtube.com/embed/4od6UKqJJ0o"; 12//const scriptElementsdkURL = "**scriptElementsdkURL**"; 13const scriptApplayChanges = "/mcit_gov_eg_version_GAW_V8/Plugs/js/applyChange.js"; 14const src = "https://mcit.gov.eg/mcit_gov_eg_version_GAW_V8/Plugs/Languages/en/plug.html"; 15const IconclassName = "the-btn Bottom_Right"; 16 17const array = scriptApplayChanges.split("/"); 18var match = "/" + array[1] + "/"; 19 20/** 21 * Optional: 22 * If you want to prefix all widget assets (fonts/images) with hosting base: 23 * Example: plugHostingUrl2020 = https://example.com/ 24 * assets will become https://example.com/daem_assets/... 25 */ 26const ASSETS_BASE = plugHostingUrl2020 + match + "Plugs/"; 27 28function isPageArabic() 29{ 30 // Based on your current routing logic: 31 if (src.includes("/en/")) return false; 32 return true; 33} 34 35function initTool() 36{ 37 const body = document.getElementsByTagName("body")[0]; 38 const head = document.head || document.getElementsByTagName("head")[0]; 39 40 // loading the voices 41 let _voicesReady = false; 42 function ensureVoices(cb) 43 { 44 if (!window.speechSynthesis) { cb([]); return; } 45 const voices = window.speechSynthesis.getVoices(); 46 if (voices.length > 0) { _voicesReady = true; cb(voices); return; } 47 window.speechSynthesis.onvoiceschanged = () => 48 { 49 _voicesReady = true; 50 window.speechSynthesis.onvoiceschanged = null; 51 cb(window.speechSynthesis.getVoices()); 52 }; 53 // Fallback: if voiceschanged never fires (Firefox), proceed after 500ms 54 setTimeout(() => { if (!_voicesReady) { _voicesReady = true; cb(window.speechSynthesis ? window.speechSynthesis.getVoices() : []); } }, 500); 55 } 56 ensureVoices(() => { }); 57 58 // Prevent double-injection 59 if (document.getElementById("daem-accessibility-widget")) return; 60 61 const IS_AR = isPageArabic(); 62 document.documentElement.lang = IS_AR ? "ar" : "en"; 63 //document.documentElement.dir = IS_AR ? "rtl" : "ltr"; 64 65 // i18n 66 const I18N = IS_AR 67 ? { 68 dir: "rtl", 69 openMenuAria: "ÙØªØ ÙØ§Ø¦Ù Ø© Ø§ÙØ¥ØªØ§ØØ© Ø§ÙØ±ÙÙ ÙØ©",
70 closeMenuAria: "Ø¥ØºÙØ§Ù اÙÙØ§Ø¦Ù Ø©", 71 resetTitle: "إعادة تعÙÙ٠اÙÙÙ", 72 title: "Ø§ÙØ¥ØªØ§ØØ© Ø§ÙØ±ÙÙ ÙØ© ٠٠داع٠", 73 profiles: "إعدادات Ù Ø³Ø¨ÙØ© ÙÙØ¥Ø®ØªÙار", 74 logo: `${ASSETS_BASE}daem_assets/ar_daem.png`, 75 signHeader: "Ù ØªØ±Ø¬Ù ÙØºØ© Ø§ÙØ¥Ø´Ø§Ø±Ø©", 76 signClose: "Ø¥ØºÙØ§Ù ÙÙØ¯ÙÙ ÙØºØ© Ø§ÙØ¥Ø´Ø§Ø±Ø©", 77 signNote: "(رابط اÙÙÙØ¯ÙÙ Ù Ø²ÙØ¯ Ù Ù Ø§ÙØ®ÙÙÙØ©)", 78 profilesLabels: { 79 motor: "Ø¥Ø¹Ø§ÙØ© ØØ±ÙÙØ©", 80 visual: "Ø¥Ø¹Ø§ÙØ© Ø¨ØµØ±ÙØ©", 81 colorblind: "ع٠٠أÙÙØ§Ù", 82 dyslexia: "عسر اÙÙØ±Ø§Ø¡Ø©", 83 lowvision: "Ø¶Ø¹Ù Ø§ÙØ¨ØµØ±", 84 cognitive: "Ø§ÙØªØ¹Ù٠اÙ٠عرÙÙ", 85 seizure: "Ø§ÙØµØ±Ø¹ ÙØ§ÙÙÙØ¨Ø§Øª", 86 adhd: "ÙØ±Ø· Ø§ÙØØ±ÙØ©", 87 }, 88 featureLabels: { 89 "sign-vid": "ÙÙØ¯ÙÙ ÙØºØ© Ø§ÙØ¥Ø´Ø§Ø±Ø©", 90 "sign-font": "خط ÙØºØ© Ø§ÙØ¥Ø´Ø§Ø±Ø©", 91 "text-speech": "تØÙÙ٠اÙÙØµ اÙÙ ØØ¯Ø¯ Ø¥ÙÙ ÙÙØ§Ù ", 92 "speech-text": "تØÙÙ٠اÙÙÙØ§Ù Ø¥ÙÙ ÙØµ", 93 contrast: "تباÙ٠عاÙÙ ÙÙØ£ÙÙØ§Ù", 94 "font-size": "ØØ¬Ù Ø§ÙØ®Ø·", 95 cursor: "٠ؤشر ÙØ¨Ùر", 96 "line-height": "Ø§Ø±ØªÙØ§Ø¹ Ø§ÙØ³Ø·Ø±", 97 spacing: "تباعد Ø§ÙØ£ØØ±Ù", 98 readable: "خط Ù ÙØ±ÙØ¡", 99 links: "ØªØØ¯Ùد Ø§ÙØ±Ùابط", 100 titles: "ØªØØ¯Ùد Ø§ÙØ¹ÙاÙÙÙ", 101 hint: "تÙÙ ÙØØ§Øª", 102 "stop-anim": "Ø¥ÙÙØ§Ù Ø§ÙØØ±ÙØ©", 103 "dyslexic-font": "خط عسر اÙÙØ±Ø§Ø¡Ø©", 104 mask: "ÙÙØ§Ø¹ اÙÙØ±Ø§Ø¡Ø©", 105 "hide-images": "Ø¥Ø®ÙØ§Ø¡ Ø§ÙØµÙر", 106 "bold-text": "ÙØµ غا٠Ù", 107 "text-align": "Ù ØØ§Ø°Ø§Ø© اÙÙØµ", 108 saturation: "Ø§ÙØªØ´Ø¨Ø¹", 109 "dark-mode": "اÙÙØ¶Ø¹ اÙ٠ظÙÙ ", 110 }, 111 dynamic: { 112 listening: "Ø¬Ø§Ø±Ù Ø§ÙØ§Ø³ØªÙ اع...", 113 readSelection: "ÙØ±Ø§Ø¡Ø© اÙÙ ØØ¯Ø¯", 114 textToSpeech: "تØÙÙ٠اÙÙØµ اÙÙ ØØ¯Ø¯ Ø¥ÙÙ ÙÙØ§Ù ", 115 speechToText: "تØÙÙ٠اÙÙÙØ§Ù Ø¥ÙÙ ÙØµ", 116 slFont: "خط ÙØºØ© Ø§ÙØ¥Ø´Ø§Ø±Ø©", 117 slFontFull: "خط Ø§ÙØ¥Ø´Ø§Ø±Ø© (ÙØ§Ù Ù)", 118 slFontSelect: "خط Ø§ÙØ¥Ø´Ø§Ø±Ø© (ØªØØ¯Ùد)", 119 contrast: "تباÙ٠عاÙÙ ÙÙØ£ÙÙØ§Ù", 120 highContrast: "تباÙ٠عاÙÙ", 121 desaturate: "Ø¥Ø²Ø§ÙØ© Ø§ÙØ£ÙÙØ§Ù", 122 readingMask: "ÙÙØ§Ø¹ اÙÙØ±Ø§Ø¡Ø©", 123 readingLine: "خط اÙÙØ±Ø§Ø¡Ø©", 124 textAlign: "Ù ØØ§Ø°Ø§Ø© اÙÙØµ", 125 alignLeft: "Ù ØØ§Ø°Ø§Ø© ÙÙÙØ³Ø§Ø±", 126 alignCenter: "Ù ØØ§Ø°Ø§Ø© ÙÙÙØ³Ø·", 127 alignRight: "Ù ØØ§Ø°Ø§Ø© ÙÙÙÙ ÙÙ", 128 signVideo: "ÙÙØ¯ÙÙ ÙØºØ© Ø§ÙØ¥Ø´Ø§Ø±Ø©", 129 hint: "تÙÙ ÙØØ§Øª / Ø£Ø¯ÙØ§Øª ٠ساعدة", 130 131 // STT Modal 132 sttTitle: "ØªØØ¯Ø« Ø¥ÙÙ ÙØµ", 133 sttHelp: "ØªØØ¯Ø« Ø§ÙØ¢Ùâ¦", 134 sttStop: "Ø¥ÙÙØ§Ù", 135 sttResume: "استÙ٠اÙ", 136 sttClear: "٠سØ", 137 sttCopy: "ÙØ³Ø®", 138 sttCopied: "ت٠اÙÙØ³Ø®!", 139 sttClose: "Ø¥ØºÙØ§Ù", 140 sttStopped: "ØªÙ Ø§ÙØ¥ÙÙØ§Ù.", 141 sttNoMatch: "ÙÙ ÙØªÙ Ø§ÙØªØ¹Ø±Ù عÙ٠اÙÙÙØ§Ù .", 142 sttError: "ØØ¯Ø« خطأ.", 143 }, 144 hintPrefixes: { 145 title: "Ø§ÙØ¹ÙÙØ§Ù", 146 alt: "ÙØµÙ Ø§ÙØµÙرة", 147 placeholder: "ÙØµ Ø§ÙØÙÙ", 148 link: "رابط", 149 button: "زر", 150 }, 151 alerts: { 152 sttUnsupported: 153 "اÙÙ ØªØµÙØ ÙØ§ ÙØ¯Ø¹Ù تØÙÙ٠اÙÙÙØ§Ù Ø¥ÙÙ ÙØµ. Ø¬Ø±ÙØ¨ Chrome Ø£Ù Edge.", 154 ttsUnsupported: "Ù ØªØµÙØÙ ÙØ§ ÙØ¯Ø¹Ù تØÙÙ٠اÙÙØµ Ø¥ÙÙ ÙÙØ§Ù .", 155 micError: "تعذر اÙÙØµÙ٠إÙ٠اÙÙ ÙÙØ±ÙÙÙÙ.", 156 }, 157 } 158 : { 159 dir: "ltr", 160 openMenuAria: "Open Accessibility Menu",
161 closeMenuAria: "Close Menu", 162 resetTitle: "Reset All", 163 title: "Daem Accessibility", 164 profiles: "Accessibility Profiles", 165 logo: `${ASSETS_BASE}daem_assets/en_daem.png`, 166 signHeader: "Sign Language Interpreter", 167 signClose: "Close Sign Language Video", 168 signNote: "(Video URL supplied by backend)", 169 profilesLabels: { 170 motor: "Motor Impairment", 171 visual: "Visual Impairment", 172 colorblind: "Color Blind", 173 dyslexia: "Dyslexia Friendly", 174 lowvision: "Low Vision", 175 cognitive: "Cognitive Learning", 176 seizure: "Seizure / Epileptic", 177 adhd: "ADHD", 178 }, 179 featureLabels: { 180 "sign-vid": "Sign Language Video", 181 "sign-font": "Sign Language Font", 182 "text-speech": "Selected Text to Speech", 183 "speech-text": "Speech to Text", 184 contrast: "High Color Contrast", 185 "font-size": "Font Size", 186 cursor: "Large Cursor", 187 "line-height": "Line Height", 188 spacing: "Character Spacing", 189 readable: "Readable Font", 190 links: "Highlight Links", 191 titles: "Highlight Titles", 192 hint: "Hint / Tooltips", 193 "stop-anim": "Stop Animation", 194 "dyslexic-font": "Dyslexic Font", 195 mask: "Reading Mask", 196 "hide-images": "Hide Images", 197 "bold-text": "Bold Text", 198 "text-align": "Text Alignment", 199 saturation: "Saturation", 200 "dark-mode": "Dark Mode", 201 }, 202 dynamic: { 203 listening: "Listening...", 204 readSelection: "Read Selection", 205 textToSpeech: "Text to Speech (Select)", 206 speechToText: "Speech to Text", 207 slFont: "Sign Language Font", 208 slFontFull: "SL Font Full", 209 slFontSelect: "SL Font Select", 210 contrast: "High Color Contrast", 211 highContrast: "High Contrast", 212 desaturate: "Desaturate", 213 readingMask: "Reading Mask", 214 readingLine: "Reading Line", 215 textAlign: "Text Alignment", 216 alignLeft: "Align Left", 217 alignCenter: "Align Center", 218 alignRight: "Align Right", 219 signVideo: "Sign Language Video", 220 hint: "Hint / Tooltips", 221 222 // STT Modal 223 sttTitle: "Speech to Text", 224 sttHelp: "Speak nowâ¦", 225 sttStop: "Stop", 226 sttResume: "Resume", 227 sttClear: "Clear", 228 sttCopy: "Copy", 229 sttCopied: "Copied!", 230 sttClose: "Close", 231 sttStopped: "Stopped.", 232 sttNoMatch: "Speech could not be recognized.", 233 sttError: "An error occurred.", 234 }, 235 hintPrefixes: { 236 title: "Title", 237 alt: "Image Alt Text", 238 placeholder: "Placeholder", 239 link: "Link", 240 button: "Button", 241 }, 242 alerts: { 243 sttUnsupported: 244 "Speech recognition is not supported in this browser. Try Chrome or Edge.", 245 ttsUnsupported: "Your browser does not support Text to Speech.", 246 micError: "Could not access microphone.", 247 }, 248 }; 249 250 /* --------------------------- 1) SVG icons (no external Font Awesome dependency) --------------------------- */ 251 const SVG_ICONS = ASSETS_BASE + "daem_assets/SVG_Icons/"; 252 253 /* --------------------------- 2) Inject widget DOM (NO iframe) --------------------------- */ 254 const profileShortcuts = { 255 motor: "K", 256 visual: "Y", 257 colorblind: "J", 258 dyslexia: "X", 259 lowvision: "W", 260 cognitive: "N", 261 seizure: "E", 262 adhd: "Z", 263 }; 264 265 // Maps each profile to the features it activates and their expected levels 266 const profileFeatureMap = { 267 motor: [{ id: "speech-text", level: 1 }], 268 visual: [{ id: "text-speech", level: 1 }], 269 colorblind: [{ id: "saturation", level: 3 }], 270 dyslexia: [{ id: "dyslexic-font", level: 1 }], 271 lowvision: [{ id: "saturation", level: 3 }, { id: "font-size", level: 2 }, { id: "cursor", level: 1 }], 272 cognitive: [{ id: "saturation", level: 2 }, { id: "font-size", level: 2 }, { id: "mask", level: 2 }], 273 seizure: [{ id: "contrast", level: 1 }, { id: "saturation", level: 4 }], 274 adhd: [{ id: "mask", level: 1 }, { id: "saturation", level: 1 }], 275 }; 276 277 const widgetWrapper = document.createElement("div"); 278 widgetWrapper.id = "daem-accessibility-widget"; 279 widgetWrapper.setAttribute("dir", I18N.dir); 280 if (IS_AR) widgetWrapper.classList.add("daem-acc-ar"); 281 282 widgetWrapper.innerHTML = ` 283 <div id="daem-acc-filter-overlay"></div> 284 285 <button id="daem-access-trigger" class="${IconclassName}" aria-label="${I18N.openMenuAria}" type="button"> 286 <img src="${SVG_ICONS}daem_mainicon.svg" class="daem-btn-icon" alt=""> 287 </button> 288 289 <div id="daem-access-panel" aria-hidden="true"> 290 <div class="daem-access-header" id="acc-drag-handle"> 291 <h3>${I18N.title}</h3> 292 <div class="daem-access-actions"> 293 <div class="daem-btn-wrap"> 294 <button id="daem-access-reset" title="${I18N.resetTitle}" type="button"> 295 <img src="${SVG_ICONS}daem_reset.svg" class="daem-btn-icon" alt=""> 296 </button>
297 <span class="daem-shortcut-hint">Alt+R</span> 298 </div> 299 <div class="daem-btn-wrap"> 300 <button id="daem-access-close" aria-label="${I18N.closeMenuAria}" type="button"> 301 <img src="${SVG_ICONS}daem_close.svg" class="daem-btn-icon" alt=""> 302 </button> 303 <span class="daem-shortcut-hint">Alt+O</span> 304 </div> 305 </div> 306 </div> 307 308 <div class="daem-profiles-accordion"> 309 <div class="daem-profiles-header" id="profiles-toggle" role="button" tabindex="0"> 310 <span>${I18N.profiles}</span> 311 <span class="daem-chevron">▼</span> 312 </div> 313 314 <div class="daem-profiles-content" id="daem-profiles-content"> 315 <div class="daem-profiles-grid"> 316 <button class="daem-profile-btn" data-profile="motor" type="button"> 317 <div class="daem-shortcut-badge">Alt+${profileShortcuts.motor}</div> 318 <img src="${SVG_ICONS}daem_profile_motor.svg" class="daem-btn-icon" alt=""> 319 ${I18N.profilesLabels.motor} 320 </button> 321 322 <button class="daem-profile-btn" data-profile="visual" type="button"> 323 <div class="daem-shortcut-badge">Alt+${profileShortcuts.visual}</div> 324 <img src="${SVG_ICONS}daem_profile_blind.svg" class="daem-btn-icon" alt=""> 325 ${I18N.profilesLabels.visual} 326 </button> 327 328 <button class="daem-profile-btn" data-profile="colorblind" type="button"> 329 <div class="daem-shortcut-badge">Alt+${profileShortcuts.colorblind}</div> 330 <img src="${SVG_ICONS}daem_profile_colorblind.svg" class="daem-btn-icon" alt=""> 331 ${I18N.profilesLabels.colorblind} 332 </button> 333 334 <button class="daem-profile-btn" data-profile="dyslexia" type="button"> 335 <div class="daem-shortcut-badge">Alt+${profileShortcuts.dyslexia}</div> 336 <img src="${SVG_ICONS}daem_profile_dyslexia.svg" class="daem-btn-icon" alt=""> 337 ${I18N.profilesLabels.dyslexia} 338 </button> 339 340 <button class="daem-profile-btn" data-profile="lowvision" type="button"> 341 <div class="daem-shortcut-badge">Alt+${profileShortcuts.lowvision}</div> 342 <img src="${SVG_ICONS}daem_profile_lowvision.svg" class="daem-btn-icon" alt=""> 343 ${I18N.profilesLabels.lowvision} 344 </button> 345 346 <button class="daem-profile-btn" data-profile="cognitive" type="button"> 347 <div class="daem-shortcut-badge">Alt+${profileShortcuts.cognitive}</div> 348 <img src="${SVG_ICONS}daem_profile_cognitive.svg" class="daem-btn-icon" alt=""> 349 ${I18N.profilesLabels.cognitive} 350 </button> 351 352 <button class="daem-profile-btn" data-profile="seizure" type="button"> 353 <div class="daem-shortcut-badge">Alt+${profileShortcuts.seizure}</div> 354 <img src="${SVG_ICONS}daem_profile_epilepsy.svg" class="daem-btn-icon" alt=""> 355 ${I18N.profilesLabels.seizure} 356 </button> 357 358 <button class="daem-profile-btn" data-profile="adhd" type="button"> 359 <div class="daem-shortcut-badge">Alt+${profileShortcuts.adhd}</div> 360 <img src="${SVG_ICONS}daem_profile_adhd.svg" class="daem-btn-icon" alt=""> 361 ${I18N.profilesLabels.adhd} 362 </button> 363 </div> 364 </div> 365 </div> 366 367 <div class="daem-access-grid"></div> 368 369 <div class="daem-access-footer"> 370 <img src="${I18N.logo}" alt="Daem Logo" style="max-height: 50px;"> 371 </div> 372 </div> 373 374 <div id="daem-reading-mask"></div> 375 <div id="daem-reading-line"></div> 376 377 <div id="daem-sign-lang-modal"> 378 <div class="daem-sign-header"> 379 <span><img src="${SVG_ICONS}daem_signlanguagevideo.svg" class="daem-btn-icon" alt=""> ${I18N.signHeader}</span> 380 <button class="daem-sign-close" aria-label="${I18N.signClose}" type="button"> 381 <img src="${SVG_ICONS}daem_close.svg" class="daem-btn-icon" alt=""> 382 </button> 383 </div> 384 385 <div class="daem-sign-video-content"> 386 <iframe 387 id="daem-sign-lang-iframe" 388 src="" 389 title="Sign Language Interpreter Video" 390 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 391 allowfullscreen 392 ></iframe> 393 394 </div> 395 </div> 396 397 <!-- Speech-to-Text Popup (NO iframe) --> 398 <div id="daem-stt-modal" aria-hidden="true"> 399 <div class="daem-stt-card" role="dialog" aria-modal="true" aria-labelledby="daem-stt-title"> 400 <div class="daem-stt-header"> 401 <div id="daem-stt-title" class="daem-stt-title">${I18N.dynamic.sttTitle}</div> 402 <button id="daem-stt-close" class="daem-stt-close" type="button" aria-label="${I18N.dynamic.sttClose}">â</button> 403 </div> 404 405 <div class="daem-stt-body"> 406 <div id="daem-stt-help" class="daem-stt-help">${I18N.dynamic.sttHelp}</div> 407 <textarea 408 id="daem-stt-text"
409 class="daem-stt-text" 410 rows="6" 411 ></textarea> 412 413 414 <div class="daem-stt-actions"> 415 <button id="daem-stt-copy" class="daem-stt-btn" type="button"> 416 <span>${I18N.dynamic.sttCopy}</span> 417 </button> 418 419 <button id="daem-stt-clear" class="daem-stt-btn" type="button"> 420 <span>${I18N.dynamic.sttClear}</span> 421 </button> 422 423 <button id="daem-stt-stop" class="daem-stt-btn" type="button"> 424 <span>${I18N.dynamic.sttStop}</span> 425 </button> 426 </div> 427 </div> 428 </div> 429 </div> 430 431 <div 432 id="daem-hint-tooltip" 433 style="position:fixed;display:none;background:#fffcc2;border:1px solid #f9e29a;padding:5px 8px;border-radius:4px;font-size:12px;pointer-events:none;z-index:10004;max-width:300px;" 434 ></div> 435 `; 436 437 /* --------------------------- 3) Inject CSS (existing + RTL patches + STT modal) --------------------------- */ 438 const styleElement = document.createElement("style"); 439 440 styleElement.innerHTML = ` 441 /* ========== Your Host Floating Button ========== */ 442 .the-btn{ 443 position: fixed; 444 background-color: #12cd6a; 445 width: 45px; 446 height: 45px; 447 border-radius: 50%; 448 text-align: center; 449 display: inline-block; 450 line-height: 35px !important; 451 z-index: 100000000; 452 background-color: #c5d6e9; 453 border: 4px solid #21c2f8; 454 padding: 0; 455 } 456 .Top_Left {left: 25px;top: 20px;} 457 .Top_Right {right: 25px;top: 20px;} 458 .Top_Center {left: 49%;top: 20px;} 459 .Middle_Left {left: 25px;top: 50%;} 460 .Middle_Right {right: 25px;top: 50%;} 461 .Bottom_Right {right: 25px;bottom: 20px;} 462 .Bottom_Center {left: 49%;bottom: 20px;} 463 .Bottom_Left {left: 25px;bottom: 20px;} 464 .the-btn img{width: 35px;height: auto;} 465 466 /* SVG icon sizes per context */ 467 #daem-access-trigger .daem-btn-icon { width: 32px; height: 32px; object-fit: contain; } 468 .daem-access-header button .daem-btn-icon { width: 16px; height: 16px; object-fit: contain; filter: brightness(0) invert(1); } 469 .daem-profiles-header .daem-btn-icon { width: 16px; height: 16px; object-fit: contain; } 470 .daem-profile-btn .daem-btn-icon { width: 28px; height: 28px; object-fit: contain; margin-bottom: 5px; } 471 .daem-access-btn .daem-btn-icon { width: 32px; height: 32px; object-fit: contain; margin-bottom: 12px; } 472 .daem-sign-header .daem-btn-icon { width: 18px; height: 18px; object-fit: contain; vertical-align: middle; } 473 .daem-sign-close .daem-btn-icon { width: 14px; height: 14px; object-fit: contain; filter: brightness(0) invert(1); } 474 475 /* ========== Widget Variables ========== */ 476 :root{ 477 --daem-acc-primary: #2563eb; 478 --daem-acc-bg: #ffffff; 479 --daem-acc-text: #1f2937; 480 --daem-acc-active-bg: #eff6ff; 481 --daem-acc-active-border: #3b82f6; 482 --daem-reading-mask-height: 100px; 483 } 484 485 /* Custom Fonts */ 486 @font-face { 487 font-family: 'AmericanSignLanguage'; 488 src: url('${ASSETS_BASE}daem_assets/AmericanSignLanguage.woff2') format('woff2'); 489 font-weight: normal; 490 font-style: normal; 491 } 492 @font-face { 493 font-family: 'ArabicSignLanguage'; 494 src: url('${ASSETS_BASE}daem_assets/ARFONTS-MANISSI-ABJAD-ISHARA.TTF') format('truetype'); 495 font-weight: normal; 496 font-style: normal; 497 unicode-range: U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFF; 498 } 499 @font-face { 500 font-family: 'OpenDyslexic'; 501 src: url('${ASSETS_BASE}daem_assets/OpenDyslexic-Bold.woff2') format('woff2'); 502 font-weight: bold; 503 font-style: normal; 504 } 505 506 /* Filter overlay */ 507 #daem-acc-filter-overlay{ 508 position: fixed; 509 top:0; 510 left:0; 511 width:100vw; 512 height:100vh; 513 pointer-events:none; 514 z-index:9998; 515 display:none; 516 } 517 body.daem-acc-contrast-1 #daem-acc-filter-overlay { display:block; backdrop-filter: contrast(150%); -webkit-backdrop-filter: contrast(150%); } 518 body.daem-acc-saturation-1 #daem-acc-filter-overlay { display:block; backdrop-filter: saturate(150%); -webkit-backdrop-filter: saturate(150%); } 519 body.daem-acc-saturation-2 #daem-acc-filter-overlay { display:block; backdrop-filter: saturate(200%); -webkit-backdrop-filter: saturate(200%); } 520 body.daem-acc-saturation-3 #daem-acc-filter-overlay { display:block; backdrop-filter: saturate(300%); -webkit-backdrop-filter: saturate(300%); } 521 body.daem-acc-saturation-4 #daem-acc-filter-overlay { display:block; backdrop-filter: grayscale(100%); -webkit-backdrop-filter: grayscale(100%); } 522 523 body.daem-acc-contrast-1{ 524 background-color:#000 !important; 525 color:#fff !important; 526 } 527 body.daem-acc-contrast-1 *:not(#daem-accessibility-widget, #daem-accessibility-widget *){ 528 background-color:#000 !important; 529 color:#fff !important; 530 border-color:#fff !important; 531 } 532 533 /* Dark mode */ 534 body.daem-acc-dark-mode { 535 color-scheme: dark; 536 } 537 body.daem-acc-dark-mode, 538 body.daem-acc-dark-mode *:not( 539 #daem-accessibility-widget, #daem-accessibility-widget *, 540 img, svg, canvas, video, picture, source, iframe 541 ) { 542 background-color: #121212 !important; 543 color: #e2e2e2 !important; 544 border-color: #3a3a3a !important; 545 } 546 body.daem-acc-dark-mode *:not(#daem-accessibility-widget, #daem-accessibility-widget *)::before, 547 body.daem-acc-dark-mode *:not(#daem-accessibility-widget, #daem-accessibility-widget *)::after { 548 color: #e2e2e2 !important; 549 } 550 /* Elements with CSS background-image: keep original bg-color so image stays visible */ 551 body.daem-acc-dark-mode *:not(#daem-accessibility-widget, #daem-accessibility-widget *).daem-has-bg-image { 552 background-color: initial !important; 553 } 554 body.daem-acc-dark-mode *:not(#daem-accessibility-widget, #daem-accessibility-widget *).daem-has-bg-image::before, 555 body.daem-acc-dark-mode *:not(#daem-accessibility-widget, #daem-accessibility-widget *).daem-has-bg-image::after { 556 background-color: initial !important; 557 } 558 /* Also cover dynamically-added children of bg-image elements (e.g. slick-dots added after JS scan) */ 559 body.daem-acc-dark-mode .daem-has-bg-image *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { 560 background-color: initial !important; 561 } 562 body.daem-acc-dark-mode .daem-has-bg-image *:not(#daem-accessibility-widget, #daem-accessibility-widget *)::before, 563 body.daem-acc-dark-mode .daem-has-bg-image *:not(#daem-accessibility-widget, #daem-accessibility-widget *)::after { 564 background-color: initial !important; 565 } 566 body.daem-acc-dark-mode a:not(#daem-accessibility-widget a) { 567 color: #7eb8f7 !important; 568 } 569 body.daem-acc-dark-mode img:not(#daem-accessibility-widget img), 570 body.daem-acc-dark-mode video:not(#daem-accessibility-widget video), 571 body.daem-acc-dark-mode svg:not(#daem-accessibility-widget svg) { 572 filter: brightness(0.85) contrast(1.05) !important; 573 background-color: transparent !important; 574 } 575 576 /* Widget container */ 577 #daem-accessibility-widget { 578 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 579 z-index: 99999999999 !important; 580 filter: none !important; 581 position: relative !important; 582 display: block; 583} 584 585 /* Widget trigger (internal) */ 586 #daem-access-trigger{ 587 position: fixed; 588 bottom: 20px; 589 left: 20px; 590 right: auto; 591 width: 60px; 592 height: 60px; 593 /*background: var(--daem-acc-primary);*/ 594 color: white; 595 border: none; 596 border-radius: 50%; 597 font-size: 28px; 598 cursor: pointer; 599 z-index: 10000; 600 background-color: #c5d6e9; 601 border: 4px solid #21c2f8 602 } 603 604 /* Panel (default LTR slide from left) */ 605 #daem-access-panel{ 606 position: fixed; 607 top: 0; 608 left: -400px; 609 right: auto; 610 width: 380px; 611 height: 100vh; 612 background: rgba(255,255,255,0.85); 613 backdrop-filter: blur(12px); 614 -webkit-backdrop-filter: blur(12px); 615 box-shadow: 10px 0 30px rgba(0,0,0,0.15); 616 transition: left 0.4s; 617 display: flex; 618 flex-direction: column; 619 z-index: 10001; 620 overflow-y: auto; 621 } 622 #daem-access-panel.daem-is-open{ left: 0; right: auto; } 623 624 /* Header */ 625.daem-access-header{ 626 padding: 12px 20px; /* â Ù Ø³Ø§ÙØ© أعÙÙ/أسÙÙ Ø£ÙØ¶Ø */ 627 min-height: 52px; /* â Ø§Ø±ØªÙاع ثابت ÙØ±Ùب Ù Ù Ø§ÙØªØµÙ ÙÙ */ 628 max-height: none; /* â Ø§ÙØºÙ Ø§ÙØªÙÙÙØ¯ اÙÙÙ Ø¨ÙØ¶ØºØ· اÙÙÙØ¯Ø± */ 629 cursor: grab; 630 display: flex; 631 align-items: center; /* â ØªÙØ³ÙØ· رأس٠ØÙÙÙÙ */ 632 justify-content: space-between; 633 background: #fff; 634} 635 636/* Title */ 637.daem-access-header h3{ 638 margin: 0 !important; 639 font-size: 17px !important; 640 font-weight: bold !important; 641 line-height: 1.2; /* â ÙÙ ÙØ¹ ØªÙØ·Ùع/ضغط */ 642} 643 644/* Actions wrapper */ 645.daem-access-actions{ 646 display: flex; 647 align-items: flex-end; 648 gap: 8px; /* â Ø¨Ø¯Ù اÙÙØ¹Ø¨ ÙÙ margins */ 649} 650.daem-btn-wrap{ 651 display: flex; 652 flex-direction: column; 653 align-items: center; 654 gap: 2px; 655}
656.daem-shortcut-hint{ 657 font-size: 9px; 658 color: #aaa; 659 line-height: 1; 660 white-space: nowrap; 661 user-select: none; 662 letter-spacing: 0.3px; 663} 664 665/* Buttons */ 666 667 .daem-access-header button{ 668 font-size: 14px; 669 background-color: var(--daem-acc-primary); 670 color: white !important; 671 border: none; 672 border-radius: 5px; 673 padding: 8px 10px; 674 line-height: 1; 675 cursor: pointer; 676 margin-left: 5px; 677 box-shadow: none !important; 678 } 679 680 /* Profiles accordion */ 681 .daem-profiles-accordion{ border-bottom: 1px solid rgba(0,0,0,0.1); background:#fff; } 682 .daem-profiles-header{ 683 padding: 15px 20px; 684 cursor: pointer; 685 font-weight: 600; 686 color: var(--daem-acc-primary); 687 display:flex; 688 justify-content:space-between; 689 align-items:center; 690 background:#f8fafc; 691 } 692 .daem-profiles-header:hover{ background:#f1f5f9; } 693 .daem-profiles-content{ display:none; padding: 15px 20px; background:#fff; } 694 .daem-profiles-content.daem-active{ display:block; } 695 .daem-profiles-grid{ display:grid; grid-template-columns: 1fr 1fr; gap: 10px; } 696 .daem-profile-btn { 697 background: white; 698 border: 1px solid #e5e7eb; 699 border-radius: 8px; 700 padding: 10px; 701 font-size: 13px; 702 cursor: pointer; 703 text-align: center; 704 color: var(--daem-acc-text) !important; 705 transition: all 0.2s; 706 display: flex; 707 flex-direction: column; 708 align-items: center; 709 justify-content: center; 710 min-height: 60px; 711 box-shadow: none !important; 712 position: relative; 713} 714 .daem-profile-btn:hover{ background:#f9fafb; border-color:#d1d5db; } 715 .daem-profile-btn.daem-active{ 716 background: var(--daem-acc-active-bg); 717 border: 2px solid var(--daem-acc-active-border); 718 color: var(--daem-acc-primary); 719 font-weight: bold; 720 } 721 .daem-profile-btn i{ margin-bottom: 5px; font-size: 18px; } 722 723 /* Feature grid */ 724 .daem-access-grid{ 725 display:grid; 726 grid-template-columns: 1fr 1fr; 727 gap: 15px; 728 padding: 25px; 729 flex-grow: 1; 730 } 731 .daem-access-btn{ 732 display:flex; 733 flex-direction:column; 734 align-items:center; 735 justify-content:center; 736 background:white; 737 border:1px solid rgba(0,0,0,0.05); 738 border-radius:16px; 739 padding:15px 10px; 740 cursor:pointer; 741 position:relative; 742 height:110px; 743 color: var(--daem-acc-text); 744 text-align:center; 745 font-size: 14px !important; 746 font-weight: 400 !important; 747 } 748 .daem-access-btn.daem-active{ 749 background: var(--daem-acc-active-bg); 750 border:2px solid var(--daem-acc-active-border); 751 color:#1d4ed8; 752 } 753 .daem-access-btn i{ 754 font-size:28px; 755 margin-bottom:12px; 756 color: var(--daem-acc-text); 757 } 758 .daem-access-btn.daem-active i{ color: var(--daem-acc-primary); } 759 760 .daem-level-badge{ 761 position:absolute; 762 top: 5px; 763 left: 5px; 764 width: 20px; 765 height: 20px; 766 background-color: var(--daem-acc-primary); 767 color: white; 768 border-radius: 50%; 769 font-size: 11px; 770 font-weight: bold; 771 display:none; 772 justify-content:center; 773 align-items:center; 774 } 775 .daem-access-btn.daem-active .daem-level-badge[data-level]:not([data-level="1"]){ display:flex; } 776 777 .daem-shortcut-badge{ 778 position:absolute; 779 top:5px; 780 right:5px; 781 font-size: 10px; 782 opacity: 0.7; 783 } 784 785 /* Recording pulse */ 786 @keyframes daem-pulse-red { 787 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } 788 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } 789 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } 790 } 791 .daem-access-btn.daem-recording-active{ 792 border-color:#ef4444 !important; 793 color:#ef4444 !important; 794 animation: daem-pulse-red 2s infinite; 795 } 796 .daem-access-btn.daem-recording-active i{ color:#ef4444 !important; } 797 798 /* TTS speaking pulse (green) */ 799 @keyframes daem-pulse-green { 800 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.8); } 801 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); } 802 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } 803 } 804 .daem-access-btn.daem-tts-speaking { 805 border-color: #10b981 !important; 806 color: #10b981 !important; 807 animation: daem-pulse-green 1s infinite !important; 808 } 809 .daem-access-btn.daem-tts-speaking .daem-btn-icon { filter: none !important; } 810 811 /* Footer */ 812 .daem-access-footer{ 813 height: 50px; 814 min-height: 50px; 815 background:#fff; 816 display:flex; 817 justify-content:center;
818 align-items:center; 819 padding: 5px 0; 820 border-top: 1px solid #d1d5db; 821 flex-shrink: 0; 822 position: sticky; 823 bottom: 0; 824 } 825 .daem-access-footer img{ max-height:100%; max-width:90%; object-fit:contain; } 826 827 /* Cursor */ 828 body.daem-acc-cursor *{ 829 cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M4,3 L4,37 L13,28 L19,43 L25,40 L19,25 L32,25 Z" fill="white" stroke="black" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>') 4 3, auto !important; 830 } 831 body.daem-acc-cursor #daem-accessibility-widget button, 832 body.daem-acc-cursor #daem-accessibility-widget [role="button"], 833 body.daem-acc-cursor #daem-accessibility-widget [tabindex] { 834 cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M4,3 L4,37 L13,28 L19,43 L25,40 L19,25 L32,25 Z" fill="white" stroke="black" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>') 4 3, auto !important; 835 pointer-events: auto !important; 836 } 837 838 /* Sign language modal */ 839 #daem-sign-lang-modal{ 840 position: fixed; 841 top: 50%; 842 left: 50%; 843 transform: translate(-50%, -50%); 844 width: 680px; 845 max-width: 90vw; 846 /* height: 550px */ 847 background: rgba(0,0,0,0.9); 848 border-radius: 10px; 849 box-shadow: 0 10px 25px rgba(0,0,0,0.5); 850 z-index: 10005; 851 display:none; 852 flex-direction:column; 853 overflow:hidden; 854 } 855 #daem-sign-lang-modal.daem-active{ display:flex; } 856 #daem-sign-lang-modal .daem-sign-header{ 857 display:flex; 858 justify-content:space-between; 859 align-items:center; 860 padding: 10px 15px; 861 background: var(--daem-acc-primary); 862 color:white; 863 flex-shrink: 0; 864 } 865 #daem-sign-lang-modal .daem-sign-header span i{ margin-right:8px; } 866 #daem-sign-lang-modal .daem-sign-close{ 867 background:none; 868 border:none; 869 color:white; 870 font-size:20px; 871 cursor:pointer; 872 } 873 /* Sign language video container */ 874#daem-sign-lang-modal .daem-sign-video-content { 875 flex-grow: 1; 876 padding: 8px; 877 display: flex; 878 align-items: center; 879 justify-content: center; 880} 881 882/* Iframe itself */ 883#daem-sign-lang-iframe { 884 aspect-ratio: 16 / 9; 885 width: 100%; 886 height: auto; 887 max-height: 100%; 888} 889 890 891 /* Ø§ÙØ£ÙÙ */ 892 object-fit: contain; /* ÙÙ ÙØ¹ اÙÙØµ */ 893 background: black; 894} 895 896 897 /* Reading mask/line */ 898 #daem-reading-mask{ 899 display:none; 900 position: fixed; 901 inset: 0; 902 z-index: 9998; 903 pointer-events:none; 904 background: linear-gradient( 905 to bottom, 906 rgba(0,0,0,0.75) 0%, 907 rgba(0,0,0,0.75) var(--rm-top, 40%), 908 rgba(0,0,0,0) var(--rm-top, 40%), 909 rgba(0,0,0,0) var(--rm-bottom, 60%), 910 rgba(0,0,0,0.75) var(--rm-bottom, 60%), 911 rgba(0,0,0,0.75) 100% 912 ); 913} 914body.daem-acc-mask-1 #daem-reading-mask{ display:block; } 915 916 #daem-reading-mask.daem-moving{ 917 mask-image: linear-gradient(to bottom, black 0%, transparent 0%, transparent var(--daem-reading-mask-height), black var(--daem-reading-mask-height)); 918 -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 0%, transparent var(--daem-reading-mask-height), black var(--daem-reading-mask-height)); 919 } 920 921 #daem-reading-mask{ 922 display:none; 923 position: fixed; 924 top:0; 925 left:0; 926 width:100%; 927 height:100%; 928 z-index: 9998; 929 background: rgba(0,0,0,0.7); 930 pointer-events:none; 931} 932body.daem-acc-mask-1 #daem-reading-mask{ display:block; } 933#daem-reading-mask.daem-moving{ 934 mask-image: linear-gradient(...); 935 -webkit-mask-image: linear-gradient(...); 936} 937 938 939 #daem-reading-line{ 940 display:none; 941 position: fixed; 942 width: 30%; 943 height: 5px; 944 background-color: yellow; 945 border-top: 2px solid black; 946 border-bottom: 2px solid black; 947 box-shadow: 0 0 10px rgba(0,0,0,0.3); 948 pointer-events:none; 949 z-index: 9999; 950 transform: translate(-50%, -50%); 951 } 952 #daem-reading-line::after{ 953 content:""; 954 position:absolute; 955 left:50%; 956 top:-10px; /* ÙÙÙ Ø§ÙØ®Ø· */ 957 transform:translateX(-50%); 958 width:0; 959 height:0; 960 border-left: 10px solid transparent; 961 border-right: 10px solid transparent; 962 border-bottom: 10px solid yellow; /* ÙÙØ³ ÙÙÙ Ø§ÙØ®Ø· */ 963 filter: drop-shadow(0 1px 0 #000); /* ØØ¯ Ø¨Ø³ÙØ· */ 964} 965 body.daem-acc-mask-2 #daem-reading-line{ display:block; } 966 967 /* Effects */ 968 969 /* Default (English/Latin) */ 970 body.daem-acc-sign-font-1 *:not( 971 #daem-accessibility-widget, #daem-accessibility-widget *, 972 #daem-sign-lang-modal, #daem-sign-lang-modal *, 973 i, i[class*="fa-"], [class*=" fa-"], 974 .material-icons, 975 [class*="icon-"], [class*="-icon"] 976 ) { 977 font-family: 'AmericanSignLanguage', sans-serif !important; 978 } 979 /* Arabic override */ 980 body.daem-acc-sign-font-1 *:lang(ar):not( 981 #daem-accessibility-widget, #daem-accessibility-widget *, 982 #daem-sign-lang-modal, #daem-sign-lang-modal *, 983 i, i[class*="fa-"], [class*=" fa-"], 984 .material-icons, 985 [class*="icon-"], [class*="-icon"] 986 ) { 987 font-family: 'ArabicSignLanguage', sans-serif !important; 988 } 989 990 /* highlight */ 991 span.daem-acc-sl-highlight { 992 font-size: inherit !important; 993 background-color: yellow !important; 994 color: black !important; 995 padding: 2px 0; 996 display: inline; 997 } 998 span.daem-acc-sl-highlight:lang(ar) { font-family: 'ArabicSignLanguage', sans-serif !important; } 999 span.daem-acc-sl-highlight:not(:lang(ar)) { font-family: 'AmericanSignLanguage', sans-serif !important; } 1000 1001 /* font-size levels handled via JS inline scaling (applyFontSizeJS) */ 1002 1003 body.daem-acc-lineheight-1 *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { line-height: 1.75 !important; } 1004 body.daem-acc-lineheight-2 *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { line-height: 2 !important; } 1005 body.daem-acc-lineheight-3 *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { line-height: 2.25 !important; } 1006 body.daem-acc-lineheight-4 *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { line-height: 2.5 !important; } 1007 1008 body.daem-acc-spacing-1 *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { letter-spacing: 1px !important; word-spacing: 2px !important; } 1009 body.daem-acc-spacing-2 *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { letter-spacing: 2px !important; word-spacing: 4px !important; } 1010 body.daem-acc-spacing-3 *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { letter-spacing: 3px !important; word-spacing: 6px !important; } 1011 body.daem-acc-spacing-4 *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { letter-spacing: 4px !important; word-spacing: 8px !important; } 1012 1013 body.daem-acc-links a:not(#daem-accessibility-widget a){ 1014 text-decoration: underline !important; 1015 text-decoration-color: #f59e0b !important; 1016 text-decoration-thickness: 2px !important; 1017 text-underline-offset: 3px !important; 1018 } 1019 body.daem-acc-links a:not(#daem-accessibility-widget a):not(:has(img)):not(:has(svg)):not(:has([class*="icon"])):not(:has(picture)) { 1020 box-shadow: 0 0 0 2px #f59e0b !important; 1021 border-radius: 3px !important; 1022 } 1023 1024 body.daem-acc-titles h1:not(#daem-accessibility-widget *), 1025 body.daem-acc-titles h2:not(#daem-accessibility-widget *), 1026 body.daem-acc-titles h3:not(#daem-accessibility-widget *), 1027 body.daem-acc-titles h4:not(#daem-accessibility-widget *),
1028 body.daem-acc-titles h5:not(#daem-accessibility-widget *), 1029 body.daem-acc-titles h6:not(#daem-accessibility-widget *){ 1030 border: 2px solid var(--daem-acc-primary) !important; 1031 padding: 5px !important; 1032 } 1033 1034 body.daem-acc-dyslexic-font *:not( 1035 #daem-accessibility-widget, 1036 #daem-accessibility-widget *, 1037 i, i[class*="fa-"], [class*=" fa-"], 1038 .material-icons, 1039 [class*="icon-"], [class*="-icon"] 1040 ) { 1041 font-family: 'OpenDyslexic', sans-serif !important; 1042 font-size: inherit !important; 1043 font-weight: normal !important; 1044 } 1045 body.daem-acc-font *:not( 1046 #daem-accessibility-widget, #daem-accessibility-widget *, 1047 i, i[class*="fa-"], [class*=" fa-"], 1048 .material-icons, 1049 [class*="icon-"], [class*="-icon"] 1050 ) { font-family: Arial, sans-serif !important; } 1051 1052 body.daem-acc-stop-anim *:not(#daem-accessibility-widget, #daem-accessibility-widget *), 1053 body.daem-acc-stop-anim *:not(#daem-accessibility-widget, #daem-accessibility-widget *):before, 1054 body.daem-acc-stop-anim *:not(#daem-accessibility-widget, #daem-accessibility-widget *):after{ 1055 animation:none !important; 1056 transition:none !important; 1057 } 1058 1059 body.daem-acc-hide-images img:not(#daem-accessibility-widget img){ display:none !important; } 1060 1061 body.daem-acc-bold-1 *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { font-weight: 700 !important; } 1062 body.daem-acc-bold-2 *:not(#daem-accessibility-widget, #daem-accessibility-widget *) { font-weight: 900 !important; } 1063 1064 body.daem-acc-align-left *:not(#daem-accessibility-widget *) { text-align:left !important; } 1065 body.daem-acc-align-center *:not(#daem-accessibility-widget *) { text-align:center !important; } 1066 body.daem-acc-align-right *:not(#daem-accessibility-widget *) { text-align:right !important; } 1067 1068 /* ===== RTL patches (Arabic) ===== */ 1069 #daem-accessibility-widget.daem-acc-ar #daem-access-trigger{ left: auto; right: 20px; } 1070 #daem-accessibility-widget.daem-acc-ar #daem-access-panel{ 1071 left: auto; 1072 right: -400px; 1073 transition: right 0.4s; 1074 box-shadow: -10px 0 30px rgba(0,0,0,0.15); 1075 } 1076 #daem-accessibility-widget.daem-acc-ar #daem-access-panel.daem-is-open{ right: 0; left: auto; } 1077 #daem-accessibility-widget.daem-acc-ar .daem-shortcut-badge{ right: auto; left: 5px; } 1078 #daem-accessibility-widget.daem-acc-ar .daem-level-badge{ left: auto; right: 5px; } 1079 1080 /* ===== Speech-to-Text Modal ===== */ 1081 #daem-stt-modal{ 1082 position: fixed; 1083 inset: 0; 1084 display: none; 1085 z-index: 10006; 1086 background: rgba(0,0,0,0.55); 1087 align-items: center; 1088 justify-content: center; 1089 padding: 14px; 1090 } 1091 #daem-stt-modal.daem-active{ display:flex; } 1092 #daem-stt-modal .daem-stt-card{ 1093 width: 560px; 1094 max-width: 95vw; 1095 background: #fff; 1096 border-radius: 14px; 1097 box-shadow: 0 16px 40px rgba(0,0,0,0.25); 1098 overflow: hidden; 1099 } 1100 #daem-stt-modal .daem-stt-header{ 1101 display:flex;
1102 align-items:center; 1103 justify-content:space-between; 1104 padding: 12px 14px; 1105 background: var(--daem-acc-primary); 1106 color: #fff; 1107 } 1108 #daem-stt-modal .daem-stt-title{ font-weight: 700; } 1109 #daem-stt-modal .daem-stt-close{ 1110 background: transparent; 1111 border: none; 1112 color: #fff !important; 1113 font-size: 18px; 1114 cursor: pointer; 1115 box-shadow: none !important; 1116 width: auto !important; 1117 } 1118 #daem-stt-modal .daem-stt-body{ padding: 14px; } 1119 #daem-stt-modal .daem-stt-help{ 1120 font-size: 13px; 1121 opacity: 0.85; 1122 margin-bottom: 8px; 1123 } 1124 #daem-stt-modal .daem-stt-text{ 1125 width: 100% !important; 1126 box-sizing: border-box; 1127 border: 1px solid #e5e7eb; 1128 border-radius: 10px; 1129 padding: 10px; 1130 resize: vertical; 1131 font-size: 14px; 1132 outline: none; 1133 box-shadow: none !important; 1134 background: #fff !important; 1135 max-width: 100% !important; 1136 min-width: 100% !important; 1137 } 1138 #daem-stt-modal .daem-stt-actions{ 1139 display:flex; 1140 gap: 10px; 1141 margin-top: 10px; 1142 justify-content: flex-end; 1143 } 1144 #daem-stt-modal .daem-stt-btn{ 1145 border: none; 1146 border-radius: 10px; 1147 padding: 10px 12px; 1148 cursor: pointer; 1149 background: #f3f4f6 !important; 1150 1151 display: inline-flex;
1152 align-items: center; 1153 gap: 8px; 1154 box-shadow: none !important; 1155 color: #343a40 !important; 1156 } 1157 #daem-stt-modal .daem-stt-btn i{ font-size: 14px; } 1158 #daem-stt-modal .daem-stt-btn:hover{ background:#e5e7eb; } 1159 1160 #daem-accessibility-widget.daem-acc-ar #daem-stt-modal .daem-stt-card, 1161 #daem-accessibility-widget.daem-acc-ar #daem-stt-modal { direction: rtl; } 1162 `; 1163 1164 /* --------------------------- 4) Inject scripts you already had --------------------------- */ 1165 const scriptElement = document.createElement("script"); 1166 scriptElement.src = plugHostingUrl2020 + scriptApplayChanges; 1167 1168 // Using native browser APIs for TTS & STT 1169 // const scriptElementsdk = document.createElement("script"); 1170 // scriptElementsdk.src = scriptElementsdkURL; 1171 1172 /* --------------------------- 5) Append everything --------------------------- */ 1173 body.appendChild(styleElement); 1174 body.appendChild(widgetWrapper); 1175 body.appendChild(scriptElement); 1176 // body.appendChild(scriptElementsdk); 1177 1178 // Patch window.onmessage after applyChange.js loads so that messages 1179 // with unknown keys don't throw an uncaught TypeError (infinite console error). 1180 scriptElement.addEventListener('load', function () 1181 { 1182 var _orig = window.onmessage; 1183 if (typeof _orig === 'function') 1184 { 1185 window.onmessage = function (mes) 1186 { 1187 try 1188 { 1189 _orig.call(this, mes); 1190 } catch (e) 1191 { 1192 // Suppress: excuteChnaged[mes.data] is not a function 1193 // Fires when the page sends postMessages unrelated to this widget. 1194 } 1195 }; 1196 } 1197 }); 1198 1199 /* --------------------------- 6) Widget behavior (JS) --------------------------- */ 1200 const panel = document.getElementById("daem-access-panel"); 1201 const accessTrigger = document.getElementById("daem-access-trigger"); 1202 const accessClose = document.getElementById("daem-access-close"); 1203 const accessReset = document.getElementById("daem-access-reset"); 1204 const container = document.querySelector("#daem-access-panel .daem-access-grid"); 1205 const readingMask = document.getElementById("daem-reading-mask"); 1206 const readingLine = document.getElementById("daem-reading-line"); 1207 const hintTooltip = document.getElementById("daem-hint-tooltip"); 1208 const signLangModal = document.getElementById("daem-sign-lang-modal"); 1209 const signLangIframe = document.getElementById("daem-sign-lang-iframe"); 1210 1211 // STT Modal elements 1212 const sttModal = document.getElementById("daem-stt-modal"); 1213 const sttTitle = document.getElementById("daem-stt-title"); 1214 const sttHelp = document.getElementById("daem-stt-help"); 1215 const sttText = document.getElementById("daem-stt-text"); 1216 const sttClose = document.getElementById("daem-stt-close"); 1217 const sttStop = document.getElementById("daem-stt-stop"); 1218 const sttClear = document.getElementById("daem-stt-clear"); 1219 const sttCopy = document.getElementById("daem-stt-copy"); 1220 1221 const SIGN_VIDEO_URL = youTubeVideoLink; // provided by backend 1222 1223 // Feature definitions (localized labels) 1224 const features = [ 1225 { 1226 id: "sign-vid", 1227 label: I18N.featureLabels["sign-vid"], 1228 icon: `${SVG_ICONS}daem_signlanguagevideo.svg`, 1229 key: "1", 1230 }, 1231 { 1232 id: "sign-font", 1233 label: I18N.featureLabels["sign-font"], 1234 icon: `${SVG_ICONS}daem_signlanguagefont.svg`, 1235 key: "2", 1236 }, 1237 { 1238 id: "text-speech", 1239 label: I18N.featureLabels["text-speech"], 1240 icon: `${SVG_ICONS}daem_tts.svg`, 1241 key: "3", 1242 }, 1243 { 1244 id: "speech-text", 1245 label: I18N.featureLabels["speech-text"], 1246 icon: `${SVG_ICONS}daem_stt.svg`, 1247 key: "4", 1248 }, 1249 { 1250 id: "dark-mode", 1251 label: I18N.featureLabels["dark-mode"], 1252 icon: `${SVG_ICONS}daem_darkmode.svg`, 1253 key: "C", 1254 }, 1255 { 1256 id: "font-size", 1257 label: I18N.featureLabels["font-size"], 1258 icon: `${SVG_ICONS}daem_fontsize.svg`, 1259 key: "F", 1260 }, 1261 { 1262 id: "cursor", 1263 label: I18N.featureLabels.cursor, 1264 icon: `${SVG_ICONS}daem_mouse.svg`, 1265 key: "M", 1266 }, 1267 { 1268 id: "line-height", 1269 label: I18N.featureLabels["line-height"], 1270 icon: `${SVG_ICONS}daem_linehight.svg`, 1271 key: "L", 1272 }, 1273 { 1274 id: "spacing", 1275 label: I18N.featureLabels.spacing, 1276 icon: `${SVG_ICONS}daem_fontsize.svg`, 1277 key: "S", 1278 }, 1279 { 1280 id: "readable", 1281 label: I18N.featureLabels.readable, 1282 icon: `${SVG_ICONS}daem_readablefont.svg`, 1283 key: "R", 1284 }, 1285 { id: "links", label: I18N.featureLabels.links, icon: `${SVG_ICONS}daem_linkhighlight.svg`, key: "H" }, 1286 { 1287 id: "titles", 1288 label: I18N.featureLabels.titles, 1289 icon: `${SVG_ICONS}daem_heading.svg`, 1290 key: "T", 1291 }, 1292 { 1293 id: "hint", 1294 label: I18N.featureLabels.hint, 1295 icon: `${SVG_ICONS}daem_hint.svg`, 1296 key: "Q", 1297 }, 1298 { 1299 id: "stop-anim", 1300 label: I18N.featureLabels["stop-anim"], 1301 icon: `${SVG_ICONS}
1301daem_stopanimation.svg`, 1302 key: "P", 1303 }, 1304 { 1305 id: "dyslexic-font", 1306 label: I18N.featureLabels["dyslexic-font"], 1307 icon: `${SVG_ICONS}daem_dyslexia.svg`, 1308 key: "D", 1309 }, 1310 { 1311 id: "mask", 1312 label: I18N.featureLabels.mask, 1313 icon: `${SVG_ICONS}daem_readingmask.svg`, 1314 key: "V", 1315 }, 1316 { 1317 id: "hide-images", 1318 label: I18N.featureLabels["hide-images"], 1319 icon: `${SVG_ICONS}daem_hideimages.svg`, 1320 key: "I", 1321 }, 1322 { 1323 id: "bold-text", 1324 label: I18N.featureLabels["bold-text"], 1325 icon: `${SVG_ICONS}daem_bold.svg`, 1326 key: "B", 1327 }, 1328 { 1329 id: "text-align", 1330 label: I18N.featureLabels["text-align"], 1331 icon: `${SVG_ICONS}daem_align.svg`, 1332 key: "G", 1333 }, 1334 { 1335 id: "saturation", 1336 label: I18N.featureLabels.saturation, 1337 icon: `${SVG_ICONS}daem_saturation.svg`, 1338 key: "U", 1339 }, 1340 ]; 1341 1342 // Build feature buttons 1343 features.forEach((feat) => 1344 { 1345 const btn = document.createElement("div"); 1346 btn.className = "daem-access-btn";
1347 btn.setAttribute("data-id", feat.id); 1348 btn.setAttribute("role", "button"); 1349 btn.setAttribute("aria-pressed", "false"); 1350 btn.innerHTML = ` 1351 <div class="daem-shortcut-badge">Alt+${feat.key}</div> 1352 <div class="daem-level-badge"></div> 1353 <img src="${feat.icon}" class="daem-btn-icon" alt=""> 1354 <span>${feat.label}</span> 1355 `; 1356 btn.addEventListener("click", () => toggleFeature(feat.id, btn)); 1357 container.appendChild(btn); 1358 }); 1359 1360 //// Remove TTS/STT features and related profiles if AllowPageReaderVoiceToText is disabled 1361 //if (AllowPageReaderVoiceToText === "false" || AllowPageReaderVoiceToText === "False" || AllowPageReaderVoiceToText === "0" || AllowPageReaderVoiceToText === "") 1362 //{ 1363 // // Remove TTS and STT feature buttons 1364 // const ttsBtn = container.querySelector('[data-id="text-speech"]'); 1365 // const sttBtn = container.querySelector('[data-id="speech-text"]'); 1366 // if (ttsBtn) ttsBtn.remove(); 1367 // if (sttBtn) sttBtn.remove(); 1368 1369 // // Remove visual (uses TTS) and motor (uses STT) profile buttons 1370 // const visualProfile = widgetWrapper.querySelector('[data-profile="visual"]'); 1371 // const motorProfile = widgetWrapper.querySelector('[data-profile="motor"]'); 1372 // if (visualProfile) visualProfile.remove(); 1373 // if (motorProfile) motorProfile.remove(); 1374 //} 1375 1376 // Draggable panel 1377 makePanelDraggable(panel, document.getElementById("acc-drag-handle")); 1378 1379 function makePanelDraggable(draggableElement, handle) 1380 { 1381 let isDragging = false; 1382 let startX; 1383 let initialOffset; 1384 const PANEL_WIDTH = 380; 1385 1386 function getCurrentOffset() 1387 { 1388 if (IS_AR) 1389 { 1390 const styleRight = draggableElement.style.right; 1391 return styleRight && styleRight !== "0px" ? parseFloat(styleRight) : 0; 1392 } 1393 const styleLeft = draggableElement.style.left; 1394 return styleLeft && styleLeft !== "0px" ? parseFloat(styleLeft) : 0; 1395 } 1396 1397 function setOffset(val) 1398 { 1399 if (IS_AR) draggableElement.style.right = `${val}px`; 1400 else draggableElement.style.left = `${val}px`; 1401 } 1402 1403 function onStart(e) 1404 { 1405 if (!draggableElement.classList.contains("daem-is-open")) return; 1406 if (e.button !== 0 && !e.touches) return; 1407 isDragging = true; 1408 document.body.style.userSelect = "none"; 1409 initialOffset = getCurrentOffset(); 1410 draggableElement.style.transition = "none"; 1411 startX = e.clientX || (e.touches && e.touches[0].clientX); 1412 } 1413 1414 function onMove(e) 1415 { 1416 if (!isDragging) return; 1417 e.preventDefault(); 1418 const currentX = e.clientX || (e.touches && e.touches[0].clientX); 1419 const dx = currentX - startX; 1420 let newOffset = IS_AR ? initialOffset - dx : initialOffset + dx; 1421 const maxOffset = Math.max(0, window.innerWidth - PANEL_WIDTH); 1422 const minOffset = 0; 1423 newOffset = Math.min(newOffset, maxOffset); 1424 newOffset = Math.max(newOffset, minOffset); 1425 setOffset(newOffset); 1426 draggableElement.classList.add("daem-is-open"); 1427 } 1428 1429 function onEnd() 1430 { 1431 if (!isDragging) return; 1432 isDragging = false; 1433 document.body.style.userSelect = ""; 1434 if (IS_AR) draggableElement.style.transition = "right 0.4s"; 1435 else draggableElement.style.transition = "left 0.4s"; 1436 const finalOffset = getCurrentOffset(); 1437 const maxOffset = Math.max(0, window.innerWidth - PANEL_WIDTH); 1438 if (finalOffset < 0) setOffset(0); 1439 else if (finalOffset > maxOffset) setOffset(maxOffset); 1440 draggableElement.classList.add("daem-is-open"); 1441 } 1442 1443 handle.addEventListener("mousedown", onStart); 1444 document.addEventListener("mousemove", onMove); 1445 document.addEventListener("mouseup", onEnd); 1446 1447 handle.addEventListener("touchstart", (e) => onStart(e), { 1448 passive: false, 1449 }); 1450 document.addEventListener("touchmove", (e) => onMove(e), { 1451 passive: false, 1452 }); 1453 document.addEventListener("touchend", onEnd); 1454 } 1455 1456 // State 1457 let fontSizeLevel = 0; 1458 let fontSizeOriginals = null; // Map<Element, {inline, computed}> captured on first activation
1459 let contrastLevel = 0; 1460 let lineHeightLevel = 0; 1461 let charSpacingLevel = 0; 1462 let textToSpeechLevel = 0; 1463 let signFontLevel = 0; 1464 let readingMaskLevel = 0; 1465 let boldLevel = 0; 1466 let textAlignLevel = 0; 1467 let saturationLevel = 0; 1468 1469 let isHintModeActive = false; 1470 1471 // Profile state: tracks active presets 1472 let activeProfiles = new Set(); 1473 let profileActivating = false; // suppresses sync checks during profile activation 1474 1475 let ttsSynth = null; // no longer used 1476 let ttsPlayer = null; // holds current SpeechSynthesisUtterance â MUST keep reference to prevent Chrome GC bug 1477 let _ttsKeepAlive = null; // interval id for Chrome 15s resume workaround 1478 let ttsIsSpeaking = false; 1479 1480 // ---------- Speech-to-Text (native Web Speech API) state ---------- 1481 let sttController = null; // { stop(), close() ... } 1482 let sttFinalText = ""; 1483 let sttRecognizer = null; 1484 let sttIsRunning = false; 1485 let sttTargetElement = null; 1486 let _sttStartFn = null; // stored restart function for resume button 1487 1488 // Helper: editable field 1489 function getActiveEditableElement() 1490 { 1491 const el = document.activeElement; 1492 if (!el) return null; 1493 const tag = (el.tagName || "").toUpperCase(); 1494 if (tag === "INPUT" || tag === "TEXTAREA") return el; 1495 if (el.isContentEditable) return el; 1496 return null; 1497 } 1498 1499 function writeTextToElement(el, value) 1500 { 1501 if (!el) return; 1502 const tag = (el.tagName || "").toUpperCase(); 1503 if (tag === "INPUT" || tag === "TEXTAREA") 1504 { 1505 el.value = value; 1506 el.dispatchEvent(new Event("input", { bubbles: true })); 1507 el.dispatchEvent(new Event("change", { bubbles: true })); 1508 return; 1509 } 1510 if (el.isContentEditable) 1511 { 1512 el.innerText = value; 1513 el.dispatchEvent(new Event("input", { bubbles: true })); 1514 } 1515 } 1516 1517 function setSttBoth(value) 1518 { 1519 sttText.value = value; 1520 writeTextToElement(sttTargetElement, value); 1521 } 1522 1523 function setStopButtonUI(isRunning) 1524 { 1525 const text = sttStop.querySelector("span"); 1526 if (isRunning) 1527 { 1528 if (text) text.innerText = I18N.dynamic.sttStop; 1529 } else 1530 { 1531 if (text) text.innerText = I18N.dynamic.sttResume; 1532 } 1533 } 1534 1535 async function copySttText() 1536 { 1537 const value = (sttText.value || "").trim(); 1538 if (!value) return; 1539 1540 try 1541 { 1542 await navigator.clipboard.writeText(value); 1543 } catch (e) 1544 { 1545 // Fallback 1546 sttText.focus(); 1547 sttText.select(); 1548 document.execCommand("copy"); 1549 sttText.setSelectionRange(0, 0); 1550 } 1551 1552 const labelSpan = sttCopy.querySelector("span"); 1553 const old = labelSpan ? labelSpan.innerText : ""; 1554 if (labelSpan) labelSpan.innerText = I18N.dynamic.sttCopied || "Copied!"; 1555 setTimeout(() => 1556 { 1557 if (labelSpan) labelSpan.innerText = I18N.dynamic.sttCopy || old; 1558 }, 900); 1559 } 1560 1561 function openSttModal() 1562 { 1563 sttTitle.textContent = I18N.dynamic.sttTitle; 1564 sttHelp.textContent = I18N.dynamic.sttHelp; 1565 1566 const clearSpan = sttClear.querySelector("span"); 1567 const copySpan = sttCopy.querySelector("span"); 1568 if (clearSpan) clearSpan.innerText = I18N.dynamic.sttClear; 1569 if (copySpan) copySpan.innerText = I18N.dynamic.sttCopy; 1570 1571 sttClose.setAttribute("aria-label", I18N.dynamic.sttClose); 1572 setStopButtonUI(sttIsRunning); 1573 1574 // Use inline style as well as class â some host-page CSS may override 1575 // the class-based display rule. 1576 sttModal.style.display = 'flex'; 1577 sttModal.classList.add("daem-active"); 1578 sttModal.setAttribute("aria-hidden", "false"); 1579 } 1580 1581 function closeSttModal() 1582 { 1583 sttModal.style.display = ''; 1584 sttModal.classList.remove("daem-active"); 1585 sttModal.setAttribute("aria-hidden", "true"); 1586 } 1587 1588 // Close on overlay click 1589 sttModal.addEventListener("click", (e) => 1590 { 1591 if (e.target === sttModal) 1592 { 1593 stopSpeechToText(true); 1594 } 1595 });
1596 1597 sttClose.addEventListener("click", () => stopSpeechToText(true)); 1598 sttCopy.addEventListener("click", copySttText); 1599 1600 sttClear.addEventListener("click", () => 1601 { 1602 sttFinalText = ""; 1603 setSttBoth(""); 1604 }); 1605 1606 // Stop/Resume toggle 1607 sttStop.addEventListener("click", () => 1608 { 1609 if (sttIsRunning) 1610 { 1611 // --- Pause --- 1612 sttIsRunning = false; 1613 _sttStartFn = true; 1614 if (sttRecognizer) 1615 { 1616 const old = sttRecognizer; 1617 sttRecognizer = null; 1618 try { old.abort(); } catch (e) { } 1619 } 1620 sttHelp.textContent = I18N.dynamic.sttStopped; 1621 setStopButtonUI(false); 1622 } 1623 else 1624 { 1625 // --- Resume --- 1626 if (!_sttStartFn) return; // no active session to resume 1627 sttIsRunning = true; 1628 sttHelp.textContent = I18N.dynamic.sttHelp; 1629 setStopButtonUI(true); 1630 speakToText(); 1631 //_sttStartFn(); 1632 } 1633 }); 1634 1635 /** 1636 * speakToText (integrated) 1637 * - Opens popup 1638 * - Starts continuous recognition using native Web Speech API 1639 * - Writes into popup + active field 1640 */ 1641 // Write text into the STT textarea, handling all cases: 1642 // 1) Direct assignment (works in plain HTML) 1643 // 2) Native prototype setter (bypasses React/Vue/Angular interceptors) 1644 // 3) Scroll to bottom so latest text is always visible 1645 function _setSttDisplay(text) 1646 { 1647 if (!sttText) return; 1648 // Set value directly â no bubbling events so the host page cannot 1649 // intercept and reset this textarea. 1650 sttText.value = text; 1651 // Fallback: native prototype setter for frameworks that override .value 1652 try 1653 { 1654 Object.getOwnPropertyDescriptor( 1655 window.HTMLTextAreaElement.prototype, 'value' 1656 ).set.call(sttText, text); 1657 } catch (_) { } 1658 // Scroll to newest content 1659 sttText.scrollTop = sttText.scrollHeight; 1660 // Only propagate to the original target field (if one was focused at STT start) 1661 if (sttTargetElement) writeTextToElement(sttTargetElement, text); 1662 } 1663 1664 function speakToText() 1665 { 1666 1667 const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; 1668 console.log('[STT] speakToText. API=', !!SpeechRecognition); 1669 if (!SpeechRecognition) 1670 { 1671 alert(I18N.alerts.sttUnsupported); 1672 return null; 1673 } 1674 1675 // Capture any currently focused editable field 1676 sttTargetElement = getActiveEditableElement(); 1677 1678 // Reset state & UI 1679 if (!sttIsRunning) 1680 { 1681 sttFinalText = ""; 1682 } 1683 sttHelp.textContent = I18N.dynamic.sttHelp; 1684 _setSttDisplay(sttFinalText); 1685 openSttModal(); 1686 1687 // Cleanly destroy any previous recognizer 1688 const old = sttRecognizer; 1689 sttRecognizer = null; 1690 sttIsRunning = false; 1691 _sttStartFn = null; 1692 if (old) { try { old.abort(); } catch (_) { } } 1693 1694 let _noSpeechStreak = 0; // tracks consecutive no-speech events 1695 1696 // startRec: create a fresh SpeechRecognition instance and start it. 1697 // Each round uses a NEW instance because Chrome's implementation does 1698 // not reliably support calling .start() on an already-ended instance. 1699 function startRec() 1700 { 1701 if (!sttIsRunning) return; 1702 1703 const rec = new SpeechRecognition(); 1704 sttRecognizer = rec; 1705 1706 // --- Config --- 1707 rec.lang = IS_AR ? "ar-SA" : "en-US"; 1708 // continuous:false = one recognition round per instance (most reliable) 1709 rec.continuous = false; 1710 // interimResults:true = live preview while speaking 1711 rec.interimResults = true; 1712 rec.maxAlternatives = 1; 1713 1714 // --- Handlers --- 1715 rec.onstart = () => 1716 { 1717 console.log('[STT] onstart lang=', rec.lang); 1718 sttHelp.textContent = I18N.dynamic.sttHelp; 1719 }; 1720 1721 rec.onresult = (e) => 1722 { 1723 // If the modal was accidentally closed while recognition is active, reopen it 1724 if (sttIsRunning && sttModal.style.display !== 'flex') 1725 openSttModal(); 1726 1727 // Case A: continuous:false â e.results always has exactly 1 item per call. 1728 // Case B: in some browsers e.results may accumulate â loop from 0 for safety. 1729 // We collect interim separately so the textarea shows live preview. 1730 let interim = ""; 1731 let roundFinal = ""; 1732 1733 for (let i = 0; i < e.results.length; i++) 1734 { 1735 const t = (e.results[i][0].transcript || "").trim(); 1736 if (!t) continue; // skip empty tokens 1737 if (e.results[i].isFinal) 1738 roundFinal += (roundFinal ? " " : "") + t; 1739 else 1740 interim += (interim ? " " : "") + t; 1741 } 1742 1743 console.log('[STT] onresult final=' + JSON.stringify(roundFinal) + ' interim=' + JSON.stringify(interim)); 1744 1745 // Append confirmed text to the session accumulator 1746 if (roundFinal) 1747 { 1748 sttFinalText = sttFinalText 1749 ? sttFinalText + " " + roundFinal
1750 : roundFinal; 1751 _noSpeechStreak = 0; 1752 } 1753 1754 // Display: confirmed text + live interim preview 1755 const display = interim 1756 ? sttFinalText + (sttFinalText ? " " : "") + interim 1757 : sttFinalText; 1758 1759 if (display) _setSttDisplay(display); 1760 }; 1761 1762 rec.onerror = (e) => 1763 { 1764 console.error('[STT] onerror:', e.error); 1765 1766 // Non-fatal â onend will restart 1767 if (e.error === "aborted") return; 1768 if (e.error === "no-speech") 1769 { 1770 _noSpeechStreak++; 1771 if (_noSpeechStreak >= 4) 1772 sttHelp.textContent = IS_AR 1773 ? "Ù٠أس٠عÙ. ØªØØ¯ÙØ« Ø¨ÙØ¶ÙØ Ø¨Ø§ØªØ¬Ø§Ù Ø§ÙÙ ÙÙØ±ÙÙÙÙ." 1774 : "No speech detected. Speak clearly into the microphone."; 1775 return; // onend will restart the round 1776 } 1777 1778 // Fatal â stop the session 1779 sttIsRunning = false; 1780 sttRecognizer = null; 1781 _sttStartFn = null; 1782 setStopButtonUI(false); 1783 1784 switch (e.error) 1785 { 1786 case "not-allowed": 1787 case "service-not-allowed": 1788 sttHelp.textContent = IS_AR 1789 ? "اÙÙ ÙÙØ±ÙÙÙÙ Ù ØØ¸Ùر. اضغط عÙ٠أÙÙÙÙØ© اÙÙÙÙ ÙÙ Ø´Ø±ÙØ· Ø§ÙØ¹ÙÙØ§Ù ÙØ§ÙØªØ Ø¥Ø°Ù Ø§ÙÙ ÙÙØ±ÙÙÙÙ." 1790 : "Microphone blocked. Click the lock icon in the address bar and allow microphone."; 1791 break; 1792 case "network": 1793 sttHelp.textContent = IS_AR 1794 ? "ÙØ§ ÙÙØ¬Ø¯ اتصا٠بخد٠ة Ø§ÙØªØ¹Ø±Ù عÙ٠اÙÙÙØ§Ù . تØÙÙ Ù Ù Ø§ÙØ¥ÙØªØ±ÙØª." 1795 : "Cannot reach speech service. Check your internet connection."; 1796 break; 1797 case "audio-capture": 1798 sttHelp.textContent = IS_AR 1799 ? "ÙØ§ ÙÙØ¬Ø¯ Ù ÙÙØ±ÙÙÙÙ. تØÙÙ Ù Ù ØªÙØµÙÙ Ø§ÙØ¬Ùاز." 1800 : "No microphone found. Check device connection."; 1801 break; 1802 default: 1803 sttHelp.textContent = (IS_AR ? "خطأ: " : "Error: ") + e.error; 1804 } 1805 }; 1806 1807 rec.onend = () => 1808 { 1809 console.log('[STT] onend. running=', sttIsRunning, 'same=', sttRecognizer === rec); 1810 // Only restart if this is still the active session 1811 if (sttIsRunning && sttRecognizer === rec) 1812 { 1813 sttRecognizer = null; 1814 // 300ms gap prevents Chrome InvalidStateError on fast re-start 1815 setTimeout(startRec, 300); 1816 } 1817 }; 1818 1819 try 1820 { 1821 rec.start(); 1822 console.log('[STT] start() called. lang=', rec.lang); 1823 } catch (err) 1824 { 1825 console.error('[STT] start() threw:', err); 1826 // If start() throws, retry once after a longer delay 1827 sttRecognizer = null; 1828 if (sttIsRunning) 1829 setTimeout(startRec, 800); 1830 else 1831 { 1832 _sttStartFn = null; 1833 sttHelp.textContent = I18N.dynamic.sttError; 1834 setStopButtonUI(false); 1835 } 1836 } 1837 } 1838 1839 _sttStartFn = startRec; 1840 sttIsRunning = true; 1841 setStopButtonUI(true); 1842 startRec(); 1843 1844 return { 1845 stop: () => stopSpeechToText(false), 1846 close: () => stopSpeechToText(true), 1847 }; 1848 } 1849
1850 function stopSpeechToText(closePopup) 1851 { 1852 sttIsRunning = false; 1853 _sttStartFn = null; 1854 1855 // Null BEFORE abort so onend won't trigger a restart 1856 if (sttRecognizer) 1857 { 1858 const old = sttRecognizer; 1859 sttRecognizer = null; 1860 try { old.abort(); } catch (e) { } 1861 } 1862 1863 sttHelp.textContent = I18N.dynamic.sttStopped; 1864 setStopButtonUI(false); 1865 1866 // Update widget button UI 1867 const sttBtn = document.querySelector('.daem-access-btn[data-id="speech-text"]'); 1868 if (sttBtn) 1869 { 1870 sttBtn.classList.remove("daem-recording-active", "daem-active"); 1871 const sp = sttBtn.querySelector("span"); 1872 if (sp) sp.innerText = I18N.dynamic.speechToText; 1873 syncProfileActiveState(); 1874 } 1875 1876 if (closePopup) closeSttModal(); 1877 sttController = null; 1878 } 1879 1880 function toggleSpeechToText(btnElement) 1881 { 1882 // If currently active => pause (keep popup open) 1883 if (btnElement.classList.contains("daem-recording-active")) 1884 { 1885 if (sttRecognizer && sttIsRunning) 1886 { 1887 sttIsRunning = false; 1888 try { sttRecognizer.stop(); } catch (e) { } 1889 sttHelp.textContent = I18N.dynamic.sttStopped; 1890 setStopButtonUI(false); 1891 } 1892 btnElement.classList.remove("daem-recording-active"); 1893 btnElement.querySelector("span").innerText = I18N.dynamic.speechToText; 1894 return; 1895 } 1896 1897 // Start 1898 sttController = speakToText(); 1899 if (!sttController) return; 1900 1901 btnElement.classList.add("daem-recording-active", "daem-active"); 1902 btnElement.querySelector("span").innerText = I18N.dynamic.listening; 1903 } 1904 1905 function stopNativeTts() 1906 { 1907 console.log('[TTS] stopNativeTts called. ttsIsSpeaking=', ttsIsSpeaking); 1908 ttsIsSpeaking = false; 1909 ttsPlayer = null; 1910 if (_ttsKeepAlive) { clearInterval(_ttsKeepAlive); _ttsKeepAlive = null; } 1911 if (window.speechSynthesis) window.speechSynthesis.cancel(); 1912 const ttsBtn = document.querySelector('.daem-access-btn[data-id="text-speech"]'); 1913 if (ttsBtn) ttsBtn.classList.remove('daem-tts-speaking'); 1914 } 1915 1916 function speakText(text) 1917 { 1918 console.log('[TTS] speakText called. text=', JSON.stringify(text.substring(0, 80))); 1919 if (!window.speechSynthesis) 1920 { 1921 alert(IS_AR ? "اÙÙ ØªØµÙØ ÙØ§ ÙØ¯Ø¹Ù تØÙÙ٠اÙÙØµ Ø¥ÙÙ ÙÙØ§Ù ." : "Browser does not support text-to-speech."); 1922 return; 1923 } 1924 1925 const synth = window.speechSynthesis; 1926 const ttsBtn = document.querySelector('.daem-access-btn[data-id="text-speech"]'); 1927 1928 function _clearSpeakingUI() 1929 { 1930 ttsIsSpeaking = false; 1931 ttsPlayer = null; 1932 if (_ttsKeepAlive) { clearInterval(_ttsKeepAlive); _ttsKeepAlive = null; } 1933 if (ttsBtn) ttsBtn.classList.remove('daem-tts-speaking'); 1934 } 1935 1936 // Clear any previous keep-alive and cancel queued speech 1937 if (_ttsKeepAlive) { clearInterval(_ttsKeepAlive); _ttsKeepAlive = null; } 1938 synth.cancel(); 1939 1940 const utterance = new SpeechSynthesisUtterance(text); 1941 utterance.rate = 1; 1942 utterance.pitch = 1; 1943 1944 // --- Voice selection --- 1945 const voices = synth.getVoices(); 1946 console.log('[TTS] voices available (' + voices.length + '):', voices.map(v => v.name + '[' + v.lang + ']').join(' | ')); 1947 1948 let matchedVoice = null; 1949 if (IS_AR) 1950 { 1951 // Try multiple Arabic locale codes in order of preference 1952 matchedVoice = 1953 voices.find(v => v.lang === 'ar-SA') || 1954 voices.find(v => v.lang === 'ar-EG') || 1955 voices.find(v => v.lang === 'ar-XA') || 1956 voices.find(v => v.lang === 'ar') || 1957 voices.find(v => v.lang.toLowerCase().startsWith('ar')); 1958 } 1959 else 1960 { 1961 matchedVoice = 1962 voices.find(v => v.lang === 'en-US') || 1963 voices.find(v => v.lang.startsWith('en')); 1964 } 1965 1966 if (matchedVoice) 1967 { 1968 utterance.voice = matchedVoice; 1969 utterance.lang = matchedVoice.lang; // align lang with the actual voice 1970 console.log('[TTS] â voice selected:', matchedVoice.name, '[' + matchedVoice.lang + ']'); 1971 } 1972 else 1973 { 1974 // No Arabic voice installed â fall back to first available voice so audio actually plays. 1975 // Chrome silently fires onstart+onend with no audio when lang='ar-SA' has no matching voice. 1976 // Setting an explicit available voice guarantees audible output. 1977 const fallback = voices.find(v => v.lang === 'en-US') || voices[0]; 1978 if (fallback) 1979 { 1980 utterance.voice = fallback; 1981 utterance.lang = fallback.lang; 1982 console.warn('[TTS] â no Arabic voice found â falling back to:', fallback.name, '[' + fallback.lang + ']', 1983 '| Install Windows Arabic TTS voice (Settings â Time & Language â Speech â Add voices) for proper Arabic speech.'); 1984 } 1985 else 1986 { 1987 utterance.lang = IS_AR ? 'ar-SA' : 'en-US'; 1988 console.warn('[TTS] â no voices available at all â attempting with lang=', utterance.lang); 1989 } 1990 } 1991 1992 // CRITICAL: keep strong reference to prevent Chrome GC bug (crbug.com/509488) 1993 ttsPlayer = utterance; 1994 ttsIsSpeaking = true; 1995 if (ttsBtn) ttsBtn.classList.add('daem-tts-speaking'); 1996 1997 // Chrome bug: utterances >~15s get silently paused â poke resume() periodically 1998 _ttsKeepAlive = setInterval(() => 1999 { 2000 if (!ttsIsSpeaking) { clearInterval(_ttsKeepAlive); _ttsKeepAlive = null; return; } 2001 if (synth.paused) { console.log('[TTS] keep-alive: resuming'); synth.resume(); } 2002 }, 5000); 2003 2004 utterance.onstart = () => { console.log('[TTS] â onstart'); }; 2005 utterance.onend = () => { console.log('[TTS] â onend'); _clearSpeakingUI(); }; 2006 utterance.onerror = (e) => { console.error('[TTS] â onerror:', e.error); _clearSpeakingUI(); }; 2007 utterance.onpause = () => { console.warn('[TTS] paused'); }; 2008 utterance.onresume = () => { console.log('[TTS] resumed'); }; 2009 2010 if (synth.paused) synth.resume(); 2011 console.log('[TTS] calling synth.speak()...'); 2012 synth.speak(utterance); 2013 console.log('[TTS] after speak(): speaking=', synth.speaking, 'pending=', synth.pending); 2014 } 2015 2016 // Open/close panel 2017 function togglePanel() 2018 { 2019 const isOpen = panel.classList.toggle("daem-is-open"); 2020 accessTrigger.setAttribute("aria-expanded", String(isOpen)); 2021 panel.setAttribute("aria-hidden", String(!isOpen)); 2022 2023 // Slide behavior differs for RTL 2024 if (IS_AR) 2025 { 2026 panel.style.transition = "right 0.4s"; 2027 panel.style.right = isOpen ? "0px" : "-400px"; 2028 panel.style.left = "auto"; 2029 } else 2030 { 2031 panel.style.transition = "left 0.4s"; 2032 panel.style.left = isOpen ? "0px" : "-400px"; 2033 panel.style.right = "auto"; 2034 } 2035 } 2036 2037 function Open_Tool() 2038 { 2039 if (!panel.classList.contains("daem-is-open")) togglePanel(); 2040 } 2041 2042 accessTrigger.addEventListener("click", togglePanel); 2043 2044 accessClose.addEventListener("click", () => 2045 { 2046 panel.classList.remove("daem-is-open"); 2047 panel.setAttribute("aria-hidden", "true"); 2048 accessTrigger.setAttribute("aria-expanded", "false"); 2049 if (IS_AR) 2050 { 2051 panel.style.transition = "right 0.4s"; 2052 panel.style.right = "-400px"; 2053 panel.style.left = "auto"; 2054 } else 2055 { 2056 panel.style.transition = "left 0.4s"; 2057 panel.style.left = "-400px"; 2058 panel.style.right = "auto"; 2059 } 2060 });
2061 2062 // Level badge 2063 function updateLevelBadge(btnElement, level) 2064 { 2065 const badge = btnElement.querySelector(".daem-level-badge"); 2066 btnElement.classList.remove("daem-active"); 2067 2068 if (level > 1 || (btnElement.dataset.id === "text-align" && level > 0)) 2069 { 2070 badge.innerText = String(level); 2071 badge.setAttribute("data-level", String(level)); 2072 btnElement.classList.add("daem-active"); 2073 } else if (level === 1) 2074 { 2075 badge.removeAttribute("data-level"); 2076 badge.innerText = ""; 2077 btnElement.classList.add("daem-active"); 2078 } else 2079 { 2080 badge.removeAttribute("data-level"); 2081 badge.innerText = ""; 2082 } 2083 syncProfileActiveState(); 2084 saveState(); 2085 } 2086 2087 // Returns the current numeric level for a given feature id 2088 function getFeatureLevel(id) 2089 { 2090 switch (id) 2091 { 2092 case "contrast": return contrastLevel; 2093 case "font-size": return fontSizeLevel; 2094 case "line-height": return lineHeightLevel; 2095 case "spacing": return charSpacingLevel; 2096 case "saturation": return saturationLevel; 2097 case "dark-mode": return document.body.classList.contains("daem-acc-dark-mode") ? 1 : 0; 2098 case "sign-font": return signFontLevel; 2099 case "mask": return readingMaskLevel; 2100 case "bold-text": return boldLevel; 2101 case "text-align": return textAlignLevel; 2102 case "text-speech": return textToSpeechLevel; 2103 case "cursor": return document.body.classList.contains("daem-acc-cursor") ? 1 : 0; 2104 case "readable": return document.body.classList.contains("daem-acc-font") ? 1 : 0; 2105 case "dyslexic-font": return document.body.classList.contains("daem-acc-dyslexic-font") ? 1 : 0; 2106 case "links": return document.body.classList.contains("daem-acc-links") ? 1 : 0; 2107 case "titles": return document.body.classList.contains("daem-acc-titles") ? 1 : 0; 2108 case "stop-anim": return document.body.classList.contains("daem-acc-stop-anim") ? 1 : 0; 2109 case "hide-images": return document.body.classList.contains("daem-acc-hide-images") ? 1 : 0; 2110 case "speech-text": { 2111 const b = document.querySelector('.daem-access-btn[data-id="speech-text"]'); 2112 return (b && b.classList.contains("daem-active")) ? 1 : 0; 2113 } 2114 default: return 0; 2115 } 2116 } 2117 2118 // Deselects a profile badge if any of its expected features no longer match 2119 function syncProfileActiveState() 2120 { 2121 if (profileActivating) return; 2122 document.querySelectorAll(".daem-profile-btn").forEach(btn => 2123 { 2124 const profileType = btn.dataset.profile; 2125 if (!activeProfiles.has(profileType)) return; 2126 const expectations = profileFeatureMap[profileType] || []; 2127 const allMatch = expectations.every(({ id, level }) => getFeatureLevel(id) === level); 2128 if (!allMatch) 2129 { 2130 activeProfiles.delete(profileType); 2131 btn.classList.remove("daem-active"); 2132 } 2133 }); 2134 } 2135 2136 // Scans page elements for CSS background-image and marks them + their descendants to prevent dark-mode bg-color override 2137 function daemScanBgImages() 2138 { 2139 var bgRects = []; 2140 2141 // Pass 1: find elements with background-image, mark them AND all DOM descendants 2142 document.querySelectorAll('*').forEach(function (el) 2143 { 2144 if (el.closest('#daem-accessibility-widget')) return; 2145 try 2146 { 2147 var cs = window.getComputedStyle(el); 2148 if (cs.backgroundImage && cs.backgroundImage !== 'none') 2149 { 2150 el.classList.add('daem-has-bg-image'); 2151 bgRects.push(el.getBoundingClientRect()); 2152 // Mark all DOM descendants so overlay children are preserved too 2153 el.querySelectorAll('*').forEach(function (child) 2154 { 2155 if (!child.closest('#daem-accessibility-widget')) 2156 { 2157 child.classList.add('daem-has-bg-image'); 2158 } 2159 }); 2160 } 2161 } catch (e) { } 2162 }); 2163 2164 if (bgRects.length === 0) return; 2165 2166 // Pass 2: find transparent/semi-transparent elements that visually overlap bg-image areas 2167 // (handles cases where text overlay is a sibling, not a child, of the bg-image element) 2168 document.querySelectorAll('div, section, article, header, footer, nav, aside, span, figure, [class*="overlay"], [class*="caption"], [class*="content"], [class*="slide"], [class*="hero"], [class*="banner"]').forEach(function (el) 2169 { 2170 if (el.closest('#daem-accessibility-widget')) return; 2171 if (el.classList.contains('daem-has-bg-image')) return; 2172 try 2173 { 2174 var bgColor = window.getComputedStyle(el).backgroundColor; 2175 var isTransparent = bgColor === 'rgba(0, 0, 0, 0)' || 2176 (bgColor.indexOf('rgba') === 0 && !bgColor.endsWith(', 1)')); 2177 if (!isTransparent) return; 2178 var rect = el.getBoundingClientRect(); 2179 var overlaps = bgRects.some(function (bgRect) 2180 { 2181 return rect.right > bgRect.left && rect.left < bgRect.right && 2182 rect.bottom > bgRect.top && rect.top < bgRect.bottom; 2183 }); 2184 if (overlaps) el.classList.add('daem-has-bg-image'); 2185 } catch (e) { } 2186 }); 2187 } 2188 2189 // Drives a feature to an exact target level (0 = off) by cycling via toggleFeature 2190 function setFeatureToLevel(id, targetLevel) 2191 { 2192 // First go to 0 2193 let safety = 20; 2194 while (getFeatureLevel(id) !== 0 && safety-- >
2194 0) toggleFeature(id); 2195 // Then step up to targetLevel 2196 safety = 20; 2197 while (getFeatureLevel(id) !== targetLevel && targetLevel > 0 && safety-- > 0) toggleFeature(id); 2198 } 2199 2200 // Selection handler (TTS + sign font select) 2201 function handleSelectionAction() 2202 { 2203 if (textToSpeechLevel === 1) readSelection(); 2204 if (signFontLevel === 2) applySignFontToSelection(); 2205 } 2206 2207 function readSelection() 2208 { 2209 const selectedText = window.getSelection().toString().trim(); 2210 console.log('[TTS] readSelection called. selectedText=', JSON.stringify(selectedText.substring(0, 60))); 2211 if (!selectedText) { console.log('[TTS] readSelection: empty, skipping'); return; } 2212 speakText(selectedText); 2213 } 2214 2215 function applySignFontToSelection() 2216 { 2217 const selection = window.getSelection(); 2218 if (!selection || selection.rangeCount === 0) return; 2219 const selectedText = selection.toString().trim(); 2220 2221 if (selectedText.length === 0) 2222 { 2223 removeSignFontHighlight(); 2224 return; 2225 } 2226 2227 removeSignFontHighlight(); 2228 2229 const range = selection.getRangeAt(0); 2230 const commonAncestor = range.commonAncestorContainer; 2231 const root = commonAncestor.nodeType === Node.TEXT_NODE 2232 ? commonAncestor.parentNode 2233 : commonAncestor; 2234 2235 // Walk every text node within the range and wrap each one individually. 2236 // This avoids the block-inside-inline invalidity that breaks across headings/links. 2237 const textNodes = []; 2238 const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); 2239 let node; 2240 while ((node = walker.nextNode())) 2241 { 2242 // Skip widget-internal nodes 2243 if (node.parentElement && node.parentElement.closest("#daem-accessibility-widget")) continue; 2244 if (range.intersectsNode(node)) textNodes.push(node); 2245 } 2246 2247 textNodes.forEach(textNode => 2248 { 2249 const nodeRange = document.createRange(); 2250 if (textNode === range.startContainer && textNode === range.endContainer) 2251 { 2252 nodeRange.setStart(textNode, range.startOffset); 2253 nodeRange.setEnd(textNode, range.endOffset); 2254 } else if (textNode === range.startContainer) 2255 { 2256 nodeRange.setStart(textNode, range.startOffset); 2257 nodeRange.setEndAfter(textNode); 2258 } else if (textNode === range.endContainer) 2259 { 2260 nodeRange.setStart(textNode, 0); 2261 nodeRange.setEnd(textNode, range.endOffset); 2262 } else 2263 { 2264 nodeRange.selectNodeContents(textNode); 2265 } 2266 2267 if (!nodeRange.toString().trim()) return; 2268 2269 const wrapper = document.createElement("span"); 2270 wrapper.setAttribute("lang", IS_AR ? "ar" : "en"); 2271 wrapper.setAttribute("dir", IS_AR ? "rtl" : "ltr"); 2272 wrapper.className = "daem-acc-sl-highlight"; 2273 2274 try 2275 { 2276 nodeRange.surroundContents(wrapper); 2277 } catch (e) 2278 { 2279 try 2280 { 2281 const frag = nodeRange.extractContents(); 2282 wrapper.appendChild(frag); 2283 nodeRange.insertNode(wrapper); 2284 } catch (e2) { /* skip malformed node */ } 2285 } 2286 }); 2287 2288 selection.removeAllRanges(); 2289 } 2290 2291 function removeSignFontHighlight() 2292 { 2293 document.querySelectorAll("span.daem-acc-sl-highlight").forEach((span) => 2294 { 2295 const parent = span.parentNode; 2296 while (span.firstChild) parent.insertBefore(span.firstChild, span); 2297 parent.removeChild(span); 2298 }); 2299 } 2300 2301 // Text to speech toggle 2302 function toggleTextToSpeech(btnElement) 2303 { 2304 console.log('[TTS] toggleTextToSpeech called. current textToSpeechLevel=', textToSpeechLevel); 2305 if (!window.speechSynthesis) 2306 { 2307 console.error('[TTS] window.speechSynthesis NOT available!'); 2308 alert( 2309 IS_AR 2310 ? "اÙÙ ØªØµÙØ ÙØ§ ÙØ¯Ø¹Ù تØÙÙ٠اÙÙØµ Ø¥ÙÙ ÙÙØ§Ù ." 2311 : "Browser does not support text-to-speech." 2312 ); 2313 return; 2314 } 2315 2316 // Stop any ongoing TTS first 2317 stopNativeTts(); 2318
2319 // Toggle level (on/off) 2320 textToSpeechLevel = textToSpeechLevel === 1 ? 0 : 1; 2321 2322 // Remove previous listeners 2323 document.removeEventListener("mouseup", handleSelectionAction); 2324 document.removeEventListener("touchend", handleSelectionAction); 2325 document.removeEventListener("selectionchange", handleTtsSelectionChange); 2326 2327 if (textToSpeechLevel === 1) 2328 { 2329 // ENABLE TTS: read selection on mouse/touch release 2330 document.addEventListener("mouseup", handleSelectionAction); 2331 document.addEventListener("touchend", handleSelectionAction); 2332 document.addEventListener("selectionchange", handleTtsSelectionChange); 2333 btnElement.classList.add("daem-active"); 2334 console.log('[TTS] ENABLED. Listening for mouseup/touchend. Voices available:', window.speechSynthesis.getVoices().length); 2335 2336 // Check if Arabic voice is available; if not, show install banner 2337 if (IS_AR) 2338 { 2339 const voices = window.speechSynthesis.getVoices(); 2340 const hasArabicVoice = voices.some(v => v.lang.toLowerCase().startsWith('ar')); 2341 if (!hasArabicVoice) 2342 { 2343 alert( 2344 'ÙØ§ ÙÙØ¬Ø¯ ØµÙØª عرب٠٠ثبت عÙÙ ÙØ°Ø§ Ø§ÙØ¬Ùاز.\n\n' + 2345 'ÙØªÙعÙ٠اÙÙØ·Ù Ø¨Ø§ÙØ¹Ø±Ø¨ÙØ©:\n' + 2346 '1. Ø§ÙØªØ إعدادات Windows\n' + 2347 '2. اÙÙÙØª ÙØ§ÙÙØºØ© â Ø§ÙÙÙØ§Ù \n' + 2348 '3. إدارة Ø§ÙØ£ØµÙات â Ø¥Ø¶Ø§ÙØ© ØµÙØª\n' + 2349 '4. Ø§Ø¨ØØ« ع٠"Arabic (Egypt)" ÙØ«Ø¨ÙتÙ\n' + 2350 '5. أعد تشغÙ٠اÙÙ ØªØµÙØ' 2351 ); 2352 } 2353 } 2354 2355 // UX fix: if text is already selected when TTS is activated, read it immediately 2356 const existingSel = window.getSelection(); 2357 if (existingSel && !existingSel.isCollapsed) 2358 { 2359 const existingText = existingSel.toString().trim(); 2360 console.log('[TTS] existing selection on activate:', JSON.stringify(existingText.substring(0, 60))); 2361 if (existingText) speakText(existingText); 2362 } 2363 } else 2364 { 2365 // DISABLE TTS 2366 btnElement.classList.remove("daem-active"); 2367 2368 // If sign-font selection mode is still active, keep selection handler 2369 if (signFontLevel === 2) 2370 { 2371 document.addEventListener("mouseup", handleSelectionAction); 2372 document.addEventListener("touchend", handleSelectionAction); 2373 } 2374 } 2375 2376 updateLevelBadge(btnElement, textToSpeechLevel); 2377 } 2378 2379 // Stop TTS immediately when the user collapses / clears their selection 2380 function handleTtsSelectionChange() 2381 { 2382 if (textToSpeechLevel !== 1) return; 2383 // Never cancel while TTS is actively speaking â selectionchange fires right after 2384 // mouseup triggers speak(), and cancelling here would kill the utterance instantly. 2385 if (ttsIsSpeaking) return; 2386 const sel = window.getSelection(); 2387 if (!sel || sel.isCollapsed || sel.toString().trim() === "") 2388 { 2389 stopNativeTts(); 2390 } 2391 } 2392 2393 // Sign font toggle 2394 function toggleSignFont(btnElement) 2395 { 2396 signFontLevel = (signFontLevel + 1) % 3; 2397 2398 document.body.classList.remove("daem-acc-sign-font-1"); 2399 document.removeEventListener("mouseup", handleSelectionAction); 2400 document.removeEventListener("touchend", handleSelectionAction); 2401 removeSignFontHighlight(); 2402 2403 if (signFontLevel === 1) 2404 { 2405 document.body.classList.add("daem-acc-sign-font-1"); 2406 btnElement.querySelector("span").innerText = I18N.dynamic.slFontFull; 2407 } else if (signFontLevel === 2) 2408 { 2409 document.addEventListener("mouseup", handleSelectionAction); 2410 document.addEventListener("touchend", handleSelectionAction); 2411 btnElement.querySelector("span").innerText = I18N.dynamic.slFontSelect; 2412 } else 2413 { 2414 btnElement.querySelector("span").innerText = I18N.dynamic.slFont; 2415 } 2416 2417 // If TTS is active, ensure handler still exists 2418 if (textToSpeechLevel === 1 && signFontLevel !== 2) 2419 { 2420 document.addEventListener("mouseup", handleSelectionAction); 2421 document.addEventListener("touchend", handleSelectionAction); 2422 } 2423 2424 updateLevelBadge(btnElement, signFontLevel); 2425 } 2426 2427 // Sign video modal 2428 function closeSignLangModal() 2429 { 2430 signLangModal.classList.remove("daem-active"); 2431 signLangIframe.src = ""; 2432 const btnElement = document.querySelector( 2433 '.daem-access-btn[data-id="sign-vid"]' 2434 ); 2435 if (btnElement) btnElement.classList.remove("daem-active"); 2436 } 2437 2438 signLangModal 2439 .querySelector(".daem-sign-close") 2440 .addEventListener("click", closeSignLangModal); 2441 2442 function toggleSignLanguage(btnElement) 2443 { 2444 const isActive = btnElement.classList.contains("daem-active"); 2445 if (!isActive) 2446 { 2447 signLangIframe.src = SIGN_VIDEO_URL || ""; 2448 signLangModal.classList.add("daem-active"); 2449 btnElement.classList.add("daem-active"); 2450 } else 2451 { 2452 closeSignLangModal(); 2453 } 2454 } 2455 2456 const MASK_HALF = 50; // = 100px total height like your design 2457 2458 function moveMask(e) 2459 { 2460 const mouseY = e.clientY || (e.touches ? e.touches[0].clientY : 0); 2461 const mouseX = e.clientX || (e.touches ? e.touches[0].clientX : 0); 2462 2463 if (readingMaskLevel === 1) 2464 { 2465 // Clamp in pixels so gradient is ALWAYS valid 2466 const topPx = Math.max(0, mouseY - MASK_HALF); 2467 const bottomPx = Math.min(window.innerHeight, mouseY + MASK_HALF); 2468 2469 const maskStyle = 2470 `linear-gradient(to bottom, 2471 rgba(0,0,0,1) 0px, 2472 rgba(0,0,0,1) ${topPx}px, 2473 rgba(0,0,0,0) ${topPx}px, 2474 rgba(0,0,0,0) ${bottomPx}px, 2475 rgba(0,0,0,1) ${bottomPx}px, 2476 rgba(0,0,0,1) 100%)`; 2477 2478 readingMask.style.webkitMaskImage = maskStyle; 2479 readingMask.style.maskImage = maskStyle; 2480 } else if (readingMaskLevel === 2) 2481 { 2482 readingLine.style.top = `${mouseY}px`; 2483 readingLine.style.left = `${mouseX}px`; 2484 } 2485 } 2486 2487 function toggleReadingMask(btnElement) 2488 { 2489 readingMaskLevel = (readingMaskLevel + 1) % 3; 2490 2491 document.body.classList.remove("daem-acc-mask-1", "daem-acc-mask-2"); 2492 2493 // Ù ÙÙ : Ø´ÙÙ listeners Ù Ù window (ÙÙØ³ØªØ®Ø¯Ù window بد٠document) 2494 window.removeEventListener("mousemove", moveMask); 2495 window.removeEventListener("touchmove", moveMask); 2496 2497 let buttonText = I18N.dynamic.readingMask; 2498 2499 if (readingMaskLevel === 1) 2500 { 2501 document.body.classList.add("daem-acc-mask-1"); 2502 readingMask.classList.add("daem-moving"); 2503 document.addEventListener("mousemove", moveMask); 2504 document.addEventListener("touchmove", moveMask); 2505 2506 // initialize immediately 2507 moveMask({ clientY: window.innerHeight / 2 }); 2508 } else if (readingMaskLevel === 2) 2509 { 2510 document.body.classList.add("daem-acc-mask-2"); 2511 2512 // â ØªØØ¯ÙØ« ÙÙØ±Ù 2513 moveMask({ clientX: window.innerWidth / 2, clientY: window.innerHeight / 2 }); 2514 2515 window.addEventListener("mousemove", moveMask); 2516 window.addEventListener("touchmove", moveMask, { passive: true });
2517 2518 btnElement.classList.add("daem-active"); 2519 buttonText = I18N.dynamic.readingLine; 2520 } else 2521 { 2522 btnElement.classList.remove("daem-active"); 2523 } 2524 2525 btnElement.querySelector("span").innerText = buttonText; 2526 updateLevelBadge(btnElement, readingMaskLevel); 2527 } 2528 2529 2530 // Font-size JS scaler (works on any page regardless of px/rem/em usage) 2531 const FONT_SIZE_SCALES = [1, 1.1, 1.25, 1.45, 1.7]; 2532 function applyFontSizeJS(level) 2533 { 2534 if (level === 0) 2535 { 2536 if (fontSizeOriginals) 2537 { 2538 fontSizeOriginals.forEach((orig, el) => 2539 { 2540 if (!document.contains(el)) return; 2541 if (orig.inline === null) el.style.removeProperty('font-size'); 2542 else el.style.fontSize = orig.inline; 2543 }); 2544 fontSizeOriginals = null; 2545 } 2546 return; 2547 } 2548 const scale = FONT_SIZE_SCALES[level]; 2549 // Capture original computed sizes once 2550 if (!fontSizeOriginals) 2551 { 2552 fontSizeOriginals = new Map(); 2553 document.querySelectorAll( 2554 '*:not(#daem-accessibility-widget):not(#daem-accessibility-widget *)' 2555 ).forEach(el => 2556 { 2557 fontSizeOriginals.set(el, { 2558 inline: el.style.fontSize || null, 2559 computed: parseFloat(window.getComputedStyle(el).fontSize) 2560 }); 2561 }); 2562 } 2563 // Apply scale from captured computed values 2564 fontSizeOriginals.forEach((orig, el) => 2565 { 2566 if (!document.contains(el)) return; 2567 el.style.setProperty('font-size', (orig.computed * scale) + 'px', 'important'); 2568 }); 2569 } 2570 2571 // Hint mode 2572 function getHintText(element) 2573 { 2574 if (element.hasAttribute && element.hasAttribute("tab-hint")) 2575 return element.getAttribute("tab-hint"); 2576 if (element.title) return `${I18N.hintPrefixes.title}: ${element.title}`; 2577 if (element.alt) return `${I18N.hintPrefixes.alt}: ${element.alt}`; 2578 if (element.placeholder) 2579 return `${I18N.hintPrefixes.placeholder}: ${element.placeholder}`; 2580 if (element.tagName === "A" && element.innerText) 2581 return `${I18N.hintPrefixes.link}: ${element.innerText}`; 2582 if (element.tagName === "BUTTON" && element.innerText) 2583 return `${I18N.hintPrefixes.button}: ${element.innerText}`; 2584 return null; 2585 } 2586 2587 function showHint(e) 2588 { 2589 const element = e.target; 2590 const hintText = getHintText(element); 2591 2592 if ( 2593 hintText && 2594 isHintModeActive && 2595 !element.closest("#daem-accessibility-widget") 2596 ) 2597 { 2598 hintTooltip.innerText = hintText; 2599 hintTooltip.style.display = "block"; 2600 const rect = element.getBoundingClientRect(); 2601 hintTooltip.style.left = `${rect.left + window.scrollX + 10}px`; 2602 hintTooltip.style.top = `${rect.top + 5}px`; 2603 /*hintTooltip.style.top = `${rect.top + window.scrollY + 5}px`;*/ 2604 } 2605 } 2606 2607 function hideHint() 2608 { 2609 hintTooltip.style.display = "none"; 2610 } 2611 2612 function toggleHintMode(btnElement) 2613 { 2614 isHintModeActive = !isHintModeActive; 2615 2616 if (isHintModeActive) 2617 { 2618 document.addEventListener("mouseover", showHint); 2619 document.addEventListener("focusin", showHint); 2620 document.addEventListener("mouseout", hideHint); 2621 document.addEventListener("focusout", hideHint); 2622 btnElement.classList.add("daem-active"); 2623 } else 2624 { 2625 document.removeEventListener("mouseover", showHint); 2626 document.removeEventListener("focusin", showHint); 2627 document.removeEventListener("mouseout", hideHint); 2628 document.removeEventListener("focusout", hideHint); 2629 hideHint(); 2630 btnElement.classList.remove("daem-active"); 2631 } 2632 } 2633 2634 // Toggle dispatcher
2635 function toggleFeature(id, btnElement) 2636 { 2637 if (!btnElement) 2638 { 2639 btnElement = document.querySelector(`.daem-access-btn[data-id="${id}"]`); 2640 if (!btnElement) return; 2641 } 2642 2643 if (id === "speech-text") return toggleSpeechToText(btnElement); 2644 if (id === "text-speech") return toggleTextToSpeech(btnElement); 2645 if (id === "sign-font") return toggleSignFont(btnElement); 2646 if (id === "sign-vid") return toggleSignLanguage(btnElement); 2647 if (id === "mask") return toggleReadingMask(btnElement); 2648 if (id === "hint") return toggleHintMode(btnElement); 2649 2650 let currentLevel = 0; 2651 2652 switch (id) 2653 { 2654 case "contrast": 2655 contrastLevel = (contrastLevel + 1) % 2; 2656 currentLevel = contrastLevel; 2657 document.body.classList.remove("daem-acc-contrast-1"); 2658 if (contrastLevel === 1) document.body.classList.add("daem-acc-contrast-1"); 2659 break; 2660 2661 case "line-height": 2662 lineHeightLevel = (lineHeightLevel + 1) % 5; 2663 currentLevel = lineHeightLevel; 2664 document.body.classList.remove( 2665 "daem-acc-lineheight-1", 2666 "daem-acc-lineheight-2", 2667 "daem-acc-lineheight-3", 2668 "daem-acc-lineheight-4" 2669 ); 2670 if (lineHeightLevel > 0) 2671 document.body.classList.add(`daem-acc-lineheight-${lineHeightLevel}`); 2672 break; 2673 2674 case "font-size": 2675 fontSizeLevel = (fontSizeLevel + 1) % 5; 2676 currentLevel = fontSizeLevel; 2677 applyFontSizeJS(fontSizeLevel); 2678 break; 2679 2680 case "spacing": 2681 charSpacingLevel = (charSpacingLevel + 1) % 5; 2682 currentLevel = charSpacingLevel; 2683 document.body.classList.remove( 2684 "daem-acc-spacing-1", 2685 "daem-acc-spacing-2", 2686 "daem-acc-spacing-3", 2687 "daem-acc-spacing-4" 2688 ); 2689 if (charSpacingLevel > 0) 2690 document.body.classList.add(`daem-acc-spacing-${charSpacingLevel}`); 2691 break; 2692 2693 case "cursor": 2694 document.body.classList.toggle("daem-acc-cursor"); 2695 currentLevel = document.body.classList.contains("daem-acc-cursor") ? 1 : 0; 2696 break; 2697 2698 case "readable": 2699 document.body.classList.toggle("daem-acc-font"); 2700 currentLevel = document.body.classList.contains("daem-acc-font") ? 1 : 0; 2701 break; 2702 2703 case "dyslexic-font": 2704 document.body.classList.toggle("daem-acc-dyslexic-font"); 2705 currentLevel = document.body.classList.contains("daem-acc-dyslexic-font") 2706 ? 1 2707 : 0; 2708 break; 2709 2710 case "links": 2711 document.body.classList.toggle("daem-acc-links"); 2712 currentLevel = document.body.classList.contains("daem-acc-links") ? 1 : 0; 2713 break; 2714 2715 case "titles": 2716 document.body.classList.toggle("daem-acc-titles"); 2717 currentLevel = document.body.classList.contains("daem-acc-titles") ? 1 : 0; 2718 break; 2719 2720 case "stop-anim": 2721 document.body.classList.toggle("daem-acc-stop-anim"); 2722 currentLevel = document.body.classList.contains("daem-acc-stop-anim") 2723 ? 1 2724 : 0; 2725 break; 2726 2727 case "hide-images": 2728 document.body.classList.toggle("daem-acc-hide-images"); 2729 currentLevel = document.body.classList.contains("daem-acc-hide-images") 2730 ? 1 2731 : 0; 2732 break; 2733 2734 case "bold-text": 2735 boldLevel = (boldLevel + 1) % 2; 2736 currentLevel = boldLevel; 2737 document.body.classList.remove("daem-acc-bold-1"); 2738 if (boldLevel > 0) document.body.classList.add(`daem-acc-bold-${boldLevel}`); 2739 break; 2740 2741 case "text-align": 2742 textAlignLevel = (textAlignLevel + 1) % 4; 2743 currentLevel = textAlignLevel; 2744 2745 document.body.classList.remove( 2746 "daem-acc-align-left", 2747 "daem-acc-align-center", 2748 "daem-acc-align-right" 2749 ); 2750 let alignLabel = I18N.dynamic.textAlign; 2751 2752 if (textAlignLevel === 1) 2753 { 2754 document.body.classList.add("daem-acc-align-left"); 2755 alignLabel = I18N.dynamic.alignLeft; 2756 } else if (textAlignLevel === 2) 2757 { 2758 document.body.classList.add("daem-acc-align-center"); 2759 alignLabel = I18N.dynamic.alignCenter; 2760 } else if (textAlignLevel === 3) 2761 { 2762 document.body.classList.add("daem-acc-align-right"); 2763 alignLabel = I18N.dynamic.alignRight; 2764 } 2765 2766 btnElement.querySelector("span").innerText = alignLabel; 2767 break; 2768 2769 case "saturation": 2770 saturationLevel = (saturationLevel + 1) % 5; 2771 currentLevel = saturationLevel; 2772 document.body.classList.remove( 2773 "daem-acc-saturation-1", 2774 "daem-acc-saturation-2", 2775 "daem-acc-saturation-3", 2776 "daem-acc-saturation-4" 2777 ); 2778 if (saturationLevel > 0) 2779 document.body.classList.add(`daem-acc-saturation-${saturationLevel}`); 2780 btnElement.querySelector("span").innerText = 2781 saturationLevel === 4 2782 ? I18N.dynamic.desaturate 2783 : I18N.featureLabels.saturation; 2784 break; 2785 2786 case "dark-mode": 2787 var turningOn = !document.body.classList.contains("daem-acc-dark-mode");
2788 if (turningOn) 2789 { 2790 daemScanBgImages(); // scan BEFORE dark mode class is added so original styles are readable 2791 document.body.classList.add("daem-acc-dark-mode"); 2792 currentLevel = 1; 2793 } else 2794 { 2795 document.body.classList.remove("daem-acc-dark-mode"); 2796 document.querySelectorAll('.daem-has-bg-image').forEach(el => el.classList.remove('daem-has-bg-image')); 2797 currentLevel = 0; 2798 } 2799 break; 2800 } 2801 2802 updateLevelBadge(btnElement, currentLevel); 2803 } 2804 2805 // All CSS classes the widget adds to <body> 2806 const WIDGET_BODY_CLASSES = [ 2807 "daem-acc-contrast-1", 2808 "daem-acc-dark-mode", 2809 "daem-acc-saturation-1", "daem-acc-saturation-2", "daem-acc-saturation-3", "daem-acc-saturation-4", 2810 "daem-acc-fontsize-1", "daem-acc-fontsize-2", "daem-acc-fontsize-3", "daem-acc-fontsize-4", 2811 "daem-acc-lineheight-1", "daem-acc-lineheight-2", "daem-acc-lineheight-3", "daem-acc-lineheight-4", 2812 "daem-acc-spacing-1", "daem-acc-spacing-2", "daem-acc-spacing-3", "daem-acc-spacing-4", 2813 "daem-acc-cursor", 2814 "daem-acc-sign-font-1", 2815 "daem-acc-links", 2816 "daem-acc-titles", 2817 "daem-acc-dyslexic-font", 2818 "daem-acc-font", 2819 "daem-acc-stop-anim", 2820 "daem-acc-hide-images", 2821 "daem-acc-bold-1", "daem-acc-bold-2", 2822 "daem-acc-align-left", "daem-acc-align-center", "daem-acc-align-right", 2823 "daem-acc-mask-1", "daem-acc-mask-2", 2824 ]; 2825 2826 // Reset function 2827 function resetAll() 2828 { 2829 applyFontSizeJS(0); // restore all inline font-size overrides before clearing body classes 2830 // Remove only the widget's own body classes â leave all site classes untouched 2831 document.body.classList.remove(...WIDGET_BODY_CLASSES); 2832 // Do NOT wipe document.body.className or removeAttribute("style") 2833 2834 fontSizeLevel = 0; 2835 contrastLevel = 0; 2836 lineHeightLevel = 0; 2837 charSpacingLevel = 0; 2838 textToSpeechLevel = 0; 2839 signFontLevel = 0; 2840 readingMaskLevel = 0; 2841 boldLevel = 0; 2842 textAlignLevel = 0; 2843 saturationLevel = 0; 2844 2845 activeProfiles.clear(); 2846 2847 // Stop STT and close popup on reset 2848 stopSpeechToText(true); 2849 2850 document.querySelectorAll(".daem-access-btn").forEach((btn) => 2851 { 2852 btn.classList.remove("daem-active", "daem-recording-active"); 2853 2854 const badge = btn.querySelector(".daem-level-badge"); 2855 if (badge) 2856 { 2857 badge.removeAttribute("data-level"); 2858 badge.innerText = ""; 2859 } 2860 2861 const id = btn.dataset.id; 2862 2863 // Restore defaults 2864 if (id && I18N.featureLabels[id]) 2865 btn.querySelector("span").innerText = I18N.featureLabels[id]; 2866 2867 // Special ones that change dynamically 2868 if (id === "contrast") 2869 btn.querySelector("span").innerText = I18N.dynamic.contrast; 2870 if (id === "sign-font") 2871 btn.querySelector("span").innerText = I18N.dynamic.slFont; 2872 if (id === "text-speech") 2873 btn.querySelector("span").innerText = I18N.dynamic.textToSpeech; 2874 if (id === "speech-text") 2875 btn.querySelector("span").innerText = I18N.dynamic.speechToText; 2876 if (id === "mask") 2877 btn.querySelector("span").innerText = I18N.dynamic.readingMask; 2878 if (id === "text-align") 2879 btn.querySelector("span").innerText = I18N.dynamic.textAlign; 2880 if (id === "sign-vid") 2881 btn.querySelector("span").innerText = I18N.dynamic.signVideo; 2882 if (id === "hint") 2883 btn.querySelector("span").innerText = I18N.dynamic.hint; 2884 }); 2885 2886 document 2887 .querySelectorAll(".daem-profile-btn") 2888 .forEach((pBtn) => pBtn.classList.remove("daem-active"));
2889 2890 window.speechSynthesis && stopNativeTts(); 2891 2892 document.removeEventListener("mouseup", handleSelectionAction); 2893 document.removeEventListener("touchend", handleSelectionAction); 2894 document.removeEventListener("selectionchange", handleTtsSelectionChange); 2895 2896 removeSignFontHighlight(); 2897 2898 document.body.classList.remove("daem-acc-mask-1", "daem-acc-mask-2"); 2899 //readingMask.classList.remove("moving"); 2900 document.removeEventListener("mousemove", moveMask); 2901 document.removeEventListener("touchmove", moveMask); 2902 2903 if (isHintModeActive) 2904 { 2905 // Turn off hint mode cleanly 2906 isHintModeActive = true; 2907 toggleHintMode(document.querySelector('.daem-access-btn[data-id="hint"]')); 2908 } 2909 2910 closeSignLangModal(); 2911 hintTooltip.style.display = "none"; 2912 localStorage.removeItem("daem_widget_state"); 2913 } 2914 2915 // ---- Persistence helpers ---- 2916 const DAEM_STORAGE_KEY = "daem_widget_state"; 2917 2918 function saveState() 2919 { 2920 try 2921 { 2922 const state = { 2923 fontSizeLevel, 2924 contrastLevel, 2925 lineHeightLevel, 2926 charSpacingLevel, 2927 signFontLevel, 2928 readingMaskLevel, 2929 boldLevel, 2930 textAlignLevel, 2931 saturationLevel, 2932 darkMode: document.body.classList.contains("daem-acc-dark-mode"), 2933 cursor: document.body.classList.contains("daem-acc-cursor"), 2934 font: document.body.classList.contains("daem-acc-font"), 2935 dyslexicFont: document.body.classList.contains("daem-acc-dyslexic-font"), 2936 links: document.body.classList.contains("daem-acc-links"), 2937 titles: document.body.classList.contains("daem-acc-titles"), 2938 stopAnim: document.body.classList.contains("daem-acc-stop-anim"), 2939 hideImages: document.body.classList.contains("daem-acc-hide-images"), 2940 activeProfiles: [...activeProfiles], 2941 }; 2942 localStorage.setItem(DAEM_STORAGE_KEY, JSON.stringify(state)); 2943 } catch (e) { /* localStorage unavailable */ } 2944 } 2945 2946 function loadState() 2947 { 2948 let saved; 2949 try { saved = JSON.parse(localStorage.getItem(DAEM_STORAGE_KEY)); } 2950 catch (e) { return; } 2951 if (!saved) return; 2952 2953 // Numeric levels â step up via setFeatureToLevel 2954 const numericFeatures = [ 2955 ["contrast", saved.contrastLevel], 2956 ["font-size", saved.fontSizeLevel], 2957 ["line-height", saved.lineHeightLevel], 2958 ["spacing", saved.charSpacingLevel], 2959 ["saturation", saved.saturationLevel], 2960 ["sign-font", saved.signFontLevel], 2961 ["mask", saved.readingMaskLevel], 2962 ["bold-text", saved.boldLevel], 2963 ["text-align", saved.textAlignLevel], 2964 ]; 2965 numericFeatures.forEach(([id, level]) => 2966 { 2967 if (level && level > 0) setFeatureToLevel(id, level); 2968 }); 2969 2970 // Boolean toggles 2971 if (saved.darkMode) { daemScanBgImages(); setFeatureToLevel("dark-mode", 1); } 2972 if (saved.cursor) setFeatureToLevel("cursor", 1); 2973 if (saved.font) setFeatureToLevel("readable", 1); 2974 if (saved.dyslexicFont) setFeatureToLevel("dyslexic-font", 1); 2975 if (saved.links) setFeatureToLevel("links", 1); 2976 if (saved.titles) setFeatureToLevel("titles", 1); 2977 if (saved.stopAnim) setFeatureToLevel("stop-anim", 1); 2978 if (saved.hideImages) setFeatureToLevel("hide-images", 1); 2979 2980 // Restore active profile badges (visual only, features already applied above) 2981 if (Array.isArray(saved.activeProfiles)) 2982 { 2983 saved.activeProfiles.forEach(profileType => 2984 { 2985 activeProfiles.add(profileType); 2986 const btn = document.querySelector(`.daem-profile-btn[data-profile="${profileType}"]`); 2987 if (btn) btn.classList.add("daem-active"); 2988 }); 2989 } 2990 } 2991 2992 // Load persisted state once the widget DOM and feature definitions are ready 2993 loadState(); 2994 2995 accessReset.addEventListener("click", resetAll); 2996 2997 // Keyboard shortcuts: Alt+A / Alt+O â toggle panel, Alt+R â reset, Alt+<key> â features, Alt+K/Y/J/X/W/N/E/Z â profiles 2998 document.addEventListener("keydown", (e) => 2999 { 3000 if (e.altKey && e.key && e.key.toUpperCase() === "A") 3001 { 3002 e.preventDefault(); 3003 togglePanel(); 3004 return; 3005 } 3006 if (e.altKey && e.code === "KeyO") 3007 { 3008 e.preventDefault(); 3009 togglePanel(); 3010 return; 3011 } 3012 if (e.altKey && e.code === "KeyR") 3013 { 3014 e.preventDefault(); 3015 resetAll(); 3016 return; 3017 } 3018 if (e.altKey && e.key) 3019 {
3020 const key = e.key.toUpperCase(); 3021 // Check profile shortcuts 3022 const profileEntry = Object.entries(profileShortcuts).find( 3023 ([, k]) => k === key 3024 ); 3025 if (profileEntry) 3026 { 3027 e.preventDefault(); 3028 const profileBtn = document.querySelector( 3029 '.daem-profile-btn[data-profile="' + profileEntry[0] + '"]' 3030 ); 3031 if (profileBtn) profileBtn.click(); 3032 return; 3033 } 3034 // Check feature shortcuts 3035 const feature = features.find((f) => f.key === key); 3036 if (feature) 3037 { 3038 e.preventDefault(); 3039 toggleFeature(feature.id); 3040 } 3041 } 3042 }); 3043 3044 // Profiles accordion logic 3045 const profilesToggle = document.getElementById("profiles-toggle"); 3046 const profilesContent = document.getElementById("daem-profiles-content"); 3047 3048 profilesToggle.addEventListener("click", () => 3049 { 3050 profilesContent.classList.toggle("daem-active"); 3051 const chevron = profilesToggle.querySelector(".daem-chevron"); 3052 if (chevron) 3053 { 3054 chevron.innerHTML = profilesContent.classList.contains("daem-active") 3055 ? "▲" 3056 : "▼"; 3057 } 3058 }); 3059 3060 // Profiles presets 3061 const profileBtns = document.querySelectorAll(".daem-profile-btn"); 3062 3063 profileBtns.forEach((btn) => 3064 { 3065 btn.addEventListener("click", () => 3066 { 3067 const profileType = btn.dataset.profile; 3068 const wasActive = btn.classList.contains("daem-active"); 3069 3070 if (wasActive) 3071 { 3072 btn.classList.remove("daem-active"); 3073 activeProfiles.delete(profileType); 3074 3075 // For each feature this profile activated, set it to the highest level 3076 // still required by any remaining active profile (or 0 if none need it) 3077 const features = profileFeatureMap[profileType] || []; 3078 features.forEach(({ id }) => 3079 { 3080 const targetLevel = [...activeProfiles].reduce((max, pid) => 3081 { 3082 const f = (profileFeatureMap[pid] || []).find(f => f.id === id); 3083 return f ? Math.max(max, f.level) : max; 3084 }, 0); 3085 setFeatureToLevel(id, targetLevel); 3086 }); 3087 return; 3088 } 3089 3090 // Activate this profile on top of any existing state (no resetAll) 3091 profileActivating = true; 3092 btn.classList.add("daem-active"); 3093 activeProfiles.add(profileType); 3094 3095 const setFeature = (id, targetLevel) => 3096 { 3097 for (let i = 0; i < targetLevel; i++) toggleFeature(id); 3098 }; 3099 3100 switch (profileType) 3101 { 3102 case "motor": 3103 toggleFeature("speech-text"); 3104 break; 3105 case "visual": 3106 toggleFeature("text-speech"); 3107 break; 3108 case "colorblind": 3109 setFeature("saturation", 3); 3110 break; 3111 case "dyslexia": 3112 toggleFeature("dyslexic-font"); 3113 break; 3114 case "lowvision": 3115 setFeature("saturation", 3); 3116 setFeature("font-size", 2); 3117 toggleFeature("cursor"); 3118 break; 3119 case "cognitive": 3120 setFeature("saturation", 2); 3121 setFeature("font-size", 2); 3122 setFeature("mask", 2); 3123 break; 3124 case "seizure": 3125 setFeature("contrast", 1); 3126 setFeature("saturation", 4); 3127 break; 3128 case "adhd": 3129 setFeature("mask", 1); 3130 setFeature("saturation", 1); 3131 break; 3132 } 3133 3134 // Commit active profile â sync checks are now live 3135 profileActivating = false; 3136 }); 3137 });
3138} 3139if (document.readyState === "complete") initTool(); 3140else 3141 document.addEventListener("readystatechange", () => 3142 { 3143 if (document.readyState === "complete") initTool(); 3144 });
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.