This commit is contained in:
cpu
2025-03-29 03:45:26 +01:00
parent 5763407edc
commit 832f19235f
31 changed files with 373 additions and 139 deletions

View File

@@ -7,8 +7,10 @@ node_modules/
npm-debug.log npm-debug.log
yarn-debug.log yarn-debug.log
yarn-error.log yarn-error.log
Dockerfile
# Development files # Development files
.dockerignore
.editorconfig .editorconfig
.eslintrc .eslintrc
.stylelintrc .stylelintrc
@@ -18,8 +20,7 @@ yarn-error.log
*.swp *.swp
*.swo *.swo
# Documentation # docs/
docs/
README.md README.md
LICENSE LICENSE
CHANGELOG.md CHANGELOG.md
@@ -45,11 +46,13 @@ dist/
build/ build/
# Environment files # Environment files
.env # We need .env for our application
#.env
.env.* .env.*
*.env
# Project specific files # Project specific files
screenshots/
labels.example labels.example
virt-game-timer.service virt-game-timer.service
package.json
package-lock.json

12
.env.example Normal file
View File

@@ -0,0 +1,12 @@
# Environment Variables Example for Game Timer Application
# Copy this file to .env and fill in your own values
# Public VAPID key for push notifications
# Generate your own VAPID keys for production:
# https://github.com/web-push-libs/web-push#generatevapidkeys
PUBLIC_VAPID_KEY=your_public_vapid_key_here
# Backend URL for your push notification server
BACKEND_URL=https://your-push-server.example.com
# Other environment variables can be added here

View File

@@ -1,12 +1,14 @@
# Use the official Nginx image as the base image # Use a lightweight server
FROM nginx:alpine FROM nginx:alpine
# Remove the default Nginx static files # Set working directory
RUN rm -rf /usr/share/nginx/html/* WORKDIR /usr/share/nginx/html
# Copy public directory contents into the Nginx directory # Copy all the application files
COPY public/ /usr/share/nginx/html/ COPY . .
COPY src/ /usr/share/nginx/html/src/
# Copy the .env file
COPY .env .
# Expose port 80 # Expose port 80
EXPOSE 80 EXPOSE 80

107
README.md
View File

@@ -6,27 +6,57 @@ Multi-player game-timer timer with carousel navigation
``` ```
game-timer/ game-timer/
├── docs/ # Documentation and project resources ├── css/ # CSS stylesheets
│ └── screenshots/ # Application screenshots ├── icons/ # App icons
├── public/ # Static assets and public-facing resources ├── images/ # Image assets
├── css/ # CSS stylesheets ├── js/ # Symbolic link to src/js for compatibility
├── images/ # Images used by the application ├── index.html # Main HTML entry point
│ ├── icons/ # App icons for PWA ├── manifest.json # PWA manifest
│ ├── audio/ # Audio files ├── sw.js # Service Worker
│ ├── index.html # Main HTML entry point
│ ├── manifest.json # PWA manifest
│ └── sw.js # Service Worker
├── src/ # Source code ├── src/ # Source code
│ └── js/ # JavaScript files │ └── js/ # JavaScript files
│ ├── core/ # Core application logic │ ├── core/ # Core application logic
│ ├── ui/ # UI-related code │ ├── ui/ # UI-related code
── services/ # External services integration ── services/ # External services integration
│ └── utils/ # Utility functions ├── Dockerfile # Docker container definition (nginx)
├── Dockerfile # Docker container definition
├── .dockerignore # Files to exclude from Docker build ├── .dockerignore # Files to exclude from Docker build
── package.json # Project metadata and dependencies ── .env # Environment variables for production
├── .env.example # Example environment variables template
└── package.json # Project metadata and deployment scripts
``` ```
## Environment Variables
The application uses environment variables for configuration. These are loaded from a `.env` file at runtime.
### Setting Up Environment Variables
1. Copy `.env.example` to `.env`:
```bash
cp .env.example .env
```
2. Edit the `.env` file with your own values:
```
# Public VAPID key for push notifications
PUBLIC_VAPID_KEY=your_public_vapid_key_here
# Backend URL for push notifications
BACKEND_URL=https://your-push-server.example.com
```
3. For security, never commit your `.env` file to version control. It's already included in `.gitignore`.
### Generating VAPID Keys
For push notifications, you need to generate your own VAPID keys:
```bash
npx web-push generate-vapid-keys
```
Use the public key in your `.env` file and keep the private key secure for your backend server.
# PWA Containerized Deployment # PWA Containerized Deployment
This document provides step-by-step instructions to pull the source code and deploy the Progressive Web App (PWA) using Docker on a production server. This document provides step-by-step instructions to pull the source code and deploy the Progressive Web App (PWA) using Docker on a production server.
@@ -56,12 +86,24 @@ From the repository root, run the following command to build your Docker image:
docker build -t 'game-timer:latest' . docker build -t 'game-timer:latest' .
``` ```
### 3. Run the Docker Container or use the npm script:
Once the image is built, run the container on port 8080 with:
```bash ```bash
docker run -d -p 8080:80 --name game-timer-container game-timer:latest npm run docker:build
```
### 3. Run the Docker Container
Once the image is built, run the container on port 80 with:
```bash
docker run -d -p 80:80 --name game-timer game-timer:latest
```
or use the npm script:
```bash
npm run start
``` ```
### 4. Verify the Deployment ### 4. Verify the Deployment
@@ -75,29 +117,42 @@ docker ps
View logs (if needed): View logs (if needed):
```bash ```bash
docker logs game-timer-container docker logs game-timer
``` ```
After running the container, open your web browser and navigate to: After running the container, open your web browser and navigate to:
```bash ```
http://localhost:8080 http://localhost
``` ```
### 5. Terminate ### 5. Terminate
To stop your running game-timer-container, use: To stop your running game-timer container, use:
```bash ```bash
docker stop game-timer-container docker stop game-timer
docker rm game-timer
```
or use the npm script:
```bash
npm run stop
``` ```
## Development ## Development
For local development without Docker, you can use: For local development without Docker, you can use any static file server such as:
```bash ```bash
npm run dev python -m http.server
``` ```
This will start a local development server and open the application in your browser. or
```bash
npx serve
```
This will start a local development server and you can access the application in your browser.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 398 B

View File

Before

Width:  |  Height:  |  Size: 770 B

After

Width:  |  Height:  |  Size: 770 B

BIN
images/screenshot1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

BIN
images/screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

View File

@@ -5,9 +5,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#2c3e50"> <meta name="theme-color" content="#2c3e50">
<title>Game Timer</title> <title>Game Timer</title>
<link rel="manifest" href="manifest.json">
<!-- Favicon links -->
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<!-- Web app manifest -->
<link rel="manifest" href="/manifest.json">
<!-- App icons for various platforms -->
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<!-- CSS stylesheets -->
<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="css/styles.css"> <link rel="stylesheet" href="/css/styles.css">
</head> </head>
<body> <body>
@@ -102,10 +113,10 @@
</div> </div>
<!-- Main application script --> <!-- Main application script -->
<script type="module" src="../src/js/app.js"></script> <script type="module" src="/js/app.js"></script>
<script> <script>
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js") navigator.serviceWorker.register("/sw.js")
.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));
} }

1
js Symbolic link
View File

@@ -0,0 +1 @@
src/js

View File

@@ -2,54 +2,50 @@
"name": "Game Timer PWA", "name": "Game Timer PWA",
"short_name": "Game Timer", "short_name": "Game Timer",
"description": "Multi-player chess-like timer with carousel navigation", "description": "Multi-player chess-like timer with carousel navigation",
"start_url": "./index.html", "start_url": "/index.html",
"id": "/index.html",
"display": "standalone", "display": "standalone",
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
"background_color": "#f5f5f5", "background_color": "#f5f5f5",
"theme_color": "#2c3e50", "theme_color": "#2c3e50",
"icons": [ "icons": [
{ {
"src": "./icons/android-chrome-192x192.png", "src": "/icons/android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "./icons/android-chrome-512x512.png", "src": "/icons/android-chrome-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "./icons/apple-touch-icon.png", "src": "/icons/apple-touch-icon.png",
"sizes": "180x180", "sizes": "180x180",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "./icons/favicon-32x32.png", "src": "/icons/favicon-32x32.png",
"sizes": "32x32", "sizes": "32x32",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "./icons/favicon-16x16.png", "src": "/icons/favicon-16x16.png",
"sizes": "16x16", "sizes": "16x16",
"type": "image/png" "type": "image/png"
} }
], ],
"screenshots": [ "screenshots": [
{ {
"src": "../docs/screenshots/screenshot1.png", "src": "/images/screenshot1.png",
"sizes": "2604x2269", "sizes": "2560x1860",
"type": "image/png", "type": "image/png",
"form_factor": "wide" "form_factor": "wide"
}, },
{ {
"src": "../docs/screenshots/screenshot2.png", "src": "/images/screenshot2.png",
"sizes": "1082x2402", "sizes": "750x1594",
"type": "image/png" "type": "image/png"
} }
], ]
"url_handlers": [
{
"origin": "https://game-timer.virtonline.eu"
}
],
"gcm_sender_id": "103953800507"
} }

View File

@@ -1,17 +1,13 @@
{ {
"name": "countdown", "name": "game-timer",
"version": "1.0.0", "version": "1.0.0",
"description": "Multi-player chess timer with carousel navigation", "description": "Multi-player chess timer with carousel navigation",
"main": "src/js/app.js", "main": "src/js/app.js",
"scripts": { "scripts": {
"start": "docker run -d -p 8080:80 --name game-timer-container game-timer:latest", "docker:build": "docker build -t 'game-timer:latest' .",
"stop": "docker stop game-timer-container && docker rm game-timer-container", "start": "docker run -d -p 80:80 --name game-timer game-timer:latest",
"build": "docker build -t 'game-timer:latest' .", "stop": "docker stop game-timer && docker rm game-timer",
"rebuild": "npm run stop || true && npm run build && npm run start", "rebuild": "npm run stop || true && npm run docker:build && npm run start"
"logs": "docker logs game-timer-container",
"status": "docker ps | grep game-timer-container",
"dev": "npx http-server . -o /public",
"clean": "docker system prune -f"
}, },
"keywords": [ "keywords": [
"timer", "timer",

View File

@@ -1,19 +0,0 @@
{
"name": "Game Timer",
"short_name": "Game Timer",
"icons": [
{
"src": "./icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -6,6 +6,7 @@ import * as timer from './core/timer.js';
import camera from './ui/camera.js'; // Default export import camera from './ui/camera.js'; // Default export
import audioManager from './ui/audio.js'; import audioManager from './ui/audio.js';
import * as pushFlic from './services/pushFlicIntegration.js'; import * as pushFlic from './services/pushFlicIntegration.js';
import { initEnv } from './env-loader.js';
// Import externalized modules // Import externalized modules
import * as gameActions from './core/gameActions.js'; import * as gameActions from './core/gameActions.js';
@@ -15,9 +16,17 @@ import * as serviceWorkerManager from './services/serviceWorkerManager.js';
// --- Initialization --- // --- Initialization ---
function initialize() { async function initialize() {
console.log("Initializing Game Timer App..."); console.log("Initializing Game Timer App...");
// 0. Wait for environment variables to load
try {
await initEnv();
console.log("Environment variables loaded");
} catch (error) {
console.warn("Failed to load environment variables, using defaults:", error);
}
// 1. Load saved state or defaults // 1. Load saved state or defaults
state.loadData(); state.loadData();
@@ -88,4 +97,11 @@ function initialize() {
} }
// --- Start the application --- // --- Start the application ---
initialize(); // We need to use an async IIFE to await the async initialize function
(async () => {
try {
await initialize();
} catch (error) {
console.error("Error initializing application:", error);
}
})();

View File

@@ -1,6 +1,17 @@
// config.js // config.js
export const PUBLIC_VAPID_KEY = 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E'; import { getEnv } from './env-loader.js';
export const BACKEND_URL = 'https://webpush.virtonline.eu';
export function getPublicVapidKey() {
// Get the VAPID key from environment variables through the env-loader
return getEnv('PUBLIC_VAPID_KEY', 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E');
}
// The VAPID key should not be exposed directly in the source code
// Use the getter function instead: getPublicVapidKey()
// export const PUBLIC_VAPID_KEY = 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E';
// Get backend URL from environment variables
export const BACKEND_URL = getEnv('BACKEND_URL', 'https://webpush.virtonline.eu');
export const FLIC_BUTTON_ID = 'game-button'; // Example ID, might need configuration export const FLIC_BUTTON_ID = 'game-button'; // Example ID, might need configuration
export const LOCAL_STORAGE_KEY = 'gameTimerData'; export const LOCAL_STORAGE_KEY = 'gameTimerData';
export const FLIC_BATTERY_THRESHOLD = 50; // Battery percentage threshold for low battery warning export const FLIC_BATTERY_THRESHOLD = 50; // Battery percentage threshold for low battery warning

View File

@@ -7,18 +7,23 @@ let timerInterval = null;
let onTimerTickCallback = null; // Callback for UI updates let onTimerTickCallback = null; // Callback for UI updates
let onPlayerSwitchCallback = null; // Callback for when player switches due to time running out let onPlayerSwitchCallback = null; // Callback for when player switches due to time running out
let onGameOverCallback = null; // Callback for when all players run out of time let onGameOverCallback = null; // Callback for when all players run out of time
let timeExpiredFlagsById = new Map(); // Track which players have had their timeout sound played
export function initTimer(options) { export function initTimer(options) {
onTimerTickCallback = options.onTimerTick; onTimerTickCallback = options.onTimerTick;
onPlayerSwitchCallback = options.onPlayerSwitch; onPlayerSwitchCallback = options.onPlayerSwitch;
onGameOverCallback = options.onGameOver; onGameOverCallback = options.onGameOver;
timeExpiredFlagsById.clear(); // Reset flags on init
} }
export function startTimer() { export function startTimer() {
if (timerInterval) clearInterval(timerInterval); // Clear existing interval if any if (timerInterval) clearInterval(timerInterval); // Clear existing interval if any
// Stop any previous sounds (like low time warning) before starting fresh // Stop any previous sounds (like low time warning) before starting fresh
audioManager.stopAllSounds(); // Consider if this is too aggressive audioManager.stopAllSounds();
// Reset the expired sound flags when starting a new timer
timeExpiredFlagsById.clear();
timerInterval = setInterval(() => { timerInterval = setInterval(() => {
const currentPlayerIndex = state.getCurrentPlayerIndex(); const currentPlayerIndex = state.getCurrentPlayerIndex();
@@ -35,32 +40,24 @@ export function startTimer() {
const newTime = currentPlayer.remainingTime - 1; const newTime = currentPlayer.remainingTime - 1;
state.updatePlayerTime(currentPlayerIndex, newTime); // Update state state.updatePlayerTime(currentPlayerIndex, newTime); // Update state
// Play timer sounds // Play timer sounds - ensure we're not leaking audio resources
audioManager.playTimerSound(newTime); audioManager.playTimerSound(newTime);
// Notify UI to update // Notify UI to update
if (onTimerTickCallback) onTimerTickCallback(); if (onTimerTickCallback) onTimerTickCallback();
} else { // Current player's time just hit 0 or was already 0 } else { // Current player's time just hit 0 or was already 0
// Ensure time is exactly 0 if it somehow went negative (unlikely with check above) // Ensure time is exactly 0 if it somehow went negative
if(currentPlayer.remainingTime < 0) { if(currentPlayer.remainingTime < 0) {
state.updatePlayerTime(currentPlayerIndex, 0); state.updatePlayerTime(currentPlayerIndex, 0);
} }
// Stop this player's timer tick sound if it was playing // Play time expired sound (only once per player per game)
// audioManager.stop('timerTick'); // Or specific low time sound if (!timeExpiredFlagsById.has(currentPlayer.id)) {
// Play time expired sound (only once)
// Check if we just hit zero to avoid playing repeatedly
// This logic might be complex, audioManager could handle idempotency
if (currentPlayer.remainingTime === 0 && !currentPlayer.timeExpiredSoundPlayed) {
audioManager.playTimerExpired(); audioManager.playTimerExpired();
// We need a way to mark that the sound played for this player this turn. timeExpiredFlagsById.set(currentPlayer.id, true);
// This might require adding a temporary flag to the player state,
// or handling it within the audioManager. Let's assume audioManager handles it for now.
} }
// Check if the game should end or switch player // Check if the game should end or switch player
if (state.areAllTimersFinished()) { if (state.areAllTimersFinished()) {
stopTimer(); stopTimer();
@@ -69,10 +66,11 @@ export function startTimer() {
// Find the *next* player who still has time // Find the *next* player who still has time
const nextPlayerIndex = state.findNextPlayerWithTime(); // This finds ANY player with time const nextPlayerIndex = state.findNextPlayerWithTime(); // This finds ANY player with time
if (nextPlayerIndex !== -1 && nextPlayerIndex !== currentPlayerIndex) { if (nextPlayerIndex !== -1 && nextPlayerIndex !== currentPlayerIndex) {
// Switch player // Switch player and ensure we stop any sounds from current player
audioManager.stopTimerSounds(); // Stop specific timer sounds before switching
if (onPlayerSwitchCallback) onPlayerSwitchCallback(nextPlayerIndex); if (onPlayerSwitchCallback) onPlayerSwitchCallback(nextPlayerIndex);
// Play switch sound (might be handled in app.js based on state change)
// audioManager.play('playerSwitch'); // Or let app.js handle sounds based on actions
// Immediately update UI after switch // Immediately update UI after switch
if (onTimerTickCallback) onTimerTickCallback(); if (onTimerTickCallback) onTimerTickCallback();
} else if (nextPlayerIndex === -1) { } else if (nextPlayerIndex === -1) {
@@ -81,7 +79,7 @@ export function startTimer() {
stopTimer(); // Stop timer if state is inconsistent stopTimer(); // Stop timer if state is inconsistent
if (onGameOverCallback) onGameOverCallback(); // Treat as game over if (onGameOverCallback) onGameOverCallback(); // Treat as game over
} }
// If nextPlayerIndex is the same as currentPlayerIndex, means others are out of time, let this timer continue (or rather, stop ticking down as remainingTime is 0) // If nextPlayerIndex is the same as currentPlayerIndex, means others are out of time, let this timer continue
} }
} }
}, 1000); }, 1000);
@@ -90,10 +88,17 @@ export function startTimer() {
export function stopTimer() { export function stopTimer() {
clearInterval(timerInterval); clearInterval(timerInterval);
timerInterval = null; timerInterval = null;
// Optionally stop timer sounds here if needed // Stop all timer-related sounds to prevent them from continuing to play
// audioManager.stop('timerTick'); audioManager.stopTimerSounds();
} }
export function isTimerRunning() { export function isTimerRunning() {
return timerInterval !== null; return timerInterval !== null;
} }
// Clean up resources when the application is closing or component unmounts
export function cleanup() {
stopTimer();
timeExpiredFlagsById.clear();
audioManager.stopAllSounds();
}

88
src/js/env-loader.js Normal file
View File

@@ -0,0 +1,88 @@
// env-loader.js
// This module is responsible for loading environment variables from .env file
// Store environment variables in a global object
window.ENV_CONFIG = {};
// Function to load environment variables from .env file
async function loadEnvVariables() {
try {
// Fetch the .env file as text
const response = await fetch('/.env');
if (!response.ok) {
console.warn('Could not load .env file. Using default values.');
setDefaultEnvValues();
return;
}
const envText = await response.text();
// Parse the .env file content
const envVars = parseEnvFile(envText);
// Store in the global ENV_CONFIG object
window.ENV_CONFIG = { ...window.ENV_CONFIG, ...envVars };
console.log('Environment variables loaded successfully');
} catch (error) {
console.error('Error loading environment variables:', error);
setDefaultEnvValues();
}
}
// Parse .env file content into key-value pairs
function parseEnvFile(envText) {
const envVars = {};
// Split by lines and process each line
envText.split('\n').forEach(line => {
// Skip empty lines and comments
if (!line || line.trim().startsWith('#')) return;
// Extract key-value pairs
const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
if (match) {
const key = match[1];
let value = match[2] || '';
// Remove quotes if present
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
envVars[key] = value;
}
});
return envVars;
}
// Set default values for required environment variables
function setDefaultEnvValues() {
window.ENV_CONFIG = {
...window.ENV_CONFIG,
PUBLIC_VAPID_KEY: 'your_public_vapid_key_here',
BACKEND_URL: 'https://your-push-server.example.com'
};
console.log('Using default environment values');
}
// Export function to initialize environment variables
export async function initEnv() {
await loadEnvVariables();
return window.ENV_CONFIG;
}
// Auto-initialize when imported
initEnv();
// Export access functions for environment variables
export function getEnv(key, defaultValue = '') {
return window.ENV_CONFIG[key] || defaultValue;
}
export default {
initEnv,
getEnv
};

View File

@@ -1,5 +1,5 @@
// pushFlicIntegration.js // pushFlicIntegration.js
import { PUBLIC_VAPID_KEY, BACKEND_URL, FLIC_BUTTON_ID, FLIC_ACTIONS, FLIC_BATTERY_THRESHOLD } from '../config.js'; import { getPublicVapidKey, BACKEND_URL, FLIC_BUTTON_ID, FLIC_ACTIONS, FLIC_BATTERY_THRESHOLD } from '../config.js';
let pushSubscription = null; // Keep track locally if needed let pushSubscription = null; // Keep track locally if needed
let actionHandlers = {}; // Store handlers for different Flic actions let actionHandlers = {}; // Store handlers for different Flic actions
@@ -139,7 +139,7 @@ async function subscribeToPush() {
if (existingSubscription) { if (existingSubscription) {
const existingKey = existingSubscription.options?.applicationServerKey; const existingKey = existingSubscription.options?.applicationServerKey;
if (!existingKey || arrayBufferToBase64(existingKey) !== PUBLIC_VAPID_KEY) { if (!existingKey || arrayBufferToBase64(existingKey) !== getPublicVapidKey()) {
console.log('VAPID key mismatch or missing. Unsubscribing old subscription.'); console.log('VAPID key mismatch or missing. Unsubscribing old subscription.');
await existingSubscription.unsubscribe(); await existingSubscription.unsubscribe();
existingSubscription = null; existingSubscription = null;
@@ -153,7 +153,7 @@ async function subscribeToPush() {
let finalSubscription = existingSubscription; let finalSubscription = existingSubscription;
if (needsResubscribe) { if (needsResubscribe) {
console.log('Subscribing for push notifications...'); console.log('Subscribing for push notifications...');
const applicationServerKey = urlBase64ToUint8Array(PUBLIC_VAPID_KEY); const applicationServerKey = urlBase64ToUint8Array(getPublicVapidKey());
finalSubscription = await registration.pushManager.subscribe({ finalSubscription = await registration.pushManager.subscribe({
userVisibleOnly: true, userVisibleOnly: true,
applicationServerKey: applicationServerKey applicationServerKey: applicationServerKey

View File

@@ -32,7 +32,7 @@ export function handleServiceWorkerMessage(event) {
export function setupServiceWorker(messageHandler) { export function setupServiceWorker(messageHandler) {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', () => { window.addEventListener('load', () => {
navigator.serviceWorker.register('/public/sw.js') navigator.serviceWorker.register('/sw.js')
.then(registration => { .then(registration => {
console.log('ServiceWorker registered successfully.'); console.log('ServiceWorker registered successfully.');

View File

@@ -302,6 +302,14 @@ const audioManager = {
this.lastTickTime = 0; this.lastTickTime = 0;
}, },
// Stop timer-specific sounds (tick, low time warning)
stopTimerSounds() {
// Reset tick fade timer when stopping timer sounds
this.lastTickTime = 0;
// In this implementation, sounds are so short-lived that
// they don't need to be explicitly stopped, just fade prevention is enough
},
// Reset the tick fading (call this when timer is paused or player changes) // Reset the tick fading (call this when timer is paused or player changes)
resetTickFade() { resetTickFade() {
this.lastTickTime = 0; this.lastTickTime = 0;

View File

@@ -1,20 +1,33 @@
// Service Worker version // Service Worker version
const CACHE_VERSION = 'v1.0.0'; const CACHE_VERSION = 'v1.0.2';
const CACHE_NAME = `game-timer-${CACHE_VERSION}`; const CACHE_NAME = `game-timer-${CACHE_VERSION}`;
// Files to cache // Files to cache
const CACHE_FILES = [ const CACHE_FILES = [
'./', '/',
'./index.html', '/sw.js',
'../src/js/app.js', '/index.html',
'../src/js/ui/audio.js', '/manifest.json',
'./css/styles.css', '/css/styles.css',
'./manifest.json', '/favicon.ico',
'./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',
'/js/app.js',
'/js/config.js',
'/js/env-loader.js',
'/js/ui/audio.js',
'/js/ui/camera.js',
'/js/ui/ui.js',
'/js/core/state.js',
'/js/core/timer.js',
'/js/core/gameActions.js',
'/js/core/playerManager.js',
'/js/core/eventHandlers.js',
'/js/services/pushFlicIntegration.js',
'/js/services/serviceWorkerManager.js'
]; ];
// Install event - Cache files // Install event - Cache files
@@ -124,7 +137,7 @@ self.addEventListener('push', event => {
// If no window is open, we MUST show a notification // If no window is open, we MUST show a notification
return self.registration.showNotification(pushData.title, { return self.registration.showNotification(pushData.title, {
body: pushData.body, body: pushData.body,
icon: './icons/android-chrome-192x192.png', // Optional: path to an icon icon: '/icons/android-chrome-192x192.png', // Updated path
data: pushData.data // Pass data if needed when notification is clicked data: pushData.data // Pass data if needed when notification is clicked
}); });
} }
@@ -144,7 +157,7 @@ self.addEventListener('push', event => {
if (!messageSent) { // Only show notification if no message was sent? Or always show? if (!messageSent) { // Only show notification if no message was sent? Or always show?
return self.registration.showNotification(pushData.title, { return self.registration.showNotification(pushData.title, {
body: pushData.body, body: pushData.body,
icon: './icons/android-chrome-192x192.png', icon: '/icons/android-chrome-192x192.png',
data: pushData.data data: pushData.data
}); });
} }
@@ -185,7 +198,7 @@ self.addEventListener('notificationclick', event => {
} }
if (self.clients.openWindow) { if (self.clients.openWindow) {
return self.clients.openWindow('./'); return self.clients.openWindow('/');
} }
}) })
); );

36
test.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Worker Test</title>
</head>
<body>
<h1>Service Worker Test</h1>
<p>This page tests if the service worker is registered correctly.</p>
<div id="status">Checking service worker registration...</div>
<script>
const statusDiv = document.getElementById('status');
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
statusDiv.innerHTML = 'Service worker registered successfully!<br>' +
'Scope: ' + registration.scope;
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(error => {
statusDiv.innerHTML = 'Service worker registration failed: ' + error;
console.error('ServiceWorker registration failed: ', error);
});
navigator.serviceWorker.ready.then(registration => {
statusDiv.innerHTML += '<br><br>Service worker is ready!';
});
} else {
statusDiv.innerHTML = 'Service workers are not supported in this browser.';
}
</script>
</body>
</html>