From a0f348965603e88c604a95c4616e5a0e7c1abad0 Mon Sep 17 00:00:00 2001 From: cpu Date: Mon, 24 Mar 2025 01:52:53 +0100 Subject: [PATCH] externalise deeplinks --- apps.js => app.js | 259 ++++++++++++++++++++-------------------------- deeplinks.js | 135 ++++++++++++++++++++++++ index.html | 3 +- manifest.json | 14 +++ sw.js | 23 +++- 5 files changed, 282 insertions(+), 152 deletions(-) rename apps.js => app.js (87%) create mode 100644 deeplinks.js diff --git a/apps.js b/app.js similarity index 87% rename from apps.js rename to app.js index b9d429c..0b23594 100644 --- a/apps.js +++ b/app.js @@ -1,5 +1,6 @@ // Import the audio manager import audioManager from './audio.js'; +import deepLinkManager from './deeplinks.js'; // Initialize variables let players = []; @@ -545,7 +546,7 @@ function stopCameraStream() { } } -// Player form submit - FIXED VERSION +// Player form submit playerForm.addEventListener('submit', (e) => { e.preventDefault(); @@ -677,154 +678,66 @@ deletePlayerButton.addEventListener('click', () => { audioManager.play('modalClose'); }); -// Function to get action from URL parameters (search or hash) -function getActionFromUrl() { - // Check for action in both search params and hash - const searchParams = new URLSearchParams(window.location.search); - const hashParams = new URLSearchParams(window.location.hash.substring(1)); - - // First check search params (for direct curl or navigation) - const searchAction = searchParams.get('action'); - if (searchAction) { - console.log('Found action in search params:', searchAction); - return searchAction; - } - - // Then check hash params (existing deep link mechanism) - const hashAction = hashParams.get('action'); - if (hashAction) { - console.log('Found action in hash params:', hashAction); - return hashAction; - } - - return null; +// Function to create deep links +function createDeepLink(action) { + return deepLinkManager.generateDeepLink(action); } -// Function to handle deep link actions -function handleActionFromUrl(action) { - if (!action) return; - - console.log('Processing action:', action); - - // Execute action based on the parameter - switch (action) { - case 'start': - if (gameState === 'setup' || gameState === 'paused') { - if (players.length < 2) { - console.log('Cannot start: Need at least 2 players'); - return; - } - gameState = 'running'; - audioManager.play('gameStart'); - startTimer(); - updateGameButton(); - renderPlayers(); - saveData(); +// Function to setup deep links +function setupDeepLinks() { + // Register handlers for each action + deepLinkManager.registerHandler('start', () => { + if (gameState === 'setup' || gameState === 'paused') { + if (players.length < 2) { + console.log('Cannot start: Need at least 2 players'); + return; } - break; - case 'pause': - if (gameState === 'running') { - gameState = 'paused'; - audioManager.play('gamePause'); - stopTimer(); - updateGameButton(); - renderPlayers(); - saveData(); - } - break; - case 'toggle': - // Toggle between start/pause depending on current state - gameButton.click(); - break; - case 'nextplayer': - if (gameState === 'running') { - const nextIndex = findNextPlayerWithTimeCircular(currentPlayerIndex, 1); - if (nextIndex !== -1 && nextIndex !== currentPlayerIndex) { - currentPlayerIndex = nextIndex; - audioManager.play('playerSwitch'); - renderPlayers(); - saveData(); - } - } - break; - default: - console.log('Unknown action:', action); - } -} - -// Function to handle deep links from URL or Flic button -function handleDeepLink() { - // Get action from URL parameters - const action = getActionFromUrl(); - - // Process the action if found - if (action) { - handleActionFromUrl(action); - - // Clear the parameters to prevent duplicate actions if page is refreshed - if (window.history && window.history.replaceState) { - // Create new URL without the action parameter - const newUrl = window.location.pathname; - window.history.replaceState({}, document.title, newUrl); + gameState = 'running'; + audioManager.play('gameStart'); + startTimer(); + updateGameButton(); + renderPlayers(); + saveData(); } - } -} - -// Listen for service worker messages (for receiving actions while app is running) -navigator.serviceWorker.addEventListener('message', (event) => { - if (event.data && event.data.type === 'ACTION') { - console.log('Received action from service worker:', event.data.action); - handleActionFromUrl(event.data.action); - } -}); - -// Service Worker Registration -if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/sw.js') - .then(registration => { - console.log('ServiceWorker registered: ', registration); - - // Check for and handle deep links after service worker is ready - handleDeepLink(); - }) - .catch(error => { - console.log('ServiceWorker registration failed: ', error); - - // Still try to handle deep links even if service worker failed - handleDeepLink(); - }); }); -} else { - // If service workers aren't supported, still handle deep links - window.addEventListener('load', handleDeepLink); + + deepLinkManager.registerHandler('pause', () => { + if (gameState === 'running') { + gameState = 'paused'; + audioManager.play('gamePause'); + stopTimer(); + updateGameButton(); + renderPlayers(); + saveData(); + } + }); + + deepLinkManager.registerHandler('toggle', () => { + // Simply trigger the game button click + gameButton.click(); + }); + + deepLinkManager.registerHandler('nextplayer', () => { + if (gameState === 'running') { + const nextIndex = findNextPlayerWithTimeCircular(currentPlayerIndex, 1); + if (nextIndex !== -1 && nextIndex !== currentPlayerIndex) { + currentPlayerIndex = nextIndex; + audioManager.play('playerSwitch'); + renderPlayers(); + saveData(); + } + } + }); + + deepLinkManager.registerHandler('reset', () => { + // Show the reset confirmation dialog + resetButton.click(); + }); + + // Process deep links on page load + deepLinkManager.processDeepLink(); } -// Also check for hash changes (needed for handling link activation when app is already open) -window.addEventListener('hashchange', () => { - console.log('Hash changed, checking for actions'); - handleDeepLink(); -}); - -// Also check for navigation events that might include search parameters -window.addEventListener('popstate', () => { - console.log('Navigation occurred, checking for actions'); - handleDeepLink(); -}); - -// Make sure to handle rotation by adding window event listener for orientation changes -window.addEventListener('orientationchange', () => { - // If camera is active, adjust video dimensions - if (cameraContainer.classList.contains('active') && stream) { - // Give a moment for the orientation to complete - setTimeout(() => { - // This may cause the video to briefly reset but will ensure proper dimensions - cameraView.srcObject = null; - cameraView.srcObject = stream; - }, 300); - } -}); - // Clean up when the modal is closed function cleanupCameraData() { // Clear any captured image data @@ -839,12 +752,60 @@ function cleanupCameraData() { cameraContainer.classList.remove('active'); } +// Make sure to handle rotation by adding window event listener for orientation changes +window.addEventListener('orientationchange', () => { + // If camera is active, adjust video dimensions + if (cameraContainer.classList.contains('active') && stream) { + // Give a moment for the orientation to complete + setTimeout(() => { + // This may cause the video to briefly reset but will ensure proper dimensions + cameraView.srcObject = null; + cameraView.srcObject = stream; + }, 300); + } +}); + +// Listen for service worker messages (for receiving actions while app is running) +navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data && event.data.type === 'ACTION') { + console.log('Received action from service worker:', event.data.action); + deepLinkManager.handleAction(event.data.action); + } +}); + +// Service Worker Registration +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js') + .then(registration => { + console.log('ServiceWorker registered: ', registration); + + // Setup and handle deep links after service worker is ready + setupDeepLinks(); + }) + .catch(error => { + console.log('ServiceWorker registration failed: ', error); + + // Still try to handle deep links even if service worker failed + setupDeepLinks(); + }); + }); +} else { + // If service workers aren't supported, still handle deep links + window.addEventListener('load', setupDeepLinks); +} + +// Also check for hash changes (needed for handling link activation when app is already open) +window.addEventListener('hashchange', () => { + console.log('Hash changed, checking for actions'); + deepLinkManager.processDeepLink(); +}); + +// Also check for navigation events that might include search parameters +window.addEventListener('popstate', () => { + console.log('Navigation occurred, checking for actions'); + deepLinkManager.processDeepLink(); +}); + // Initialize the app loadData(); - -// Process URL parameters on initial load (needed for direct curl requests) -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', handleDeepLink); -} else { - handleDeepLink(); -} \ No newline at end of file diff --git a/deeplinks.js b/deeplinks.js new file mode 100644 index 0000000..374f0f6 --- /dev/null +++ b/deeplinks.js @@ -0,0 +1,135 @@ +// deeplinks.js - Deep Link Manager for Game Timer + +// Available actions +const VALID_ACTIONS = ['start', 'pause', 'toggle', 'nextplayer', 'reset']; + +// Class to manage all deep link functionality +class DeepLinkManager { + constructor() { + this.actionHandlers = {}; + + // Initialize listeners + this.initServiceWorkerListener(); + this.initHashChangeListener(); + this.initPopStateListener(); + } + + // Register action handlers + registerHandler(action, handlerFn) { + if (VALID_ACTIONS.includes(action)) { + this.actionHandlers[action] = handlerFn; + console.log(`Registered handler for action: ${action}`); + } else { + console.warn(`Attempted to register handler for invalid action: ${action}`); + } + } + + // Extract action from URL parameters (search or hash) + getActionFromUrl() { + // Check for action in both search params and hash + const searchParams = new URLSearchParams(window.location.search); + const hashParams = new URLSearchParams(window.location.hash.substring(1)); + + // First check search params (for direct curl or navigation) + const searchAction = searchParams.get('action'); + if (searchAction && VALID_ACTIONS.includes(searchAction)) { + console.log('Found action in search params:', searchAction); + return searchAction; + } + + // Then check hash params (existing deep link mechanism) + const hashAction = hashParams.get('action'); + if (hashAction && VALID_ACTIONS.includes(hashAction)) { + console.log('Found action in hash params:', hashAction); + return hashAction; + } + + return null; + } + + // Process an action + handleAction(action) { + if (!action) return; + + console.log('Processing action:', action); + + if (this.actionHandlers[action]) { + this.actionHandlers[action](); + } else { + console.log('No handler registered for action:', action); + } + } + + // Handle deep links from URL + processDeepLink() { + // Get action from URL parameters + const action = this.getActionFromUrl(); + + // Process the action if found + if (action) { + this.handleAction(action); + + // Clear the parameters to prevent duplicate actions if page is refreshed + if (window.history && window.history.replaceState) { + // Create new URL without the action parameter + const newUrl = window.location.pathname; + window.history.replaceState({}, document.title, newUrl); + } + } + } + + // Initialize service worker message listener + initServiceWorkerListener() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data && event.data.type === 'ACTION') { + console.log('Received action from service worker:', event.data.action); + this.handleAction(event.data.action); + } + }); + } + } + + // Initialize hash change listener + initHashChangeListener() { + window.addEventListener('hashchange', () => { + console.log('Hash changed, checking for actions'); + this.processDeepLink(); + }); + } + + // Initialize popstate listener for navigation events + initPopStateListener() { + window.addEventListener('popstate', () => { + console.log('Navigation occurred, checking for actions'); + this.processDeepLink(); + }); + } + + // Send an action to the service worker + sendActionToServiceWorker(action) { + if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ + type: 'PROCESS_ACTION', + action: action + }); + } + } + + // Generate a deep link URL for a specific action + generateDeepLink(action, useHash = false) { + if (!VALID_ACTIONS.includes(action)) { + console.warn(`Cannot generate deep link for invalid action: ${action}`); + return null; + } + + const baseUrl = window.location.origin + window.location.pathname; + return useHash ? + `${baseUrl}#action=${action}` : + `${baseUrl}?action=${action}`; + } +} + +// Export singleton instance +const deepLinkManager = new DeepLinkManager(); +export default deepLinkManager; \ No newline at end of file diff --git a/index.html b/index.html index 891ebbe..0d76fbf 100644 --- a/index.html +++ b/index.html @@ -156,8 +156,7 @@ - - +