PageSourceSearch

https://bil-tm.web.app/src/pwa/pwa-manager.js

js bil-tm.web.app collected 2026-10-03 12:18:26 UTC 8,689 bytes, 319 lines download raw bytes

1/**
2 * PWA Manager - Handles service worker registration and install prompts
3 * @module pwa/pwa-manager
4 */
5
6class PWAManager {
7  constructor() {
8    this.deferredPrompt = null;
9    this.isInstalled = false;
10    this.registration = null;
11    this.updateAvailable = false;
12  }
13
14  /**
15   * Initialize PWA functionality
16   */
17  async init() {
18    // Check if already installed
19    this.isInstalled = this.checkIfInstalled();
20    
21    // Register service worker
22    await this.registerServiceWorker();
23    
24    // Set up install prompt handler
25    this.setupInstallPrompt();
26    
27    // Set up app installed handler
28    this.setupAppInstalledHandler();
29    
30    // Set up offline detection
31    this.setupOfflineDetection();
32    
33    console.log('[PWA] Initialized. Installed:', this.isInstalled);
34  }
35
36  /**
37   * Register the service worker
38   */
39  async registerServiceWorker() {
40    if (!('serviceWorker' in navigator)) {
41      console.warn('[PWA] Service workers not supported');
42      return null;
43    }
44
45    try {
46      this.registration = await navigator.serviceWorker.register('/sw.js', {
47        scope: '/'
48      });
49
50      console.log('[PWA] Service worker registered with scope:', this.registration.scope);
51
52      // Check for updates
53      this.registration.addEventListener('updatefound', () => {
54        const newWorker = this.registration.installing;
55        
56        newWorker.addEventListener('statechange', () => {
57          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
58            this.updateAvailable = true;
59            this.showUpdateNotification();
60          }
61        });
62      });
63
64      // Handle controller change (new SW activated)
65      navigator.serviceWorker.addEventListener('controllerchange', () => {
66        console.log('[PWA] New service worker activated');
67      });
68
69      return this.registration;
70    } catch (error) {
71      console.error('[PWA] Service worker registration failed:', error);
72      return null;
73    }
74  }
75
76  /**
77   * Set up the beforeinstallprompt event handler
78   */
79  setupInstallPrompt() {
80    window.addEventListener('beforeinstallprompt', (event) => {
81      // Prevent the mini-infobar from appearing on mobile
82      event.preventDefault();
83      
84      // Store the event for later use
85      this.deferredPrompt = event;
86      
87      // Show custom install button
88      this.showInstallButton();
89      
90      console.log('[PWA] Install prompt available');
91    });
92  }
93
94  /**
95   * Set up handler for when app is installed
96   */
97  setupAppInstalledHandler() {
98    window.addEventListener('appinstalled', () => {
99      this.isInstalled = true;
100      this.deferredPrompt = null;
101      this.hideInstallButton();
102      
103      console.log('[PWA] App installed successfully');
104      
105      // Track installation (optional analytics)
106      this.trackInstallation();
107    });
108  }
109
110  /**
111   * Check if the app is already installed
112   */
113  checkIfInstalled() {
114    // Check display-mode media query
115    if (window.matchMedia('(display-mode: standalone)').matches) {
116      return true;
117    }
118    
119    // Check navigator.standalone for iOS
120    if (window.navigator.standalone === true) {
121      return true;
122    }
123    
124    // Check if launched from home screen on Android
125    if (document.referrer.includes('android-app://')) {
126      return true;
127    }
128    
129    return false;
130  }
131
132  /**
133   * Prompt user to install the app
134   */
135  async promptInstall() {
136    if (!this.deferredPrompt) {
137      console.log('[PWA] No install prompt available');
138      return false;
139    }
140
141    // Show the install prompt
142    this.deferredPrompt.prompt();
143
144    // Wait for user response
145    const { outcome } = await this.deferredPrompt.userChoice;
146    
147    console.log('[PWA] User response to install prompt:', outcome);
148    
149    // Clear the deferred prompt
150    this.deferredPrompt = null;
151    
152    return outcome === 'accepted';
153  }
154
155  /**
156   * Show the install button in the UI
157   */
158  showInstallButton() {
159    // Create install button if it doesn't exist
160    let installBtn = document.getElementById('pwaInstallBtn');
161    
162    if (!installBtn) {
163      installBtn = document.createElement('button');
164      installBtn.id = 'pwaInstallBtn';
165      installBtn.className = 'pwa-install-btn';
166      installBtn.innerHTML = '<i class="bi bi-download"></i> Install App';
167      installBtn.title = 'Install Teachable Machine as an app';
168      installBtn.setAttribute('aria-label', 'Install app on your device');
169      
170      installBtn.addEventListener('click', () => this.promptInstall());
171      
172      // Add to footer links
173      const footerLinks = document.querySelector('.footer-links');
174      if (footerLinks) {
175        footerLinks.appendChild(installBtn);
176      }
177    }
178    
179    installBtn.style.display = 'inline-flex';
180  }
181
182  /**
183   * Hide the install button
184   */
185  hideInstallButton() {
186    const installBtn = document.getElementById('pwaInstallBtn');
187    if (installBtn) {
188      installBtn.style.display = 'none';
189    }
190  }
191
192  /**
193   * Show notification when update is available
194   */
195  showUpdateNotification() {
196    // Create update notification
197    const notification = document.createElement('div');
198    notification.id = 'pwaUpdateNotification';
199    notification.className = 'pwa-update-notification';
200    notification.innerHTML = `
201      <div class="pwa-update-content">
202        <span><i class="bi bi-arrow-repeat"></i> A new version is available!</span>
203        <button id="pwaUpdateBtn" class="pwa-update-btn">Update</button>
204        <button id="pwaUpdateDismiss" class="pwa-update-dismiss">&times;</button>
205      </div>
206    `;
207    
208    document.body.appendChild(notification);
209    
210    // Handle update button click
211    document.getElementById('pwaUpdateBtn').addEventListener('click', () => {
212      this.applyUpdate();
213    });
214    
215    // Handle dismiss button click
216    document.getElementById('pwaUpdateDismiss').addEventListener('click', () => {
217      notification.remove();
218    });
219  }
220
221  /**
222   * Apply the pending update
223   */
224  applyUpdate() {
225    if (this.registration && this.registration.waiting) {
226      // Tell the waiting service worker to skip waiting
227      this.registration.waiting.postMessage({ type: 'SKIP_WAITING' });
228      
229      // Reload the page to use new service worker
230      window.location.reload();
231    }
232  }
233
234  /**
235   * Clear the service worker cache
236   */
237  async clearCache() {
238    if (this.registration && this.registration.active) {
239      this.registration.active.postMessage({ type: 'CLEAR_CACHE' });
240      console.log('[PWA] Cache clear requested');
241    }
242  }
243
244  /**
245   * Unregister the service worker
246   */
247  async unregister() {
248    if (this.registration) {
249      const success = await this.registration.unregister();
250      console.log('[PWA] Service worker unregistered:', success);
251      return success;
252    }
253    return false;
254  }
255
256  /**
257   * Track installation for analytics (optional)
258   */
259  trackInstallation() {
260    // You can add analytics tracking here
261    console.log('[PWA] Installation tracked');
262  }
263
264  /**
265   * Set up offline/online detection
266   */
267  setupOfflineDetection() {
268    const indicator = document.getElementById('pwaOfflineIndicator');
269    
270    const updateOnlineStatus = () => {
271      if (indicator) {
272        if (navigator.onLine) {
273          indicator.classList.remove('visible');
274        } else {
275          indicator.classList.add('visible');
276        }
277      }
278      console.log('[PWA] Online status:', navigator.onLine);
279    };
280    
281    window.addEventListener('online', updateOnlineStatus);
282    window.addEventListener('offline', updateOnlineStatus);
283    
284    // Check initial status
285    updateOnlineStatus();
286  }
287
288  /**
289   * Check if the app can be installed
290   */
291  canInstall() {
292    return this.deferredPrompt !== null && !this.isInstalled;
293  }
294
295  /**
296   * Get PWA status information
297   */
298  getStatus() {
299    return {
300      isInstalled: this.isInstalled,
301      canInstall: this.canInstall(),
302      updateAvailable: this.updateAvailable,
303      serviceWorkerActive: this.registration?.active !== null
304    };
305  }
306}
307
308// Create singleton instance
309const pwaManager = new PWAManager();
310
311// Auto-initialize when DOM is ready
312if (document.readyState === 'loading') {
313  document.addEventListener('DOMContentLoaded', () => pwaManager.init());
314} else {
315  pwaManager.init();
316}
317
318// Export for module usage
319export { pwaManager, PWAManager };

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.