externalise deeplinks
This commit is contained in:
199
apps.js → app.js
199
apps.js → app.js
@@ -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)
|
// Function to setup deep links
|
||||||
const hashAction = hashParams.get('action');
|
function setupDeepLinks() {
|
||||||
if (hashAction) {
|
// Register handlers for each action
|
||||||
console.log('Found action in hash params:', hashAction);
|
deepLinkManager.registerHandler('start', () => {
|
||||||
return hashAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (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
|
deepLinkManager.registerHandler('reset', () => {
|
||||||
if ('serviceWorker' in navigator) {
|
// Show the reset confirmation dialog
|
||||||
window.addEventListener('load', () => {
|
resetButton.click();
|
||||||
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 {
|
// Process deep links on page load
|
||||||
// If service workers aren't supported, still handle deep links
|
deepLinkManager.processDeepLink();
|
||||||
window.addEventListener('load', handleDeepLink);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
135
deeplinks.js
Normal 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;
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
21
sw.js
@@ -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 => {
|
||||||
|
|||||||
Reference in New Issue
Block a user