318 lines
10 KiB
JavaScript
318 lines
10 KiB
JavaScript
// Service Worker version
|
|
const CACHE_VERSION = 'v1.0.2';
|
|
const CACHE_NAME = `game-timer-${CACHE_VERSION}`;
|
|
|
|
// Store last battery warning timestamp to prevent repeated notifications
|
|
let lastBatteryWarningTimestamp = 0;
|
|
const FOUR_HOURS_MS = 4 * 60 * 60 * 1000; // 4 hours in milliseconds
|
|
|
|
// Settings cache for storing timestamps
|
|
const SETTINGS_CACHE = 'settings-cache-v1';
|
|
const TIMESTAMP_URL = new Request('/_timestamp/battery-warning');
|
|
|
|
// Function to load the timestamp from cache
|
|
async function loadTimestamp() {
|
|
try {
|
|
const cache = await caches.open(SETTINGS_CACHE);
|
|
const response = await cache.match(TIMESTAMP_URL);
|
|
|
|
if (response) {
|
|
const data = await response.json();
|
|
lastBatteryWarningTimestamp = data.timestamp;
|
|
console.log('[ServiceWorker] Loaded battery warning timestamp:', new Date(lastBatteryWarningTimestamp));
|
|
} else {
|
|
console.log('[ServiceWorker] No saved timestamp found');
|
|
}
|
|
return true;
|
|
} catch (error) {
|
|
console.error('[ServiceWorker] Error loading timestamp:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Function to save the timestamp to cache
|
|
async function saveTimestamp(timestamp) {
|
|
try {
|
|
const cache = await caches.open(SETTINGS_CACHE);
|
|
const response = new Response(JSON.stringify({ timestamp }), {
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
|
|
await cache.put(TIMESTAMP_URL, response);
|
|
console.log('[ServiceWorker] Saved battery warning timestamp:', new Date(timestamp));
|
|
return true;
|
|
} catch (error) {
|
|
console.error('[ServiceWorker] Error saving timestamp:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Initialize and load data when the service worker starts
|
|
loadTimestamp().then(success => {
|
|
console.log('[ServiceWorker] Timestamp loading ' + (success ? 'successful' : 'failed'));
|
|
});
|
|
|
|
// Files to cache
|
|
const CACHE_FILES = [
|
|
'/',
|
|
'/sw.js',
|
|
'/index.html',
|
|
'/manifest.json',
|
|
'/css/styles.css',
|
|
'/favicon.ico',
|
|
'/icons/android-chrome-192x192.png',
|
|
'/icons/android-chrome-512x512.png',
|
|
'/icons/apple-touch-icon.png',
|
|
'/icons/favicon-32x32.png',
|
|
'/icons/favicon-16x16.png',
|
|
'/js/app.js',
|
|
'/js/config.js',
|
|
'/js/env-loader.js',
|
|
'/js/ui/audio.js',
|
|
'/js/ui/camera.js',
|
|
'/js/ui/ui.js',
|
|
'/js/core/state.js',
|
|
'/js/core/timer.js',
|
|
'/js/core/gameActions.js',
|
|
'/js/core/playerManager.js',
|
|
'/js/core/eventHandlers.js',
|
|
'/js/services/pushFlicIntegration.js',
|
|
'/js/services/serviceWorkerManager.js'
|
|
];
|
|
|
|
// Install event - Cache files
|
|
self.addEventListener('install', event => {
|
|
console.log('[ServiceWorker] Install');
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME)
|
|
.then(cache => {
|
|
console.log('[ServiceWorker] Caching app shell');
|
|
return cache.addAll(CACHE_FILES);
|
|
})
|
|
.then(() => {
|
|
console.log('[ServiceWorker] Skip waiting on install');
|
|
return self.skipWaiting();
|
|
})
|
|
);
|
|
});
|
|
|
|
// Activate event - Clean old caches
|
|
self.addEventListener('activate', event => {
|
|
console.log('[ServiceWorker] Activate');
|
|
event.waitUntil(
|
|
caches.keys().then(keyList => {
|
|
return Promise.all(keyList.map(key => {
|
|
if (key !== CACHE_NAME) {
|
|
console.log('[ServiceWorker] Removing old cache', key);
|
|
return caches.delete(key);
|
|
}
|
|
}));
|
|
})
|
|
.then(() => {
|
|
console.log('[ServiceWorker] Claiming clients');
|
|
return self.clients.claim();
|
|
})
|
|
);
|
|
});
|
|
|
|
// Helper function to determine if a response should be cached
|
|
function shouldCacheResponse(request, response) {
|
|
// Only cache GET requests
|
|
if (request.method !== 'GET') return false;
|
|
|
|
// Don't cache errors
|
|
if (!response || response.status !== 200) return false;
|
|
|
|
// Check if URL should be cached
|
|
const url = new URL(request.url);
|
|
|
|
// Don't cache query parameters (except common ones for content)
|
|
if (url.search && !url.search.match(/\?(v|version|cache)=/)) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
self.addEventListener('push', event => {
|
|
console.log('[ServiceWorker] Push received');
|
|
|
|
let pushData = {
|
|
title: 'Flic Action',
|
|
body: 'Button pressed!',
|
|
data: {
|
|
action: 'Unknown',
|
|
button: 'Unknown',
|
|
batteryLevel: undefined,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
};
|
|
|
|
// --- Attempt to parse data payload ---
|
|
if (event.data) {
|
|
try {
|
|
const parsedData = event.data.json();
|
|
console.log('[ServiceWorker] Push data:', parsedData);
|
|
|
|
// Use parsed data for notification and message
|
|
pushData = {
|
|
title: parsedData.title || pushData.title,
|
|
body: parsedData.body || pushData.body,
|
|
data: parsedData.data || pushData.data // Expecting { action: 'SingleClick', button: 'game-button', batteryLevel: 75 }
|
|
};
|
|
|
|
// Ensure all required fields are present in data
|
|
pushData.data = pushData.data || {};
|
|
if (!pushData.data.timestamp) {
|
|
pushData.data.timestamp = new Date().toISOString();
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error('[ServiceWorker] Error parsing push data:', e);
|
|
// Use default notification if parsing fails
|
|
pushData.body = event.data.text() || pushData.body; // Fallback to text
|
|
}
|
|
} else {
|
|
console.log('[ServiceWorker] Push event but no data');
|
|
}
|
|
|
|
// --- Send message to client(s) ---
|
|
const messagePayload = {
|
|
type: 'flic-action', // Custom message type
|
|
action: pushData.data.action || 'Unknown', // e.g., 'SingleClick', 'DoubleClick', 'Hold'
|
|
button: pushData.data.button || 'Unknown', // e.g., the button name
|
|
timestamp: pushData.data.timestamp || new Date().toISOString(), // e.g., the timestamp of the action
|
|
batteryLevel: pushData.data.batteryLevel // e.g., the battery level percentage
|
|
};
|
|
|
|
console.log('[ServiceWorker] Preparing message payload:', messagePayload);
|
|
|
|
// Check if this is a low battery alert that needs a notification
|
|
const batteryLevel = messagePayload.batteryLevel;
|
|
const isBatteryLow = batteryLevel !== undefined && batteryLevel < 50; // Use the same threshold as in the app
|
|
|
|
// Determine if we should show a battery notification (throttle to once every 4 hours)
|
|
let shouldShowBatteryNotification = false;
|
|
|
|
if (isBatteryLow) {
|
|
const now = Date.now();
|
|
if (now - lastBatteryWarningTimestamp > FOUR_HOURS_MS) {
|
|
// It's been more than 4 hours since the last battery notification
|
|
console.log(`[ServiceWorker] Low battery (${batteryLevel}%) - showing notification`);
|
|
lastBatteryWarningTimestamp = now;
|
|
|
|
// Save the timestamp to cache
|
|
saveTimestamp(now).catch(error => {
|
|
console.warn('[ServiceWorker] Failed to save battery warning timestamp:', error);
|
|
});
|
|
|
|
shouldShowBatteryNotification = true;
|
|
|
|
// Change notification title/body for battery alerts
|
|
pushData.title = 'Flic Button Low Battery';
|
|
pushData.body = `Battery level is ${batteryLevel}%. Please replace batteries soon.`;
|
|
} else {
|
|
console.log(`[ServiceWorker] Low battery (${batteryLevel}%) - suppressing notification (shown recently)`);
|
|
}
|
|
}
|
|
|
|
// Send message to all open PWA windows controlled by this SW
|
|
event.waitUntil(
|
|
self.clients.matchAll({
|
|
type: 'window', // Only target window clients
|
|
includeUncontrolled: true // Include clients that might not be fully controlled yet
|
|
}).then(clientList => {
|
|
if (!clientList || clientList.length === 0) {
|
|
console.log('[ServiceWorker] No client windows found to send message to.');
|
|
// If no window is open AND this is a battery alert that should be shown, show a notification
|
|
if (shouldShowBatteryNotification) {
|
|
return self.registration.showNotification(pushData.title, {
|
|
body: pushData.body,
|
|
icon: '/icons/android-chrome-192x192.png', // Updated path
|
|
data: pushData.data // Pass data if needed when notification is clicked
|
|
});
|
|
}
|
|
// Otherwise, don't show notification for regular button presses
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// Post message to each client with improved reliability
|
|
let messageSent = false;
|
|
const sendPromises = clientList.map(client => {
|
|
console.log(`[ServiceWorker] Posting message to client: ${client.id}`, messagePayload);
|
|
try {
|
|
// Try to send the message and mark it as sent
|
|
client.postMessage(messagePayload);
|
|
messageSent = true;
|
|
|
|
// Just return true to indicate message was sent
|
|
return Promise.resolve(true);
|
|
} catch (error) {
|
|
console.error('[ServiceWorker] Error posting message to client:', error);
|
|
return Promise.resolve(false);
|
|
}
|
|
});
|
|
|
|
return Promise.all(sendPromises).then(() => {
|
|
// Only show a notification if this is a battery alert that should be shown
|
|
if (shouldShowBatteryNotification) {
|
|
return self.registration.showNotification(pushData.title, {
|
|
body: pushData.body,
|
|
icon: '/icons/android-chrome-192x192.png',
|
|
data: pushData.data
|
|
});
|
|
}
|
|
// For regular button presses or throttled battery alerts, don't show notifications
|
|
return Promise.resolve();
|
|
});
|
|
})
|
|
);
|
|
});
|
|
|
|
// Listen for messages from client
|
|
self.addEventListener('message', event => {
|
|
const message = event.data;
|
|
|
|
if (!message || typeof message !== 'object') {
|
|
return;
|
|
}
|
|
|
|
// Handle get timestamp request
|
|
if (message.type === 'get-battery-timestamp') {
|
|
console.log('[ServiceWorker] Client requested battery warning timestamp');
|
|
event.source.postMessage({
|
|
type: 'battery-timestamp',
|
|
timestamp: lastBatteryWarningTimestamp
|
|
});
|
|
}
|
|
|
|
// Handle update timestamp request
|
|
if (message.type === 'update-battery-timestamp' && message.timestamp) {
|
|
console.log('[ServiceWorker] Updating battery warning timestamp to:', new Date(message.timestamp));
|
|
lastBatteryWarningTimestamp = message.timestamp;
|
|
saveTimestamp(message.timestamp).catch(error => {
|
|
console.warn('[ServiceWorker] Failed to save updated battery warning timestamp:', error);
|
|
});
|
|
}
|
|
});
|
|
|
|
// This helps with navigation after app is installed
|
|
self.addEventListener('notificationclick', event => {
|
|
console.log('[ServiceWorker] Notification click received');
|
|
|
|
event.notification.close();
|
|
|
|
// Handle the notification click
|
|
event.waitUntil(
|
|
self.clients.matchAll({ type: 'window' })
|
|
.then(clientList => {
|
|
for (const client of clientList) {
|
|
if (client.url.startsWith(self.location.origin) && 'focus' in client) {
|
|
return client.focus();
|
|
}
|
|
}
|
|
|
|
if (self.clients.openWindow) {
|
|
return self.clients.openWindow('/');
|
|
}
|
|
})
|
|
);
|
|
}); |