Initial commit
This commit is contained in:
251
js/services/pushFlicIntegration.js
Normal file
251
js/services/pushFlicIntegration.js
Normal file
@@ -0,0 +1,251 @@
|
||||
// pushFlicIntegration.js
|
||||
import { getPublicVapidKey, getBackendUrl, FLIC_BUTTON_ID} from '../config.js';
|
||||
|
||||
let pushSubscription = null; // Keep track locally if needed
|
||||
let actionHandlers = {}; // Store handlers for different Flic actions
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
// Get stored basic auth credentials or prompt user for them
|
||||
function getBasicAuthCredentials() {
|
||||
const storedAuth = localStorage.getItem('basicAuthCredentials');
|
||||
if (storedAuth) {
|
||||
try {
|
||||
const credentials = JSON.parse(storedAuth);
|
||||
// Check if the credentials are valid
|
||||
if (credentials.username && credentials.password) {
|
||||
console.log('Using stored basic auth credentials.');
|
||||
return credentials;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse stored credentials:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// No valid stored credentials found
|
||||
// The function will return null and the caller should handle prompting if needed
|
||||
console.log('No valid stored credentials found.');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create Basic Auth header string
|
||||
function createBasicAuthHeader(credentials) {
|
||||
if (!credentials?.username || !credentials.password) return null;
|
||||
return 'Basic ' + btoa(`${credentials.username}:${credentials.password}`);
|
||||
}
|
||||
|
||||
// Convert URL-safe base64 string to Uint8Array
|
||||
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;
|
||||
}
|
||||
|
||||
// Convert ArrayBuffer to URL-safe Base64 string
|
||||
function arrayBufferToBase64(buffer) {
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer);
|
||||
for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); }
|
||||
return window.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
// --- Push Subscription Logic ---
|
||||
|
||||
async function subscribeToPush() {
|
||||
const buttonId = FLIC_BUTTON_ID; // Use configured button ID
|
||||
|
||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
||||
console.error('Push Messaging is not supported.');
|
||||
alert('Push Notifications are not supported by your browser.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// First request notification permission
|
||||
console.log('Requesting notification permission...');
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission !== 'granted') {
|
||||
console.warn('Notification permission denied.');
|
||||
alert('Please enable notifications to link the Flic button.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Notification permission granted.');
|
||||
|
||||
// Get stored credentials but don't prompt
|
||||
let credentials = getBasicAuthCredentials();
|
||||
const hasExistingCreds = !!credentials;
|
||||
console.log('Has existing credentials:', hasExistingCreds);
|
||||
|
||||
// No prompting for credentials - user must enter them manually in the UI
|
||||
if (!credentials) {
|
||||
console.log('No credentials found. User needs to enter them manually.');
|
||||
// Just return if no credentials are available
|
||||
return;
|
||||
}
|
||||
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
let existingSubscription = await registration.pushManager.getSubscription();
|
||||
let needsResubscribe = !existingSubscription;
|
||||
|
||||
console.log('Existing subscription found:', !!existingSubscription);
|
||||
|
||||
if (existingSubscription) {
|
||||
const existingKey = existingSubscription.options?.applicationServerKey;
|
||||
if (!existingKey || arrayBufferToBase64(existingKey) !== getPublicVapidKey()) {
|
||||
console.log('VAPID key mismatch or missing. Unsubscribing old subscription.');
|
||||
await existingSubscription.unsubscribe();
|
||||
existingSubscription = null;
|
||||
needsResubscribe = true;
|
||||
} else {
|
||||
console.log('Existing valid subscription found.');
|
||||
pushSubscription = existingSubscription; // Store it
|
||||
}
|
||||
}
|
||||
|
||||
let finalSubscription = existingSubscription;
|
||||
if (needsResubscribe) {
|
||||
console.log('Subscribing for push notifications...');
|
||||
const applicationServerKey = urlBase64ToUint8Array(getPublicVapidKey());
|
||||
try {
|
||||
finalSubscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: applicationServerKey
|
||||
});
|
||||
console.log('New push subscription obtained:', finalSubscription);
|
||||
pushSubscription = finalSubscription; // Store it
|
||||
} catch (subscribeError) {
|
||||
console.error('Error subscribing to push:', subscribeError);
|
||||
alert(`Failed to subscribe: ${subscribeError.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalSubscription) {
|
||||
console.error("Failed to obtain a subscription object.");
|
||||
alert("Could not get subscription details.");
|
||||
return;
|
||||
}
|
||||
|
||||
await sendSubscriptionToServer(finalSubscription, buttonId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during push subscription:', error);
|
||||
alert(`Subscription failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendSubscriptionToServer(subscription, buttonId) {
|
||||
console.log(`Sending subscription for button "${buttonId}" to backend...`);
|
||||
const credentials = getBasicAuthCredentials();
|
||||
if (!credentials) {
|
||||
console.log('No credentials found. User needs to enter them manually.');
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const authHeader = createBasicAuthHeader(credentials);
|
||||
if (authHeader) headers['Authorization'] = authHeader;
|
||||
|
||||
try {
|
||||
// Add support for handling CORS preflight with credentials
|
||||
const response = await fetch(`${getBackendUrl()}/subscribe`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ button_id: buttonId, subscription: subscription }),
|
||||
headers: headers,
|
||||
credentials: 'include' // This ensures credentials are sent with OPTIONS requests too
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log('Subscription sent successfully:', result.message);
|
||||
|
||||
// Update the UI to show subscription status as active
|
||||
const subscriptionStatusElement = document.getElementById('subscriptionStatus');
|
||||
if (subscriptionStatusElement) {
|
||||
subscriptionStatusElement.textContent = 'active';
|
||||
subscriptionStatusElement.className = 'status-active';
|
||||
|
||||
// Enable unsubscribe button when subscription is active
|
||||
const unsubscribeButton = document.getElementById('pushUnsubscribeButton');
|
||||
if (unsubscribeButton) unsubscribeButton.disabled = false;
|
||||
|
||||
// Change subscribe button text to "Re-subscribe"
|
||||
const resubscribeButton = document.getElementById('pushResubscribeButton');
|
||||
if (resubscribeButton) resubscribeButton.textContent = 'Re-subscribe';
|
||||
|
||||
// Enable simulate button when subscription is active
|
||||
const simulateButton = document.getElementById('simulateClickButton');
|
||||
if (simulateButton) simulateButton.disabled = false;
|
||||
}
|
||||
|
||||
// Success alert removed as requested
|
||||
} else {
|
||||
let errorMsg = `Server error: ${response.status}`;
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
localStorage.removeItem('basicAuthCredentials'); // Clear bad creds
|
||||
errorMsg = 'Authentication failed. Please try again.';
|
||||
} else {
|
||||
try { errorMsg = (await response.json()).message || errorMsg; } catch (e) { /* use default */ }
|
||||
}
|
||||
console.error('Failed to send subscription:', errorMsg);
|
||||
alert(`Failed to save link: ${errorMsg}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error sending subscription:', error);
|
||||
alert(`Network error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Flic Action Handling ---
|
||||
|
||||
// Called by app.js when a message is received from the service worker
|
||||
export function handleFlicAction(action, buttonId, timestamp, batteryLevel) {
|
||||
console.log(`[PushFlic] Received Action: ${action} from Button: ${buttonId} battery: ${batteryLevel}% at ${timestamp}`);
|
||||
|
||||
// Ignore actions from buttons other than the configured one
|
||||
if (buttonId !== FLIC_BUTTON_ID) {
|
||||
console.warn(`[PushFlic] Ignoring action from unknown button: ${buttonId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the registered handler for this action
|
||||
const handler = actionHandlers[action];
|
||||
|
||||
if (handler && typeof handler === 'function') {
|
||||
console.log(`[PushFlic] Executing handler for ${action}`);
|
||||
try {
|
||||
// Execute the handler registered in app.js
|
||||
handler();
|
||||
// Log success
|
||||
console.log(`[PushFlic] Successfully executed handler for ${action}`);
|
||||
} catch (error) {
|
||||
console.error(`[PushFlic] Error executing handler for ${action}:`, error);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[PushFlic] No handler registered for action: ${action}. Available handlers:`, Object.keys(actionHandlers));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Initialization ---
|
||||
|
||||
// Initialize PushFlic with action handlers
|
||||
export function initPushFlic(handlers) {
|
||||
if (handlers && Object.keys(handlers).length > 0) {
|
||||
actionHandlers = handlers;
|
||||
console.log('[PushFlic] Stored action handlers:', Object.keys(actionHandlers));
|
||||
} else {
|
||||
console.warn('[PushFlic] No action handlers provided to initPushFlic!');
|
||||
}
|
||||
}
|
||||
|
||||
// New function to manually trigger the subscription process
|
||||
export function setupPushNotifications() {
|
||||
console.log('[PushFlic] Manually triggering push notification setup');
|
||||
subscribeToPush();
|
||||
}
|
||||
128
js/services/screenLockManager.js
Normal file
128
js/services/screenLockManager.js
Normal file
@@ -0,0 +1,128 @@
|
||||
// screenLockManager.js - Manages screen wake lock to prevent screen from turning off
|
||||
// Uses the Screen Wake Lock API: https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API
|
||||
|
||||
let wakeLock = null;
|
||||
let isLockEnabled = false;
|
||||
|
||||
/**
|
||||
* Requests a screen wake lock to prevent the screen from turning off
|
||||
* @returns {Promise<boolean>} - True if wake lock was acquired successfully
|
||||
*/
|
||||
export async function acquireWakeLock() {
|
||||
if (!isScreenWakeLockSupported()) {
|
||||
console.warn('[ScreenLockManager] Screen Wake Lock API not supported in this browser');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Release any existing wake lock first
|
||||
await releaseWakeLock();
|
||||
|
||||
// Request a new wake lock
|
||||
wakeLock = await navigator.wakeLock.request('screen');
|
||||
isLockEnabled = true;
|
||||
|
||||
console.log('[ScreenLockManager] Screen Wake Lock acquired');
|
||||
|
||||
// Add event listeners to reacquire the lock when needed
|
||||
setupWakeLockListeners();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[ScreenLockManager] Error acquiring wake lock:', error);
|
||||
isLockEnabled = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the screen wake lock if one is active
|
||||
* @returns {Promise<boolean>} - True if wake lock was released successfully
|
||||
*/
|
||||
export async function releaseWakeLock() {
|
||||
if (!wakeLock) {
|
||||
return true; // No wake lock to release
|
||||
}
|
||||
|
||||
try {
|
||||
await wakeLock.release();
|
||||
wakeLock = null;
|
||||
isLockEnabled = false;
|
||||
console.log('[ScreenLockManager] Screen Wake Lock released');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[ScreenLockManager] Error releasing wake lock:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Screen Wake Lock API is supported in this browser
|
||||
* @returns {boolean} - True if supported
|
||||
*/
|
||||
export function isScreenWakeLockSupported() {
|
||||
return 'wakeLock' in navigator && 'request' in navigator.wakeLock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current status of the wake lock
|
||||
* @returns {boolean} - True if wake lock is currently active
|
||||
*/
|
||||
export function isWakeLockActive() {
|
||||
return isLockEnabled && wakeLock !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event listeners to reacquire the wake lock when needed
|
||||
* (e.g., when the page becomes visible again after being hidden)
|
||||
*/
|
||||
function setupWakeLockListeners() {
|
||||
// When the page becomes visible again, reacquire the wake lock
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// When the screen orientation changes, reacquire the wake lock
|
||||
if ('screen' in window && 'orientation' in window.screen) {
|
||||
window.screen.orientation.addEventListener('change', handleOrientationChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles visibility change events to reacquire wake lock when page becomes visible
|
||||
*/
|
||||
async function handleVisibilityChange() {
|
||||
if (isLockEnabled && document.visibilityState === 'visible') {
|
||||
// Only try to reacquire if we previously had a lock
|
||||
await acquireWakeLock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles orientation change events to reacquire wake lock
|
||||
*/
|
||||
async function handleOrientationChange() {
|
||||
if (isLockEnabled) {
|
||||
// Some devices may release the wake lock on orientation change
|
||||
await acquireWakeLock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the screen lock manager
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {boolean} options.autoAcquire - Whether to automatically acquire wake lock on init
|
||||
* @returns {Promise<boolean>} - True if initialization was successful
|
||||
*/
|
||||
export async function initScreenLockManager(options = {}) {
|
||||
const { autoAcquire = true } = options; // Default to true - automatically acquire on init
|
||||
|
||||
// Check for support
|
||||
const isSupported = isScreenWakeLockSupported();
|
||||
console.log(`[ScreenLockManager] Screen Wake Lock API ${isSupported ? 'is' : 'is not'} supported`);
|
||||
|
||||
// Automatically acquire wake lock if supported (now default behavior)
|
||||
if (autoAcquire && isSupported) {
|
||||
return await acquireWakeLock();
|
||||
}
|
||||
|
||||
return isSupported;
|
||||
}
|
||||
116
js/services/serviceWorkerManager.js
Normal file
116
js/services/serviceWorkerManager.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// serviceWorkerManager.js - Service worker registration and Flic integration
|
||||
import * as pushFlic from './pushFlicIntegration.js';
|
||||
|
||||
// Store the action handlers passed from app.js
|
||||
let flicActionHandlers = {};
|
||||
|
||||
export function setFlicActionHandlers(handlers) {
|
||||
if (handlers && Object.keys(handlers).length > 0) {
|
||||
flicActionHandlers = handlers;
|
||||
console.log('[ServiceWorkerManager] Stored action handlers:', Object.keys(flicActionHandlers));
|
||||
|
||||
// Always pass handlers to pushFlic, regardless of service worker state
|
||||
pushFlic.initPushFlic(flicActionHandlers);
|
||||
} else {
|
||||
console.warn('[ServiceWorkerManager] No action handlers provided to setFlicActionHandlers!');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Flic Integration Setup ---
|
||||
export function initFlic() {
|
||||
// Make sure we have handlers before initializing
|
||||
if (Object.keys(flicActionHandlers).length === 0) {
|
||||
console.warn('[ServiceWorkerManager] No Flic handlers registered before initFlic! Actions may not work.');
|
||||
}
|
||||
|
||||
// This function is used by setupServiceWorker and relies on
|
||||
// flicActionHandlers being set before this is called
|
||||
console.log('[ServiceWorkerManager] Initializing PushFlic with handlers:', Object.keys(flicActionHandlers));
|
||||
pushFlic.initPushFlic(flicActionHandlers);
|
||||
}
|
||||
|
||||
// Export functions for manually triggering push notifications setup
|
||||
export function setupPushNotifications() {
|
||||
pushFlic.setupPushNotifications();
|
||||
}
|
||||
|
||||
// --- Handle Messages from Service Worker ---
|
||||
|
||||
export function flicMessageHandler(event) {
|
||||
// This function is passed to setupServiceWorker and called when a message arrives from the service worker
|
||||
console.log('[App] Message received from Service Worker:', event.data);
|
||||
|
||||
// Check if this is a Flic action message
|
||||
if (event.data && event.data.type === 'flic-action') {
|
||||
const { action, button, timestamp, batteryLevel } = event.data;
|
||||
|
||||
try {
|
||||
// Pass to push-flic service to handle
|
||||
pushFlic.handleFlicAction(action, button, timestamp, batteryLevel);
|
||||
} catch (error) {
|
||||
console.error('[App] Error handling flic action:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global message handler function to ensure we catch all service worker messages
|
||||
function handleServiceWorkerMessage(event) {
|
||||
// Check if the message might be from our service worker
|
||||
if (event.data && typeof event.data === 'object') {
|
||||
console.log('[App] Potential window message received:', event.data);
|
||||
|
||||
// If it looks like a flic action message, handle it
|
||||
if (event.data.type === 'flic-action') {
|
||||
try {
|
||||
// Process the message with our flicMessageHandler
|
||||
flicMessageHandler(event);
|
||||
} catch (error) {
|
||||
console.error('[App] Error handling window message:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Service Worker and PWA Setup ---
|
||||
export function setupServiceWorker(messageHandler) {
|
||||
if ('serviceWorker' in navigator) {
|
||||
console.log('[ServiceWorkerManager] Setting up service worker...');
|
||||
|
||||
// Set up global message event listener on window object
|
||||
window.addEventListener('message', handleServiceWorkerMessage);
|
||||
|
||||
// Listen for messages FROM the Service Worker
|
||||
// This is the main way messages from the service worker are received
|
||||
navigator.serviceWorker.addEventListener('message', event => {
|
||||
console.log('[ServiceWorkerManager] Service worker message received:', event.data);
|
||||
messageHandler(event);
|
||||
});
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
console.log('[ServiceWorkerManager] Window loaded, registering service worker...');
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('[ServiceWorkerManager] ServiceWorker registered successfully.');
|
||||
|
||||
// Add an event listener that will work with service worker controlled clients
|
||||
if (navigator.serviceWorker.controller) {
|
||||
console.log('[ServiceWorkerManager] Service worker already controlling the page.');
|
||||
}
|
||||
|
||||
// Initialize Flic integration
|
||||
initFlic();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[ServiceWorkerManager] ServiceWorker registration failed:', error);
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for SW controller changes
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
console.log('[ServiceWorkerManager] Service Worker controller changed, potentially updated.');
|
||||
});
|
||||
|
||||
} else {
|
||||
console.warn('[ServiceWorkerManager] ServiceWorker not supported.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user