push notifications

This commit is contained in:
cpu
2025-03-26 05:04:50 +01:00
parent a0f3489656
commit fc278ed256
4 changed files with 125 additions and 30 deletions

77
app.js
View File

@@ -9,6 +9,8 @@ let gameState = 'setup'; // setup, running, paused, over
let carouselPosition = 0; let carouselPosition = 0;
let startX = 0; let startX = 0;
let currentX = 0; let currentX = 0;
let pushSubscription = null;
const PUBLIC_VAPID_KEY = 'BNIXGVBzq6SNqvlDMFylw_hLTpf_J96ddbwfMa9Cn1tFQ1-vDqmz_NQS0a5UiczqAJ-uYs-6EeuwrnwaMRGtifk=';
// DOM Elements // DOM Elements
const carousel = document.getElementById('carousel'); const carousel = document.getElementById('carousel');
@@ -37,6 +39,47 @@ const cameraCancelButton = document.getElementById('cameraCancelButton');
let stream = null; let stream = null;
async function subscribeToPushNotifications() {
if ('serviceWorker' in navigator && 'PushManager' in window) {
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
});
// Send subscription to your server
await fetch('https://webpush.virtonline.eu/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'Content-Type': 'application/json'
}
});
pushSubscription = subscription;
console.log('Push subscription successful');
} catch (error) {
console.error('Error subscribing to push:', error);
}
}
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// Add sound toggle button // Add sound toggle button
const createSoundToggleButton = () => { const createSoundToggleButton = () => {
const headerButtons = document.querySelector('.header-buttons'); const headerButtons = document.querySelector('.header-buttons');
@@ -177,11 +220,13 @@ gameButton.addEventListener('click', () => {
gameState = 'paused'; gameState = 'paused';
audioManager.play('gamePause'); audioManager.play('gamePause');
stopTimer(); stopTimer();
// sendPushNotification('Game Paused', 'The game timer has paused!');
break; break;
case 'paused': case 'paused':
gameState = 'running'; gameState = 'running';
audioManager.play('gameResume'); audioManager.play('gameResume');
startTimer(); startTimer();
// sendPushNotification('Game Resumed', 'The game timer has resumed!');
break; break;
case 'over': case 'over':
// Reset timers and start new game // Reset timers and start new game
@@ -198,6 +243,26 @@ gameButton.addEventListener('click', () => {
saveData(); saveData();
}); });
async function sendPushNotification(title, message) {
if (!pushSubscription) return;
try {
await fetch('https://webpush.virtonline.eu/flic-webhook', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: title,
body: message,
action: 'game_update'
})
});
} catch (error) {
console.error('Error sending push notification:', error);
}
}
// Timer variables // Timer variables
let timerInterval = null; let timerInterval = null;
@@ -778,15 +843,19 @@ if ('serviceWorker' in navigator) {
window.addEventListener('load', () => { window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js') navigator.serviceWorker.register('/sw.js')
.then(registration => { .then(registration => {
console.log('ServiceWorker registered: ', registration); console.log('ServiceWorker registered');
// Request notification permission and subscribe
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
subscribeToPushNotifications();
}
});
// Setup and handle deep links after service worker is ready
setupDeepLinks(); setupDeepLinks();
}) })
.catch(error => { .catch(error => {
console.log('ServiceWorker registration failed:', error); console.log('ServiceWorker registration failed:', error);
// Still try to handle deep links even if service worker failed
setupDeepLinks(); setupDeepLinks();
}); });
}); });

View File

@@ -164,6 +164,16 @@
.catch((err) => console.log("Service Worker Failed", err)); .catch((err) => console.log("Service Worker Failed", err));
} }
</script> </script>
<script>
// Request notification permission on page load
document.addEventListener('DOMContentLoaded', () => {
if ('Notification' in window) {
Notification.requestPermission().then(permission => {
console.log('Notification permission:', permission);
});
}
});
</script>
<footer class="app-footer"> <footer class="app-footer">
<div class="author-info"> <div class="author-info">
<p>Vibe coded by Martin</p> <p>Vibe coded by Martin</p>

View File

@@ -95,5 +95,6 @@
"url": "/?action=reset", "url": "/?action=reset",
"icons": [{ "src": "/icons/reset.png", "sizes": "192x192" }] "icons": [{ "src": "/icons/reset.png", "sizes": "192x192" }]
} }
] ],
"gcm_sender_id": "103953800507"
} }

29
sw.js
View File

@@ -148,24 +148,39 @@ self.addEventListener('message', event => {
} }
}); });
self.addEventListener('push', event => {
console.log('[ServiceWorker] Push received');
const data = event.data ? event.data.json() : {};
const title = data.title || 'Game Timer Notification';
const options = {
body: data.body || 'You have a new notification',
icon: '/icons/android-chrome-192x192.png',
badge: '/icons/android-chrome-192x192.png',
data: data
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// This helps with navigation after app is installed // This helps with navigation after app is installed
self.addEventListener('notificationclick', event => { self.addEventListener('notificationclick', event => {
console.log('[ServiceWorker] Notification click received');
event.notification.close(); event.notification.close();
// This looks to see if the current is already open and focuses if it is // Handle the notification click
event.waitUntil( event.waitUntil(
self.clients.matchAll({ self.clients.matchAll({ type: 'window' })
type: 'window'
})
.then(clientList => { .then(clientList => {
// Check if there is already a window/tab open with the target URL
for (const client of clientList) { for (const client of clientList) {
// If so, just focus it
if (client.url.startsWith(self.location.origin) && 'focus' in client) { if (client.url.startsWith(self.location.origin) && 'focus' in client) {
return client.focus(); return client.focus();
} }
} }
// If not, open a new window/tab
if (self.clients.openWindow) { if (self.clients.openWindow) {
return self.clients.openWindow('/'); return self.clients.openWindow('/');
} }