refactor
@@ -7,8 +7,10 @@ node_modules/
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
Dockerfile
|
||||
|
||||
# Development files
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.eslintrc
|
||||
.stylelintrc
|
||||
@@ -18,8 +20,7 @@ yarn-error.log
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
# docs/
|
||||
README.md
|
||||
LICENSE
|
||||
CHANGELOG.md
|
||||
@@ -45,11 +46,13 @@ dist/
|
||||
build/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
# We need .env for our application
|
||||
#.env
|
||||
.env.*
|
||||
*.env
|
||||
|
||||
|
||||
# Project specific files
|
||||
screenshots/
|
||||
labels.example
|
||||
virt-game-timer.service
|
||||
package.json
|
||||
package-lock.json
|
||||
12
.env.example
Normal 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
|
||||
14
Dockerfile
@@ -1,12 +1,14 @@
|
||||
# Use the official Nginx image as the base image
|
||||
# Use a lightweight server
|
||||
FROM nginx:alpine
|
||||
|
||||
# Remove the default Nginx static files
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
# Set working directory
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
# Copy public directory contents into the Nginx directory
|
||||
COPY public/ /usr/share/nginx/html/
|
||||
COPY src/ /usr/share/nginx/html/src/
|
||||
# Copy all the application files
|
||||
COPY . .
|
||||
|
||||
# Copy the .env file
|
||||
COPY .env .
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
107
README.md
@@ -6,27 +6,57 @@ Multi-player game-timer timer with carousel navigation
|
||||
|
||||
```
|
||||
game-timer/
|
||||
├── docs/ # Documentation and project resources
|
||||
│ └── screenshots/ # Application screenshots
|
||||
├── public/ # Static assets and public-facing resources
|
||||
│ ├── css/ # CSS stylesheets
|
||||
│ ├── images/ # Images used by the application
|
||||
│ ├── icons/ # App icons for PWA
|
||||
│ ├── audio/ # Audio files
|
||||
│ ├── index.html # Main HTML entry point
|
||||
│ ├── manifest.json # PWA manifest
|
||||
│ └── sw.js # Service Worker
|
||||
├── css/ # CSS stylesheets
|
||||
├── icons/ # App icons
|
||||
├── images/ # Image assets
|
||||
├── js/ # Symbolic link to src/js for compatibility
|
||||
├── index.html # Main HTML entry point
|
||||
├── manifest.json # PWA manifest
|
||||
├── sw.js # Service Worker
|
||||
├── src/ # Source code
|
||||
│ └── js/ # JavaScript files
|
||||
│ ├── core/ # Core application logic
|
||||
│ ├── ui/ # UI-related code
|
||||
│ ├── services/ # External services integration
|
||||
│ └── utils/ # Utility functions
|
||||
├── Dockerfile # Docker container definition
|
||||
│ └── services/ # External services integration
|
||||
├── Dockerfile # Docker container definition (nginx)
|
||||
├── .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
|
||||
|
||||
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' .
|
||||
```
|
||||
|
||||
### 3. Run the Docker Container
|
||||
|
||||
Once the image is built, run the container on port 8080 with:
|
||||
or use the npm script:
|
||||
|
||||
```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
|
||||
@@ -75,29 +117,42 @@ docker ps
|
||||
View logs (if needed):
|
||||
|
||||
```bash
|
||||
docker logs game-timer-container
|
||||
docker logs game-timer
|
||||
```
|
||||
|
||||
After running the container, open your web browser and navigate to:
|
||||
|
||||
```bash
|
||||
http://localhost:8080
|
||||
```
|
||||
http://localhost
|
||||
```
|
||||
|
||||
### 5. Terminate
|
||||
|
||||
To stop your running game-timer-container, use:
|
||||
To stop your running game-timer container, use:
|
||||
|
||||
```bash
|
||||
docker stop game-timer-container
|
||||
docker stop game-timer
|
||||
docker rm game-timer
|
||||
```
|
||||
|
||||
or use the npm script:
|
||||
|
||||
```bash
|
||||
npm run stop
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
For local development without Docker, you can use:
|
||||
For local development without Docker, you can use any static file server such as:
|
||||
|
||||
```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.
|
||||
|
Before Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 416 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 398 B |
|
Before Width: | Height: | Size: 770 B After Width: | Height: | Size: 770 B |
BIN
images/screenshot1.png
Normal file
|
After Width: | Height: | Size: 427 KiB |
BIN
images/screenshot2.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
@@ -5,9 +5,20 @@
|
||||
<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">
|
||||
|
||||
<!-- 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="css/styles.css">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
@@ -102,10 +113,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Main application script -->
|
||||
<script type="module" src="../src/js/app.js"></script>
|
||||
<script type="module" src="/js/app.js"></script>
|
||||
<script>
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("sw.js")
|
||||
navigator.serviceWorker.register("/sw.js")
|
||||
.then(() => console.log("Service Worker Registered"))
|
||||
.catch((err) => console.log("Service Worker Failed", err));
|
||||
}
|
||||
@@ -2,54 +2,50 @@
|
||||
"name": "Game Timer PWA",
|
||||
"short_name": "Game Timer",
|
||||
"description": "Multi-player chess-like timer with carousel navigation",
|
||||
"start_url": "./index.html",
|
||||
"start_url": "/index.html",
|
||||
"id": "/index.html",
|
||||
"display": "standalone",
|
||||
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
|
||||
"background_color": "#f5f5f5",
|
||||
"theme_color": "#2c3e50",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./icons/android-chrome-192x192.png",
|
||||
"src": "/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./icons/android-chrome-512x512.png",
|
||||
"src": "/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./icons/apple-touch-icon.png",
|
||||
"src": "/icons/apple-touch-icon.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./icons/favicon-32x32.png",
|
||||
"src": "/icons/favicon-32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./icons/favicon-16x16.png",
|
||||
"src": "/icons/favicon-16x16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "../docs/screenshots/screenshot1.png",
|
||||
"sizes": "2604x2269",
|
||||
"src": "/images/screenshot1.png",
|
||||
"sizes": "2560x1860",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide"
|
||||
},
|
||||
{
|
||||
"src": "../docs/screenshots/screenshot2.png",
|
||||
"sizes": "1082x2402",
|
||||
"src": "/images/screenshot2.png",
|
||||
"sizes": "750x1594",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"url_handlers": [
|
||||
{
|
||||
"origin": "https://game-timer.virtonline.eu"
|
||||
}
|
||||
],
|
||||
"gcm_sender_id": "103953800507"
|
||||
]
|
||||
}
|
||||
14
package.json
@@ -1,17 +1,13 @@
|
||||
{
|
||||
"name": "countdown",
|
||||
"name": "game-timer",
|
||||
"version": "1.0.0",
|
||||
"description": "Multi-player chess timer with carousel navigation",
|
||||
"main": "src/js/app.js",
|
||||
"scripts": {
|
||||
"start": "docker run -d -p 8080:80 --name game-timer-container game-timer:latest",
|
||||
"stop": "docker stop game-timer-container && docker rm game-timer-container",
|
||||
"build": "docker build -t 'game-timer:latest' .",
|
||||
"rebuild": "npm run stop || true && npm run 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"
|
||||
"docker:build": "docker build -t 'game-timer:latest' .",
|
||||
"start": "docker run -d -p 80:80 --name game-timer game-timer:latest",
|
||||
"stop": "docker stop game-timer && docker rm game-timer",
|
||||
"rebuild": "npm run stop || true && npm run docker:build && npm run start"
|
||||
},
|
||||
"keywords": [
|
||||
"timer",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import * as timer from './core/timer.js';
|
||||
import camera from './ui/camera.js'; // Default export
|
||||
import audioManager from './ui/audio.js';
|
||||
import * as pushFlic from './services/pushFlicIntegration.js';
|
||||
import { initEnv } from './env-loader.js';
|
||||
|
||||
// Import externalized modules
|
||||
import * as gameActions from './core/gameActions.js';
|
||||
@@ -15,9 +16,17 @@ import * as serviceWorkerManager from './services/serviceWorkerManager.js';
|
||||
|
||||
// --- Initialization ---
|
||||
|
||||
function initialize() {
|
||||
async function initialize() {
|
||||
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
|
||||
state.loadData();
|
||||
|
||||
@@ -88,4 +97,11 @@ function initialize() {
|
||||
}
|
||||
|
||||
// --- 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);
|
||||
}
|
||||
})();
|
||||
@@ -1,6 +1,17 @@
|
||||
// config.js
|
||||
export const PUBLIC_VAPID_KEY = 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E';
|
||||
export const BACKEND_URL = 'https://webpush.virtonline.eu';
|
||||
import { getEnv } from './env-loader.js';
|
||||
|
||||
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 LOCAL_STORAGE_KEY = 'gameTimerData';
|
||||
export const FLIC_BATTERY_THRESHOLD = 50; // Battery percentage threshold for low battery warning
|
||||
|
||||
@@ -7,18 +7,23 @@ let timerInterval = null;
|
||||
let onTimerTickCallback = null; // Callback for UI updates
|
||||
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 timeExpiredFlagsById = new Map(); // Track which players have had their timeout sound played
|
||||
|
||||
export function initTimer(options) {
|
||||
onTimerTickCallback = options.onTimerTick;
|
||||
onPlayerSwitchCallback = options.onPlayerSwitch;
|
||||
onGameOverCallback = options.onGameOver;
|
||||
timeExpiredFlagsById.clear(); // Reset flags on init
|
||||
}
|
||||
|
||||
export function startTimer() {
|
||||
if (timerInterval) clearInterval(timerInterval); // Clear existing interval if any
|
||||
|
||||
// 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(() => {
|
||||
const currentPlayerIndex = state.getCurrentPlayerIndex();
|
||||
@@ -35,32 +40,24 @@ export function startTimer() {
|
||||
const newTime = currentPlayer.remainingTime - 1;
|
||||
state.updatePlayerTime(currentPlayerIndex, newTime); // Update state
|
||||
|
||||
// Play timer sounds
|
||||
// Play timer sounds - ensure we're not leaking audio resources
|
||||
audioManager.playTimerSound(newTime);
|
||||
|
||||
// Notify UI to update
|
||||
if (onTimerTickCallback) onTimerTickCallback();
|
||||
|
||||
} 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) {
|
||||
state.updatePlayerTime(currentPlayerIndex, 0);
|
||||
}
|
||||
|
||||
// Stop this player's timer tick sound if it was playing
|
||||
// audioManager.stop('timerTick'); // Or specific low time sound
|
||||
|
||||
// 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) {
|
||||
// Play time expired sound (only once per player per game)
|
||||
if (!timeExpiredFlagsById.has(currentPlayer.id)) {
|
||||
audioManager.playTimerExpired();
|
||||
// We need a way to mark that the sound played for this player this turn.
|
||||
// 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.
|
||||
timeExpiredFlagsById.set(currentPlayer.id, true);
|
||||
}
|
||||
|
||||
|
||||
// Check if the game should end or switch player
|
||||
if (state.areAllTimersFinished()) {
|
||||
stopTimer();
|
||||
@@ -69,10 +66,11 @@ export function startTimer() {
|
||||
// Find the *next* player who still has time
|
||||
const nextPlayerIndex = state.findNextPlayerWithTime(); // This finds ANY player with time
|
||||
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);
|
||||
// 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
|
||||
if (onTimerTickCallback) onTimerTickCallback();
|
||||
} else if (nextPlayerIndex === -1) {
|
||||
@@ -81,7 +79,7 @@ export function startTimer() {
|
||||
stopTimer(); // Stop timer if state is inconsistent
|
||||
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);
|
||||
@@ -90,10 +88,17 @@ export function startTimer() {
|
||||
export function stopTimer() {
|
||||
clearInterval(timerInterval);
|
||||
timerInterval = null;
|
||||
// Optionally stop timer sounds here if needed
|
||||
// audioManager.stop('timerTick');
|
||||
// Stop all timer-related sounds to prevent them from continuing to play
|
||||
audioManager.stopTimerSounds();
|
||||
}
|
||||
|
||||
export function isTimerRunning() {
|
||||
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
@@ -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
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
// 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 actionHandlers = {}; // Store handlers for different Flic actions
|
||||
@@ -139,7 +139,7 @@ async function subscribeToPush() {
|
||||
|
||||
if (existingSubscription) {
|
||||
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.');
|
||||
await existingSubscription.unsubscribe();
|
||||
existingSubscription = null;
|
||||
@@ -153,7 +153,7 @@ async function subscribeToPush() {
|
||||
let finalSubscription = existingSubscription;
|
||||
if (needsResubscribe) {
|
||||
console.log('Subscribing for push notifications...');
|
||||
const applicationServerKey = urlBase64ToUint8Array(PUBLIC_VAPID_KEY);
|
||||
const applicationServerKey = urlBase64ToUint8Array(getPublicVapidKey());
|
||||
finalSubscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: applicationServerKey
|
||||
|
||||
@@ -32,7 +32,7 @@ export function handleServiceWorkerMessage(event) {
|
||||
export function setupServiceWorker(messageHandler) {
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/public/sw.js')
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('ServiceWorker registered successfully.');
|
||||
|
||||
|
||||
@@ -302,6 +302,14 @@ const audioManager = {
|
||||
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)
|
||||
resetTickFade() {
|
||||
this.lastTickTime = 0;
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
// Service Worker version
|
||||
const CACHE_VERSION = 'v1.0.0';
|
||||
const CACHE_VERSION = 'v1.0.2';
|
||||
const CACHE_NAME = `game-timer-${CACHE_VERSION}`;
|
||||
|
||||
// Files to cache
|
||||
const CACHE_FILES = [
|
||||
'./',
|
||||
'./index.html',
|
||||
'../src/js/app.js',
|
||||
'../src/js/ui/audio.js',
|
||||
'./css/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'
|
||||
'/',
|
||||
'/sw.js',
|
||||
'/index.html',
|
||||
'/manifest.json',
|
||||
'/css/styles.css',
|
||||
'/favicon.ico',
|
||||
'/icons/android-chrome-192x192.png',
|
||||
'/icons/android-chrome-512x512.png',
|
||||
'/icons/apple-touch-icon.png',
|
||||
'/icons/favicon-32x32.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
|
||||
@@ -124,7 +137,7 @@ self.addEventListener('push', event => {
|
||||
// If no window is open, we MUST show a notification
|
||||
return self.registration.showNotification(pushData.title, {
|
||||
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
|
||||
});
|
||||
}
|
||||
@@ -144,7 +157,7 @@ self.addEventListener('push', event => {
|
||||
if (!messageSent) { // Only show notification if no message was sent? Or always show?
|
||||
return self.registration.showNotification(pushData.title, {
|
||||
body: pushData.body,
|
||||
icon: './icons/android-chrome-192x192.png',
|
||||
icon: '/icons/android-chrome-192x192.png',
|
||||
data: pushData.data
|
||||
});
|
||||
}
|
||||
@@ -185,7 +198,7 @@ self.addEventListener('notificationclick', event => {
|
||||
}
|
||||
|
||||
if (self.clients.openWindow) {
|
||||
return self.clients.openWindow('./');
|
||||
return self.clients.openWindow('/');
|
||||
}
|
||||
})
|
||||
);
|
||||
36
test.html
Normal 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>
|
||||