1/** 2 * ScanQR â pwa.js 3 * Link before </body> on every page: 4 * <script src="/js/pwa.js" defer><\/script> 5 * 6 * Handles: 7 * 1. Service worker registration + update detection 8 * 2. Nav "Install App" button (appears only when installable) 9 * 3. SW update toast (user-initiated update â no auto-reload) 10 * 4. Offline ad hiding 11 * 5. Network quality indicator (optional nav badge) 12 * 13 * Nav HTML required (add once to your navbar): 14 * <button id="pwa-install-nav-btn" hidden aria-label="Install ScanQR app"> 15 * ð² Install App 16 * </button> 17 * 18 * Optional network badge (add to navbar): 19 * <span id="pwa-net-badge" hidden></span> 20 * 21 * NO inline install banner â removed. Nav button only. 22 * NO auto-reload on update â user must click Update in the toast. 23 * This prevents data loss if the user has an active scan result open. 24 */ 25 26(function () { 27 'use strict'; 28 29 if (!('serviceWorker' in navigator)) return; 30 31 // ââ 1. Register SW ââââââââââââââââââââââââââââââââââââââ 32 let newWorker = null; 33 let reg = null; 34 35 navigator.serviceWorker.register('/service-worker.js', { scope: '/' }) 36 .then(registration => { 37 reg = registration; 38 39 // Detect new SW waiting to install 40 reg.addEventListener('updatefound', () => { 41 newWorker = reg.installing; 42 newWorker.addEventListener('statechange', () => { 43 if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { 44 // New version available â show toast, do NOT auto-reload 45 showUpdateToast(); 46 } 47 }); 48 }); 49 50 // Poll for updates every hour 51 setInterval(() => reg.update(), 60 * 60 * 1000); 52 }) 53 .catch(err => console.warn('[PWA] SW registration failed:', err)); 54 55 // ââ REMOVED: auto-reload on controllerchange ââââââââââââ 56 // Previously: navigator.serviceWorker.addEventListener('controllerchange', () => location.reload()) 57 // Removed because it fires immediately when user clicks Update in the toast, 58 // causing an unexpected reload mid-use. The toast's Update button now handles 59 // the reload explicitly after skipWaiting, giving the user control. 60 61 // ââ 2. Nav install button âââââââââââââââââââââââââââââââ 62 let deferredPrompt = null; 63 64 window.addEventListener('beforeinstallprompt', e => { 65 e.preventDefault(); 66 deferredPrompt = e; 67 showInstallButton(); 68 }); 69 70 window.addEventListener('appinstalled', () => { 71 deferredPrompt = null; 72 hideInstallButton(); 73 localStorage.setItem('pwa-installed', '1'); 74 trackEvent('pwa_install', 'PWA', 'installed'); 75 }); 76 77 function showInstallButton() { 78 if (localStorage.getItem('pwa-installed')) return; 79 const btn = document.getElementById('pwa-install-nav-btn'); 80 if (!btn) return; 81 btn.hidden = false; 82 btn.style.display = ''; 83 btn.addEventListener('click', triggerInstall, { once: true }); 84 } 85 86 function hideInstallButton() { 87 const btn = document.getElementById('pwa-install-nav-btn'); 88 if (btn) { btn.hidden = true; btn.style.display = 'none'; } 89 } 90 91 function triggerInstall() { 92 if (!deferredPrompt) return; 93 deferredPrompt.prompt(); 94 deferredPrompt.userChoice.then(choice => { 95 if (choice.outcome === 'accepted') { 96 localStorage.setItem('pwa-installed', '1'); 97 trackEvent('pwa_install', 'PWA', 'installed'); 98 } 99 deferredPrompt = null; 100 hideInstallButton(); 101 }); 102 } 103 104 // ââ 3. Update toast âââââââââââââââââââââââââââââââââââââ 105 // Injected into DOM automatically â no markup needed in HTML. 106 // User MUST click Update to reload â no automatic reload. 107 function createUpdateToast() { 108 if (document.getElementById('pwa-update-toast')) return; 109 110 const toast = document.createElement('div'); 111 toast.id = 'pwa-update-toast'; 112 toast.setAttribute('role', 'alert'); 113 toast.setAttribute('aria-live', 'polite'); 114 toast.style.cssText = [ 115 'position:fixed', 'top:16px', 'right:16px', 116 'background:#1a2e35', 'color:#fff', 117 'padding:12px 16px', 'border-radius:12px', 118 'display:none', 'align-items:center', 'gap:12px', 119 'z-index:9999', 'font-size:14px', 'font-family:inherit', 120 'box-shadow:0 4px 20px rgba(0,0,0,.25)', 121 'max-width:calc(100vw - 32px)' 122 ].join(';'); 123 124 toast.innerHTML = `
125 <span>ð New version available</span> 126 <button id="pwa-update-btn" style=" 127 background:#2b6777;color:#fff;border:none; 128 padding:5px 14px;border-radius:7px; 129 cursor:pointer;font-size:13px;font-weight:600; 130 font-family:inherit; 131 ">Update</button> 132 <button id="pwa-update-dismiss" style=" 133 background:transparent;border:none;color:#aaa; 134 cursor:pointer;font-size:18px;line-height:1;padding:0 2px; 135 " aria-label="Dismiss">Ã</button> 136 `; 137 document.body.appendChild(toast); 138 } 139 140 function showUpdateToast() { 141 createUpdateToast(); 142 const toast = document.getElementById('pwa-update-toast'); 143 if (!toast) return; 144 toast.style.display = 'flex'; 145 146 document.getElementById('pwa-update-btn')?.addEventListener('click', () => { 147 toast.style.display = 'none'; 148 if (newWorker) { 149 // Tell the new SW to take over 150 newWorker.postMessage({ action: 'skipWaiting' }); 151 // Wait briefly then reload â user clicked Update so this is intentional 152 setTimeout(() => window.location.reload(), 300); 153 } 154 }, { once: true }); 155 156 document.getElementById('pwa-update-dismiss')?.addEventListener('click', () => { 157 toast.style.display = 'none'; 158 }, { once: true }); 159 } 160 161 // ââ 4. Offline ad hiding ââââââââââââââââââââââââââââââââ 162 function handleAdVisibility() { 163 const ads = document.querySelectorAll('.adsbygoogle'); 164 if (!navigator.onLine) { 165 ads.forEach(ad => { 166 if (ad.dataset.offlineHidden) return; 167 ad.dataset.offlineHidden = '1'; 168 ad.style.display = 'none'; 169 const msg = document.createElement('div'); 170 msg.className = 'ad-offline-placeholder'; 171 msg.style.cssText = 'text-align:center;padding:8px;font-size:11px;color:#9aa0a6;'; 172 msg.textContent = 'Ad unavailable offline'; 173 ad.after(msg); 174 }); 175 } else { 176 ads.forEach(ad => { 177 if (!ad.dataset.offlineHidden) return; 178 delete ad.dataset.offlineHidden; 179 ad.style.display = ''; 180 }); 181 document.querySelectorAll('.ad-offline-placeholder').forEach(m => m.remove()); 182 } 183 } 184 185 // ââ 5. Network quality badge (optional) ââââââââââââââââ 186 function updateNetworkBadge() { 187 const badge = document.getElementById('pwa-net-badge'); 188 if (!badge) return; 189 190 const cn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; 191 192 if (!navigator.onLine) { 193 badge.hidden = false; 194 badge.textContent = 'â Offline'; 195 badge.style.color = '#c62828'; 196 return; 197 } 198 199 if (cn) { 200 const type = cn.effectiveType || ''; 201 const slow = type === 'slow-2g' || type === '2g' || cn.rtt > 500; 202 if (slow) { 203 badge.hidden = false; 204 badge.textContent = 'â Slow'; 205 badge.style.color = '#e65100'; 206 return; 207 } 208 } 209 210 badge.hidden = true; 211 } 212 213 // ââ Init ââââââââââââââââââââââââââââââââââââââââââââââââ 214 handleAdVisibility(); 215 updateNetworkBadge(); 216 217 window.addEventListener('online', () => { handleAdVisibility(); updateNetworkBadge(); }); 218 window.addEventListener('offline', () => { handleAdVisibility(); updateNetworkBadge(); }); 219 220 const cn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; 221 if (cn) cn.addEventListener('change', updateNetworkBadge); 222 223 // ââ Utility âââââââââââââââââââââââââââââââââââââââââââââ 224 function trackEvent(name, category, label) { 225 if (typeof gtag !== 'undefined') { 226 gtag('event', name, { event_category: category, event_label: label }); 227 } 228 } 229 230})();
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.