added URL scheme/deep linking

This commit is contained in:
cpu
2025-03-24 00:43:55 +01:00
parent 8a6947f4ea
commit 2838df5e05
8 changed files with 1142 additions and 31 deletions

65
apps.js
View File

@@ -677,6 +677,65 @@ deletePlayerButton.addEventListener('click', () => {
audioManager.play('modalClose'); 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 // Service Worker Registration
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', () => { 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 // Make sure to handle rotation by adding window event listener for orientation changes
window.addEventListener('orientationchange', () => { window.addEventListener('orientationchange', () => {
// If camera is active, adjust video dimensions // If camera is active, adjust video dimensions

View File

@@ -8,6 +8,15 @@
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<!-- Deep Linking - App Links for Android -->
<link rel="alternate" href="android-app://eu.virtonline.gametimer/https://game-timer.virtonline.eu" />
<!-- Deep Linking - Universal Links for iOS -->
<meta name="apple-itunes-app" content="app-id=yourAppID, app-argument=https://game-timer.virtonline.eu">
<!-- Deep Linking - Web App URL Handling -->
<link rel="alternate" href="web+gametimer://action" />
</head> </head>
<body> <body>
<div class="app-container"> <div class="app-container">
@@ -87,7 +96,8 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Camera Capture UI -->
<div id="cameraContainer" class="camera-container"> <div id="cameraContainer" class="camera-container">
<div class="camera-view"> <div class="camera-view">
<video id="cameraView" autoplay playsinline></video> <video id="cameraView" autoplay playsinline></video>
@@ -98,7 +108,54 @@
<button id="cameraCaptureButton" class="camera-button-large"></button> <button id="cameraCaptureButton" class="camera-button-large"></button>
</div> </div>
</div> </div>
<!-- Script for handling URL scheme and deep links -->
<script type="module">
// Register the custom URL protocol handler (web+gametimer://)
if ('registerProtocolHandler' in navigator) {
try {
navigator.registerProtocolHandler(
'web+gametimer',
'https://game-timer.virtonline.eu/?action=%s',
'Game Timer'
);
console.log('Protocol handler registered');
} catch (e) {
console.error('Failed to register protocol handler:', e);
}
}
// Function to parse URL parameters
function getUrlParams() {
const searchParams = new URLSearchParams(window.location.search);
const hashParams = new URLSearchParams(window.location.hash.substring(1));
// Check search parameters first (for direct links)
const action = searchParams.get('action');
if (action) {
// Clean the action parameter (remove 'web+gametimer://' if present)
return action.replace('web+gametimer://', '');
}
// Then check hash parameters (for deep links)
return hashParams.get('action');
}
// Initialize URL handling
function initUrlHandling() {
const action = getUrlParams();
if (action) {
console.log('URL action detected:', action);
// Set the action in the hash to be processed by the main app
window.location.hash = `action=${action}`;
}
}
// Run initialization when DOM is fully loaded
document.addEventListener('DOMContentLoaded', initUrlHandling);
</script>
<!-- Main application script -->
<script type="module" src="audio.js"></script> <script type="module" src="audio.js"></script>
<script type="module" src="apps.js"></script> <script type="module" src="apps.js"></script>
<script> <script>
@@ -107,12 +164,12 @@
.then(() => console.log("Service Worker Registered")) .then(() => console.log("Service Worker Registered"))
.catch((err) => console.log("Service Worker Failed", err)); .catch((err) => console.log("Service Worker Failed", err));
} }
</script> </script>
<footer class="app-footer"> <footer class="app-footer">
<div class="author-info"> <div class="author-info">
<p>Vibe coded by Martin</p> <p>Vibe coded by Martin</p>
<p>Version 0.0.1</p> <p>Version 0.0.1</p>
</div> </div>
</footer> </footer>
</body> </body>
</html> </html>

213
index.html?action=toggle Normal file
View File

@@ -0,0 +1,213 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#2c3e50">
<title>Game Timer</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link rel="stylesheet" href="styles.css">
<!-- Deep Linking - App Links for Android -->
<link rel="alternate" href="android-app://eu.virtonline.gametimer/https/game-timer.virtonline.eu" />
<!-- Deep Linking - Universal Links for iOS -->
<meta name="apple-itunes-app" content="app-id=yourAppID, app-argument=https://game-timer.virtonline.eu">
<!-- Deep Linking - Web App URL Handling -->
<link rel="alternate" href="web+gametimer://action" />
</head>
<body>
<div class="app-container">
<header class="header">
<div class="game-controls">
<button id="gameButton" class="game-button">Start Game</button>
</div>
<div class="header-buttons">
<button id="resetButton" class="header-button" title="Reset All Data">
<i class="fas fa-redo-alt"></i>
</button>
<button id="addPlayerButton" class="header-button" title="Add New Player">
<i class="fas fa-user-plus"></i>
</button>
<button id="setupButton" class="header-button" title="Setup Current Player">
<i class="fas fa-cog"></i>
</button>
</div>
</header>
<div class="carousel-container">
<div id="carousel" class="carousel">
<!-- Player cards will be dynamically inserted here -->
</div>
</div>
</div>
<!-- Player Edit Modal -->
<div id="playerModal" class="modal">
<div class="modal-content">
<h2 id="modalTitle">Edit Player</h2>
<form id="playerForm">
<div class="form-group">
<label for="playerName">Name</label>
<input type="text" id="playerName" required>
</div>
<div class="form-group">
<label for="playerImage">Image</label>
<div class="image-input-container">
<input type="file" id="playerImage" accept="image/*">
<button type="button" id="cameraButton" class="camera-button">
<i class="fas fa-camera"></i> Take Photo
</button>
</div>
<div id="imagePreview" class="player-image" style="margin-top: 0.5rem;">
<i class="fas fa-user"></i>
</div>
</div>
<div id="playerTimeContainer" class="form-group">
<label for="playerTime">Time (minutes)</label>
<input type="number" id="playerTime" min="1" max="180" value="5" required>
</div>
<div id="remainingTimeContainer" class="form-group" style="display: none;">
<label for="playerRemainingTime">Remaining Time (MM:SS)</label>
<input type="text" id="playerRemainingTime" pattern="[0-9]{2}:[0-9]{2}" placeholder="05:00">
<small>Format: Minutes:Seconds (e.g., 05:30)</small>
</div>
<div class="form-buttons">
<button type="button" id="cancelButton" class="cancel-button">Cancel</button>
<button type="submit" class="save-button">Save</button>
</div>
<div class="form-buttons delete-button-container">
<button type="button" id="deletePlayerButton" class="delete-button">Delete Player</button>
</div>
</form>
</div>
</div>
<!-- Reset Confirmation Modal -->
<div id="resetModal" class="modal">
<div class="modal-content">
<h2>Reset All Data</h2>
<p>Are you sure you want to reset all players and timers? This action cannot be undone.</p>
<div class="form-buttons">
<button type="button" id="resetCancelButton" class="cancel-button">Cancel</button>
<button type="button" id="resetConfirmButton" class="save-button">Reset</button>
</div>
</div>
</div>
<!-- Camera Capture UI -->
<div id="cameraContainer" class="camera-container">
<div class="camera-view">
<video id="cameraView" autoplay playsinline></video>
<canvas id="cameraCanvas" style="display: none;"></canvas>
</div>
<div class="camera-controls">
<button id="cameraCancelButton" class="camera-button-cancel">Cancel</button>
<button id="cameraCaptureButton" class="camera-button-large"></button>
</div>
</div>
<!-- Script for handling URL scheme and deep links -->
<script type="module">
// Register the custom URL protocol handler (web+gametimer://)
if ('registerProtocolHandler' in navigator) {
try {
navigator.registerProtocolHandler(
'web+gametimer',
'https://game-timer.virtonline.eu/?action=%s',
'Game Timer'
);
console.log('Protocol handler registered');
} catch (e) {
console.error('Failed to register protocol handler:', e);
}
}
// Function to parse URL parameters
function getUrlParams() {
const searchParams = new URLSearchParams(window.location.search);
const hashParams = new URLSearchParams(window.location.hash.substring(1));
// Check search parameters first (for direct links)
const action = searchParams.get('action');
if (action) {
// Clean the action parameter (remove 'web+gametimer://' if present)
return action.replace('web+gametimer://', '');
}
// Then check hash parameters (for deep links)
return hashParams.get('action');
}
// Initialize URL handling
function initUrlHandling() {
const action = getUrlParams();
if (action) {
console.log('URL action detected:', action);
// Set the action in the hash to be processed by the main app
window.location.hash = `action=${action}`;
}
}
// Run initialization when DOM is fully loaded
document.addEventListener('DOMContentLoaded', initUrlHandling);
</script>
<!-- Main application script -->
<script type="module" src="audio.js"></script>
<script type="module" src="apps.js"></script>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js")
.then(() => console.log("Service Worker Registered"))
.catch((err) => console.log("Service Worker Failed", err));
}
</script>
<footer class="app-footer">
<div class="author-info">
<p>Vibe coded by Martin</p>
<p>Version 0.0.1</p>
</div>
</footer>
<!-- Code injected by live-server -->
<script>
// <![CDATA[ <-- For SVG support
if ('WebSocket' in window) {
(function () {
function refreshCSS() {
var sheets = [].slice.call(document.getElementsByTagName("link"));
var head = document.getElementsByTagName("head")[0];
for (var i = 0; i < sheets.length; ++i) {
var elem = sheets[i];
var parent = elem.parentElement || head;
parent.removeChild(elem);
var rel = elem.rel;
if (elem.href && typeof rel != "string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
var url = elem.href.replace(/(&|\?)_cacheOverride=\d+/, '');
elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf());
}
parent.appendChild(elem);
}
}
var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
var address = protocol + window.location.host + window.location.pathname + '/ws';
var socket = new WebSocket(address);
socket.onmessage = function (msg) {
if (msg.data == 'reload') window.location.reload();
else if (msg.data == 'refreshcss') refreshCSS();
};
if (sessionStorage && !sessionStorage.getItem('IsThisFirstTime_Log_From_LiveServer')) {
console.log('Live reload enabled.');
sessionStorage.setItem('IsThisFirstTime_Log_From_LiveServer', true);
}
})();
}
else {
console.error('Upgrade your browser. This Browser is NOT supported WebSocket for Live-Reloading.');
}
// ]]>
</script>
</body>
</html>

213
index.html?action=toggle.1 Normal file
View File

@@ -0,0 +1,213 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#2c3e50">
<title>Game Timer</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link rel="stylesheet" href="styles.css">
<!-- Deep Linking - App Links for Android -->
<link rel="alternate" href="android-app://eu.virtonline.gametimer/https/game-timer.virtonline.eu" />
<!-- Deep Linking - Universal Links for iOS -->
<meta name="apple-itunes-app" content="app-id=yourAppID, app-argument=https://game-timer.virtonline.eu">
<!-- Deep Linking - Web App URL Handling -->
<link rel="alternate" href="web+gametimer://action" />
</head>
<body>
<div class="app-container">
<header class="header">
<div class="game-controls">
<button id="gameButton" class="game-button">Start Game</button>
</div>
<div class="header-buttons">
<button id="resetButton" class="header-button" title="Reset All Data">
<i class="fas fa-redo-alt"></i>
</button>
<button id="addPlayerButton" class="header-button" title="Add New Player">
<i class="fas fa-user-plus"></i>
</button>
<button id="setupButton" class="header-button" title="Setup Current Player">
<i class="fas fa-cog"></i>
</button>
</div>
</header>
<div class="carousel-container">
<div id="carousel" class="carousel">
<!-- Player cards will be dynamically inserted here -->
</div>
</div>
</div>
<!-- Player Edit Modal -->
<div id="playerModal" class="modal">
<div class="modal-content">
<h2 id="modalTitle">Edit Player</h2>
<form id="playerForm">
<div class="form-group">
<label for="playerName">Name</label>
<input type="text" id="playerName" required>
</div>
<div class="form-group">
<label for="playerImage">Image</label>
<div class="image-input-container">
<input type="file" id="playerImage" accept="image/*">
<button type="button" id="cameraButton" class="camera-button">
<i class="fas fa-camera"></i> Take Photo
</button>
</div>
<div id="imagePreview" class="player-image" style="margin-top: 0.5rem;">
<i class="fas fa-user"></i>
</div>
</div>
<div id="playerTimeContainer" class="form-group">
<label for="playerTime">Time (minutes)</label>
<input type="number" id="playerTime" min="1" max="180" value="5" required>
</div>
<div id="remainingTimeContainer" class="form-group" style="display: none;">
<label for="playerRemainingTime">Remaining Time (MM:SS)</label>
<input type="text" id="playerRemainingTime" pattern="[0-9]{2}:[0-9]{2}" placeholder="05:00">
<small>Format: Minutes:Seconds (e.g., 05:30)</small>
</div>
<div class="form-buttons">
<button type="button" id="cancelButton" class="cancel-button">Cancel</button>
<button type="submit" class="save-button">Save</button>
</div>
<div class="form-buttons delete-button-container">
<button type="button" id="deletePlayerButton" class="delete-button">Delete Player</button>
</div>
</form>
</div>
</div>
<!-- Reset Confirmation Modal -->
<div id="resetModal" class="modal">
<div class="modal-content">
<h2>Reset All Data</h2>
<p>Are you sure you want to reset all players and timers? This action cannot be undone.</p>
<div class="form-buttons">
<button type="button" id="resetCancelButton" class="cancel-button">Cancel</button>
<button type="button" id="resetConfirmButton" class="save-button">Reset</button>
</div>
</div>
</div>
<!-- Camera Capture UI -->
<div id="cameraContainer" class="camera-container">
<div class="camera-view">
<video id="cameraView" autoplay playsinline></video>
<canvas id="cameraCanvas" style="display: none;"></canvas>
</div>
<div class="camera-controls">
<button id="cameraCancelButton" class="camera-button-cancel">Cancel</button>
<button id="cameraCaptureButton" class="camera-button-large"></button>
</div>
</div>
<!-- Script for handling URL scheme and deep links -->
<script type="module">
// Register the custom URL protocol handler (web+gametimer://)
if ('registerProtocolHandler' in navigator) {
try {
navigator.registerProtocolHandler(
'web+gametimer',
'https://game-timer.virtonline.eu/?action=%s',
'Game Timer'
);
console.log('Protocol handler registered');
} catch (e) {
console.error('Failed to register protocol handler:', e);
}
}
// Function to parse URL parameters
function getUrlParams() {
const searchParams = new URLSearchParams(window.location.search);
const hashParams = new URLSearchParams(window.location.hash.substring(1));
// Check search parameters first (for direct links)
const action = searchParams.get('action');
if (action) {
// Clean the action parameter (remove 'web+gametimer://' if present)
return action.replace('web+gametimer://', '');
}
// Then check hash parameters (for deep links)
return hashParams.get('action');
}
// Initialize URL handling
function initUrlHandling() {
const action = getUrlParams();
if (action) {
console.log('URL action detected:', action);
// Set the action in the hash to be processed by the main app
window.location.hash = `action=${action}`;
}
}
// Run initialization when DOM is fully loaded
document.addEventListener('DOMContentLoaded', initUrlHandling);
</script>
<!-- Main application script -->
<script type="module" src="audio.js"></script>
<script type="module" src="apps.js"></script>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js")
.then(() => console.log("Service Worker Registered"))
.catch((err) => console.log("Service Worker Failed", err));
}
</script>
<footer class="app-footer">
<div class="author-info">
<p>Vibe coded by Martin</p>
<p>Version 0.0.1</p>
</div>
</footer>
<!-- Code injected by live-server -->
<script>
// <![CDATA[ <-- For SVG support
if ('WebSocket' in window) {
(function () {
function refreshCSS() {
var sheets = [].slice.call(document.getElementsByTagName("link"));
var head = document.getElementsByTagName("head")[0];
for (var i = 0; i < sheets.length; ++i) {
var elem = sheets[i];
var parent = elem.parentElement || head;
parent.removeChild(elem);
var rel = elem.rel;
if (elem.href && typeof rel != "string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
var url = elem.href.replace(/(&|\?)_cacheOverride=\d+/, '');
elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf());
}
parent.appendChild(elem);
}
}
var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
var address = protocol + window.location.host + window.location.pathname + '/ws';
var socket = new WebSocket(address);
socket.onmessage = function (msg) {
if (msg.data == 'reload') window.location.reload();
else if (msg.data == 'refreshcss') refreshCSS();
};
if (sessionStorage && !sessionStorage.getItem('IsThisFirstTime_Log_From_LiveServer')) {
console.log('Live reload enabled.');
sessionStorage.setItem('IsThisFirstTime_Log_From_LiveServer', true);
}
})();
}
else {
console.error('Upgrade your browser. This Browser is NOT supported WebSocket for Live-Reloading.');
}
// ]]>
</script>
</body>
</html>

213
index.html?action=toggle.2 Normal file
View File

@@ -0,0 +1,213 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#2c3e50">
<title>Game Timer</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link rel="stylesheet" href="styles.css">
<!-- Deep Linking - App Links for Android -->
<link rel="alternate" href="android-app://eu.virtonline.gametimer/https/game-timer.virtonline.eu" />
<!-- Deep Linking - Universal Links for iOS -->
<meta name="apple-itunes-app" content="app-id=yourAppID, app-argument=https://game-timer.virtonline.eu">
<!-- Deep Linking - Web App URL Handling -->
<link rel="alternate" href="web+gametimer://action" />
</head>
<body>
<div class="app-container">
<header class="header">
<div class="game-controls">
<button id="gameButton" class="game-button">Start Game</button>
</div>
<div class="header-buttons">
<button id="resetButton" class="header-button" title="Reset All Data">
<i class="fas fa-redo-alt"></i>
</button>
<button id="addPlayerButton" class="header-button" title="Add New Player">
<i class="fas fa-user-plus"></i>
</button>
<button id="setupButton" class="header-button" title="Setup Current Player">
<i class="fas fa-cog"></i>
</button>
</div>
</header>
<div class="carousel-container">
<div id="carousel" class="carousel">
<!-- Player cards will be dynamically inserted here -->
</div>
</div>
</div>
<!-- Player Edit Modal -->
<div id="playerModal" class="modal">
<div class="modal-content">
<h2 id="modalTitle">Edit Player</h2>
<form id="playerForm">
<div class="form-group">
<label for="playerName">Name</label>
<input type="text" id="playerName" required>
</div>
<div class="form-group">
<label for="playerImage">Image</label>
<div class="image-input-container">
<input type="file" id="playerImage" accept="image/*">
<button type="button" id="cameraButton" class="camera-button">
<i class="fas fa-camera"></i> Take Photo
</button>
</div>
<div id="imagePreview" class="player-image" style="margin-top: 0.5rem;">
<i class="fas fa-user"></i>
</div>
</div>
<div id="playerTimeContainer" class="form-group">
<label for="playerTime">Time (minutes)</label>
<input type="number" id="playerTime" min="1" max="180" value="5" required>
</div>
<div id="remainingTimeContainer" class="form-group" style="display: none;">
<label for="playerRemainingTime">Remaining Time (MM:SS)</label>
<input type="text" id="playerRemainingTime" pattern="[0-9]{2}:[0-9]{2}" placeholder="05:00">
<small>Format: Minutes:Seconds (e.g., 05:30)</small>
</div>
<div class="form-buttons">
<button type="button" id="cancelButton" class="cancel-button">Cancel</button>
<button type="submit" class="save-button">Save</button>
</div>
<div class="form-buttons delete-button-container">
<button type="button" id="deletePlayerButton" class="delete-button">Delete Player</button>
</div>
</form>
</div>
</div>
<!-- Reset Confirmation Modal -->
<div id="resetModal" class="modal">
<div class="modal-content">
<h2>Reset All Data</h2>
<p>Are you sure you want to reset all players and timers? This action cannot be undone.</p>
<div class="form-buttons">
<button type="button" id="resetCancelButton" class="cancel-button">Cancel</button>
<button type="button" id="resetConfirmButton" class="save-button">Reset</button>
</div>
</div>
</div>
<!-- Camera Capture UI -->
<div id="cameraContainer" class="camera-container">
<div class="camera-view">
<video id="cameraView" autoplay playsinline></video>
<canvas id="cameraCanvas" style="display: none;"></canvas>
</div>
<div class="camera-controls">
<button id="cameraCancelButton" class="camera-button-cancel">Cancel</button>
<button id="cameraCaptureButton" class="camera-button-large"></button>
</div>
</div>
<!-- Script for handling URL scheme and deep links -->
<script type="module">
// Register the custom URL protocol handler (web+gametimer://)
if ('registerProtocolHandler' in navigator) {
try {
navigator.registerProtocolHandler(
'web+gametimer',
'https://game-timer.virtonline.eu/?action=%s',
'Game Timer'
);
console.log('Protocol handler registered');
} catch (e) {
console.error('Failed to register protocol handler:', e);
}
}
// Function to parse URL parameters
function getUrlParams() {
const searchParams = new URLSearchParams(window.location.search);
const hashParams = new URLSearchParams(window.location.hash.substring(1));
// Check search parameters first (for direct links)
const action = searchParams.get('action');
if (action) {
// Clean the action parameter (remove 'web+gametimer://' if present)
return action.replace('web+gametimer://', '');
}
// Then check hash parameters (for deep links)
return hashParams.get('action');
}
// Initialize URL handling
function initUrlHandling() {
const action = getUrlParams();
if (action) {
console.log('URL action detected:', action);
// Set the action in the hash to be processed by the main app
window.location.hash = `action=${action}`;
}
}
// Run initialization when DOM is fully loaded
document.addEventListener('DOMContentLoaded', initUrlHandling);
</script>
<!-- Main application script -->
<script type="module" src="audio.js"></script>
<script type="module" src="apps.js"></script>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js")
.then(() => console.log("Service Worker Registered"))
.catch((err) => console.log("Service Worker Failed", err));
}
</script>
<footer class="app-footer">
<div class="author-info">
<p>Vibe coded by Martin</p>
<p>Version 0.0.1</p>
</div>
</footer>
<!-- Code injected by live-server -->
<script>
// <![CDATA[ <-- For SVG support
if ('WebSocket' in window) {
(function () {
function refreshCSS() {
var sheets = [].slice.call(document.getElementsByTagName("link"));
var head = document.getElementsByTagName("head")[0];
for (var i = 0; i < sheets.length; ++i) {
var elem = sheets[i];
var parent = elem.parentElement || head;
parent.removeChild(elem);
var rel = elem.rel;
if (elem.href && typeof rel != "string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
var url = elem.href.replace(/(&|\?)_cacheOverride=\d+/, '');
elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf());
}
parent.appendChild(elem);
}
}
var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
var address = protocol + window.location.host + window.location.pathname + '/ws';
var socket = new WebSocket(address);
socket.onmessage = function (msg) {
if (msg.data == 'reload') window.location.reload();
else if (msg.data == 'refreshcss') refreshCSS();
};
if (sessionStorage && !sessionStorage.getItem('IsThisFirstTime_Log_From_LiveServer')) {
console.log('Live reload enabled.');
sessionStorage.setItem('IsThisFirstTime_Log_From_LiveServer', true);
}
})();
}
else {
console.error('Upgrade your browser. This Browser is NOT supported WebSocket for Live-Reloading.');
}
// ]]>
</script>
</body>
</html>

213
index.html?action=toggle.3 Normal file
View File

@@ -0,0 +1,213 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#2c3e50">
<title>Game Timer</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link rel="stylesheet" href="styles.css">
<!-- Deep Linking - App Links for Android -->
<link rel="alternate" href="android-app://eu.virtonline.gametimer/https/game-timer.virtonline.eu" />
<!-- Deep Linking - Universal Links for iOS -->
<meta name="apple-itunes-app" content="app-id=yourAppID, app-argument=https://game-timer.virtonline.eu">
<!-- Deep Linking - Web App URL Handling -->
<link rel="alternate" href="web+gametimer://action" />
</head>
<body>
<div class="app-container">
<header class="header">
<div class="game-controls">
<button id="gameButton" class="game-button">Start Game</button>
</div>
<div class="header-buttons">
<button id="resetButton" class="header-button" title="Reset All Data">
<i class="fas fa-redo-alt"></i>
</button>
<button id="addPlayerButton" class="header-button" title="Add New Player">
<i class="fas fa-user-plus"></i>
</button>
<button id="setupButton" class="header-button" title="Setup Current Player">
<i class="fas fa-cog"></i>
</button>
</div>
</header>
<div class="carousel-container">
<div id="carousel" class="carousel">
<!-- Player cards will be dynamically inserted here -->
</div>
</div>
</div>
<!-- Player Edit Modal -->
<div id="playerModal" class="modal">
<div class="modal-content">
<h2 id="modalTitle">Edit Player</h2>
<form id="playerForm">
<div class="form-group">
<label for="playerName">Name</label>
<input type="text" id="playerName" required>
</div>
<div class="form-group">
<label for="playerImage">Image</label>
<div class="image-input-container">
<input type="file" id="playerImage" accept="image/*">
<button type="button" id="cameraButton" class="camera-button">
<i class="fas fa-camera"></i> Take Photo
</button>
</div>
<div id="imagePreview" class="player-image" style="margin-top: 0.5rem;">
<i class="fas fa-user"></i>
</div>
</div>
<div id="playerTimeContainer" class="form-group">
<label for="playerTime">Time (minutes)</label>
<input type="number" id="playerTime" min="1" max="180" value="5" required>
</div>
<div id="remainingTimeContainer" class="form-group" style="display: none;">
<label for="playerRemainingTime">Remaining Time (MM:SS)</label>
<input type="text" id="playerRemainingTime" pattern="[0-9]{2}:[0-9]{2}" placeholder="05:00">
<small>Format: Minutes:Seconds (e.g., 05:30)</small>
</div>
<div class="form-buttons">
<button type="button" id="cancelButton" class="cancel-button">Cancel</button>
<button type="submit" class="save-button">Save</button>
</div>
<div class="form-buttons delete-button-container">
<button type="button" id="deletePlayerButton" class="delete-button">Delete Player</button>
</div>
</form>
</div>
</div>
<!-- Reset Confirmation Modal -->
<div id="resetModal" class="modal">
<div class="modal-content">
<h2>Reset All Data</h2>
<p>Are you sure you want to reset all players and timers? This action cannot be undone.</p>
<div class="form-buttons">
<button type="button" id="resetCancelButton" class="cancel-button">Cancel</button>
<button type="button" id="resetConfirmButton" class="save-button">Reset</button>
</div>
</div>
</div>
<!-- Camera Capture UI -->
<div id="cameraContainer" class="camera-container">
<div class="camera-view">
<video id="cameraView" autoplay playsinline></video>
<canvas id="cameraCanvas" style="display: none;"></canvas>
</div>
<div class="camera-controls">
<button id="cameraCancelButton" class="camera-button-cancel">Cancel</button>
<button id="cameraCaptureButton" class="camera-button-large"></button>
</div>
</div>
<!-- Script for handling URL scheme and deep links -->
<script type="module">
// Register the custom URL protocol handler (web+gametimer://)
if ('registerProtocolHandler' in navigator) {
try {
navigator.registerProtocolHandler(
'web+gametimer',
'https://game-timer.virtonline.eu/?action=%s',
'Game Timer'
);
console.log('Protocol handler registered');
} catch (e) {
console.error('Failed to register protocol handler:', e);
}
}
// Function to parse URL parameters
function getUrlParams() {
const searchParams = new URLSearchParams(window.location.search);
const hashParams = new URLSearchParams(window.location.hash.substring(1));
// Check search parameters first (for direct links)
const action = searchParams.get('action');
if (action) {
// Clean the action parameter (remove 'web+gametimer://' if present)
return action.replace('web+gametimer://', '');
}
// Then check hash parameters (for deep links)
return hashParams.get('action');
}
// Initialize URL handling
function initUrlHandling() {
const action = getUrlParams();
if (action) {
console.log('URL action detected:', action);
// Set the action in the hash to be processed by the main app
window.location.hash = `action=${action}`;
}
}
// Run initialization when DOM is fully loaded
document.addEventListener('DOMContentLoaded', initUrlHandling);
</script>
<!-- Main application script -->
<script type="module" src="audio.js"></script>
<script type="module" src="apps.js"></script>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js")
.then(() => console.log("Service Worker Registered"))
.catch((err) => console.log("Service Worker Failed", err));
}
</script>
<footer class="app-footer">
<div class="author-info">
<p>Vibe coded by Martin</p>
<p>Version 0.0.1</p>
</div>
</footer>
<!-- Code injected by live-server -->
<script>
// <![CDATA[ <-- For SVG support
if ('WebSocket' in window) {
(function () {
function refreshCSS() {
var sheets = [].slice.call(document.getElementsByTagName("link"));
var head = document.getElementsByTagName("head")[0];
for (var i = 0; i < sheets.length; ++i) {
var elem = sheets[i];
var parent = elem.parentElement || head;
parent.removeChild(elem);
var rel = elem.rel;
if (elem.href && typeof rel != "string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
var url = elem.href.replace(/(&|\?)_cacheOverride=\d+/, '');
elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf());
}
parent.appendChild(elem);
}
}
var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
var address = protocol + window.location.host + window.location.pathname + '/ws';
var socket = new WebSocket(address);
socket.onmessage = function (msg) {
if (msg.data == 'reload') window.location.reload();
else if (msg.data == 'refreshcss') refreshCSS();
};
if (sessionStorage && !sessionStorage.getItem('IsThisFirstTime_Log_From_LiveServer')) {
console.log('Live reload enabled.');
sessionStorage.setItem('IsThisFirstTime_Log_From_LiveServer', true);
}
})();
}
else {
console.error('Upgrade your browser. This Browser is NOT supported WebSocket for Live-Reloading.');
}
// ]]>
</script>
</body>
</html>

View File

@@ -45,5 +45,41 @@
"sizes": "1082x2402", "sizes": "1082x2402",
"type": "image/png" "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" }]
}
] ]
} }

155
sw.js
View File

@@ -1,52 +1,153 @@
// Updated service worker code - sw.js // Service Worker version
const CACHE_NAME = 'timer-cache-v1'; const CACHE_VERSION = 'v1.0.0';
const urlsToCache = [ const CACHE_NAME = `game-timer-${CACHE_VERSION}`;
// Files to cache
const CACHE_FILES = [
'/', '/',
'/index.html', '/index.html',
'/styles.css', '/app.js',
'/apps.js',
'/audio.js', '/audio.js',
'/styles.css',
'/manifest.json',
'/icons/android-chrome-192x192.png', '/icons/android-chrome-192x192.png',
'/icons/android-chrome-512x512.png', '/icons/android-chrome-512x512.png',
'/icons/apple-touch-icon.png', '/icons/apple-touch-icon.png',
'/icons/favicon-32x32.png', '/icons/favicon-32x32.png',
'/icons/favicon-16x16.png', '/icons/favicon-16x16.png'
'/favicon.ico',
'/manifest.json',
'/site.webmanifest'
]; ];
// Install event - Cache files
self.addEventListener('install', event => { self.addEventListener('install', event => {
console.log('[ServiceWorker] Install');
event.waitUntil( event.waitUntil(
caches.open(CACHE_NAME) caches.open(CACHE_NAME)
.then(cache => { .then(cache => {
console.log('Opened cache'); console.log('[ServiceWorker] Caching app shell');
// Use individual cache.add calls in a Promise.all to handle failures better return cache.addAll(CACHE_FILES);
return Promise.all( })
urlsToCache.map(url => { .then(() => {
return cache.add(url).catch(err => { console.log('[ServiceWorker] Skip waiting on install');
console.log('Failed to cache:', url, err); return self.skipWaiting();
// Continue despite individual failures
return Promise.resolve();
});
})
);
}) })
); );
}); });
// 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 => { 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( event.respondWith(
caches.match(event.request) caches.match(event.request)
.then(response => { .then(response => {
// Cache hit - return response return response || fetch(event.request)
if (response) { .then(res => {
return response; // Check if we should cache this response
} if (shouldCacheResponse(event.request, res)) {
return fetch(event.request); 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 => { .catch(error => {
console.log('Fetch handler failed:', err); 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('/');
}
})
);
}); });