From 5ebd2cf005c6e79b3912bb9a9f6cf48e4469f6e8 Mon Sep 17 00:00:00 2001 From: cpu Date: Sat, 29 Mar 2025 06:01:10 +0100 Subject: [PATCH] push setup --- css/styles.css | 54 +++++++ index.html | 34 +++++ src/js/app.js | 12 +- src/js/ui/pushSettingsUI.js | 280 ++++++++++++++++++++++++++++++++++++ 4 files changed, 376 insertions(+), 4 deletions(-) create mode 100644 src/js/ui/pushSettingsUI.js diff --git a/css/styles.css b/css/styles.css index d7e3c1e..80b1d25 100644 --- a/css/styles.css +++ b/css/styles.css @@ -367,4 +367,58 @@ input[type="file"] { flex-direction: column; align-items: center; gap: 0.3rem; +} + +/* Push notification controls */ +.push-notification-controls { + margin-right: 10px; +} + +.notification-status-container { + margin: 1rem 0; + padding: 1rem; + background-color: #f8f9fa; + border-radius: 4px; +} + +.notification-status p { + margin-bottom: 0.5rem; +} + +.advanced-options { + margin-top: 1rem; + display: flex; + justify-content: space-between; + border-top: 1px solid #eee; + padding-top: 1rem; +} + +.advanced-options button { + width: 48%; +} + +/* Status indicators */ +.status-granted { + color: #28a745; + font-weight: bold; +} + +.status-denied { + color: #dc3545; + font-weight: bold; +} + +.status-default { + color: #ffc107; + font-weight: bold; +} + +.status-active { + color: #28a745; + font-weight: bold; +} + +.status-inactive { + color: #6c757d; + font-weight: bold; } \ No newline at end of file diff --git a/index.html b/index.html index 397d2a3..84ff5e8 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,11 @@
+
+ +
@@ -111,6 +116,35 @@
+ + + diff --git a/src/js/app.js b/src/js/app.js index 593d356..91010e7 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -7,6 +7,7 @@ import camera from './ui/camera.js'; // Default export import audioManager from './ui/audio.js'; import * as pushFlic from './services/pushFlicIntegration.js'; import { initEnv } from './env-loader.js'; +import * as pushSettingsUI from './ui/pushSettingsUI.js'; // Import the new push settings UI module // Import externalized modules import * as gameActions from './core/gameActions.js'; @@ -70,7 +71,10 @@ async function initialize() { ui.elements.resetCancelButton.addEventListener('click', eventHandlers.handleResetCancel); ui.elements.cameraButton.addEventListener('click', eventHandlers.handleCameraButtonClick); - // 6. Setup Flic action handlers + // 6. Initialize Push Notification Settings UI + pushSettingsUI.initPushSettingsUI(); + + // 7. Setup Flic action handlers const flicActionHandlers = { [config.FLIC_ACTIONS.SINGLE_CLICK]: playerManager.nextPlayer, [config.FLIC_ACTIONS.DOUBLE_CLICK]: playerManager.previousPlayer, @@ -78,14 +82,14 @@ async function initialize() { }; serviceWorkerManager.setFlicActionHandlers(flicActionHandlers); - // 7. Setup Service Worker (which also initializes Flic) + // 8. Setup Service Worker (which also initializes Flic) serviceWorkerManager.setupServiceWorker(serviceWorkerManager.handleServiceWorkerMessage); - // 8. Initial UI Update based on loaded state + // 9. Initial UI Update based on loaded state ui.renderPlayers(); ui.updateGameButton(); - // 9. Reset running state to paused on load + // 10. Reset running state to paused on load if (state.getGameState() === config.GAME_STATES.RUNNING) { console.log("Game was running on load, setting to paused."); state.setGameState(config.GAME_STATES.PAUSED); diff --git a/src/js/ui/pushSettingsUI.js b/src/js/ui/pushSettingsUI.js new file mode 100644 index 0000000..6c28ae0 --- /dev/null +++ b/src/js/ui/pushSettingsUI.js @@ -0,0 +1,280 @@ +// pushSettingsUI.js - UI handling for push notification settings +import { setupPushNotifications, forceCredentialsPrompt } from '../services/serviceWorkerManager.js'; +import { FLIC_BUTTON_ID } from '../config.js'; + +// --- DOM Elements --- +const elements = { + pushSettingsButton: null, + pushSettingsModal: null, + notificationPermissionStatus: null, + subscriptionStatus: null, + pushUsername: null, + pushPassword: null, + pushSaveButton: null, + pushCancelButton: null, + pushUnsubscribeButton: null, + pushResubscribeButton: null +}; + +// --- State --- +let currentSubscription = null; + +// --- Initialization --- +export function initPushSettingsUI() { + // Cache DOM elements + elements.pushSettingsButton = document.getElementById('pushSettingsButton'); + elements.pushSettingsModal = document.getElementById('pushSettingsModal'); + elements.notificationPermissionStatus = document.getElementById('notificationPermissionStatus'); + elements.subscriptionStatus = document.getElementById('subscriptionStatus'); + elements.pushUsername = document.getElementById('pushUsername'); + elements.pushPassword = document.getElementById('pushPassword'); + elements.pushSaveButton = document.getElementById('pushSaveButton'); + elements.pushCancelButton = document.getElementById('pushCancelButton'); + elements.pushUnsubscribeButton = document.getElementById('pushUnsubscribeButton'); + elements.pushResubscribeButton = document.getElementById('pushResubscribeButton'); + + // Set up event listeners + elements.pushSettingsButton.addEventListener('click', openPushSettingsModal); + elements.pushCancelButton.addEventListener('click', closePushSettingsModal); + elements.pushSaveButton.addEventListener('click', saveCredentialsAndSubscribe); + elements.pushUnsubscribeButton.addEventListener('click', unsubscribeFromPush); + elements.pushResubscribeButton.addEventListener('click', resubscribeToPush); + + // Initial status check + updateNotificationStatus(); +} + +// --- UI Functions --- + +// Open the push settings modal and update statuses +function openPushSettingsModal() { + // Update status displays + updateNotificationStatus(); + updateSubscriptionStatus(); + + // Load saved credentials if available + loadSavedCredentials(); + + // Show the modal + elements.pushSettingsModal.classList.add('active'); +} + +// Close the push settings modal +function closePushSettingsModal() { + elements.pushSettingsModal.classList.remove('active'); +} + +// Update the notification permission status display +function updateNotificationStatus() { + if (!('Notification' in window)) { + elements.notificationPermissionStatus.textContent = 'Not Supported'; + elements.notificationPermissionStatus.className = 'status-denied'; + return; + } + + const permission = Notification.permission; + elements.notificationPermissionStatus.textContent = permission; + + switch (permission) { + case 'granted': + elements.notificationPermissionStatus.className = 'status-granted'; + break; + case 'denied': + elements.notificationPermissionStatus.className = 'status-denied'; + break; + default: + elements.notificationPermissionStatus.className = 'status-default'; + } +} + +// Update the subscription status display +async function updateSubscriptionStatus() { + if (!('serviceWorker' in navigator) || !('PushManager' in window)) { + elements.subscriptionStatus.textContent = 'Not Supported'; + elements.subscriptionStatus.className = 'status-denied'; + return; + } + + try { + const registration = await navigator.serviceWorker.ready; + currentSubscription = await registration.pushManager.getSubscription(); + + if (currentSubscription) { + elements.subscriptionStatus.textContent = 'Active'; + elements.subscriptionStatus.className = 'status-active'; + } else { + elements.subscriptionStatus.textContent = 'Not Subscribed'; + elements.subscriptionStatus.className = 'status-inactive'; + } + } catch (error) { + console.error('Error checking subscription status:', error); + elements.subscriptionStatus.textContent = 'Error'; + elements.subscriptionStatus.className = 'status-denied'; + } +} + +// Load saved credentials from localStorage +function loadSavedCredentials() { + try { + const storedAuth = localStorage.getItem('basicAuthCredentials'); + if (storedAuth) { + const credentials = JSON.parse(storedAuth); + if (credentials.username && credentials.password) { + elements.pushUsername.value = credentials.username; + elements.pushPassword.value = credentials.password; + } + } + } catch (error) { + console.error('Error loading saved credentials:', error); + } +} + +// --- Action Functions --- + +// Save credentials and subscribe to push notifications +async function saveCredentialsAndSubscribe() { + const username = elements.pushUsername.value.trim(); + const password = elements.pushPassword.value.trim(); + + if (!username || !password) { + alert('Please enter both username and password'); + return; + } + + // Save credentials to localStorage + const credentials = { username, password }; + localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials)); + + // Request notification permission if needed + if (Notification.permission !== 'granted') { + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + alert('Notification permission is required for push notifications'); + updateNotificationStatus(); + return; + } + } + + // Subscribe using the service worker + try { + await setupPushNotifications(); + await updateSubscriptionStatus(); + alert('Subscription successful!'); + } catch (error) { + console.error('Subscription error:', error); + alert(`Error subscribing: ${error.message}`); + } + + // Close the modal + closePushSettingsModal(); +} + +// Unsubscribe from push notifications +async function unsubscribeFromPush() { + if (!currentSubscription) { + alert('No active subscription to unsubscribe from'); + return; + } + + try { + await currentSubscription.unsubscribe(); + await updateSubscriptionStatus(); + alert('Successfully unsubscribed from push notifications'); + } catch (error) { + console.error('Error unsubscribing:', error); + alert(`Error unsubscribing: ${error.message}`); + } +} + +// Force resubscription to push notifications +async function resubscribeToPush() { + try { + // Force credential prompt and resubscribe + await forceCredentialsPrompt(); + await updateSubscriptionStatus(); + } catch (error) { + console.error('Error resubscribing:', error); + alert(`Error resubscribing: ${error.message}`); + } +} + +// Manually trigger sendSubscriptionToServer with the current subscription +export async function sendSubscriptionToServer() { + if (!currentSubscription) { + await updateSubscriptionStatus(); + if (!currentSubscription) { + alert('No active subscription available. Please subscribe first.'); + return; + } + } + + // Get stored credentials + let credentials; + try { + const storedAuth = localStorage.getItem('basicAuthCredentials'); + if (storedAuth) { + credentials = JSON.parse(storedAuth); + if (!credentials.username || !credentials.password) { + throw new Error('Invalid credentials'); + } + } else { + throw new Error('No stored credentials'); + } + } catch (error) { + alert('No valid credentials found. Please set them up first.'); + openPushSettingsModal(); + return; + } + + // Create Basic Auth header + const createBasicAuthHeader = (creds) => { + return 'Basic ' + btoa(`${creds.username}:${creds.password}`); + }; + + const headers = { + 'Content-Type': 'application/json', + 'Authorization': createBasicAuthHeader(credentials) + }; + + // Import the backend URL from config + let backendUrl; + try { + const configModule = await import('../config.js'); + backendUrl = configModule.BACKEND_URL; + } catch (error) { + alert('Could not get backend URL from config.'); + return; + } + + try { + // Make the request to the server + const response = await fetch(`${backendUrl}/subscribe`, { + method: 'POST', + body: JSON.stringify({ + button_id: FLIC_BUTTON_ID, + subscription: currentSubscription + }), + headers: headers, + credentials: 'include' + }); + + if (response.ok) { + const result = await response.json(); + alert(`Subscription sent successfully: ${result.message || 'OK'}`); + } else { + let errorMsg = `Server error: ${response.status}`; + if (response.status === 401 || response.status === 403) { + localStorage.removeItem('basicAuthCredentials'); + errorMsg = 'Authentication failed. Credentials cleared.'; + } else { + try { + const errorData = await response.json(); + errorMsg = errorData.message || errorMsg; + } catch (e) { /* use default */ } + } + alert(`Failed to send subscription: ${errorMsg}`); + } + } catch (error) { + alert(`Network error: ${error.message}`); + } +} \ No newline at end of file