push setup

This commit is contained in:
cpu
2025-03-29 06:01:10 +01:00
parent 2d8c1a6fff
commit 5ebd2cf005
4 changed files with 376 additions and 4 deletions

View File

@@ -367,4 +367,58 @@ input[type="file"] {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 0.3rem; 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;
} }

View File

@@ -24,6 +24,11 @@
<body> <body>
<div class="app-container"> <div class="app-container">
<header class="header"> <header class="header">
<div class="push-notification-controls">
<button id="pushSettingsButton" class="header-button" title="Push Notification Settings">
<i class="fas fa-bell"></i>
</button>
</div>
<div class="game-controls"> <div class="game-controls">
<button id="gameButton" class="game-button">Start Game</button> <button id="gameButton" class="game-button">Start Game</button>
</div> </div>
@@ -111,6 +116,35 @@
<button id="cameraCaptureButton" class="camera-button-large"></button> <button id="cameraCaptureButton" class="camera-button-large"></button>
</div> </div>
</div> </div>
<!-- Push Notification Settings Modal -->
<div id="pushSettingsModal" class="modal">
<div class="modal-content">
<h2>Push Notification Settings</h2>
<div class="notification-status-container">
<div class="notification-status">
<p><strong>Notification Permission: </strong><span id="notificationPermissionStatus">Unknown</span></p>
<p><strong>Subscription Status: </strong><span id="subscriptionStatus">Unknown</span></p>
</div>
</div>
<div class="form-group">
<label for="pushUsername">Username for Push Service</label>
<input type="text" id="pushUsername" placeholder="Enter username">
</div>
<div class="form-group">
<label for="pushPassword">Password</label>
<input type="password" id="pushPassword" placeholder="Enter password">
</div>
<div class="form-buttons">
<button type="button" id="pushCancelButton" class="cancel-button">Cancel</button>
<button type="button" id="pushSaveButton" class="save-button">Save & Subscribe</button>
</div>
<div class="advanced-options">
<button type="button" id="pushUnsubscribeButton" class="cancel-button">Unsubscribe</button>
<button type="button" id="pushResubscribeButton" class="save-button">Resubscribe</button>
</div>
</div>
</div>
<!-- Main application script --> <!-- Main application script -->
<script type="module" src="/js/app.js"></script> <script type="module" src="/js/app.js"></script>

View File

@@ -7,6 +7,7 @@ import camera from './ui/camera.js'; // Default export
import audioManager from './ui/audio.js'; import audioManager from './ui/audio.js';
import * as pushFlic from './services/pushFlicIntegration.js'; import * as pushFlic from './services/pushFlicIntegration.js';
import { initEnv } from './env-loader.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 externalized modules
import * as gameActions from './core/gameActions.js'; import * as gameActions from './core/gameActions.js';
@@ -70,7 +71,10 @@ async function initialize() {
ui.elements.resetCancelButton.addEventListener('click', eventHandlers.handleResetCancel); ui.elements.resetCancelButton.addEventListener('click', eventHandlers.handleResetCancel);
ui.elements.cameraButton.addEventListener('click', eventHandlers.handleCameraButtonClick); 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 = { const flicActionHandlers = {
[config.FLIC_ACTIONS.SINGLE_CLICK]: playerManager.nextPlayer, [config.FLIC_ACTIONS.SINGLE_CLICK]: playerManager.nextPlayer,
[config.FLIC_ACTIONS.DOUBLE_CLICK]: playerManager.previousPlayer, [config.FLIC_ACTIONS.DOUBLE_CLICK]: playerManager.previousPlayer,
@@ -78,14 +82,14 @@ async function initialize() {
}; };
serviceWorkerManager.setFlicActionHandlers(flicActionHandlers); serviceWorkerManager.setFlicActionHandlers(flicActionHandlers);
// 7. Setup Service Worker (which also initializes Flic) // 8. Setup Service Worker (which also initializes Flic)
serviceWorkerManager.setupServiceWorker(serviceWorkerManager.handleServiceWorkerMessage); serviceWorkerManager.setupServiceWorker(serviceWorkerManager.handleServiceWorkerMessage);
// 8. Initial UI Update based on loaded state // 9. Initial UI Update based on loaded state
ui.renderPlayers(); ui.renderPlayers();
ui.updateGameButton(); 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) { if (state.getGameState() === config.GAME_STATES.RUNNING) {
console.log("Game was running on load, setting to paused."); console.log("Game was running on load, setting to paused.");
state.setGameState(config.GAME_STATES.PAUSED); state.setGameState(config.GAME_STATES.PAUSED);

280
src/js/ui/pushSettingsUI.js Normal file
View File

@@ -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}`);
}
}