From 2838df5e056278f3b6c78870f1d9c7244bc07cb0 Mon Sep 17 00:00:00 2001 From: cpu Date: Mon, 24 Mar 2025 00:43:55 +0100 Subject: [PATCH] added URL scheme/deep linking --- apps.js | 65 +++++++++++ index.html | 65 ++++++++++- index.html?action=toggle | 213 +++++++++++++++++++++++++++++++++++++ index.html?action=toggle.1 | 213 +++++++++++++++++++++++++++++++++++++ index.html?action=toggle.2 | 213 +++++++++++++++++++++++++++++++++++++ index.html?action=toggle.3 | 213 +++++++++++++++++++++++++++++++++++++ manifest.json | 36 +++++++ sw.js | 155 ++++++++++++++++++++++----- 8 files changed, 1142 insertions(+), 31 deletions(-) create mode 100644 index.html?action=toggle create mode 100644 index.html?action=toggle.1 create mode 100644 index.html?action=toggle.2 create mode 100644 index.html?action=toggle.3 diff --git a/apps.js b/apps.js index 99987c8..e4d699a 100644 --- a/apps.js +++ b/apps.js @@ -677,6 +677,65 @@ deletePlayerButton.addEventListener('click', () => { audioManager.play('modalClose'); }); +// Flic button action handler - Parse URL parameters and execute corresponding actions +function handleDeepLink() { + if (!window.location.hash) return; + + // Parse the hash to get action parameters + const params = new URLSearchParams(window.location.hash.substring(1)); + const action = params.get('action'); + + console.log('Received action from deep link:', 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(); + } + 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); + } + + // Clear the hash to prevent duplicate actions if page is refreshed + history.replaceState(null, null, ' '); +} + // Service Worker Registration if ('serviceWorker' in navigator) { window.addEventListener('load', () => { @@ -690,6 +749,12 @@ if ('serviceWorker' in navigator) { }); } +// Check for deep links when the page loads +window.addEventListener('load', handleDeepLink); + +// Also check for hash changes (needed for handling link activation when app is already open) +window.addEventListener('hashchange', handleDeepLink); + // Make sure to handle rotation by adding window event listener for orientation changes window.addEventListener('orientationchange', () => { // If camera is active, adjust video dimensions diff --git a/index.html b/index.html index 33c2d32..891ebbe 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,15 @@ + + + + + + + + +
@@ -87,7 +96,8 @@
- + +
@@ -98,7 +108,54 @@
- + + + + + + + \ No newline at end of file diff --git a/index.html?action=toggle b/index.html?action=toggle new file mode 100644 index 0000000..4cc4068 --- /dev/null +++ b/index.html?action=toggle @@ -0,0 +1,213 @@ + + + + + + + Game Timer + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + +
+
+ + +
+ + + + + + + + +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html?action=toggle.1 b/index.html?action=toggle.1 new file mode 100644 index 0000000..4cc4068 --- /dev/null +++ b/index.html?action=toggle.1 @@ -0,0 +1,213 @@ + + + + + + + Game Timer + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + +
+
+ + +
+ + + + + + + + +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html?action=toggle.2 b/index.html?action=toggle.2 new file mode 100644 index 0000000..4cc4068 --- /dev/null +++ b/index.html?action=toggle.2 @@ -0,0 +1,213 @@ + + + + + + + Game Timer + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + +
+
+ + +
+ + + + + + + + +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html?action=toggle.3 b/index.html?action=toggle.3 new file mode 100644 index 0000000..4cc4068 --- /dev/null +++ b/index.html?action=toggle.3 @@ -0,0 +1,213 @@ + + + + + + + Game Timer + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + +
+
+ + +
+ + + + + + + + +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/manifest.json b/manifest.json index 18f4f65..2d9091e 100644 --- a/manifest.json +++ b/manifest.json @@ -45,5 +45,41 @@ "sizes": "1082x2402", "type": "image/png" } + ], + "url_handlers": [ + { + "origin": "https://game-timer.virtonline.eu" + } + ], + "handle_links": "preferred", + "file_handlers": [], + "protocol_handlers": [ + { + "protocol": "web+gametimer", + "url": "/?action=%s" + } + ], + "shortcuts": [ + { + "name": "Start Game", + "short_name": "Start", + "description": "Start the game timer", + "url": "/?action=start", + "icons": [{ "src": "/icons/play.png", "sizes": "192x192" }] + }, + { + "name": "Pause Game", + "short_name": "Pause", + "description": "Pause the game timer", + "url": "/?action=pause", + "icons": [{ "src": "/icons/pause.png", "sizes": "192x192" }] + }, + { + "name": "Next Player", + "short_name": "Next", + "description": "Go to next player", + "url": "/?action=nextplayer", + "icons": [{ "src": "/icons/next.png", "sizes": "192x192" }] + } ] } \ No newline at end of file diff --git a/sw.js b/sw.js index 273a818..157e536 100644 --- a/sw.js +++ b/sw.js @@ -1,52 +1,153 @@ -// Updated service worker code - sw.js -const CACHE_NAME = 'timer-cache-v1'; -const urlsToCache = [ +// Service Worker version +const CACHE_VERSION = 'v1.0.0'; +const CACHE_NAME = `game-timer-${CACHE_VERSION}`; + +// Files to cache +const CACHE_FILES = [ '/', '/index.html', - '/styles.css', - '/apps.js', + '/app.js', '/audio.js', + '/styles.css', + '/manifest.json', '/icons/android-chrome-192x192.png', '/icons/android-chrome-512x512.png', '/icons/apple-touch-icon.png', '/icons/favicon-32x32.png', - '/icons/favicon-16x16.png', - '/favicon.ico', - '/manifest.json', - '/site.webmanifest' + '/icons/favicon-16x16.png' ]; +// Install event - Cache files self.addEventListener('install', event => { + console.log('[ServiceWorker] Install'); event.waitUntil( caches.open(CACHE_NAME) .then(cache => { - console.log('Opened cache'); - // Use individual cache.add calls in a Promise.all to handle failures better - return Promise.all( - urlsToCache.map(url => { - return cache.add(url).catch(err => { - console.log('Failed to cache:', url, err); - // Continue despite individual failures - return Promise.resolve(); - }); - }) - ); + console.log('[ServiceWorker] Caching app shell'); + return cache.addAll(CACHE_FILES); + }) + .then(() => { + console.log('[ServiceWorker] Skip waiting on install'); + return self.skipWaiting(); }) ); }); +// Activate event - Clean old caches +self.addEventListener('activate', event => { + console.log('[ServiceWorker] Activate'); + event.waitUntil( + caches.keys().then(keyList => { + return Promise.all(keyList.map(key => { + if (key !== CACHE_NAME) { + console.log('[ServiceWorker] Removing old cache', key); + return caches.delete(key); + } + })); + }) + .then(() => { + console.log('[ServiceWorker] Claiming clients'); + return self.clients.claim(); + }) + ); +}); + +// Fetch event - Serve from cache, fallback to network self.addEventListener('fetch', event => { + console.log('[ServiceWorker] Fetch', event.request.url); + + // For navigation requests that include our deep link parameters, + // skip the cache and go straight to network + if (event.request.mode === 'navigate') { + const url = new URL(event.request.url); + + // Check if request has action parameter or hash + if (url.searchParams.has('action') || url.hash.includes('action=')) { + console.log('[ServiceWorker] Processing deep link navigation'); + return; + } + } + event.respondWith( caches.match(event.request) .then(response => { - // Cache hit - return response - if (response) { - return response; - } - return fetch(event.request); + return response || fetch(event.request) + .then(res => { + // Check if we should cache this response + if (shouldCacheResponse(event.request, res)) { + return caches.open(CACHE_NAME) + .then(cache => { + console.log('[ServiceWorker] Caching new resource:', event.request.url); + cache.put(event.request, res.clone()); + return res; + }); + } + return res; + }); }) - .catch(err => { - console.log('Fetch handler failed:', err); + .catch(error => { + console.log('[ServiceWorker] Fetch failed; returning offline page', error); + // You could return a custom offline page here }) ); +}); + +// Helper function to determine if a response should be cached +function shouldCacheResponse(request, response) { + // Only cache GET requests + if (request.method !== 'GET') return false; + + // Don't cache errors + if (!response || response.status !== 200) return false; + + // Check if URL should be cached + const url = new URL(request.url); + + // Don't cache query parameters (except common ones for content) + if (url.search && !url.search.match(/\?(v|version|cache)=/)) return false; + + return true; +} + +// Handle deep links from other apps (including Flic) +self.addEventListener('message', event => { + if (event.data && event.data.type === 'PROCESS_ACTION') { + const action = event.data.action; + console.log('[ServiceWorker] Received action message:', action); + + // Broadcast the action to all clients + self.clients.matchAll().then(clients => { + clients.forEach(client => { + client.postMessage({ + type: 'ACTION', + action: action + }); + }); + }); + } +}); + +// This helps with navigation after app is installed +self.addEventListener('notificationclick', event => { + event.notification.close(); + + // This looks to see if the current is already open and focuses if it is + event.waitUntil( + self.clients.matchAll({ + type: 'window' + }) + .then(clientList => { + // Check if there is already a window/tab open with the target URL + for (const client of clientList) { + // If so, just focus it + if (client.url.startsWith(self.location.origin) && 'focus' in client) { + return client.focus(); + } + } + // If not, open a new window/tab + if (self.clients.openWindow) { + return self.clients.openWindow('/'); + } + }) + ); }); \ No newline at end of file