1470 lines
47 KiB
HTML
1470 lines
47 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Point Tracker</title>
|
||
<script src="/socket.io/socket.io.js"></script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background: #2a2a2a;
|
||
border-radius: 15px;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(45deg, #404040, #606060);
|
||
color: white;
|
||
padding: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.connection-status {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
padding: 5px 15px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.connection-status.connected {
|
||
background: #4CAF50;
|
||
color: white;
|
||
}
|
||
|
||
.connection-status.disconnected {
|
||
background: #f44336;
|
||
color: white;
|
||
}
|
||
|
||
.main-content {
|
||
padding: 30px;
|
||
}
|
||
|
||
.admin-bar {
|
||
background: #1a1a1a;
|
||
padding: 15px;
|
||
margin-bottom: 30px;
|
||
border-radius: 10px;
|
||
border: 2px solid #404040;
|
||
display: none;
|
||
}
|
||
|
||
.admin-bar.active {
|
||
display: block;
|
||
}
|
||
|
||
.admin-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.admin-controls span {
|
||
color: #ff6b6b;
|
||
font-weight: bold;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.points-section {
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 1.5em;
|
||
margin-bottom: 20px;
|
||
color: #cccccc;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.points-grid {
|
||
display: flex;
|
||
gap: 15px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.point-bubble {
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 50%;
|
||
border: 3px solid #404040;
|
||
background: #2a2a2a;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.point-bubble:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.point-bubble.available {
|
||
border-color: #4CAF50;
|
||
background: #1a2e1a;
|
||
color: #4CAF50;
|
||
}
|
||
|
||
.point-bubble.used {
|
||
border-color: #f44336;
|
||
background: #2e1a1a;
|
||
color: #f44336;
|
||
}
|
||
|
||
.point-bubble.active {
|
||
border-color: #ff9800;
|
||
background: #2e261a;
|
||
color: #ff9800;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.point-bubble.paused {
|
||
border-color: #2196f3;
|
||
background: #1a222e;
|
||
color: #2196f3;
|
||
}
|
||
|
||
.point-bubble.disabled {
|
||
border-color: #666;
|
||
background: #333;
|
||
color: #666;
|
||
cursor: not-allowed;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.add-point-btn {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 50%;
|
||
border: 3px dashed #4CAF50;
|
||
background: #1a2e1a;
|
||
color: #4CAF50;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.add-point-btn:hover {
|
||
transform: scale(1.05);
|
||
background: #2a4a2a;
|
||
}
|
||
|
||
.admin-bar.active ~ .main-content .add-point-btn {
|
||
display: flex;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { box-shadow: 0 0 0 0 rgba(255, 152, 0, 0.7); }
|
||
70% { box-shadow: 0 0 0 10px rgba(255, 152, 0, 0); }
|
||
100% { box-shadow: 0 0 0 0 rgba(255, 152, 0, 0); }
|
||
}
|
||
|
||
.timer-display {
|
||
position: absolute;
|
||
top: -25px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: #1a1a1a;
|
||
color: #cccccc;
|
||
padding: 5px 10px;
|
||
border-radius: 15px;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
border: 1px solid #404040;
|
||
}
|
||
|
||
.admin-overlay {
|
||
position: absolute;
|
||
top: -5px;
|
||
right: -5px;
|
||
display: none;
|
||
}
|
||
|
||
.admin-bar.active ~ .main-content .admin-overlay {
|
||
display: block;
|
||
}
|
||
|
||
.admin-btn {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
margin: 1px;
|
||
}
|
||
|
||
.admin-btn.expire {
|
||
background: #ff4757;
|
||
color: white;
|
||
}
|
||
|
||
.admin-btn.remove {
|
||
background: #ff3838;
|
||
color: white;
|
||
}
|
||
|
||
.timer-controls {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #404040;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #505050;
|
||
}
|
||
|
||
.btn-warning {
|
||
background: #ff9800;
|
||
color: white;
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background: #f57c00;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #f44336;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #d32f2f;
|
||
}
|
||
|
||
.btn-success {
|
||
background: #4CAF50;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: #388e3c;
|
||
}
|
||
|
||
.btn-small {
|
||
padding: 5px 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.admin-section {
|
||
border-top: 2px solid #404040;
|
||
padding-top: 30px;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.log-section {
|
||
background: #1a1a1a;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
margin-top: 30px;
|
||
border: 1px solid #404040;
|
||
}
|
||
|
||
.log-entry {
|
||
background: #2a2a2a;
|
||
padding: 15px;
|
||
margin-bottom: 10px;
|
||
border-radius: 8px;
|
||
border-left: 4px solid #cccccc;
|
||
}
|
||
|
||
.log-entry:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.log-time {
|
||
font-size: 0.9em;
|
||
color: #999;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 1000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0,0,0,0.8);
|
||
}
|
||
|
||
.modal.no-close {
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: #2a2a2a;
|
||
margin: 15% auto;
|
||
padding: 30px;
|
||
border-radius: 15px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
border: 2px solid #404040;
|
||
position: relative;
|
||
}
|
||
|
||
.modal.no-close .modal-content {
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.modal h2 {
|
||
margin-bottom: 20px;
|
||
color: #cccccc;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: bold;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.form-group input, .form-group select {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 2px solid #404040;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
background: #1a1a1a;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.form-group input:focus, .form-group select:focus {
|
||
outline: none;
|
||
border-color: #cccccc;
|
||
}
|
||
|
||
.daily-limit {
|
||
background: #2e261a;
|
||
border: 1px solid #ff9800;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.daily-limit.exceeded {
|
||
background: #2e1a1a;
|
||
border-color: #f44336;
|
||
}
|
||
|
||
.status-indicator {
|
||
display: inline-block;
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.status-available { background: #4CAF50; }
|
||
.status-used { background: #f44336; }
|
||
.status-active { background: #ff9800; }
|
||
.status-paused { background: #2196f3; }
|
||
|
||
.solo-limit-warning {
|
||
background: #2e1a1a;
|
||
border: 1px solid #f44336;
|
||
padding: 10px;
|
||
border-radius: 8px;
|
||
margin-bottom: 15px;
|
||
color: #f44336;
|
||
text-align: center;
|
||
display: none;
|
||
}
|
||
|
||
.solo-limit-warning.active {
|
||
display: block;
|
||
}
|
||
|
||
.quick-add {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.custom-timer-section {
|
||
background: #1a1a1a;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-top: 15px;
|
||
border: 1px solid #404040;
|
||
}
|
||
|
||
.time-input-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.time-input {
|
||
width: 60px;
|
||
padding: 5px;
|
||
border: 1px solid #404040;
|
||
border-radius: 4px;
|
||
background: #2a2a2a;
|
||
color: #e0e0e0;
|
||
text-align: center;
|
||
}
|
||
|
||
.episode-movie-section {
|
||
margin-top: 20px;
|
||
padding-top: 15px;
|
||
border-top: 1px solid #404040;
|
||
}
|
||
|
||
.episode-movie-section .btn {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.point-selection-mode {
|
||
background: #1a2e1a !important;
|
||
border-color: #4CAF50 !important;
|
||
animation: selectPulse 1s infinite;
|
||
}
|
||
|
||
.point-selected {
|
||
background: #2e4a2e !important;
|
||
border-color: #66ff66 !important;
|
||
box-shadow: 0 0 10px rgba(102, 255, 102, 0.5);
|
||
}
|
||
|
||
@keyframes selectPulse {
|
||
0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); }
|
||
70% { box-shadow: 0 0 0 5px rgba(76, 175, 80, 0); }
|
||
100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
|
||
}
|
||
|
||
.selection-instructions {
|
||
background: #1a2e1a;
|
||
border: 2px solid #4CAF50;
|
||
color: #4CAF50;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-bottom: 15px;
|
||
text-align: center;
|
||
display: none;
|
||
}
|
||
|
||
.selection-instructions.active {
|
||
display: block;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="connection-status disconnected" id="connectionStatus">Disconnected</div>
|
||
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🎯 Point Tracker</h1>
|
||
<p>Manage your solo and together points</p>
|
||
</div>
|
||
|
||
<!-- Admin Bar -->
|
||
<div class="admin-bar" id="adminBar">
|
||
<div class="admin-controls">
|
||
<span>🔧 ADMIN MODE ACTIVE</span>
|
||
<div class="quick-add">
|
||
<button class="btn btn-success btn-small" onclick="quickAddPoint('solo')">+Solo</button>
|
||
<button class="btn btn-success btn-small" onclick="quickAddPoint('together')">+Together</button>
|
||
<button class="btn btn-primary btn-small" onclick="showCustomTimerModal()">+Custom</button>
|
||
<button class="btn btn-warning btn-small" onclick="clearLog()">Clear Log</button>
|
||
<button class="btn btn-warning btn-small" onclick="resetDay()">Reset Daily Limit</button>
|
||
<button class="btn btn-danger btn-small" onclick="resetAllPoints()">Reset All</button>
|
||
<button class="btn btn-warning btn-small" onclick="testAudio()">Test Audio</button>
|
||
<button class="btn btn-primary btn-small" onclick="exitAdminMode()">Exit Admin</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main-content">
|
||
<!-- Solo Points Section -->
|
||
<div class="points-section">
|
||
<div class="section-title">Solo Points (15 minutes each) - One at a time</div>
|
||
<div class="solo-limit-warning" id="soloLimitWarning">
|
||
⚠️ You can only run one solo point at a time. Finish or stop the current timer first.
|
||
</div>
|
||
<div class="selection-instructions" id="selectionInstructions">
|
||
<div id="selectionText">Click on points to select them</div>
|
||
<button class="btn btn-success btn-small" onclick="confirmSelection()" id="confirmBtn" style="margin-left: 10px;">Confirm Selection</button>
|
||
<button class="btn btn-danger btn-small" onclick="cancelSelection()" style="margin-left: 5px;">Cancel</button>
|
||
</div>
|
||
<div class="points-grid" id="soloPoints">
|
||
<!-- Solo point bubbles will be generated here -->
|
||
</div>
|
||
<div class="add-point-btn" onclick="quickAddPoint('solo')" title="Add Solo Point">+</div>
|
||
|
||
<!-- Episode/Movie Actions -->
|
||
<div class="episode-movie-section">
|
||
<div class="timer-controls">
|
||
<button class="btn btn-warning" onclick="startEpisodeSelection()" id="episodeBtn">📺 Watch Episode (2 solo = 30min)</button>
|
||
<button class="btn btn-warning" onclick="startMovieSelection()" id="movieBtn">🎬 Watch Movie (4 solo = 2hr)</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Together Points Section -->
|
||
<div class="points-section">
|
||
<div class="section-title">Together Points (1 hour each)</div>
|
||
<div class="daily-limit" id="togetherLimit">
|
||
<strong>Daily Limit:</strong> <span id="togetherUsedToday">0</span>/1 together points used today
|
||
</div>
|
||
<div class="points-grid" id="togetherPoints">
|
||
<!-- Together point bubbles will be generated here -->
|
||
</div>
|
||
<div class="add-point-btn" onclick="quickAddPoint('together')" title="Add Together Point">+</div>
|
||
</div>
|
||
|
||
<!-- Admin Controls -->
|
||
<div class="admin-section">
|
||
<div class="section-title">Management</div>
|
||
<div class="timer-controls">
|
||
<button class="btn btn-primary" onclick="showAddPointsModal()">Add Points</button>
|
||
<button class="btn btn-primary" onclick="showAdminModal()">Admin Panel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Activity Log -->
|
||
<div class="log-section">
|
||
<div class="section-title">Recent Activity (Last 5 uses)</div>
|
||
<div id="activityLog">
|
||
<p style="color: #999;">No activity yet...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add Points Modal -->
|
||
<div id="addPointsModal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Add Points</h2>
|
||
<div class="form-group">
|
||
<label>Password:</label>
|
||
<input type="password" id="addPointsPassword" placeholder="Enter password">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Point Type:</label>
|
||
<select id="pointType">
|
||
<option value="solo">Solo Points (15 min)</option>
|
||
<option value="together">Together Points (1 hour)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Number of Points:</label>
|
||
<input type="number" id="pointCount" min="1" max="10" value="1">
|
||
</div>
|
||
<div class="timer-controls">
|
||
<button class="btn btn-success" onclick="addPoints()">Add Points</button>
|
||
<button class="btn btn-danger" onclick="closeModal('addPointsModal')">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Admin Panel Modal -->
|
||
<div id="adminModal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Admin Panel</h2>
|
||
<div class="form-group">
|
||
<label>Admin Password:</label>
|
||
<input type="password" id="adminPassword" placeholder="Enter admin password">
|
||
</div>
|
||
<div class="timer-controls">
|
||
<button class="btn btn-primary" onclick="adminLogin()">Enter Admin Mode</button>
|
||
<button class="btn btn-danger" onclick="closeModal('adminModal')">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Custom Timer Modal -->
|
||
<div id="customTimerModal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Create Custom Timer</h2>
|
||
<div class="form-group">
|
||
<label>Timer Name:</label>
|
||
<input type="text" id="customTimerName" placeholder="e.g., Quick Task, Deep Work" maxlength="20">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Duration:</label>
|
||
<div class="time-input-group">
|
||
<input type="number" id="customHours" class="time-input" min="0" max="5" value="0" placeholder="H">
|
||
<span>hours</span>
|
||
<input type="number" id="customMinutes" class="time-input" min="0" max="59" value="30" placeholder="M">
|
||
<span>minutes</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Point Type:</label>
|
||
<select id="customPointType">
|
||
<option value="solo">Solo Type (one at a time)</option>
|
||
<option value="together">Together Type (daily limit)</option>
|
||
<option value="custom">Custom Type (no limits)</option>
|
||
</select>
|
||
</div>
|
||
<div class="timer-controls">
|
||
<button class="btn btn-success" onclick="createCustomTimer()">Create Timer</button>
|
||
<button class="btn btn-danger" onclick="closeModal('customTimerModal')">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Custom Dialog Modal -->
|
||
<div id="customDialog" class="modal">
|
||
<div class="modal-content" style="max-width: 400px;">
|
||
<h2 id="dialogTitle">Confirm</h2>
|
||
<p id="dialogMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
|
||
<div class="timer-controls">
|
||
<button class="btn btn-success" id="dialogConfirm">Yes</button>
|
||
<button class="btn btn-danger" id="dialogCancel">No</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Alert Modal -->
|
||
<div id="alertModal" class="modal">
|
||
<div class="modal-content" style="max-width: 400px;">
|
||
<h2 id="alertTitle">Alert</h2>
|
||
<p id="alertMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
|
||
<div class="timer-controls">
|
||
<button class="btn btn-primary" onclick="closeModal('alertModal')">OK</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Finish-Up Modal -->
|
||
<div id="finishUpModal" class="modal no-close">
|
||
<div class="modal-content" style="max-width: 400px;">
|
||
<h2>🔔 Timer Completed!</h2>
|
||
<p id="finishUpMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
|
||
<p style="color: #e0e0e0;">Would you like 2 minutes of finish-up time?</p>
|
||
<div class="timer-controls">
|
||
<button class="btn btn-success" id="finishUpYes">Yes</button>
|
||
<button class="btn btn-danger" id="finishUpNo">No</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Timer Complete Modal -->
|
||
<div id="timerCompleteModal" class="modal no-close">
|
||
<div class="modal-content" style="max-width: 400px;">
|
||
<h2>🔔 Timer Completed!</h2>
|
||
<p id="timerCompleteMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
|
||
<div class="timer-controls">
|
||
<button class="btn btn-primary" onclick="dismissTimerComplete()">Dismiss</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Audio element for timer sounds -->
|
||
<audio id="timerAudio" preload="auto" loop>
|
||
<source src="timer-complete.mp3" type="audio/mpeg">
|
||
<source src="timer-complete.wav" type="audio/wav">
|
||
<source src="timer-complete.ogg" type="audio/ogg">
|
||
</audio>
|
||
|
||
<script>
|
||
// Socket.IO connection
|
||
const socket = io();
|
||
|
||
// Client state
|
||
let gameState = {
|
||
soloPoints: [],
|
||
togetherPoints: [],
|
||
customPoints: [],
|
||
activeTimers: {},
|
||
pausedTimers: {},
|
||
activityLog: [],
|
||
togetherUsedToday: 0,
|
||
lastResetDate: new Date().toDateString(),
|
||
adminMode: false,
|
||
selectionMode: null,
|
||
selectedPoints: []
|
||
};
|
||
|
||
// Connection status
|
||
socket.on('connect', () => {
|
||
console.log('Connected to server');
|
||
document.getElementById('connectionStatus').textContent = 'Connected';
|
||
document.getElementById('connectionStatus').className = 'connection-status connected';
|
||
});
|
||
|
||
socket.on('disconnect', () => {
|
||
console.log('Disconnected from server');
|
||
document.getElementById('connectionStatus').textContent = 'Disconnected';
|
||
document.getElementById('connectionStatus').className = 'connection-status disconnected';
|
||
});
|
||
|
||
// Receive initial state
|
||
socket.on('initialState', (state) => {
|
||
console.log('Received initial state:', state);
|
||
gameState = { ...gameState, ...state };
|
||
renderPoints();
|
||
updateTogetherLimit();
|
||
renderLog();
|
||
updateSoloLimitWarning();
|
||
});
|
||
|
||
// Receive state updates
|
||
socket.on('stateUpdate', (state) => {
|
||
console.log('State update received:', state);
|
||
gameState = { ...gameState, ...state };
|
||
renderPoints();
|
||
updateTogetherLimit();
|
||
renderLog();
|
||
updateSoloLimitWarning();
|
||
});
|
||
|
||
// Timer updates
|
||
socket.on('timerUpdate', (data) => {
|
||
const { pointId, remainingSeconds, type } = data;
|
||
updateTimerDisplay(pointId, remainingSeconds, type);
|
||
});
|
||
|
||
// Timer completion
|
||
socket.on('timerComplete', (data) => {
|
||
const { pointId, type, offerFinishUp } = data;
|
||
|
||
if (offerFinishUp) {
|
||
// Play sound and show finish-up modal
|
||
startAlarmSound();
|
||
|
||
const point = findPoint(pointId);
|
||
const pointName = point ? (point.customName || point.type) : 'Timer';
|
||
document.getElementById('finishUpMessage').textContent = `${pointName} point completed!`;
|
||
document.getElementById('finishUpModal').style.display = 'block';
|
||
|
||
// Set up button handlers
|
||
document.getElementById('finishUpYes').onclick = () => {
|
||
stopAlarmSound();
|
||
socket.emit('finishUpResponse', { pointId, accepted: true });
|
||
closeModal('finishUpModal');
|
||
};
|
||
|
||
document.getElementById('finishUpNo').onclick = () => {
|
||
stopAlarmSound();
|
||
socket.emit('finishUpResponse', { pointId, accepted: false });
|
||
closeModal('finishUpModal');
|
||
};
|
||
} else if (type === 'finishUp') {
|
||
// Finish-up completed - show completion dialog
|
||
startAlarmSound();
|
||
|
||
const point = findPoint(pointId);
|
||
const pointName = point ? (point.customName || point.type) : 'Timer';
|
||
document.getElementById('timerCompleteMessage').textContent = `${pointName} point finish-up time completed!`;
|
||
document.getElementById('timerCompleteModal').style.display = 'block';
|
||
}
|
||
});
|
||
|
||
// Admin auth success
|
||
socket.on('adminAuthSuccess', () => {
|
||
gameState.adminMode = true;
|
||
document.getElementById('adminBar').classList.add('active');
|
||
closeModal('adminModal');
|
||
});
|
||
|
||
socket.on('adminAuthFailed', () => {
|
||
alert('Incorrect admin password!');
|
||
});
|
||
|
||
// Custom dialog and alert functions
|
||
function showAlert(message, title = 'Alert') {
|
||
document.getElementById('alertTitle').textContent = title;
|
||
document.getElementById('alertMessage').textContent = message;
|
||
document.getElementById('alertModal').style.display = 'block';
|
||
}
|
||
|
||
function showConfirm(message, title = 'Confirm') {
|
||
return new Promise((resolve) => {
|
||
document.getElementById('dialogTitle').textContent = title;
|
||
document.getElementById('dialogMessage').textContent = message;
|
||
document.getElementById('customDialog').style.display = 'block';
|
||
|
||
document.getElementById('dialogConfirm').onclick = () => {
|
||
closeModal('customDialog');
|
||
resolve(true);
|
||
};
|
||
|
||
document.getElementById('dialogCancel').onclick = () => {
|
||
closeModal('customDialog');
|
||
resolve(false);
|
||
};
|
||
});
|
||
}
|
||
|
||
// Error handling
|
||
socket.on('error', (data) => {
|
||
showAlert(data.message, 'Error');
|
||
});
|
||
|
||
function renderPoints() {
|
||
renderSoloPoints();
|
||
renderTogetherPoints();
|
||
renderCustomPoints();
|
||
}
|
||
|
||
function renderSoloPoints() {
|
||
const container = document.getElementById('soloPoints');
|
||
container.innerHTML = '';
|
||
|
||
gameState.soloPoints.forEach(point => {
|
||
const bubble = createPointBubble(point);
|
||
container.appendChild(bubble);
|
||
});
|
||
}
|
||
|
||
function renderTogetherPoints() {
|
||
const container = document.getElementById('togetherPoints');
|
||
container.innerHTML = '';
|
||
|
||
gameState.togetherPoints.forEach(point => {
|
||
const bubble = createPointBubble(point);
|
||
container.appendChild(bubble);
|
||
});
|
||
}
|
||
|
||
function renderCustomPoints() {
|
||
gameState.customPoints.forEach(point => {
|
||
let container;
|
||
if (point.type === 'solo') {
|
||
container = document.getElementById('soloPoints');
|
||
} else if (point.type === 'together') {
|
||
container = document.getElementById('togetherPoints');
|
||
} else {
|
||
container = document.getElementById('soloPoints');
|
||
}
|
||
|
||
const bubble = createPointBubble(point);
|
||
container.appendChild(bubble);
|
||
});
|
||
}
|
||
|
||
function createPointBubble(point) {
|
||
const bubble = document.createElement('div');
|
||
const isDisabled = isPointDisabled(point);
|
||
|
||
let className = `point-bubble ${isDisabled ? 'disabled' : point.status}`;
|
||
|
||
// Add selection mode classes
|
||
if (gameState.selectionMode && point.type === 'solo' && point.status === 'available') {
|
||
className += ' point-selection-mode';
|
||
if (gameState.selectedPoints.includes(point.id)) {
|
||
className += ' point-selected';
|
||
}
|
||
}
|
||
|
||
bubble.className = className;
|
||
bubble.innerHTML = getPointLabel(point);
|
||
|
||
// Handle clicks based on mode
|
||
if (gameState.selectionMode && point.type === 'solo' && point.status === 'available') {
|
||
bubble.onclick = () => togglePointSelection(point.id);
|
||
} else if (!isDisabled) {
|
||
if (point.status === 'available') {
|
||
bubble.onclick = () => usePoint(point.id);
|
||
} else if (point.status === 'active') {
|
||
bubble.onclick = () => pauseTimer(point.id);
|
||
} else if (point.status === 'paused') {
|
||
bubble.onclick = () => resumeTimer(point.id);
|
||
}
|
||
}
|
||
|
||
// Add timer display if active or paused
|
||
if (point.status === 'active' || point.status === 'paused') {
|
||
const timerDisplay = document.createElement('div');
|
||
timerDisplay.className = 'timer-display';
|
||
timerDisplay.id = `timer_${point.id}`;
|
||
bubble.appendChild(timerDisplay);
|
||
}
|
||
|
||
// Add admin controls (only if not in selection mode)
|
||
if (gameState.adminMode && !gameState.selectionMode) {
|
||
const adminOverlay = document.createElement('div');
|
||
adminOverlay.className = 'admin-overlay';
|
||
|
||
if (point.status === 'active' || point.status === 'paused') {
|
||
const expireBtn = document.createElement('button');
|
||
expireBtn.className = 'admin-btn expire';
|
||
expireBtn.innerHTML = '⏰';
|
||
expireBtn.title = 'Expire Timer (Trigger Ring)';
|
||
expireBtn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
expireTimer(point.id);
|
||
};
|
||
adminOverlay.appendChild(expireBtn);
|
||
}
|
||
|
||
const removeBtn = document.createElement('button');
|
||
removeBtn.className = 'admin-btn remove';
|
||
removeBtn.innerHTML = '×';
|
||
removeBtn.title = 'Remove Point';
|
||
removeBtn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
removePoint(point.id);
|
||
};
|
||
adminOverlay.appendChild(removeBtn);
|
||
|
||
bubble.appendChild(adminOverlay);
|
||
}
|
||
|
||
return bubble;
|
||
}
|
||
|
||
function isPointDisabled(point) {
|
||
if ((point.type === 'solo' || point.customType === 'solo') && point.status === 'available') {
|
||
const allPoints = [...gameState.soloPoints, ...gameState.customPoints];
|
||
return allPoints.some(p => (p.type === 'solo' || p.customType === 'solo') && (p.status === 'active' || p.status === 'paused'));
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function getPointLabel(point) {
|
||
if (point.customName) {
|
||
switch (point.status) {
|
||
case 'available': return point.label;
|
||
case 'used': return '✓';
|
||
case 'active': return '⏱️';
|
||
case 'paused': return '⏸️';
|
||
default: return '?';
|
||
}
|
||
}
|
||
|
||
switch (point.status) {
|
||
case 'available':
|
||
return point.label || (point.type === 'solo' ? '15m' : '1h');
|
||
case 'used':
|
||
return '✓';
|
||
case 'active':
|
||
return '⏱️';
|
||
case 'paused':
|
||
return '⏸️';
|
||
default:
|
||
return '?';
|
||
}
|
||
}
|
||
|
||
function findPoint(pointId) {
|
||
return [...gameState.soloPoints, ...gameState.togetherPoints, ...gameState.customPoints]
|
||
.find(p => p.id === pointId);
|
||
}
|
||
|
||
// User actions
|
||
function usePoint(pointId) {
|
||
socket.emit('usePoint', { pointId });
|
||
}
|
||
|
||
function pauseTimer(pointId) {
|
||
socket.emit('pauseTimer', { pointId });
|
||
}
|
||
|
||
function resumeTimer(pointId) {
|
||
socket.emit('resumeTimer', { pointId });
|
||
}
|
||
|
||
function updateTimerDisplay(pointId, remainingSeconds, type) {
|
||
const display = document.getElementById(`timer_${pointId}`);
|
||
if (display) {
|
||
const prefix = type === 'finishUp' ? 'Finish: ' : '';
|
||
display.textContent = prefix + formatTime(remainingSeconds);
|
||
}
|
||
}
|
||
|
||
function formatTime(seconds) {
|
||
const hours = Math.floor(seconds / 3600);
|
||
const mins = Math.floor((seconds % 3600) / 60);
|
||
const secs = seconds % 60;
|
||
|
||
if (hours > 0) {
|
||
return `${hours}:${mins.toString().padStart(2, '0')}h`;
|
||
} else {
|
||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||
}
|
||
}
|
||
|
||
function updateSoloLimitWarning() {
|
||
const warning = document.getElementById('soloLimitWarning');
|
||
const allPoints = [...gameState.soloPoints, ...gameState.customPoints];
|
||
const hasActiveSolo = allPoints.some(p => (p.type === 'solo' || p.customType === 'solo') && (p.status === 'active' || p.status === 'paused'));
|
||
|
||
if (hasActiveSolo) {
|
||
warning.classList.add('active');
|
||
} else {
|
||
warning.classList.remove('active');
|
||
}
|
||
|
||
updateEpisodeMovieButtons();
|
||
}
|
||
|
||
function updateEpisodeMovieButtons() {
|
||
const episodeBtn = document.getElementById('episodeBtn');
|
||
const movieBtn = document.getElementById('movieBtn');
|
||
|
||
if (!episodeBtn || !movieBtn) return;
|
||
|
||
const availableSoloCount = gameState.soloPoints.filter(p => p.status === 'available').length;
|
||
const allPoints = [...gameState.soloPoints, ...gameState.customPoints];
|
||
const hasActiveSolo = allPoints.some(p => (p.type === 'solo' || p.customType === 'solo') && (p.status === 'active' || p.status === 'paused'));
|
||
|
||
episodeBtn.disabled = hasActiveSolo || availableSoloCount < 2 || gameState.selectionMode;
|
||
movieBtn.disabled = hasActiveSolo || availableSoloCount < 4 || gameState.selectionMode;
|
||
|
||
if (episodeBtn.disabled) {
|
||
episodeBtn.style.opacity = '0.5';
|
||
episodeBtn.style.cursor = 'not-allowed';
|
||
} else {
|
||
episodeBtn.style.opacity = '1';
|
||
episodeBtn.style.cursor = 'pointer';
|
||
}
|
||
|
||
if (movieBtn.disabled) {
|
||
movieBtn.style.opacity = '0.5';
|
||
movieBtn.style.cursor = 'not-allowed';
|
||
} else {
|
||
movieBtn.style.opacity = '1';
|
||
movieBtn.style.cursor = 'pointer';
|
||
}
|
||
}
|
||
|
||
function updateTogetherLimit() {
|
||
const element = document.getElementById('togetherUsedToday');
|
||
const container = document.getElementById('togetherLimit');
|
||
|
||
element.textContent = gameState.togetherUsedToday;
|
||
|
||
if (gameState.togetherUsedToday >= 1) {
|
||
container.classList.add('exceeded');
|
||
} else {
|
||
container.classList.remove('exceeded');
|
||
}
|
||
}
|
||
|
||
function renderLog() {
|
||
const container = document.getElementById('activityLog');
|
||
|
||
if (gameState.activityLog.length === 0) {
|
||
container.innerHTML = '<p style="color: #999;">No activity yet...</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = gameState.activityLog.map(entry => {
|
||
const timestamp = new Date(entry.timestamp);
|
||
return `
|
||
<div class="log-entry">
|
||
<div class="log-time">${timestamp.toLocaleString()}</div>
|
||
<div>${entry.message}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Episode/Movie selection
|
||
function startEpisodeSelection() {
|
||
const availableCount = gameState.soloPoints.filter(p => p.status === 'available').length;
|
||
if (availableCount < 2) {
|
||
showAlert('You need at least 2 available solo points to watch an episode.');
|
||
return;
|
||
}
|
||
|
||
const allPoints = [...gameState.soloPoints, ...gameState.customPoints];
|
||
const hasActiveSolo = allPoints.some(p => (p.type === 'solo' || p.customType === 'solo') && (p.status === 'active' || p.status === 'paused'));
|
||
if (hasActiveSolo) {
|
||
showAlert('You can only run one solo timer at a time. Finish or stop the current timer first.');
|
||
return;
|
||
}
|
||
|
||
gameState.selectionMode = 'episode';
|
||
gameState.selectedPoints = [];
|
||
document.getElementById('selectionInstructions').classList.add('active');
|
||
document.getElementById('selectionText').textContent = 'Select 2 solo points for episode (30 minutes)';
|
||
renderPoints();
|
||
}
|
||
|
||
function startMovieSelection() {
|
||
const availableCount = gameState.soloPoints.filter(p => p.status === 'available').length;
|
||
if (availableCount < 4) {
|
||
showAlert('You need at least 4 available solo points to watch a movie.');
|
||
return;
|
||
}
|
||
|
||
const allPoints = [...gameState.soloPoints, ...gameState.customPoints];
|
||
const hasActiveSolo = allPoints.some(p => (p.type === 'solo' || p.customType === 'solo') && (p.status === 'active' || p.status === 'paused'));
|
||
if (hasActiveSolo) {
|
||
showAlert('You can only run one solo timer at a time. Finish or stop the current timer first.');
|
||
return;
|
||
}
|
||
|
||
gameState.selectionMode = 'movie';
|
||
gameState.selectedPoints = [];
|
||
document.getElementById('selectionInstructions').classList.add('active');
|
||
document.getElementById('selectionText').textContent = 'Select 4 solo points for movie (2 hours)';
|
||
renderPoints();
|
||
}
|
||
|
||
function togglePointSelection(pointId) {
|
||
const requiredCount = gameState.selectionMode === 'episode' ? 2 : 4;
|
||
|
||
if (gameState.selectedPoints.includes(pointId)) {
|
||
gameState.selectedPoints = gameState.selectedPoints.filter(id => id !== pointId);
|
||
} else {
|
||
if (gameState.selectedPoints.length < requiredCount) {
|
||
gameState.selectedPoints.push(pointId);
|
||
}
|
||
}
|
||
|
||
const selectedCount = gameState.selectedPoints.length;
|
||
const modeText = gameState.selectionMode === 'episode' ? 'episode' : 'movie';
|
||
document.getElementById('selectionText').textContent =
|
||
`Select ${requiredCount} solo points for ${modeText} (${selectedCount}/${requiredCount} selected)`;
|
||
|
||
renderPoints();
|
||
}
|
||
|
||
async function confirmSelection() {
|
||
const requiredCount = gameState.selectionMode === 'episode' ? 2 : 4;
|
||
|
||
if (gameState.selectedPoints.length !== requiredCount) {
|
||
showAlert(`Please select exactly ${requiredCount} points.`);
|
||
return;
|
||
}
|
||
|
||
const modeText = gameState.selectionMode === 'episode' ? 'episode' : 'movie';
|
||
const duration = gameState.selectionMode === 'episode' ? '30 minutes' : '2 hours';
|
||
|
||
const confirmed = await showConfirm(`Use ${requiredCount} selected solo points to watch ${modeText} (${duration})?`);
|
||
if (!confirmed) return;
|
||
|
||
socket.emit('confirmSelection', {
|
||
selectedPoints: gameState.selectedPoints,
|
||
type: gameState.selectionMode
|
||
});
|
||
|
||
cancelSelection();
|
||
}
|
||
|
||
function cancelSelection() {
|
||
gameState.selectionMode = null;
|
||
gameState.selectedPoints = [];
|
||
document.getElementById('selectionInstructions').classList.remove('active');
|
||
renderPoints();
|
||
}
|
||
|
||
// Admin functions
|
||
function showAdminModal() {
|
||
document.getElementById('adminModal').style.display = 'block';
|
||
}
|
||
|
||
function adminLogin() {
|
||
const password = document.getElementById('adminPassword').value;
|
||
socket.emit('adminAuth', { password });
|
||
}
|
||
|
||
function exitAdminMode() {
|
||
gameState.adminMode = false;
|
||
document.getElementById('adminBar').classList.remove('active');
|
||
renderPoints();
|
||
}
|
||
|
||
function quickAddPoint(type) {
|
||
if (!gameState.adminMode) return;
|
||
socket.emit('quickAddPoint', { type });
|
||
}
|
||
|
||
function expireTimer(pointId) {
|
||
if (!gameState.adminMode) return;
|
||
socket.emit('expireTimer', { pointId });
|
||
}
|
||
|
||
async function removePoint(pointId) {
|
||
if (!gameState.adminMode) return;
|
||
const confirmed = await showConfirm('Remove this point permanently?');
|
||
if (!confirmed) return;
|
||
socket.emit('removePoint', { pointId });
|
||
}
|
||
|
||
async function resetAllPoints() {
|
||
if (!gameState.adminMode) return;
|
||
const confirmed = await showConfirm('Reset all points to 5 solo and 4 together? This will stop all timers and remove custom points.');
|
||
if (!confirmed) return;
|
||
socket.emit('resetAllPoints');
|
||
}
|
||
|
||
async function resetDay() {
|
||
if (!gameState.adminMode) return;
|
||
const confirmed = await showConfirm('Reset daily together point limit? This will allow using another together point today.');
|
||
if (!confirmed) return;
|
||
socket.emit('resetDailyLimit');
|
||
}
|
||
|
||
function clearLog() {
|
||
if (!gameState.adminMode) return;
|
||
socket.emit('clearLog');
|
||
}
|
||
|
||
// Add points modal
|
||
function showAddPointsModal() {
|
||
document.getElementById('addPointsModal').style.display = 'block';
|
||
}
|
||
|
||
function addPoints() {
|
||
const password = document.getElementById('addPointsPassword').value;
|
||
const type = document.getElementById('pointType').value;
|
||
const count = parseInt(document.getElementById('pointCount').value);
|
||
|
||
socket.emit('addPoints', { password, type, count });
|
||
closeModal('addPointsModal');
|
||
}
|
||
|
||
// Custom timer modal
|
||
function showCustomTimerModal() {
|
||
if (!gameState.adminMode) return;
|
||
document.getElementById('customTimerModal').style.display = 'block';
|
||
}
|
||
|
||
function createCustomTimer() {
|
||
const name = document.getElementById('customTimerName').value.trim();
|
||
const hours = parseInt(document.getElementById('customHours').value) || 0;
|
||
const minutes = parseInt(document.getElementById('customMinutes').value) || 0;
|
||
const pointType = document.getElementById('customPointType').value;
|
||
|
||
if (!name) {
|
||
showAlert('Please enter a timer name');
|
||
return;
|
||
}
|
||
|
||
if (hours === 0 && minutes === 0) {
|
||
showAlert('Please set a duration greater than 0');
|
||
return;
|
||
}
|
||
|
||
socket.emit('createCustomTimer', { name, hours, minutes, pointType });
|
||
closeModal('customTimerModal');
|
||
}
|
||
|
||
// Modal functions
|
||
function closeModal(modalId) {
|
||
document.getElementById(modalId).style.display = 'none';
|
||
const modal = document.getElementById(modalId);
|
||
const inputs = modal.querySelectorAll('input, select');
|
||
inputs.forEach(input => {
|
||
if (input.type === 'password' || input.type === 'text') {
|
||
input.value = '';
|
||
} else if (input.type === 'number') {
|
||
input.value = input.getAttribute('value') || '';
|
||
} else if (input.tagName === 'SELECT') {
|
||
input.selectedIndex = 0;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Audio functions
|
||
let audioPlaying = false;
|
||
|
||
function startAlarmSound() {
|
||
console.log('Starting alarm sound...');
|
||
const audioElement = document.getElementById('timerAudio');
|
||
|
||
// Force load the audio
|
||
audioElement.load();
|
||
|
||
// Set volume to max
|
||
audioElement.volume = 1.0;
|
||
audioElement.loop = true;
|
||
|
||
// Try to play
|
||
const playPromise = audioElement.play();
|
||
|
||
if (playPromise !== undefined) {
|
||
playPromise
|
||
.then(() => {
|
||
console.log('Audio playing successfully');
|
||
audioPlaying = true;
|
||
})
|
||
.catch(error => {
|
||
console.error('Audio playback failed:', error);
|
||
console.log('Audio src:', audioElement.src);
|
||
console.log('Audio readyState:', audioElement.readyState);
|
||
console.log('Audio error:', audioElement.error);
|
||
|
||
// Try fallback beep
|
||
createBeepSound();
|
||
});
|
||
}
|
||
}
|
||
|
||
function stopAlarmSound() {
|
||
console.log('Stopping alarm sound...');
|
||
const audioElement = document.getElementById('timerAudio');
|
||
audioElement.pause();
|
||
audioElement.currentTime = 0;
|
||
audioPlaying = false;
|
||
|
||
// Stop any fallback beep
|
||
if (window.audioContext) {
|
||
window.audioContext.close();
|
||
window.audioContext = null;
|
||
}
|
||
}
|
||
|
||
function playCompletionSound() {
|
||
console.log('Playing completion sound (single)...');
|
||
const audioElement = document.getElementById('timerAudio');
|
||
audioElement.loop = false;
|
||
audioElement.volume = 1.0;
|
||
audioElement.play().catch(error => {
|
||
console.error('Single play failed:', error);
|
||
});
|
||
}
|
||
|
||
function createBeepSound() {
|
||
console.log('Creating fallback beep sound...');
|
||
try {
|
||
window.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
const oscillator = window.audioContext.createOscillator();
|
||
const gainNode = window.audioContext.createGain();
|
||
|
||
oscillator.connect(gainNode);
|
||
gainNode.connect(window.audioContext.destination);
|
||
|
||
oscillator.frequency.value = 800; // Frequency in Hz
|
||
oscillator.type = 'sine';
|
||
gainNode.gain.value = 0.3; // Volume
|
||
|
||
oscillator.start();
|
||
audioPlaying = true;
|
||
|
||
// Create beeping pattern
|
||
const beepPattern = () => {
|
||
if (!audioPlaying) {
|
||
oscillator.stop();
|
||
window.audioContext.close();
|
||
window.audioContext = null;
|
||
return;
|
||
}
|
||
gainNode.gain.value = gainNode.gain.value > 0 ? 0 : 0.3;
|
||
setTimeout(beepPattern, 500);
|
||
};
|
||
|
||
beepPattern();
|
||
console.log('Beep sound started');
|
||
} catch (e) {
|
||
console.error('Web Audio API error:', e);
|
||
// Last resort - use notification API if available
|
||
if ('Notification' in window && Notification.permission === 'granted') {
|
||
new Notification('Timer Complete!', {
|
||
body: 'Your timer has finished.',
|
||
requireInteraction: true
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Test audio on page load
|
||
window.addEventListener('load', () => {
|
||
const audioElement = document.getElementById('timerAudio');
|
||
console.log('Audio element found:', audioElement);
|
||
console.log('Audio sources:', audioElement.getElementsByTagName('source').length);
|
||
|
||
// Check if audio can play
|
||
audioElement.addEventListener('canplay', () => {
|
||
console.log('Audio can play');
|
||
});
|
||
|
||
audioElement.addEventListener('error', (e) => {
|
||
console.error('Audio error event:', e);
|
||
});
|
||
|
||
// Request notification permission for fallback
|
||
if ('Notification' in window && Notification.permission === 'default') {
|
||
Notification.requestPermission();
|
||
}
|
||
});
|
||
|
||
function dismissTimerComplete() {
|
||
stopAlarmSound();
|
||
closeModal('timerCompleteModal');
|
||
}
|
||
|
||
// Test audio function
|
||
function testAudio() {
|
||
console.log('Testing audio...');
|
||
startAlarmSound();
|
||
|
||
// Show test modal
|
||
showAlert('Testing audio alarm. Click OK to stop.', 'Audio Test');
|
||
|
||
// Override OK button to stop sound
|
||
setTimeout(() => {
|
||
const okButton = document.querySelector('#alertModal button');
|
||
okButton.onclick = () => {
|
||
stopAlarmSound();
|
||
closeModal('alertModal');
|
||
};
|
||
}, 100);
|
||
}
|
||
|
||
// Close modals when clicking outside (except for no-close modals)
|
||
window.onclick = function(event) {
|
||
const modals = document.querySelectorAll('.modal:not(.no-close)');
|
||
modals.forEach(modal => {
|
||
if (event.target === modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
});
|
||
};
|
||
</script>
|
||
</body>
|
||
</html> |