Compare commits
5 Commits
832f19235f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| de2beab911 | |||
| db06e71e69 | |||
| a480ecaf65 | |||
| 2bebd8cc88 | |||
| 1cd819938f |
@@ -49,9 +49,12 @@ build/
|
|||||||
# We need .env for our application
|
# We need .env for our application
|
||||||
#.env
|
#.env
|
||||||
.env.*
|
.env.*
|
||||||
|
# Don't ignore config.env.js
|
||||||
|
!config.env.js
|
||||||
|
|
||||||
# Project specific files
|
# Project specific files
|
||||||
|
dev-start.sh
|
||||||
|
generate-config.sh
|
||||||
labels.example
|
labels.example
|
||||||
virt-game-timer.service
|
virt-game-timer.service
|
||||||
package.json
|
package.json
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ yarn-error.log*
|
|||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
config.env.js
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
28
Dockerfile
28
Dockerfile
@@ -1,14 +1,38 @@
|
|||||||
# Use a lightweight server
|
# Use a lightweight server
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Install bash for the script execution
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /usr/share/nginx/html
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
# Copy all the application files
|
# Copy all the application files
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Copy the .env file
|
# Create a simple script to generate config.env.js
|
||||||
COPY .env .
|
RUN echo '#!/bin/sh' > /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo 'echo "// config.env.js - Generated from .env" > config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo 'echo "// This file contains environment variables for the PWA" >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo 'echo "// Generated on $(date)" >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo 'echo "" >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo 'echo "window.ENV_CONFIG = {" >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo 'grep -v "^#" .env | grep "=" | while read line; do' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo ' key=$(echo $line | cut -d= -f1)' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo ' value=$(echo $line | cut -d= -f2-)' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo ' echo " $key: \"$value\"," >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo 'done' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
echo 'echo "};" >> config.env.js' >> /usr/share/nginx/html/docker-generate-config.sh && \
|
||||||
|
chmod +x /usr/share/nginx/html/docker-generate-config.sh
|
||||||
|
|
||||||
|
# Generate config.env.js from .env
|
||||||
|
RUN /usr/share/nginx/html/docker-generate-config.sh
|
||||||
|
|
||||||
|
# Uninstall bash
|
||||||
|
RUN apk del bash
|
||||||
|
|
||||||
|
# Remove the .env file and the generation script for security
|
||||||
|
RUN rm .env docker-generate-config.sh
|
||||||
|
|
||||||
# Expose port 80
|
# Expose port 80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ game-timer/
|
|||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
The application uses environment variables for configuration. These are loaded from a `.env` file at runtime.
|
The application uses environment variables for configuration. These are loaded from a `.env` file and converted to a `config.env.js` file that is served by the web server.
|
||||||
|
|
||||||
### Setting Up Environment Variables
|
### Setting Up Environment Variables
|
||||||
|
|
||||||
@@ -45,7 +45,12 @@ The application uses environment variables for configuration. These are loaded f
|
|||||||
BACKEND_URL=https://your-push-server.example.com
|
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`.
|
3. Generate the `config.env.js` file using the provided script:
|
||||||
|
```bash
|
||||||
|
./generate-config.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. For security, never commit your `.env` file to version control. It's already included in `.gitignore`.
|
||||||
|
|
||||||
### Generating VAPID Keys
|
### Generating VAPID Keys
|
||||||
|
|
||||||
|
|||||||
130
css/styles.css
130
css/styles.css
@@ -353,18 +353,124 @@ input[type="file"] {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-footer {
|
/* Push notification controls */
|
||||||
background-color: #2c3e50;
|
.push-notification-controls {
|
||||||
color: white;
|
margin-right: 10px;
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-top: auto; /* This pushes the footer to the bottom when possible */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.author-info {
|
.notification-status-container {
|
||||||
display: flex;
|
margin: 1rem 0;
|
||||||
flex-direction: column;
|
padding: 1rem;
|
||||||
align-items: center;
|
background-color: #f8f9fa;
|
||||||
gap: 0.3rem;
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-status p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-options {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-options button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-options button:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-options button:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status indicators */
|
||||||
|
.status-granted {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-denied {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-default {
|
||||||
|
color: #ffc107;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inactive {
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
/* Service Worker Message Monitor Styles */
|
||||||
|
.message-monitor-section {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-monitor-section h3 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #cccccc !important; /* Gray background */
|
||||||
|
color: #888888 !important; /* Darker gray text */
|
||||||
|
cursor: not-allowed !important; /* Change cursor */
|
||||||
|
opacity: 0.7 !important; /* Reduce opacity */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button:disabled {
|
||||||
|
background-color: #e0e0e0 !important; /* Light gray */
|
||||||
|
color: #999999 !important;
|
||||||
|
border: 1px solid #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-output {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
30
dev-start.sh
Executable file
30
dev-start.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script to start a local development server with environment variables
|
||||||
|
|
||||||
|
# Check if .env file exists
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "Error: .env file not found!"
|
||||||
|
echo "Please create a .env file based on .env.example"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate config.env.js from .env
|
||||||
|
echo "Generating config.env.js from .env..."
|
||||||
|
./generate-config.sh
|
||||||
|
|
||||||
|
# Start a local development server
|
||||||
|
echo "Starting development server..."
|
||||||
|
if command -v python3 &> /dev/null; then
|
||||||
|
echo "Using Python3 HTTP server on port 8000..."
|
||||||
|
python3 -m http.server 8000
|
||||||
|
elif command -v python &> /dev/null; then
|
||||||
|
echo "Using Python HTTP server on port 8000..."
|
||||||
|
python -m SimpleHTTPServer 8000
|
||||||
|
elif command -v npx &> /dev/null; then
|
||||||
|
echo "Using npx serve on port 8000..."
|
||||||
|
npx serve -l 8000
|
||||||
|
else
|
||||||
|
echo "Error: Could not find a suitable static file server."
|
||||||
|
echo "Please install Python or Node.js, or manually start a server."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
39
generate-config.sh
Executable file
39
generate-config.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script to generate config.env.js from .env file
|
||||||
|
# Usage: ./generate-config.sh
|
||||||
|
|
||||||
|
# Check if .env file exists
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "Error: .env file not found!"
|
||||||
|
echo "Please create a .env file based on .env.example"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Generating config.env.js from .env..."
|
||||||
|
|
||||||
|
# Create config.env.js file
|
||||||
|
echo "// config.env.js - Generated from .env" > config.env.js
|
||||||
|
echo "// This file contains environment variables for the PWA" >> config.env.js
|
||||||
|
echo "// Generated on $(date)" >> config.env.js
|
||||||
|
echo "" >> config.env.js
|
||||||
|
echo "window.ENV_CONFIG = {" >> config.env.js
|
||||||
|
|
||||||
|
# Read .env file line by line
|
||||||
|
while IFS="=" read -r key value || [ -n "$key" ]; do
|
||||||
|
# Skip comments and empty lines
|
||||||
|
[[ $key =~ ^#.*$ ]] && continue
|
||||||
|
[[ -z $key ]] && continue
|
||||||
|
|
||||||
|
# Remove quotes if present
|
||||||
|
value="${value%\"}"
|
||||||
|
value="${value#\"}"
|
||||||
|
value="${value%\'}"
|
||||||
|
value="${value#\'}"
|
||||||
|
|
||||||
|
# Add the key-value pair to config.env.js
|
||||||
|
echo " $key: \"$value\"," >> config.env.js
|
||||||
|
done < .env
|
||||||
|
|
||||||
|
echo "};" >> config.env.js
|
||||||
|
|
||||||
|
echo "config.env.js generated successfully!"
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 427 KiB After Width: | Height: | Size: 410 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 349 KiB |
151
index.html
151
index.html
@@ -24,6 +24,11 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
|
<div class="push-notification-controls">
|
||||||
|
<button id="pushSettingsButton" class="header-button" title="Push Notification Settings">
|
||||||
|
<i class="fas fa-bell"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="game-controls">
|
<div class="game-controls">
|
||||||
<button id="gameButton" class="game-button">Start Game</button>
|
<button id="gameButton" class="game-button">Start Game</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,6 +117,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Push Notification Settings Modal -->
|
||||||
|
<div id="pushSettingsModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>Push Notification Settings</h2>
|
||||||
|
<div class="notification-status-container">
|
||||||
|
<div class="notification-status">
|
||||||
|
<p><strong>Notification Permission: </strong><span id="notificationPermissionStatus">Unknown</span></p>
|
||||||
|
<p><strong>Subscription Status: </strong><span id="subscriptionStatus">Unknown</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pushUsername">Username for Push Service</label>
|
||||||
|
<input type="text" id="pushUsername" placeholder="Enter username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pushPassword">Password</label>
|
||||||
|
<input type="password" id="pushPassword" placeholder="Enter password">
|
||||||
|
</div>
|
||||||
|
<div class="advanced-options">
|
||||||
|
<button type="button" id="pushUnsubscribeButton" class="cancel-button">Unsubscribe</button>
|
||||||
|
<button type="button" id="pushResubscribeButton" class="save-button">Subscribe</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-buttons">
|
||||||
|
<button type="button" id="pushCancelButton" class="cancel-button">Cancel</button>
|
||||||
|
<button type="button" id="pushSaveButton" class="save-button">Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message Monitor Section - No visible title -->
|
||||||
|
<div class="message-monitor-section">
|
||||||
|
<div class="monitor-controls">
|
||||||
|
<button type="button" id="simulateClickButton" class="action-button" style="margin-right: 10px;">Simulate Remote Button Click</button>
|
||||||
|
</div>
|
||||||
|
<pre id="swMessagesOutput" class="message-output">Monitoring for service worker messages...</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Load environment configuration first -->
|
||||||
|
<script src="/config.env.js"></script>
|
||||||
|
|
||||||
<!-- Main application script -->
|
<!-- Main application script -->
|
||||||
<script type="module" src="/js/app.js"></script>
|
<script type="module" src="/js/app.js"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -129,13 +174,107 @@
|
|||||||
console.log('Notification permission:', permission);
|
console.log('Notification permission:', permission);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to get stored credentials
|
||||||
|
async function getStoredCredentials() {
|
||||||
|
const storedAuth = localStorage.getItem('basicAuthCredentials');
|
||||||
|
if (!storedAuth) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const credentials = JSON.parse(storedAuth);
|
||||||
|
if (!credentials.username || !credentials.password) return null;
|
||||||
|
return credentials;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse stored credentials:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to simulate a button click with default values
|
||||||
|
async function simulateButtonClick() {
|
||||||
|
const action = 'SingleClick'; // Default action
|
||||||
|
const buttonName = 'Game-button'; // Default button name
|
||||||
|
const batteryLevel = 100; // Default battery level
|
||||||
|
|
||||||
|
const output = document.getElementById('swMessagesOutput');
|
||||||
|
// Don't show the simulating text
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get credentials
|
||||||
|
const credentials = await getStoredCredentials();
|
||||||
|
if (!credentials) {
|
||||||
|
output.textContent = 'No credentials found. Please set up credentials first.\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create basic auth header
|
||||||
|
const authHeader = 'Basic ' + btoa(`${credentials.username}:${credentials.password}`);
|
||||||
|
|
||||||
|
// Create timestamp (current time)
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
// Prepare request to backend webhook
|
||||||
|
let backendUrl;
|
||||||
|
try {
|
||||||
|
const configModule = await import('/js/config.js');
|
||||||
|
backendUrl = configModule.getBackendUrl();
|
||||||
|
} catch (error) {
|
||||||
|
output.textContent = 'Failed to load backend URL. Please check configuration.\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhookUrl = `${backendUrl}/webhook/${action}`;
|
||||||
|
output.textContent = `Sending request to: ${webhookUrl}\n`;
|
||||||
|
output.textContent += `Authorization: Basic ****\n`;
|
||||||
|
output.textContent += `Button-Name: ${buttonName}\n`;
|
||||||
|
output.textContent += `Timestamp: ${timestamp}\n`;
|
||||||
|
output.textContent += `Button-Battery-Level: ${batteryLevel}\n\n`;
|
||||||
|
|
||||||
|
// Headers similar to the curl command
|
||||||
|
const headers = {
|
||||||
|
'Authorization': authHeader,
|
||||||
|
'Button-Name': buttonName,
|
||||||
|
'Timestamp': timestamp,
|
||||||
|
'Button-Battery-Level': batteryLevel.toString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send GET request to webhook
|
||||||
|
const response = await fetch(webhookUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: headers,
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await response.json();
|
||||||
|
output.textContent += `Success! Response: ${JSON.stringify(result, null, 2)}\n`;
|
||||||
|
} catch (e) {
|
||||||
|
// Text response
|
||||||
|
result = await response.text();
|
||||||
|
output.textContent += `Success! Response: ${result}\n`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let errorText;
|
||||||
|
try {
|
||||||
|
errorText = await response.text();
|
||||||
|
} catch (e) {
|
||||||
|
errorText = `Status ${response.status}`;
|
||||||
|
}
|
||||||
|
output.textContent += `Error: ${errorText}\n`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
output.textContent += `Error: ${error.message}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach click event to the new button
|
||||||
|
const simulateClickButton = document.getElementById('simulateClickButton');
|
||||||
|
if (simulateClickButton) {
|
||||||
|
simulateClickButton.addEventListener('click', simulateButtonClick);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<footer class="app-footer">
|
|
||||||
<div class="author-info">
|
|
||||||
<p>Vibe coded by Martin</p>
|
|
||||||
<p>Version 0.0.1</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -4,15 +4,14 @@ import * as state from './core/state.js';
|
|||||||
import * as ui from './ui/ui.js';
|
import * as ui from './ui/ui.js';
|
||||||
import * as timer from './core/timer.js';
|
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 * as pushSettingsUI from './ui/pushSettingsUI.js'; // Import the new push settings UI module
|
||||||
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';
|
||||||
import * as playerManager from './core/playerManager.js';
|
import * as playerManager from './core/playerManager.js';
|
||||||
import * as eventHandlers from './core/eventHandlers.js';
|
import * as eventHandlers from './core/eventHandlers.js';
|
||||||
import * as serviceWorkerManager from './services/serviceWorkerManager.js';
|
import * as serviceWorkerManager from './services/serviceWorkerManager.js';
|
||||||
|
import * as screenLockManager from './services/screenLockManager.js'; // Import the screen lock manager
|
||||||
|
|
||||||
// --- Initialization ---
|
// --- Initialization ---
|
||||||
|
|
||||||
@@ -21,8 +20,9 @@ async function initialize() {
|
|||||||
|
|
||||||
// 0. Wait for environment variables to load
|
// 0. Wait for environment variables to load
|
||||||
try {
|
try {
|
||||||
await initEnv();
|
// Use the ensureEnvLoaded function from config.js to make sure environment variables are loaded
|
||||||
console.log("Environment variables loaded");
|
await config.ensureEnvLoaded();
|
||||||
|
console.log("Environment variables loaded and verified");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to load environment variables, using defaults:", error);
|
console.warn("Failed to load environment variables, using defaults:", error);
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,23 @@ async function initialize() {
|
|||||||
// 1. Load saved state or defaults
|
// 1. Load saved state or defaults
|
||||||
state.loadData();
|
state.loadData();
|
||||||
|
|
||||||
|
// Setup Flic action handlers early in the initialization process
|
||||||
|
// to ensure they're available when the service worker initializes
|
||||||
|
const flicActionHandlers = {
|
||||||
|
[config.FLIC_ACTIONS.SINGLE_CLICK]: playerManager.nextPlayer,
|
||||||
|
[config.FLIC_ACTIONS.DOUBLE_CLICK]: playerManager.previousPlayer,
|
||||||
|
[config.FLIC_ACTIONS.HOLD]: gameActions.togglePauseResume,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log the registered handlers for debugging
|
||||||
|
console.log("Registering Flic action handlers:", {
|
||||||
|
"SingleClick": config.FLIC_ACTIONS.SINGLE_CLICK,
|
||||||
|
"DoubleClick": config.FLIC_ACTIONS.DOUBLE_CLICK,
|
||||||
|
"Hold": config.FLIC_ACTIONS.HOLD
|
||||||
|
});
|
||||||
|
|
||||||
|
serviceWorkerManager.setFlicActionHandlers(flicActionHandlers);
|
||||||
|
|
||||||
// 2. Initialize UI (pass carousel swipe handler)
|
// 2. Initialize UI (pass carousel swipe handler)
|
||||||
ui.initUI({
|
ui.initUI({
|
||||||
onCarouselSwipe: (direction) => {
|
onCarouselSwipe: (direction) => {
|
||||||
@@ -70,22 +87,21 @@ async function initialize() {
|
|||||||
ui.elements.resetCancelButton.addEventListener('click', eventHandlers.handleResetCancel);
|
ui.elements.resetCancelButton.addEventListener('click', eventHandlers.handleResetCancel);
|
||||||
ui.elements.cameraButton.addEventListener('click', eventHandlers.handleCameraButtonClick);
|
ui.elements.cameraButton.addEventListener('click', eventHandlers.handleCameraButtonClick);
|
||||||
|
|
||||||
// 6. Setup Flic action handlers
|
// 6. Initialize Push Notification Settings UI
|
||||||
const flicActionHandlers = {
|
pushSettingsUI.initPushSettingsUI();
|
||||||
[config.FLIC_ACTIONS.SINGLE_CLICK]: playerManager.nextPlayer,
|
|
||||||
[config.FLIC_ACTIONS.DOUBLE_CLICK]: playerManager.previousPlayer,
|
|
||||||
[config.FLIC_ACTIONS.HOLD]: gameActions.togglePauseResume,
|
|
||||||
};
|
|
||||||
serviceWorkerManager.setFlicActionHandlers(flicActionHandlers);
|
|
||||||
|
|
||||||
// 7. Setup Service Worker (which also initializes Flic)
|
// 7. Setup Service Worker (which also initializes Flic)
|
||||||
serviceWorkerManager.setupServiceWorker(serviceWorkerManager.handleServiceWorkerMessage);
|
serviceWorkerManager.setupServiceWorker(serviceWorkerManager.flicMessageHandler);
|
||||||
|
|
||||||
// 8. Initial UI Update based on loaded state
|
// 8. Initialize Screen Lock Manager (automatically acquires wake lock)
|
||||||
|
const screenLockSupported = await screenLockManager.initScreenLockManager();
|
||||||
|
console.log(`Screen Wake Lock API ${screenLockSupported ? 'is' : 'is not'} supported`);
|
||||||
|
|
||||||
|
// 9. Initial UI Update based on loaded state
|
||||||
ui.renderPlayers();
|
ui.renderPlayers();
|
||||||
ui.updateGameButton();
|
ui.updateGameButton();
|
||||||
|
|
||||||
// 9. Reset running state to paused on load
|
// 10. Reset running state to paused on load
|
||||||
if (state.getGameState() === config.GAME_STATES.RUNNING) {
|
if (state.getGameState() === config.GAME_STATES.RUNNING) {
|
||||||
console.log("Game was running on load, setting to paused.");
|
console.log("Game was running on load, setting to paused.");
|
||||||
state.setGameState(config.GAME_STATES.PAUSED);
|
state.setGameState(config.GAME_STATES.PAUSED);
|
||||||
@@ -1,20 +1,38 @@
|
|||||||
// config.js
|
// config.js
|
||||||
import { getEnv } from './env-loader.js';
|
import { getEnv, waitForEnv } from './env-loader.js';
|
||||||
|
|
||||||
export function getPublicVapidKey() {
|
// Initialize environment variables
|
||||||
// Get the VAPID key from environment variables through the env-loader
|
let envInitialized = false;
|
||||||
return getEnv('PUBLIC_VAPID_KEY', 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E');
|
let initPromise = null;
|
||||||
|
|
||||||
|
// Function to ensure environment variables are loaded
|
||||||
|
export async function ensureEnvLoaded() {
|
||||||
|
if (envInitialized) return;
|
||||||
|
|
||||||
|
if (!initPromise) {
|
||||||
|
initPromise = waitForEnv().then(() => {
|
||||||
|
envInitialized = true;
|
||||||
|
console.log('Environment variables loaded in config.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return initPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The VAPID key should not be exposed directly in the source code
|
// Initialize immediately
|
||||||
// Use the getter function instead: getPublicVapidKey()
|
ensureEnvLoaded();
|
||||||
// export const PUBLIC_VAPID_KEY = 'BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E';
|
|
||||||
|
// Direct access to environment variables (synchronous, may return default values if called too early)
|
||||||
|
export function getPublicVapidKey() {
|
||||||
|
return getEnv('PUBLIC_VAPID_KEY');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBackendUrl() {
|
||||||
|
return getEnv('BACKEND_URL');
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// Default player settings
|
// Default player settings
|
||||||
export const DEFAULT_PLAYER_TIME_SECONDS = 300; // 5 minutes
|
export const DEFAULT_PLAYER_TIME_SECONDS = 300; // 5 minutes
|
||||||
@@ -4,6 +4,7 @@ import * as state from './state.js';
|
|||||||
import * as ui from '../ui/ui.js';
|
import * as ui from '../ui/ui.js';
|
||||||
import * as timer from './timer.js';
|
import * as timer from './timer.js';
|
||||||
import audioManager from '../ui/audio.js';
|
import audioManager from '../ui/audio.js';
|
||||||
|
import * as screenLockManager from '../services/screenLockManager.js'; // Import screen lock manager
|
||||||
|
|
||||||
// --- Core Game Actions ---
|
// --- Core Game Actions ---
|
||||||
|
|
||||||
@@ -12,6 +13,14 @@ export function handleGameOver() {
|
|||||||
state.setGameState(config.GAME_STATES.OVER);
|
state.setGameState(config.GAME_STATES.OVER);
|
||||||
audioManager.play('gameOver');
|
audioManager.play('gameOver');
|
||||||
timer.stopTimer(); // Ensure timer is stopped
|
timer.stopTimer(); // Ensure timer is stopped
|
||||||
|
|
||||||
|
// Release screen wake lock when game is over
|
||||||
|
screenLockManager.releaseWakeLock().then(success => {
|
||||||
|
if (success) {
|
||||||
|
console.log('Screen wake lock released on game over');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ui.updateGameButton();
|
ui.updateGameButton();
|
||||||
ui.renderPlayers(); // Update to show final state
|
ui.renderPlayers(); // Update to show final state
|
||||||
}
|
}
|
||||||
@@ -25,6 +34,16 @@ export function startGame() {
|
|||||||
state.setGameState(config.GAME_STATES.RUNNING);
|
state.setGameState(config.GAME_STATES.RUNNING);
|
||||||
audioManager.play('gameStart');
|
audioManager.play('gameStart');
|
||||||
timer.startTimer();
|
timer.startTimer();
|
||||||
|
|
||||||
|
// Acquire screen wake lock when game starts
|
||||||
|
screenLockManager.acquireWakeLock().then(success => {
|
||||||
|
if (success) {
|
||||||
|
console.log('Screen wake lock acquired for game');
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to acquire screen wake lock');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ui.updateGameButton();
|
ui.updateGameButton();
|
||||||
ui.renderPlayers(); // Ensure active timer styling is applied
|
ui.renderPlayers(); // Ensure active timer styling is applied
|
||||||
}
|
}
|
||||||
@@ -35,6 +54,14 @@ export function pauseGame() {
|
|||||||
state.setGameState(config.GAME_STATES.PAUSED);
|
state.setGameState(config.GAME_STATES.PAUSED);
|
||||||
audioManager.play('gamePause');
|
audioManager.play('gamePause');
|
||||||
timer.stopTimer();
|
timer.stopTimer();
|
||||||
|
|
||||||
|
// Release screen wake lock when game is paused
|
||||||
|
screenLockManager.releaseWakeLock().then(success => {
|
||||||
|
if (success) {
|
||||||
|
console.log('Screen wake lock released on pause');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ui.updateGameButton();
|
ui.updateGameButton();
|
||||||
ui.renderPlayers(); // Ensure active timer styling is removed
|
ui.renderPlayers(); // Ensure active timer styling is removed
|
||||||
}
|
}
|
||||||
@@ -76,6 +103,14 @@ export function resetGame() {
|
|||||||
state.resetPlayersTime();
|
state.resetPlayersTime();
|
||||||
state.setGameState(config.GAME_STATES.SETUP);
|
state.setGameState(config.GAME_STATES.SETUP);
|
||||||
state.setCurrentPlayerIndex(0); // Go back to first player
|
state.setCurrentPlayerIndex(0); // Go back to first player
|
||||||
|
|
||||||
|
// Release screen wake lock when game is reset
|
||||||
|
screenLockManager.releaseWakeLock().then(success => {
|
||||||
|
if (success) {
|
||||||
|
console.log('Screen wake lock released on reset');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
audioManager.play('buttonClick'); // Or a specific reset sound?
|
audioManager.play('buttonClick'); // Or a specific reset sound?
|
||||||
ui.updateGameButton();
|
ui.updateGameButton();
|
||||||
ui.renderPlayers();
|
ui.renderPlayers();
|
||||||
@@ -84,6 +119,14 @@ export function resetGame() {
|
|||||||
export function fullResetApp() {
|
export function fullResetApp() {
|
||||||
timer.stopTimer();
|
timer.stopTimer();
|
||||||
state.resetToDefaults();
|
state.resetToDefaults();
|
||||||
|
|
||||||
|
// Release screen wake lock on full reset
|
||||||
|
screenLockManager.releaseWakeLock().then(success => {
|
||||||
|
if (success) {
|
||||||
|
console.log('Screen wake lock released on full reset');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
audioManager.play('gameOver'); // Use game over sound for full reset
|
audioManager.play('gameOver'); // Use game over sound for full reset
|
||||||
ui.hideResetModal();
|
ui.hideResetModal();
|
||||||
ui.updateGameButton();
|
ui.updateGameButton();
|
||||||
87
js/env-loader.js
Normal file
87
js/env-loader.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// env-loader.js
|
||||||
|
// This module is responsible for loading environment variables for the PWA
|
||||||
|
|
||||||
|
// Store environment variables in a global object
|
||||||
|
window.ENV_CONFIG = {
|
||||||
|
// Default values that will be overridden when config.env.js loads
|
||||||
|
PUBLIC_VAPID_KEY: 'your_public_vapid_key_here',
|
||||||
|
BACKEND_URL: 'https://your-push-server.example.com'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to load environment variables from config.env.js
|
||||||
|
async function loadEnvVariables() {
|
||||||
|
try {
|
||||||
|
// Try to fetch the config.env.js file which will be generated at build time or deployment
|
||||||
|
const response = await fetch('/config.env.js');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn('Could not load config.env.js file. Using default values.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configText = await response.text();
|
||||||
|
|
||||||
|
// Extract the configuration object from the JavaScript file
|
||||||
|
// The file should be in format: window.ENV_CONFIG = { key: "value" };
|
||||||
|
try {
|
||||||
|
// Create a safe way to evaluate the config file
|
||||||
|
const configScript = document.createElement('script');
|
||||||
|
configScript.textContent = configText;
|
||||||
|
document.head.appendChild(configScript);
|
||||||
|
document.head.removeChild(configScript);
|
||||||
|
|
||||||
|
console.log('Environment variables loaded successfully from config.env.js');
|
||||||
|
|
||||||
|
// Dispatch an event to notify that environment variables have been loaded
|
||||||
|
window.dispatchEvent(new CustomEvent('env-config-loaded', {
|
||||||
|
detail: { config: window.ENV_CONFIG }
|
||||||
|
}));
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('Error parsing config.env.js:', parseError);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading environment variables:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export function to initialize environment variables
|
||||||
|
export async function initEnv() {
|
||||||
|
await loadEnvVariables();
|
||||||
|
return window.ENV_CONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start loading environment variables immediately
|
||||||
|
initEnv();
|
||||||
|
|
||||||
|
// Export access functions for environment variables
|
||||||
|
export function getEnv(key, defaultValue = '') {
|
||||||
|
return window.ENV_CONFIG[key] || defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export a function to wait for environment variables to be loaded
|
||||||
|
export function waitForEnv() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// If we already have non-default values, resolve immediately
|
||||||
|
if (window.ENV_CONFIG.BACKEND_URL !== 'https://your-push-server.example.com') {
|
||||||
|
resolve(window.ENV_CONFIG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, wait for the env-config-loaded event
|
||||||
|
window.addEventListener('env-config-loaded', (event) => {
|
||||||
|
resolve(event.detail.config);
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
// Set a timeout to resolve with current values if loading takes too long
|
||||||
|
setTimeout(() => {
|
||||||
|
console.warn('Environment loading timed out, using current values');
|
||||||
|
resolve(window.ENV_CONFIG);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initEnv,
|
||||||
|
getEnv,
|
||||||
|
waitForEnv
|
||||||
|
};
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
// pushFlicIntegration.js
|
// pushFlicIntegration.js
|
||||||
import { getPublicVapidKey, BACKEND_URL, FLIC_BUTTON_ID, FLIC_ACTIONS, FLIC_BATTERY_THRESHOLD } from '../config.js';
|
import { getPublicVapidKey, getBackendUrl, FLIC_BUTTON_ID} 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
|
||||||
let lastBatteryWarningTimestamp = 0; // Track when last battery warning was shown
|
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
|
|
||||||
@@ -29,19 +28,6 @@ function getBasicAuthCredentials() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt the user for credentials after permissions are granted
|
|
||||||
function promptForCredentials() {
|
|
||||||
console.log('Prompting user for auth credentials.');
|
|
||||||
const username = prompt('Please enter your username for backend authentication:');
|
|
||||||
if (!username) return null;
|
|
||||||
const password = prompt('Please enter your password:');
|
|
||||||
if (!password) return null;
|
|
||||||
|
|
||||||
const credentials = { username, password };
|
|
||||||
localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials));
|
|
||||||
return credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Basic Auth header string
|
// Create Basic Auth header string
|
||||||
function createBasicAuthHeader(credentials) {
|
function createBasicAuthHeader(credentials) {
|
||||||
if (!credentials?.username || !credentials.password) return null;
|
if (!credentials?.username || !credentials.password) return null;
|
||||||
@@ -68,31 +54,6 @@ function arrayBufferToBase64(buffer) {
|
|||||||
return window.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
return window.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a popup notification to the user
|
|
||||||
function showBatteryWarning(batteryLevel) {
|
|
||||||
// Only show warning once every 4 hours (to avoid annoying users)
|
|
||||||
const now = Date.now();
|
|
||||||
const fourHoursInMs = 4 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
if (now - lastBatteryWarningTimestamp < fourHoursInMs) {
|
|
||||||
console.log(`[PushFlic] Battery warning suppressed (shown recently): ${batteryLevel}%`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastBatteryWarningTimestamp = now;
|
|
||||||
|
|
||||||
// Show the notification
|
|
||||||
const message = `Flic button battery is low (${batteryLevel}%). Please replace the battery soon.`;
|
|
||||||
|
|
||||||
// Show browser notification if permission granted
|
|
||||||
if (Notification.permission === 'granted') {
|
|
||||||
new Notification('Flic Button Low Battery', {
|
|
||||||
body: message,
|
|
||||||
icon: '/public/favicon.ico'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Push Subscription Logic ---
|
// --- Push Subscription Logic ---
|
||||||
|
|
||||||
async function subscribeToPush() {
|
async function subscribeToPush() {
|
||||||
@@ -116,27 +77,24 @@ async function subscribeToPush() {
|
|||||||
|
|
||||||
console.log('Notification permission granted.');
|
console.log('Notification permission granted.');
|
||||||
|
|
||||||
// After permission is granted, check for stored credentials or prompt user
|
// Get stored credentials but don't prompt
|
||||||
let credentials = getBasicAuthCredentials();
|
let credentials = getBasicAuthCredentials();
|
||||||
if (!credentials) {
|
const hasExistingCreds = !!credentials;
|
||||||
const confirmAuth = confirm('Do you want to set up credentials for push notifications now?');
|
console.log('Has existing credentials:', hasExistingCreds);
|
||||||
if (!confirmAuth) {
|
|
||||||
console.log('User declined to provide auth credentials.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
credentials = promptForCredentials();
|
// No prompting for credentials - user must enter them manually in the UI
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
console.log('User canceled credential input.');
|
console.log('No credentials found. User needs to enter them manually.');
|
||||||
alert('Authentication required to set up push notifications.');
|
// Just return if no credentials are available
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const registration = await navigator.serviceWorker.ready;
|
const registration = await navigator.serviceWorker.ready;
|
||||||
let existingSubscription = await registration.pushManager.getSubscription();
|
let existingSubscription = await registration.pushManager.getSubscription();
|
||||||
let needsResubscribe = !existingSubscription;
|
let needsResubscribe = !existingSubscription;
|
||||||
|
|
||||||
|
console.log('Existing subscription found:', !!existingSubscription);
|
||||||
|
|
||||||
if (existingSubscription) {
|
if (existingSubscription) {
|
||||||
const existingKey = existingSubscription.options?.applicationServerKey;
|
const existingKey = existingSubscription.options?.applicationServerKey;
|
||||||
if (!existingKey || arrayBufferToBase64(existingKey) !== getPublicVapidKey()) {
|
if (!existingKey || arrayBufferToBase64(existingKey) !== getPublicVapidKey()) {
|
||||||
@@ -154,12 +112,18 @@ async function subscribeToPush() {
|
|||||||
if (needsResubscribe) {
|
if (needsResubscribe) {
|
||||||
console.log('Subscribing for push notifications...');
|
console.log('Subscribing for push notifications...');
|
||||||
const applicationServerKey = urlBase64ToUint8Array(getPublicVapidKey());
|
const applicationServerKey = urlBase64ToUint8Array(getPublicVapidKey());
|
||||||
finalSubscription = await registration.pushManager.subscribe({
|
try {
|
||||||
userVisibleOnly: true,
|
finalSubscription = await registration.pushManager.subscribe({
|
||||||
applicationServerKey: applicationServerKey
|
userVisibleOnly: true,
|
||||||
});
|
applicationServerKey: applicationServerKey
|
||||||
console.log('New push subscription obtained:', finalSubscription);
|
});
|
||||||
pushSubscription = finalSubscription; // Store it
|
console.log('New push subscription obtained:', finalSubscription);
|
||||||
|
pushSubscription = finalSubscription; // Store it
|
||||||
|
} catch (subscribeError) {
|
||||||
|
console.error('Error subscribing to push:', subscribeError);
|
||||||
|
alert(`Failed to subscribe: ${subscribeError.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!finalSubscription) {
|
if (!finalSubscription) {
|
||||||
@@ -180,20 +144,8 @@ async function sendSubscriptionToServer(subscription, buttonId) {
|
|||||||
console.log(`Sending subscription for button "${buttonId}" to backend...`);
|
console.log(`Sending subscription for button "${buttonId}" to backend...`);
|
||||||
const credentials = getBasicAuthCredentials();
|
const credentials = getBasicAuthCredentials();
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
// One more chance to enter credentials if needed
|
console.log('No credentials found. User needs to enter them manually.');
|
||||||
const confirmAuth = confirm('Authentication required to complete setup. Provide credentials now?');
|
return;
|
||||||
if (!confirmAuth) {
|
|
||||||
alert('Authentication required to save button link.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCredentials = promptForCredentials();
|
|
||||||
if (!newCredentials) {
|
|
||||||
alert('Authentication required to save button link.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
credentials = newCredentials;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = { 'Content-Type': 'application/json' };
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
@@ -202,7 +154,7 @@ async function sendSubscriptionToServer(subscription, buttonId) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Add support for handling CORS preflight with credentials
|
// Add support for handling CORS preflight with credentials
|
||||||
const response = await fetch(`${BACKEND_URL}/subscribe`, {
|
const response = await fetch(`${getBackendUrl()}/subscribe`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ button_id: buttonId, subscription: subscription }),
|
body: JSON.stringify({ button_id: buttonId, subscription: subscription }),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@@ -212,7 +164,27 @@ async function sendSubscriptionToServer(subscription, buttonId) {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log('Subscription sent successfully:', result.message);
|
console.log('Subscription sent successfully:', result.message);
|
||||||
alert('Push notification setup completed successfully!');
|
|
||||||
|
// Update the UI to show subscription status as active
|
||||||
|
const subscriptionStatusElement = document.getElementById('subscriptionStatus');
|
||||||
|
if (subscriptionStatusElement) {
|
||||||
|
subscriptionStatusElement.textContent = 'active';
|
||||||
|
subscriptionStatusElement.className = 'status-active';
|
||||||
|
|
||||||
|
// Enable unsubscribe button when subscription is active
|
||||||
|
const unsubscribeButton = document.getElementById('pushUnsubscribeButton');
|
||||||
|
if (unsubscribeButton) unsubscribeButton.disabled = false;
|
||||||
|
|
||||||
|
// Change subscribe button text to "Re-subscribe"
|
||||||
|
const resubscribeButton = document.getElementById('pushResubscribeButton');
|
||||||
|
if (resubscribeButton) resubscribeButton.textContent = 'Re-subscribe';
|
||||||
|
|
||||||
|
// Enable simulate button when subscription is active
|
||||||
|
const simulateButton = document.getElementById('simulateClickButton');
|
||||||
|
if (simulateButton) simulateButton.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success alert removed as requested
|
||||||
} else {
|
} else {
|
||||||
let errorMsg = `Server error: ${response.status}`;
|
let errorMsg = `Server error: ${response.status}`;
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
@@ -236,11 +208,6 @@ async function sendSubscriptionToServer(subscription, buttonId) {
|
|||||||
export function handleFlicAction(action, buttonId, timestamp, batteryLevel) {
|
export function handleFlicAction(action, buttonId, timestamp, batteryLevel) {
|
||||||
console.log(`[PushFlic] Received Action: ${action} from Button: ${buttonId} battery: ${batteryLevel}% at ${timestamp}`);
|
console.log(`[PushFlic] Received Action: ${action} from Button: ${buttonId} battery: ${batteryLevel}% at ${timestamp}`);
|
||||||
|
|
||||||
// Check if battery is below threshold and show warning if needed
|
|
||||||
if (batteryLevel < FLIC_BATTERY_THRESHOLD) {
|
|
||||||
showBatteryWarning(batteryLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore actions from buttons other than the configured one
|
// Ignore actions from buttons other than the configured one
|
||||||
if (buttonId !== FLIC_BUTTON_ID) {
|
if (buttonId !== FLIC_BUTTON_ID) {
|
||||||
console.warn(`[PushFlic] Ignoring action from unknown button: ${buttonId}`);
|
console.warn(`[PushFlic] Ignoring action from unknown button: ${buttonId}`);
|
||||||
@@ -249,35 +216,36 @@ export function handleFlicAction(action, buttonId, timestamp, batteryLevel) {
|
|||||||
|
|
||||||
// Find the registered handler for this action
|
// Find the registered handler for this action
|
||||||
const handler = actionHandlers[action];
|
const handler = actionHandlers[action];
|
||||||
|
|
||||||
if (handler && typeof handler === 'function') {
|
if (handler && typeof handler === 'function') {
|
||||||
console.log(`[PushFlic] Executing handler for ${action}`);
|
console.log(`[PushFlic] Executing handler for ${action}`);
|
||||||
// Execute the handler registered in app.js
|
try {
|
||||||
handler(); // Use the handler function directly instead of hardcoded function calls
|
// Execute the handler registered in app.js
|
||||||
|
handler();
|
||||||
|
// Log success
|
||||||
|
console.log(`[PushFlic] Successfully executed handler for ${action}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[PushFlic] Error executing handler for ${action}:`, error);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[PushFlic] No handler registered for action: ${action}`);
|
console.warn(`[PushFlic] No handler registered for action: ${action}. Available handlers:`, Object.keys(actionHandlers));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Initialization ---
|
// --- Initialization ---
|
||||||
|
|
||||||
|
// Initialize PushFlic with action handlers
|
||||||
export function initPushFlic(handlers) {
|
export function initPushFlic(handlers) {
|
||||||
actionHandlers = handlers; // Store the handlers passed from app.js
|
if (handlers && Object.keys(handlers).length > 0) {
|
||||||
// Example: handlers = { SingleClick: handleNextPlayer, Hold: handleTogglePause }
|
actionHandlers = handlers;
|
||||||
|
console.log('[PushFlic] Stored action handlers:', Object.keys(actionHandlers));
|
||||||
// Attempt to subscribe immediately if permission might already be granted
|
} else {
|
||||||
// Or trigger subscription on a user action (e.g., a "Link Flic Button" button)
|
console.warn('[PushFlic] No action handlers provided to initPushFlic!');
|
||||||
// For simplicity, let's try subscribing if SW is ready and permission allows
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
Notification.requestPermission().then(permission => {
|
|
||||||
if (permission === 'granted') {
|
|
||||||
console.log('[PushFlic] Permission granted, attempting subscription.');
|
|
||||||
subscribeToPush();
|
|
||||||
} else {
|
|
||||||
console.log('[PushFlic] Notification permission not granted.');
|
|
||||||
// Optionally provide a button for the user to trigger subscription later
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New function to manually trigger the subscription process
|
||||||
|
export function setupPushNotifications() {
|
||||||
|
console.log('[PushFlic] Manually triggering push notification setup');
|
||||||
|
subscribeToPush();
|
||||||
|
}
|
||||||
128
js/services/screenLockManager.js
Normal file
128
js/services/screenLockManager.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// screenLockManager.js - Manages screen wake lock to prevent screen from turning off
|
||||||
|
// Uses the Screen Wake Lock API: https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API
|
||||||
|
|
||||||
|
let wakeLock = null;
|
||||||
|
let isLockEnabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests a screen wake lock to prevent the screen from turning off
|
||||||
|
* @returns {Promise<boolean>} - True if wake lock was acquired successfully
|
||||||
|
*/
|
||||||
|
export async function acquireWakeLock() {
|
||||||
|
if (!isScreenWakeLockSupported()) {
|
||||||
|
console.warn('[ScreenLockManager] Screen Wake Lock API not supported in this browser');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Release any existing wake lock first
|
||||||
|
await releaseWakeLock();
|
||||||
|
|
||||||
|
// Request a new wake lock
|
||||||
|
wakeLock = await navigator.wakeLock.request('screen');
|
||||||
|
isLockEnabled = true;
|
||||||
|
|
||||||
|
console.log('[ScreenLockManager] Screen Wake Lock acquired');
|
||||||
|
|
||||||
|
// Add event listeners to reacquire the lock when needed
|
||||||
|
setupWakeLockListeners();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ScreenLockManager] Error acquiring wake lock:', error);
|
||||||
|
isLockEnabled = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases the screen wake lock if one is active
|
||||||
|
* @returns {Promise<boolean>} - True if wake lock was released successfully
|
||||||
|
*/
|
||||||
|
export async function releaseWakeLock() {
|
||||||
|
if (!wakeLock) {
|
||||||
|
return true; // No wake lock to release
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wakeLock.release();
|
||||||
|
wakeLock = null;
|
||||||
|
isLockEnabled = false;
|
||||||
|
console.log('[ScreenLockManager] Screen Wake Lock released');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ScreenLockManager] Error releasing wake lock:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the Screen Wake Lock API is supported in this browser
|
||||||
|
* @returns {boolean} - True if supported
|
||||||
|
*/
|
||||||
|
export function isScreenWakeLockSupported() {
|
||||||
|
return 'wakeLock' in navigator && 'request' in navigator.wakeLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current status of the wake lock
|
||||||
|
* @returns {boolean} - True if wake lock is currently active
|
||||||
|
*/
|
||||||
|
export function isWakeLockActive() {
|
||||||
|
return isLockEnabled && wakeLock !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up event listeners to reacquire the wake lock when needed
|
||||||
|
* (e.g., when the page becomes visible again after being hidden)
|
||||||
|
*/
|
||||||
|
function setupWakeLockListeners() {
|
||||||
|
// When the page becomes visible again, reacquire the wake lock
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
|
// When the screen orientation changes, reacquire the wake lock
|
||||||
|
if ('screen' in window && 'orientation' in window.screen) {
|
||||||
|
window.screen.orientation.addEventListener('change', handleOrientationChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles visibility change events to reacquire wake lock when page becomes visible
|
||||||
|
*/
|
||||||
|
async function handleVisibilityChange() {
|
||||||
|
if (isLockEnabled && document.visibilityState === 'visible') {
|
||||||
|
// Only try to reacquire if we previously had a lock
|
||||||
|
await acquireWakeLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles orientation change events to reacquire wake lock
|
||||||
|
*/
|
||||||
|
async function handleOrientationChange() {
|
||||||
|
if (isLockEnabled) {
|
||||||
|
// Some devices may release the wake lock on orientation change
|
||||||
|
await acquireWakeLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the screen lock manager
|
||||||
|
* @param {Object} options - Configuration options
|
||||||
|
* @param {boolean} options.autoAcquire - Whether to automatically acquire wake lock on init
|
||||||
|
* @returns {Promise<boolean>} - True if initialization was successful
|
||||||
|
*/
|
||||||
|
export async function initScreenLockManager(options = {}) {
|
||||||
|
const { autoAcquire = true } = options; // Default to true - automatically acquire on init
|
||||||
|
|
||||||
|
// Check for support
|
||||||
|
const isSupported = isScreenWakeLockSupported();
|
||||||
|
console.log(`[ScreenLockManager] Screen Wake Lock API ${isSupported ? 'is' : 'is not'} supported`);
|
||||||
|
|
||||||
|
// Automatically acquire wake lock if supported (now default behavior)
|
||||||
|
if (autoAcquire && isSupported) {
|
||||||
|
return await acquireWakeLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSupported;
|
||||||
|
}
|
||||||
116
js/services/serviceWorkerManager.js
Normal file
116
js/services/serviceWorkerManager.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// serviceWorkerManager.js - Service worker registration and Flic integration
|
||||||
|
import * as pushFlic from './pushFlicIntegration.js';
|
||||||
|
|
||||||
|
// Store the action handlers passed from app.js
|
||||||
|
let flicActionHandlers = {};
|
||||||
|
|
||||||
|
export function setFlicActionHandlers(handlers) {
|
||||||
|
if (handlers && Object.keys(handlers).length > 0) {
|
||||||
|
flicActionHandlers = handlers;
|
||||||
|
console.log('[ServiceWorkerManager] Stored action handlers:', Object.keys(flicActionHandlers));
|
||||||
|
|
||||||
|
// Always pass handlers to pushFlic, regardless of service worker state
|
||||||
|
pushFlic.initPushFlic(flicActionHandlers);
|
||||||
|
} else {
|
||||||
|
console.warn('[ServiceWorkerManager] No action handlers provided to setFlicActionHandlers!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Flic Integration Setup ---
|
||||||
|
export function initFlic() {
|
||||||
|
// Make sure we have handlers before initializing
|
||||||
|
if (Object.keys(flicActionHandlers).length === 0) {
|
||||||
|
console.warn('[ServiceWorkerManager] No Flic handlers registered before initFlic! Actions may not work.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is used by setupServiceWorker and relies on
|
||||||
|
// flicActionHandlers being set before this is called
|
||||||
|
console.log('[ServiceWorkerManager] Initializing PushFlic with handlers:', Object.keys(flicActionHandlers));
|
||||||
|
pushFlic.initPushFlic(flicActionHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export functions for manually triggering push notifications setup
|
||||||
|
export function setupPushNotifications() {
|
||||||
|
pushFlic.setupPushNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Handle Messages from Service Worker ---
|
||||||
|
|
||||||
|
export function flicMessageHandler(event) {
|
||||||
|
// This function is passed to setupServiceWorker and called when a message arrives from the service worker
|
||||||
|
console.log('[App] Message received from Service Worker:', event.data);
|
||||||
|
|
||||||
|
// Check if this is a Flic action message
|
||||||
|
if (event.data && event.data.type === 'flic-action') {
|
||||||
|
const { action, button, timestamp, batteryLevel } = event.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Pass to push-flic service to handle
|
||||||
|
pushFlic.handleFlicAction(action, button, timestamp, batteryLevel);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[App] Error handling flic action:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global message handler function to ensure we catch all service worker messages
|
||||||
|
function handleServiceWorkerMessage(event) {
|
||||||
|
// Check if the message might be from our service worker
|
||||||
|
if (event.data && typeof event.data === 'object') {
|
||||||
|
console.log('[App] Potential window message received:', event.data);
|
||||||
|
|
||||||
|
// If it looks like a flic action message, handle it
|
||||||
|
if (event.data.type === 'flic-action') {
|
||||||
|
try {
|
||||||
|
// Process the message with our flicMessageHandler
|
||||||
|
flicMessageHandler(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[App] Error handling window message:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Service Worker and PWA Setup ---
|
||||||
|
export function setupServiceWorker(messageHandler) {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
console.log('[ServiceWorkerManager] Setting up service worker...');
|
||||||
|
|
||||||
|
// Set up global message event listener on window object
|
||||||
|
window.addEventListener('message', handleServiceWorkerMessage);
|
||||||
|
|
||||||
|
// Listen for messages FROM the Service Worker
|
||||||
|
// This is the main way messages from the service worker are received
|
||||||
|
navigator.serviceWorker.addEventListener('message', event => {
|
||||||
|
console.log('[ServiceWorkerManager] Service worker message received:', event.data);
|
||||||
|
messageHandler(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
console.log('[ServiceWorkerManager] Window loaded, registering service worker...');
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then(registration => {
|
||||||
|
console.log('[ServiceWorkerManager] ServiceWorker registered successfully.');
|
||||||
|
|
||||||
|
// Add an event listener that will work with service worker controlled clients
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
console.log('[ServiceWorkerManager] Service worker already controlling the page.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Flic integration
|
||||||
|
initFlic();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[ServiceWorkerManager] ServiceWorker registration failed:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for SW controller changes
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
|
console.log('[ServiceWorkerManager] Service Worker controller changed, potentially updated.');
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.warn('[ServiceWorkerManager] ServiceWorker not supported.');
|
||||||
|
}
|
||||||
|
}
|
||||||
435
js/ui/pushSettingsUI.js
Normal file
435
js/ui/pushSettingsUI.js
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
// pushSettingsUI.js - UI handling for push notification settings
|
||||||
|
import { setupPushNotifications } from '../services/serviceWorkerManager.js';
|
||||||
|
import { FLIC_BUTTON_ID, getBackendUrl} from '../config.js';
|
||||||
|
|
||||||
|
// --- DOM Elements ---
|
||||||
|
const elements = {
|
||||||
|
pushSettingsButton: null,
|
||||||
|
pushSettingsModal: null,
|
||||||
|
notificationPermissionStatus: null,
|
||||||
|
subscriptionStatus: null,
|
||||||
|
pushUsername: null,
|
||||||
|
pushPassword: null,
|
||||||
|
pushSaveButton: null,
|
||||||
|
pushCancelButton: null,
|
||||||
|
pushUnsubscribeButton: null,
|
||||||
|
pushResubscribeButton: null,
|
||||||
|
// Message monitor elements
|
||||||
|
swMessagesOutput: null,
|
||||||
|
simulateClickButton: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
let currentSubscription = null;
|
||||||
|
let messageListener = null;
|
||||||
|
let isMonitoring = false;
|
||||||
|
|
||||||
|
// --- Initialization ---
|
||||||
|
export function initPushSettingsUI() {
|
||||||
|
// Cache DOM elements
|
||||||
|
elements.pushSettingsButton = document.getElementById('pushSettingsButton');
|
||||||
|
elements.pushSettingsModal = document.getElementById('pushSettingsModal');
|
||||||
|
elements.notificationPermissionStatus = document.getElementById('notificationPermissionStatus');
|
||||||
|
elements.subscriptionStatus = document.getElementById('subscriptionStatus');
|
||||||
|
elements.pushUsername = document.getElementById('pushUsername');
|
||||||
|
elements.pushPassword = document.getElementById('pushPassword');
|
||||||
|
elements.pushSaveButton = document.getElementById('pushSaveButton');
|
||||||
|
elements.pushCancelButton = document.getElementById('pushCancelButton');
|
||||||
|
elements.pushUnsubscribeButton = document.getElementById('pushUnsubscribeButton');
|
||||||
|
elements.pushResubscribeButton = document.getElementById('pushResubscribeButton');
|
||||||
|
|
||||||
|
// Message Monitor elements
|
||||||
|
elements.swMessagesOutput = document.getElementById('swMessagesOutput');
|
||||||
|
elements.simulateClickButton = document.getElementById('simulateClickButton');
|
||||||
|
|
||||||
|
// Set up event listeners
|
||||||
|
elements.pushSettingsButton.addEventListener('click', openPushSettingsModal);
|
||||||
|
elements.pushCancelButton.addEventListener('click', closePushSettingsModal);
|
||||||
|
elements.pushSaveButton.addEventListener('click', saveCredentialsAndSubscribe);
|
||||||
|
elements.pushUnsubscribeButton.addEventListener('click', unsubscribeFromPush);
|
||||||
|
elements.pushResubscribeButton.addEventListener('click', resubscribeToPush);
|
||||||
|
|
||||||
|
// Initial status check
|
||||||
|
updateNotificationStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UI Functions ---
|
||||||
|
|
||||||
|
// Open the push settings modal and update statuses
|
||||||
|
function openPushSettingsModal() {
|
||||||
|
// Update status displays
|
||||||
|
updateNotificationStatus();
|
||||||
|
updateSubscriptionStatus();
|
||||||
|
|
||||||
|
// Load saved credentials if available
|
||||||
|
loadSavedCredentials();
|
||||||
|
|
||||||
|
// Start monitoring automatically when modal opens
|
||||||
|
startMessageMonitoring();
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
elements.pushSettingsModal.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the push settings modal
|
||||||
|
function closePushSettingsModal() {
|
||||||
|
// Stop monitoring when the modal is closed to avoid unnecessary processing
|
||||||
|
stopMessageMonitoring();
|
||||||
|
elements.pushSettingsModal.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Message Monitor Functions ---
|
||||||
|
|
||||||
|
// Start monitoring service worker messages
|
||||||
|
function startMessageMonitoring() {
|
||||||
|
// If already monitoring, don't set up a new listener
|
||||||
|
if (isMonitoring) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
elements.swMessagesOutput.textContent = 'Service Worker not supported in this browser.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the output area
|
||||||
|
elements.swMessagesOutput.textContent = 'Monitoring for service worker messages...';
|
||||||
|
|
||||||
|
// Create and register the message listener
|
||||||
|
messageListener = function(event) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const formattedMessage = `[${now}] Message received: \n${JSON.stringify(event.data, null, 2)}\n\n`;
|
||||||
|
elements.swMessagesOutput.textContent += formattedMessage;
|
||||||
|
|
||||||
|
// Auto-scroll to the bottom
|
||||||
|
elements.swMessagesOutput.scrollTop = elements.swMessagesOutput.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the listener
|
||||||
|
navigator.serviceWorker.addEventListener('message', messageListener);
|
||||||
|
isMonitoring = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop monitoring service worker messages
|
||||||
|
function stopMessageMonitoring() {
|
||||||
|
if (messageListener) {
|
||||||
|
navigator.serviceWorker.removeEventListener('message', messageListener);
|
||||||
|
messageListener = null;
|
||||||
|
isMonitoring = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the notification permission status display
|
||||||
|
function updateNotificationStatus() {
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
elements.notificationPermissionStatus.textContent = 'Not Supported';
|
||||||
|
elements.notificationPermissionStatus.className = 'status-denied';
|
||||||
|
// Disable subscribe button when notifications are not supported
|
||||||
|
elements.pushResubscribeButton.disabled = true;
|
||||||
|
elements.pushResubscribeButton.classList.add('disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = Notification.permission;
|
||||||
|
elements.notificationPermissionStatus.textContent = permission;
|
||||||
|
|
||||||
|
switch (permission) {
|
||||||
|
case 'granted':
|
||||||
|
elements.notificationPermissionStatus.className = 'status-granted';
|
||||||
|
// Enable subscribe button when permission is granted
|
||||||
|
elements.pushResubscribeButton.disabled = false;
|
||||||
|
elements.pushResubscribeButton.classList.remove('disabled');
|
||||||
|
break;
|
||||||
|
case 'denied':
|
||||||
|
elements.notificationPermissionStatus.className = 'status-denied';
|
||||||
|
// Disable subscribe button when permission is denied
|
||||||
|
elements.pushResubscribeButton.disabled = true;
|
||||||
|
elements.pushResubscribeButton.classList.add('disabled');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
elements.notificationPermissionStatus.className = 'status-default';
|
||||||
|
// Enable subscribe button for default state (prompt)
|
||||||
|
elements.pushResubscribeButton.disabled = false;
|
||||||
|
elements.pushResubscribeButton.classList.remove('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the subscription status display
|
||||||
|
async function updateSubscriptionStatus() {
|
||||||
|
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
||||||
|
elements.subscriptionStatus.textContent = 'Not Supported';
|
||||||
|
elements.subscriptionStatus.className = 'status-denied';
|
||||||
|
// Disable unsubscribe button when not supported
|
||||||
|
elements.pushUnsubscribeButton.disabled = true;
|
||||||
|
// Set subscribe button text
|
||||||
|
elements.pushResubscribeButton.textContent = 'Subscribe';
|
||||||
|
// Disable simulate button when not supported
|
||||||
|
if (elements.simulateClickButton) {
|
||||||
|
elements.simulateClickButton.disabled = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
currentSubscription = await registration.pushManager.getSubscription();
|
||||||
|
|
||||||
|
if (currentSubscription) {
|
||||||
|
elements.subscriptionStatus.textContent = 'active';
|
||||||
|
elements.subscriptionStatus.className = 'status-active';
|
||||||
|
// Enable unsubscribe button when subscription is active
|
||||||
|
elements.pushUnsubscribeButton.disabled = false;
|
||||||
|
// Change subscribe button text to "Re-subscribe"
|
||||||
|
elements.pushResubscribeButton.textContent = 'Re-subscribe';
|
||||||
|
// Enable simulate button when subscription is active
|
||||||
|
if (elements.simulateClickButton) {
|
||||||
|
elements.simulateClickButton.disabled = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements.subscriptionStatus.textContent = 'Not Subscribed';
|
||||||
|
elements.subscriptionStatus.className = 'status-inactive';
|
||||||
|
// Disable unsubscribe button when not subscribed
|
||||||
|
elements.pushUnsubscribeButton.disabled = true;
|
||||||
|
// Set subscribe button text
|
||||||
|
elements.pushResubscribeButton.textContent = 'Subscribe';
|
||||||
|
// Disable simulate button when not subscribed
|
||||||
|
if (elements.simulateClickButton) {
|
||||||
|
elements.simulateClickButton.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking subscription status:', error);
|
||||||
|
elements.subscriptionStatus.textContent = 'Error';
|
||||||
|
elements.subscriptionStatus.className = 'status-denied';
|
||||||
|
// Disable unsubscribe button on error
|
||||||
|
elements.pushUnsubscribeButton.disabled = true;
|
||||||
|
// Set subscribe button text
|
||||||
|
elements.pushResubscribeButton.textContent = 'Subscribe';
|
||||||
|
// Disable simulate button on error
|
||||||
|
if (elements.simulateClickButton) {
|
||||||
|
elements.simulateClickButton.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved credentials from localStorage
|
||||||
|
function loadSavedCredentials() {
|
||||||
|
try {
|
||||||
|
const storedAuth = localStorage.getItem('basicAuthCredentials');
|
||||||
|
if (storedAuth) {
|
||||||
|
const credentials = JSON.parse(storedAuth);
|
||||||
|
if (credentials.username && credentials.password) {
|
||||||
|
elements.pushUsername.value = credentials.username;
|
||||||
|
elements.pushPassword.value = credentials.password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading saved credentials:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Action Functions ---
|
||||||
|
|
||||||
|
// Save credentials and close the modal
|
||||||
|
async function saveCredentialsAndSubscribe() {
|
||||||
|
const username = elements.pushUsername.value.trim();
|
||||||
|
const password = elements.pushPassword.value.trim();
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
alert('Please enter both username and password');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save credentials to localStorage
|
||||||
|
const credentials = { username, password };
|
||||||
|
localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials));
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
closePushSettingsModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe from push notifications
|
||||||
|
async function unsubscribeFromPush() {
|
||||||
|
if (!currentSubscription) {
|
||||||
|
alert('No active subscription to unsubscribe from');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await currentSubscription.unsubscribe();
|
||||||
|
await updateSubscriptionStatus();
|
||||||
|
// No success alert
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unsubscribing:', error);
|
||||||
|
alert(`Error unsubscribing: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to push notifications
|
||||||
|
async function resubscribeToPush() {
|
||||||
|
try {
|
||||||
|
let username = elements.pushUsername.value.trim();
|
||||||
|
let password = elements.pushPassword.value.trim();
|
||||||
|
|
||||||
|
// If fields are empty, try to use stored credentials
|
||||||
|
if (!username || !password) {
|
||||||
|
try {
|
||||||
|
const storedAuth = localStorage.getItem('basicAuthCredentials');
|
||||||
|
if (storedAuth) {
|
||||||
|
const credentials = JSON.parse(storedAuth);
|
||||||
|
if (credentials.username && credentials.password) {
|
||||||
|
username = credentials.username;
|
||||||
|
password = credentials.password;
|
||||||
|
|
||||||
|
// Update the form fields with stored values
|
||||||
|
elements.pushUsername.value = username;
|
||||||
|
elements.pushPassword.value = password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading stored credentials:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have credentials, show alert if missing
|
||||||
|
if (!username || !password) {
|
||||||
|
console.log('No credentials available. Showing alert.');
|
||||||
|
alert('Please enter your username and password to subscribe.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the credentials to localStorage
|
||||||
|
const credentials = { username, password };
|
||||||
|
localStorage.setItem('basicAuthCredentials', JSON.stringify(credentials));
|
||||||
|
console.log('Saved credentials to localStorage');
|
||||||
|
|
||||||
|
// Use the credentials to subscribe
|
||||||
|
await setupPushNotifications();
|
||||||
|
|
||||||
|
// Wait a moment for the subscription to complete
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// Get the updated subscription
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
currentSubscription = await registration.pushManager.getSubscription();
|
||||||
|
|
||||||
|
// Update the UI directly
|
||||||
|
if (currentSubscription && elements.subscriptionStatus) {
|
||||||
|
elements.subscriptionStatus.textContent = 'active';
|
||||||
|
elements.subscriptionStatus.className = 'status-active';
|
||||||
|
// Enable unsubscribe button when subscription is active
|
||||||
|
elements.pushUnsubscribeButton.disabled = false;
|
||||||
|
// Change subscribe button text to "Re-subscribe"
|
||||||
|
elements.pushResubscribeButton.textContent = 'Re-subscribe';
|
||||||
|
// Enable simulate button when subscription is active
|
||||||
|
if (elements.simulateClickButton) {
|
||||||
|
elements.simulateClickButton.disabled = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Disable unsubscribe button when not subscribed
|
||||||
|
elements.pushUnsubscribeButton.disabled = true;
|
||||||
|
// Set subscribe button text
|
||||||
|
elements.pushResubscribeButton.textContent = 'Subscribe';
|
||||||
|
// Disable simulate button when not subscribed
|
||||||
|
if (elements.simulateClickButton) {
|
||||||
|
elements.simulateClickButton.disabled = true;
|
||||||
|
}
|
||||||
|
// Fall back to the standard update function
|
||||||
|
await updateSubscriptionStatus();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error subscribing:', error);
|
||||||
|
alert(`Error subscribing: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually trigger sendSubscriptionToServer with the current subscription
|
||||||
|
export async function sendSubscriptionToServer() {
|
||||||
|
if (!currentSubscription) {
|
||||||
|
await updateSubscriptionStatus();
|
||||||
|
if (!currentSubscription) {
|
||||||
|
// No alert, just return silently
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stored credentials
|
||||||
|
let credentials;
|
||||||
|
try {
|
||||||
|
const storedAuth = localStorage.getItem('basicAuthCredentials');
|
||||||
|
if (storedAuth) {
|
||||||
|
credentials = JSON.parse(storedAuth);
|
||||||
|
if (!credentials.username || !credentials.password) {
|
||||||
|
throw new Error('Invalid credentials');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('No stored credentials');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// No alert, just open the modal to let the user set credentials
|
||||||
|
openPushSettingsModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Basic Auth header
|
||||||
|
const createBasicAuthHeader = (creds) => {
|
||||||
|
return 'Basic ' + btoa(`${creds.username}:${creds.password}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': createBasicAuthHeader(credentials)
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Make the request to the server
|
||||||
|
const response = await fetch(`${getBackendUrl()}/subscribe`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
button_id: FLIC_BUTTON_ID,
|
||||||
|
subscription: currentSubscription
|
||||||
|
}),
|
||||||
|
headers: headers,
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
// No success alert
|
||||||
|
|
||||||
|
// Update the currentSubscription variable
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
currentSubscription = await registration.pushManager.getSubscription();
|
||||||
|
|
||||||
|
// Directly update the subscription status element in the DOM
|
||||||
|
if (currentSubscription && elements.subscriptionStatus) {
|
||||||
|
elements.subscriptionStatus.textContent = 'active';
|
||||||
|
elements.subscriptionStatus.className = 'status-active';
|
||||||
|
// Enable unsubscribe button when subscription is active
|
||||||
|
elements.pushUnsubscribeButton.disabled = false;
|
||||||
|
// Change subscribe button text to "Re-subscribe"
|
||||||
|
elements.pushResubscribeButton.textContent = 'Re-subscribe';
|
||||||
|
// Enable simulate button when subscription is active
|
||||||
|
if (elements.simulateClickButton) {
|
||||||
|
elements.simulateClickButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let errorMsg = `Server error: ${response.status}`;
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
localStorage.removeItem('basicAuthCredentials');
|
||||||
|
errorMsg = 'Authentication failed. Credentials cleared.';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorMsg = errorData.message || errorMsg;
|
||||||
|
} catch (e) { /* use default */ }
|
||||||
|
}
|
||||||
|
// No error alert, just log the error
|
||||||
|
console.error(`Failed to send subscription: ${errorMsg}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// No error alert, just log the error
|
||||||
|
console.error(`Network error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "Game Timer PWA",
|
"name": "Game Timer",
|
||||||
"short_name": "Game Timer",
|
"short_name": "Timer",
|
||||||
|
"author": "Martin",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"version_name": "0.0.1",
|
||||||
"description": "Multi-player chess-like timer with carousel navigation",
|
"description": "Multi-player chess-like timer with carousel navigation",
|
||||||
"start_url": "/index.html",
|
"start_url": "/",
|
||||||
"id": "/index.html",
|
"id": "/index.html",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
|
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
|
||||||
"background_color": "#f5f5f5",
|
"background_color": "#ffffff",
|
||||||
"theme_color": "#2c3e50",
|
"theme_color": "#000000",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/icons/android-chrome-192x192.png",
|
"src": "/icons/android-chrome-192x192.png",
|
||||||
@@ -38,13 +41,13 @@
|
|||||||
"screenshots": [
|
"screenshots": [
|
||||||
{
|
{
|
||||||
"src": "/images/screenshot1.png",
|
"src": "/images/screenshot1.png",
|
||||||
"sizes": "2560x1860",
|
"sizes": "2212×1614",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"form_factor": "wide"
|
"form_factor": "wide"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/images/screenshot2.png",
|
"src": "/images/screenshot2.png",
|
||||||
"sizes": "750x1594",
|
"sizes": "828×1912",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
"docker:build": "docker build -t 'game-timer:latest' .",
|
"docker:build": "docker build -t 'game-timer:latest' .",
|
||||||
"start": "docker run -d -p 80:80 --name game-timer 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",
|
"stop": "docker stop game-timer && docker rm game-timer",
|
||||||
"rebuild": "npm run stop || true && npm run docker:build && npm run start"
|
"rebuild": "npm run stop || true && npm run docker:build && npm run start",
|
||||||
|
"generate-config": "./generate-config.sh",
|
||||||
|
"dev": "./dev-start.sh"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"timer",
|
"timer",
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
// 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,58 +0,0 @@
|
|||||||
// serviceWorkerManager.js - Service worker registration and Flic integration
|
|
||||||
import * as config from '../config.js';
|
|
||||||
import * as pushFlic from './pushFlicIntegration.js';
|
|
||||||
|
|
||||||
// Store the action handlers passed from app.js
|
|
||||||
let flicActionHandlers = {};
|
|
||||||
|
|
||||||
export function setFlicActionHandlers(handlers) {
|
|
||||||
flicActionHandlers = handlers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Flic Integration Setup ---
|
|
||||||
export function initFlic() {
|
|
||||||
// This function is used by setupServiceWorker and relies on
|
|
||||||
// flicActionHandlers being set before this is called
|
|
||||||
pushFlic.initPushFlic(flicActionHandlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleServiceWorkerMessage(event) {
|
|
||||||
console.log('[App] Message received from Service Worker:', event.data);
|
|
||||||
if (event.data?.type === 'flic-action') {
|
|
||||||
const { action, button, timestamp, batteryLevel } = event.data;
|
|
||||||
if (flicActionHandlers[action]) {
|
|
||||||
flicActionHandlers[action]();
|
|
||||||
} else {
|
|
||||||
pushFlic.handleFlicAction(action, button, timestamp, batteryLevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Service Worker and PWA Setup ---
|
|
||||||
export function setupServiceWorker(messageHandler) {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
navigator.serviceWorker.register('/sw.js')
|
|
||||||
.then(registration => {
|
|
||||||
console.log('ServiceWorker registered successfully.');
|
|
||||||
|
|
||||||
// Listen for messages FROM the Service Worker (e.g., Flic actions)
|
|
||||||
navigator.serviceWorker.addEventListener('message', messageHandler);
|
|
||||||
|
|
||||||
// Initialize Flic integration (which will try to subscribe)
|
|
||||||
initFlic();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('ServiceWorker registration failed:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Listen for SW controller changes
|
|
||||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
||||||
console.log('Service Worker controller changed, potentially updated.');
|
|
||||||
// window.location.reload(); // Consider prompting user to reload
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.warn('ServiceWorker not supported.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
122
sw.js
122
sw.js
@@ -5,29 +5,34 @@ const CACHE_NAME = `game-timer-${CACHE_VERSION}`;
|
|||||||
// Files to cache
|
// Files to cache
|
||||||
const CACHE_FILES = [
|
const CACHE_FILES = [
|
||||||
'/',
|
'/',
|
||||||
'/sw.js',
|
|
||||||
'/index.html',
|
'/index.html',
|
||||||
'/manifest.json',
|
'/manifest.json',
|
||||||
'/css/styles.css',
|
'/sw.js',
|
||||||
'/favicon.ico',
|
'/favicon.ico',
|
||||||
|
'/config.env.js',
|
||||||
|
'/css/styles.css',
|
||||||
'/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',
|
||||||
|
'/images/screenshot1.png',
|
||||||
|
'/images/screenshot2.png',
|
||||||
'/js/app.js',
|
'/js/app.js',
|
||||||
'/js/config.js',
|
'/js/config.js',
|
||||||
'/js/env-loader.js',
|
'/js/env-loader.js',
|
||||||
'/js/ui/audio.js',
|
'/js/core/eventHandlers.js',
|
||||||
'/js/ui/camera.js',
|
|
||||||
'/js/ui/ui.js',
|
|
||||||
'/js/core/state.js',
|
|
||||||
'/js/core/timer.js',
|
|
||||||
'/js/core/gameActions.js',
|
'/js/core/gameActions.js',
|
||||||
'/js/core/playerManager.js',
|
'/js/core/playerManager.js',
|
||||||
'/js/core/eventHandlers.js',
|
'/js/core/state.js',
|
||||||
|
'/js/core/timer.js',
|
||||||
'/js/services/pushFlicIntegration.js',
|
'/js/services/pushFlicIntegration.js',
|
||||||
'/js/services/serviceWorkerManager.js'
|
'/js/services/screenLockManager.js',
|
||||||
|
'/js/services/serviceWorkerManager.js',
|
||||||
|
'/js/ui/audio.js',
|
||||||
|
'/js/ui/camera.js',
|
||||||
|
'/js/ui/pushSettingsUI.js',
|
||||||
|
'/js/ui/ui.js'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Install event - Cache files
|
// Install event - Cache files
|
||||||
@@ -65,6 +70,15 @@ self.addEventListener('activate', event => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
console.log('[ServiceWorker] Fetch');
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((cachedResponse) => {
|
||||||
|
return cachedResponse || fetch(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Helper function to determine if a response should be cached
|
// Helper function to determine if a response should be cached
|
||||||
function shouldCacheResponse(request, response) {
|
function shouldCacheResponse(request, response) {
|
||||||
// Only cache GET requests
|
// Only cache GET requests
|
||||||
@@ -91,7 +105,8 @@ self.addEventListener('push', event => {
|
|||||||
data: {
|
data: {
|
||||||
action: 'Unknown',
|
action: 'Unknown',
|
||||||
button: 'Unknown',
|
button: 'Unknown',
|
||||||
batteryLevel: undefined
|
batteryLevel: undefined,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,9 +120,15 @@ self.addEventListener('push', event => {
|
|||||||
pushData = {
|
pushData = {
|
||||||
title: parsedData.title || pushData.title,
|
title: parsedData.title || pushData.title,
|
||||||
body: parsedData.body || pushData.body,
|
body: parsedData.body || pushData.body,
|
||||||
data: parsedData.data || pushData.data // Expecting { action: 'SingleClick', button: 'game-button', batteryLevel: 75 }
|
data: parsedData.data || pushData.data
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ensure all required fields are present in data
|
||||||
|
pushData.data = pushData.data || {};
|
||||||
|
if (!pushData.data.timestamp) {
|
||||||
|
pushData.data.timestamp = new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[ServiceWorker] Error parsing push data:', e);
|
console.error('[ServiceWorker] Error parsing push data:', e);
|
||||||
// Use default notification if parsing fails
|
// Use default notification if parsing fails
|
||||||
@@ -120,65 +141,58 @@ self.addEventListener('push', event => {
|
|||||||
// --- Send message to client(s) ---
|
// --- Send message to client(s) ---
|
||||||
const messagePayload = {
|
const messagePayload = {
|
||||||
type: 'flic-action', // Custom message type
|
type: 'flic-action', // Custom message type
|
||||||
action: pushData.data.action, // e.g., 'SingleClick', 'DoubleClick', 'Hold'
|
action: pushData.data.action || 'Unknown', // e.g., 'SingleClick', 'DoubleClick', 'Hold'
|
||||||
button: pushData.data.button, // e.g., the button name
|
button: pushData.data.button || 'Unknown', // e.g., the button name
|
||||||
timestamp: pushData.data.timestamp, // e.g., the timestamp of the action
|
timestamp: pushData.data.timestamp || new Date().toISOString(), // e.g., the timestamp of the action
|
||||||
batteryLevel: pushData.data.batteryLevel // e.g., the battery level percentage
|
batteryLevel: pushData.data.batteryLevel // e.g., the battery level percentage
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send message to all open PWA windows controlled by this SW
|
console.log('[ServiceWorker] Preparing message payload:', messagePayload);
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
self.clients.matchAll({
|
self.clients.matchAll({
|
||||||
type: 'window', // Only target window clients
|
type: 'window',
|
||||||
includeUncontrolled: true // Include clients that might not be fully controlled yet
|
includeUncontrolled: true
|
||||||
}).then(clientList => {
|
}).then(clientList => {
|
||||||
if (!clientList || clientList.length === 0) {
|
if (!clientList || clientList.length === 0) {
|
||||||
console.log('[ServiceWorker] No client windows found to send message to.');
|
// No clients available, just resolve
|
||||||
// If no window is open, we MUST show a notification
|
return Promise.resolve();
|
||||||
return self.registration.showNotification(pushData.title, {
|
|
||||||
body: pushData.body,
|
|
||||||
icon: '/icons/android-chrome-192x192.png', // Updated path
|
|
||||||
data: pushData.data // Pass data if needed when notification is clicked
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post message to each client
|
// Post message to each client with improved reliability
|
||||||
let messageSent = false;
|
let messageSent = false;
|
||||||
clientList.forEach(client => {
|
const sendPromises = clientList.map(client => {
|
||||||
console.log(`[ServiceWorker] Posting message to client: ${client.id}`, messagePayload);
|
console.log(`[ServiceWorker] Posting message to client: ${client.id}`, messagePayload);
|
||||||
client.postMessage(messagePayload);
|
try {
|
||||||
messageSent = true; // Mark that we at least tried to send a message
|
// Try to send the message and mark it as sent
|
||||||
|
client.postMessage(messagePayload);
|
||||||
|
messageSent = true;
|
||||||
|
|
||||||
|
// Just return true to indicate message was sent
|
||||||
|
return Promise.resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ServiceWorker] Error posting message to client:', error);
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Decide whether to still show a notification even if a window is open.
|
return Promise.all(sendPromises).then(() => {
|
||||||
// Generally good practice unless you are SURE the app will handle it visibly.
|
// No notifications will be shown for any action, including low battery
|
||||||
// You might choose *not* to show a notification if a client was found and focused.
|
return Promise.resolve();
|
||||||
// For simplicity here, we'll still show one. Adjust as needed.
|
});
|
||||||
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',
|
|
||||||
data: pushData.data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// --- Show a notification (Important!) ---
|
// Listen for messages from client
|
||||||
// Push notifications generally REQUIRE showing a notification to the user
|
self.addEventListener('message', event => {
|
||||||
// unless the PWA is already in the foreground AND handles the event visually.
|
const message = event.data;
|
||||||
// It's safer to always show one unless you have complex foreground detection.
|
|
||||||
/* This part is now handled inside the clients.matchAll promise */
|
if (!message || typeof message !== 'object') {
|
||||||
/*
|
return;
|
||||||
const notificationOptions = {
|
}
|
||||||
body: pushData.body,
|
|
||||||
icon: './icons/android-chrome-192x192.png', // Optional: path to an icon
|
// No battery-related handling
|
||||||
data: pushData.data // Attach data if needed when notification is clicked
|
|
||||||
};
|
|
||||||
event.waitUntil(
|
|
||||||
self.registration.showNotification(pushData.title, notificationOptions)
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// This helps with navigation after app is installed
|
// This helps with navigation after app is installed
|
||||||
|
|||||||
36
test.html
36
test.html
@@ -1,36 +0,0 @@
|
|||||||
<!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>
|
|
||||||
Reference in New Issue
Block a user