externalise deeplinks

This commit is contained in:
cpu
2025-03-24 01:52:53 +01:00
parent d959f4929d
commit a0f3489656
5 changed files with 282 additions and 152 deletions

View File

@@ -1,5 +1,6 @@
// Import the audio manager // Import the audio manager
import audioManager from './audio.js'; import audioManager from './audio.js';
import deepLinkManager from './deeplinks.js';
// Initialize variables // Initialize variables
let players = []; let players = [];
@@ -545,7 +546,7 @@ function stopCameraStream() {
} }
} }
// Player form submit - FIXED VERSION // Player form submit
playerForm.addEventListener('submit', (e) => { playerForm.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
@@ -677,38 +678,15 @@ deletePlayerButton.addEventListener('click', () => {
audioManager.play('modalClose'); audioManager.play('modalClose');
}); });
// Function to get action from URL parameters (search or hash) // Function to create deep links
function getActionFromUrl() { function createDeepLink(action) {
// Check for action in both search params and hash return deepLinkManager.generateDeepLink(action);
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 handle deep link actions // Function to setup deep links
function handleActionFromUrl(action) { function setupDeepLinks() {
if (!action) return; // Register handlers for each action
deepLinkManager.registerHandler('start', () => {
console.log('Processing action:', action);
// Execute action based on the parameter
switch (action) {
case 'start':
if (gameState === 'setup' || gameState === 'paused') { if (gameState === 'setup' || gameState === 'paused') {
if (players.length < 2) { if (players.length < 2) {
console.log('Cannot start: Need at least 2 players'); console.log('Cannot start: Need at least 2 players');
@@ -721,8 +699,9 @@ function handleActionFromUrl(action) {
renderPlayers(); renderPlayers();
saveData(); saveData();
} }
break; });
case 'pause':
deepLinkManager.registerHandler('pause', () => {
if (gameState === 'running') { if (gameState === 'running') {
gameState = 'paused'; gameState = 'paused';
audioManager.play('gamePause'); audioManager.play('gamePause');
@@ -731,12 +710,14 @@ function handleActionFromUrl(action) {
renderPlayers(); renderPlayers();
saveData(); saveData();
} }
break; });
case 'toggle':
// Toggle between start/pause depending on current state deepLinkManager.registerHandler('toggle', () => {
// Simply trigger the game button click
gameButton.click(); gameButton.click();
break; });
case 'nextplayer':
deepLinkManager.registerHandler('nextplayer', () => {
if (gameState === 'running') { if (gameState === 'running') {
const nextIndex = findNextPlayerWithTimeCircular(currentPlayerIndex, 1); const nextIndex = findNextPlayerWithTimeCircular(currentPlayerIndex, 1);
if (nextIndex !== -1 && nextIndex !== currentPlayerIndex) { if (nextIndex !== -1 && nextIndex !== currentPlayerIndex) {
@@ -746,85 +727,17 @@ function handleActionFromUrl(action) {
saveData(); 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);
}
}
}
// 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();
}); });
deepLinkManager.registerHandler('reset', () => {
// Show the reset confirmation dialog
resetButton.click();
}); });
} else {
// If service workers aren't supported, still handle deep links // Process deep links on page load
window.addEventListener('load', handleDeepLink); 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 // Clean up when the modal is closed
function cleanupCameraData() { function cleanupCameraData() {
// Clear any captured image data // Clear any captured image data
@@ -839,12 +752,60 @@ function cleanupCameraData() {
cameraContainer.classList.remove('active'); 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 // Initialize the app
loadData(); loadData();
// Process URL parameters on initial load (needed for direct curl requests)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', handleDeepLink);
} else {
handleDeepLink();
}

135
deeplinks.js Normal file
View File

@@ -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;

View File

@@ -156,8 +156,7 @@
</script> </script>
<!-- Main application script --> <!-- Main application script -->
<script type="module" src="audio.js"></script> <script type="module" src="app.js"></script>
<script type="module" src="apps.js"></script>
<script> <script>
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js") navigator.serviceWorker.register("/sw.js")

View File

@@ -74,12 +74,26 @@
"url": "/?action=pause", "url": "/?action=pause",
"icons": [{ "src": "/icons/pause.png", "sizes": "192x192" }] "icons": [{ "src": "/icons/pause.png", "sizes": "192x192" }]
}, },
{
"name": "Toggle Game",
"short_name": "Toggle",
"description": "Toggle game state (start/pause)",
"url": "/?action=toggle",
"icons": [{ "src": "/icons/toggle.png", "sizes": "192x192" }]
},
{ {
"name": "Next Player", "name": "Next Player",
"short_name": "Next", "short_name": "Next",
"description": "Go to next player", "description": "Go to next player",
"url": "/?action=nextplayer", "url": "/?action=nextplayer",
"icons": [{ "src": "/icons/next.png", "sizes": "192x192" }] "icons": [{ "src": "/icons/next.png", "sizes": "192x192" }]
},
{
"name": "Reset Game",
"short_name": "Reset",
"description": "Reset the entire game",
"url": "/?action=reset",
"icons": [{ "src": "/icons/reset.png", "sizes": "192x192" }]
} }
] ]
} }

21
sw.js
View File

@@ -8,6 +8,7 @@ const CACHE_FILES = [
'/index.html', '/index.html',
'/app.js', '/app.js',
'/audio.js', '/audio.js',
'/deeplinks.js',
'/styles.css', '/styles.css',
'/manifest.json', '/manifest.json',
'/icons/android-chrome-192x192.png', '/icons/android-chrome-192x192.png',
@@ -17,6 +18,9 @@ const CACHE_FILES = [
'/icons/favicon-16x16.png' '/icons/favicon-16x16.png'
]; ];
// Valid deep link actions
const VALID_ACTIONS = ['start', 'pause', 'toggle', 'nextplayer', 'reset'];
// Install event - Cache files // Install event - Cache files
self.addEventListener('install', event => { self.addEventListener('install', event => {
console.log('[ServiceWorker] Install'); console.log('[ServiceWorker] Install');
@@ -64,9 +68,20 @@ self.addEventListener('fetch', event => {
// Check if request has action parameter or hash // Check if request has action parameter or hash
if (url.searchParams.has('action') || url.hash.includes('action=')) { if (url.searchParams.has('action') || url.hash.includes('action=')) {
console.log('[ServiceWorker] Processing deep link navigation'); console.log('[ServiceWorker] Processing deep link navigation');
// Verify the action is valid
const action = url.searchParams.get('action') ||
new URLSearchParams(url.hash.substring(1)).get('action');
if (action && VALID_ACTIONS.includes(action)) {
console.log('[ServiceWorker] Valid action found:', action);
// For navigation requests with valid actions, let the request go through
// so our app can handle the deep link
return; return;
} }
} }
}
event.respondWith( event.respondWith(
caches.match(event.request) caches.match(event.request)
@@ -115,6 +130,12 @@ self.addEventListener('message', event => {
const action = event.data.action; const action = event.data.action;
console.log('[ServiceWorker] Received action message:', action); console.log('[ServiceWorker] Received action message:', action);
// Validate the action
if (!VALID_ACTIONS.includes(action)) {
console.warn('[ServiceWorker] Invalid action received:', action);
return;
}
// Broadcast the action to all clients // Broadcast the action to all clients
self.clients.matchAll().then(clients => { self.clients.matchAll().then(clients => {
clients.forEach(client => { clients.forEach(client => {