diff --git a/assets/js/main.js b/assets/js/main.js
index 835a5ee..53c024d 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -84,7 +84,7 @@ document.addEventListener('DOMContentLoaded', () => {
fullscreenButton.hidden = true;
}
- let audioCtx = null;
+ let audioCtx = null;
const initAudio = () => {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
@@ -99,14 +99,23 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
+ let availableVoices = [];
+ if ('speechSynthesis' in window) {
+ const updateVoices = () => { availableVoices = window.speechSynthesis.getVoices(); };
+ updateVoices();
+ if (window.speechSynthesis.onvoiceschanged !== undefined) {
+ window.speechSynthesis.onvoiceschanged = updateVoices;
+ }
+ }
+
const audioBtn = document.querySelector('.js-audio-toggle');
if (audioBtn) {
const updateAudioBtnState = () => {
const isEnabled = window.localStorage.getItem('hospitalQueue:audioEnabled') === 'true';
if (isEnabled) {
- audioBtn.innerHTML = '';
+ audioBtn.innerHTML = '';
} else {
- audioBtn.innerHTML = '';
+ audioBtn.innerHTML = '';
}
audioBtn.setAttribute('aria-pressed', isEnabled.toString());
@@ -183,10 +192,10 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
const text = locale === 'ar' ? (latest.dataset.announcementAr || '') : (latest.dataset.announcementEn || '');
if (text) {
- window.speechSynthesis.cancel();
+ // Removed window.speechSynthesis.cancel(); as it can prevent speech in some browsers
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = locale === 'ar' ? 'ar-SA' : 'en-US';
- const voices = window.speechSynthesis.getVoices();
+ const voices = availableVoices.length > 0 ? availableVoices : window.speechSynthesis.getVoices();
const langPrefix = locale === 'ar' ? 'ar' : 'en';
const langVoices = voices.filter(v => v.lang.toLowerCase().startsWith(langPrefix));
@@ -218,7 +227,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
- checkAnnouncements();
+ checkAnnouncements();
const autoRefreshSeconds = parseInt(document.querySelector('[data-auto-refresh]')?.dataset.autoRefresh || '0', 10);
if (autoRefreshSeconds > 0) {
@@ -252,4 +261,4 @@ document.addEventListener('DOMContentLoaded', () => {
}, autoRefreshSeconds * 1000);
}
}
-});
+});
\ No newline at end of file
diff --git a/display.php b/display.php
index 979ea49..1ad1876 100644
--- a/display.php
+++ b/display.php
@@ -60,7 +60,7 @@ qh_page_start(
= qh_h(date('H:i')) ?>
diff --git a/patch_audio.js b/patch_audio.js
new file mode 100644
index 0000000..ce05434
--- /dev/null
+++ b/patch_audio.js
@@ -0,0 +1,87 @@
+const fs = require('fs');
+const path = require('path');
+
+const jsPath = path.join(__dirname, 'assets/js/main.js');
+let jsCode = fs.readFileSync(jsPath, 'utf8');
+
+// Replace the icon logic
+jsCode = jsCode.replace(
+ /audioBtn\.innerHTML = '<\/i>';/g,
+ `audioBtn.innerHTML = '';`
+).replace(
+ /audioBtn\.innerHTML = '<\/i>';/g,
+ `audioBtn.innerHTML = '';`
+);
+
+// Add logic to test sound when audio is enabled
+const repl1 = `if (nextState) {
+ initAudio();
+ if ('speechSynthesis' in window) {
+ const primeUtterance = new SpeechSynthesisUtterance('');
+ window.speechSynthesis.speak(primeUtterance);
+ }
+ }`;
+const repl2 = `if (nextState) {
+ initAudio();
+ if ('speechSynthesis' in window) {
+ const primeUtterance = new SpeechSynthesisUtterance('');
+ window.speechSynthesis.speak(primeUtterance);
+ }
+
+ // Force latest announcement to replay
+ const cards = Array.from(document.querySelectorAll('.announcement-card'));
+ const latest = cards[0];
+ if (latest) {
+ const storageKey =
+`hospitalQueue:lastAnnouncement:${locale}
+`;
+ window.localStorage.removeItem(storageKey);
+ setTimeout(checkAnnouncements, 200);
+ }
+ }`;
+jsCode = jsCode.replace(repl1, repl2);
+
+// Make sure speech synthesis gets voices early, and handle fallback properly
+const oldVoiceLogic = `if (langVoices.length > 0) {
+ // Try to find a high-quality (Google/Microsoft Natural) voice
+ const bestVoice = langVoices.find(v =>
+ v.name.includes('Google') ||
+ v.name.includes('Natural') ||
+ v.name.includes('Premium') ||
+ v.name.includes('Online')
+ ) || langVoices.find(v => v.name.includes('Microsoft')) || langVoices[0];
+
+ utterance.voice = bestVoice;
+ }`;
+
+const newVoiceLogic = `if (langVoices.length > 0) {
+ // Try to find a high-quality (Google/Microsoft Natural) voice
+ const bestVoice = langVoices.find(v =>
+ v.name.includes('Google') ||
+ v.name.includes('Natural') ||
+ v.name.includes('Premium') ||
+ v.name.includes('Online')
+ ) || langVoices.find(v => v.name.includes('Microsoft')) || langVoices[0];
+
+ utterance.voice = bestVoice;
+ } else if (voices.length > 0) {
+ // Fallback to any available voice to ensure sound plays
+ utterance.voice = voices[0];
+ }`;
+
+jsCode = jsCode.replace(oldVoiceLogic, newVoiceLogic);
+
+// Call getVoices early
+if (!jsCode.includes('window.speechSynthesis.getVoices();')) {
+ jsCode = jsCode.replace(
+ `document.addEventListener('DOMContentLoaded', () => {`,
+ `document.addEventListener('DOMContentLoaded', () => {
+ if ('speechSynthesis' in window) {
+ window.speechSynthesis.onvoiceschanged = () => { window.speechSynthesis.getVoices(); };
+ window.speechSynthesis.getVoices();
+ }`
+ );
+}
+
+fs.writeFileSync(jsPath, jsCode);
+console.log('patched');
diff --git a/queue_bootstrap.php b/queue_bootstrap.php
index 249685a..90d0e20 100644
--- a/queue_bootstrap.php
+++ b/queue_bootstrap.php
@@ -625,6 +625,7 @@ function qh_page_start(string $activePage, string $pageTitle, string $metaDescri
}
echo ' ';
echo ' ';
+ echo ' ';
echo ' ';
echo ' ';
echo '';