auto reload and estimate progress
are-we-there-yet-estimator.user.js edited
606 lines 23 kB view raw
1// ==UserScript== 2// @name Are We There Yet - Time Estimator 3// @namespace http://tampermonkey.net/ 4// @version 2.3 5// @description Estimates completion time based on progress rate 6// @author You 7// @match https://are-we-there-yet.hackclub.com/* 8// @grant GM_setValue 9// @grant GM_getValue 10// @run-at document-end 11// ==/UserScript== 12 13(function() { 14 'use strict'; 15 16 const STORAGE_KEY = 'awtyt_progress_history'; 17 const CHECK_INTERVAL = 5000; // Check every 5 seconds 18 const MIN_DATA_POINTS = 3; // Minimum data points needed for estimation 19 const RELOAD_INTERVAL = 60000; // Reload page every 60 seconds (1 minute) 20 const RELOAD_KEY = 'awtyt_auto_reload'; 21 22 // Get current progress percentage from the page 23 function getCurrentProgress() { 24 // Try the progress element first 25 const progressEl = document.getElementById('progress'); 26 if (progressEl && progressEl.value) { 27 return parseFloat(progressEl.value); 28 } 29 30 // Fallback to parsing the label text 31 const labelEl = document.querySelector('label[for="progress"]'); 32 if (labelEl) { 33 const match = labelEl.textContent.match(/(\d+\.?\d*)%/); 34 if (match) { 35 return parseFloat(match[1]); 36 } 37 } 38 39 return null; 40 } 41 42 // Load progress history from storage 43 function loadHistory() { 44 const stored = GM_getValue(STORAGE_KEY, '[]'); 45 return JSON.parse(stored); 46 } 47 48 // Save progress history to storage 49 function saveHistory(history) { 50 GM_setValue(STORAGE_KEY, JSON.stringify(history)); 51 } 52 53 // Calculate estimated completion time 54 function calculateEstimation(history) { 55 if (history.length < MIN_DATA_POINTS) { 56 return null; 57 } 58 59 // Use linear regression to estimate rate of change 60 const n = history.length; 61 let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; 62 63 // Use only recent data points (last 20) for better accuracy 64 const recentHistory = history.slice(-20); 65 const baseTime = recentHistory[0].timestamp; 66 67 recentHistory.forEach((point) => { 68 const x = (point.timestamp - baseTime) / 1000; // seconds since first point 69 const y = point.progress; 70 sumX += x; 71 sumY += y; 72 sumXY += x * y; 73 sumX2 += x * x; 74 }); 75 76 const slope = (recentHistory.length * sumXY - sumX * sumY) / 77 (recentHistory.length * sumX2 - sumX * sumX); 78 const intercept = (sumY - slope * sumX) / recentHistory.length; 79 80 // If slope is zero or negative, we can't estimate 81 if (slope <= 0) { 82 return null; 83 } 84 85 const currentProgress = recentHistory[recentHistory.length - 1].progress; 86 const currentTime = recentHistory[recentHistory.length - 1].timestamp; 87 const timeElapsed = (currentTime - baseTime) / 1000; 88 89 // Calculate when we'll reach 100% 90 const timeToCompletion = (100 - currentProgress) / slope; 91 const estimatedCompletionTime = new Date(currentTime + timeToCompletion * 1000); 92 93 return { 94 slope: slope, 95 currentProgress: currentProgress, 96 estimatedCompletionTime: estimatedCompletionTime, 97 timeRemaining: timeToCompletion 98 }; 99 } 100 101 // Format time remaining 102 function formatTimeRemaining(seconds) { 103 if (seconds < 0) return "Completed!"; 104 105 const days = Math.floor(seconds / 86400); 106 const hours = Math.floor((seconds % 86400) / 3600); 107 const minutes = Math.floor((seconds % 3600) / 60); 108 const secs = Math.floor(seconds % 60); 109 110 const parts = []; 111 if (days > 0) parts.push(`${days}d`); 112 if (hours > 0) parts.push(`${hours}h`); 113 if (minutes > 0) parts.push(`${minutes}m`); 114 if (secs > 0 || parts.length === 0) parts.push(`${secs}s`); 115 116 return parts.join(' '); 117 } 118 119 // Create and inject the full history graph into the page 120 function createPageGraph() { 121 // Check if already exists 122 if (document.getElementById('awtyt-page-graph-container')) { 123 return; 124 } 125 126 const progressBar = document.getElementById('progress'); 127 if (!progressBar) { 128 return; // Progress bar not found yet 129 } 130 131 const container = document.createElement('div'); 132 container.id = 'awtyt-page-graph-container'; 133 container.style.cssText = ` 134 margin: 3rem 0 2rem 0; 135 `; 136 137 container.innerHTML = ` 138 <div style=" 139 font-weight: 700; 140 font-size: 0.9em; 141 color: #e0e0e0; 142 letter-spacing: 0.05em; 143 margin-bottom: 1rem; 144 ">Progress History</div> 145 <div style=" 146 background: #222; 147 border-left: 4px solid #60a5fa; 148 padding: 1.5em; 149 "> 150 <canvas id="awtyt-graph-full" width="750" height="300" style=" 151 background: #1a1a1a; 152 border: 1px solid #333; 153 border-radius: 4px; 154 max-width: 100%; 155 display: block; 156 "></canvas> 157 <div style=" 158 margin-top: 1em; 159 font-size: 0.8em; 160 color: #888; 161 letter-spacing: 0.05em; 162 " id="awtyt-page-graph-info">Collecting data...</div> 163 </div> 164 `; 165 166 // Insert after the progress bar 167 progressBar.parentNode.insertBefore(container, progressBar.nextSibling); 168 } 169 170 // Create and inject the UI 171 function createUI() { 172 const container = document.createElement('div'); 173 container.id = 'awtyt-estimator'; 174 container.style.cssText = ` 175 position: fixed; 176 top: 10px; 177 right: 10px; 178 background: rgba(0, 0, 0, 0.9); 179 color: #00ff00; 180 padding: 15px; 181 border-radius: 8px; 182 font-family: 'Courier New', monospace; 183 font-size: 14px; 184 z-index: 10000; 185 min-width: 350px; 186 max-width: 90vw; 187 box-shadow: 0 0 10px rgba(0, 255, 0, 0.3); 188 `; 189 190 container.innerHTML = ` 191 <div style="margin-bottom: 10px; font-weight: bold; border-bottom: 1px solid #00ff00; padding-bottom: 5px;"> 192 ⏱️ Progress Tracker 193 </div> 194 <div id="awtyt-status">Collecting data...</div> 195 <div id="awtyt-progress" style="margin-top: 5px;"></div> 196 <div id="awtyt-rate" style="margin-top: 5px;"></div> 197 <div id="awtyt-eta" style="margin-top: 5px;"></div> 198 <div id="awtyt-remaining" style="margin-top: 5px; font-weight: bold;"></div> 199 <div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #00ff00;"> 200 <div style="font-size: 11px; margin-bottom: 5px; opacity: 0.8;">Recent (Last 4 Hours)</div> 201 <canvas id="awtyt-graph-recent" width="320" height="100" style="background: #000; border: 1px solid #00ff00; border-radius: 4px; max-width: 100%;"></canvas> 202 </div> 203 <div style="margin-top: 10px; font-size: 11px; opacity: 0.7; border-top: 1px solid #00ff00; padding-top: 5px;"> 204 <span id="awtyt-datapoints">0</span> data points collected 205 <button id="awtyt-reset" style="margin-left: 10px; background: #00ff00; color: black; border: none; padding: 2px 8px; cursor: pointer; border-radius: 3px;">Reset</button> 206 </div> 207 <div style="margin-top: 8px; font-size: 11px;"> 208 <label style="display: flex; align-items: center; cursor: pointer;"> 209 <input type="checkbox" id="awtyt-autoreload" style="margin-right: 5px; cursor: pointer;"> 210 Auto-reload every <input type="number" id="awtyt-reload-interval" min="30" max="600" value="60" style="width: 50px; margin: 0 3px; background: #001100; color: #00ff00; border: 1px solid #00ff00; padding: 2px; text-align: center;"> sec 211 </label> 212 <div id="awtyt-next-reload" style="margin-top: 3px; font-size: 10px; opacity: 0.6;"></div> 213 </div> 214 `; 215 216 document.body.appendChild(container); 217 218 // Add reset button handler 219 document.getElementById('awtyt-reset').addEventListener('click', () => { 220 saveHistory([]); 221 updateUI(); 222 }); 223 224 // Load auto-reload preference 225 const autoReloadEnabled = GM_getValue(RELOAD_KEY + '_enabled', false); 226 const reloadInterval = GM_getValue(RELOAD_KEY + '_interval', 60); 227 document.getElementById('awtyt-autoreload').checked = autoReloadEnabled; 228 document.getElementById('awtyt-reload-interval').value = reloadInterval; 229 230 // Auto-reload checkbox handler 231 document.getElementById('awtyt-autoreload').addEventListener('change', (e) => { 232 GM_setValue(RELOAD_KEY + '_enabled', e.target.checked); 233 if (e.target.checked) { 234 setupAutoReload(); 235 } else { 236 clearAutoReload(); 237 } 238 }); 239 240 // Reload interval input handler 241 document.getElementById('awtyt-reload-interval').addEventListener('change', (e) => { 242 const interval = parseInt(e.target.value); 243 if (interval >= 30 && interval <= 600) { 244 GM_setValue(RELOAD_KEY + '_interval', interval); 245 if (document.getElementById('awtyt-autoreload').checked) { 246 clearAutoReload(); 247 setupAutoReload(); 248 } 249 } 250 }); 251 } 252 253 // Thin out data points for display based on time window 254 function thinData(history, maxPoints, timeWindow = null) { 255 if (history.length <= maxPoints) { 256 return history; 257 } 258 259 const now = Date.now(); 260 let filtered = history; 261 262 // Filter by time window if specified 263 if (timeWindow) { 264 filtered = history.filter(p => (now - p.timestamp) <= timeWindow); 265 } 266 267 if (filtered.length <= maxPoints) { 268 return filtered; 269 } 270 271 // Thin data by taking every Nth point, but always keep first and last 272 const step = Math.ceil(filtered.length / maxPoints); 273 const thinned = []; 274 275 for (let i = 0; i < filtered.length; i += step) { 276 thinned.push(filtered[i]); 277 } 278 279 // Ensure we include the last point 280 if (thinned[thinned.length - 1] !== filtered[filtered.length - 1]) { 281 thinned.push(filtered[filtered.length - 1]); 282 } 283 284 return thinned; 285 } 286 287 // Draw a progress graph on a specific canvas 288 function drawGraph(canvasId, dataPoints, title = '') { 289 const canvas = document.getElementById(canvasId); 290 if (!canvas) { 291 console.log('[AWTYT] Canvas not found:', canvasId); 292 return; 293 } 294 295 const ctx = canvas.getContext('2d'); 296 const width = canvas.width; 297 const height = canvas.height; 298 299 const padding = { top: 15, right: 10, bottom: 25, left: 45 }; 300 const graphWidth = width - padding.left - padding.right; 301 const graphHeight = height - padding.top - padding.bottom; 302 303 // Determine if this is the page graph or popup graph 304 const isPageGraph = canvasId === 'awtyt-graph-full'; 305 const bgColor = isPageGraph ? '#1a1a1a' : '#000'; 306 const lineColor = isPageGraph ? '#60a5fa' : '#00ff00'; 307 const gridColor = isPageGraph ? '#2a2a2a' : '#003300'; 308 const textColor = isPageGraph ? '#e0e0e0' : '#00ff00'; 309 const trendColor = isPageGraph ? '#60a5fa66' : '#00ff0066'; 310 311 // Clear canvas 312 ctx.fillStyle = bgColor; 313 ctx.fillRect(0, 0, width, height); 314 315 if (dataPoints.length < 2) { 316 // Not enough data to draw 317 ctx.fillStyle = textColor; 318 ctx.font = '12px "Courier New", monospace'; 319 ctx.textAlign = 'center'; 320 ctx.fillText('Collecting data...', width / 2, height / 2); 321 console.log('[AWTYT] Not enough data points:', dataPoints.length); 322 return; 323 } 324 325 // Find min/max values for scaling 326 const minTime = dataPoints[0].timestamp; 327 const maxTime = dataPoints[dataPoints.length - 1].timestamp; 328 const timeRange = maxTime - minTime || 1; 329 330 const progressValues = dataPoints.map(h => h.progress); 331 const minProgress = Math.max(0, Math.min(...progressValues) - 0.5); 332 const maxProgress = Math.min(100, Math.max(...progressValues) + 0.5); 333 const progressRange = maxProgress - minProgress || 1; 334 335 // Draw grid lines 336 ctx.strokeStyle = gridColor; 337 ctx.lineWidth = 1; 338 339 // Horizontal grid lines (progress) 340 for (let i = 0; i <= 5; i++) { 341 const y = padding.top + (graphHeight * i / 5); 342 ctx.beginPath(); 343 ctx.moveTo(padding.left, y); 344 ctx.lineTo(padding.left + graphWidth, y); 345 ctx.stroke(); 346 } 347 348 // Vertical grid lines (time) 349 for (let i = 0; i <= 5; i++) { 350 const x = padding.left + (graphWidth * i / 5); 351 ctx.beginPath(); 352 ctx.moveTo(x, padding.top); 353 ctx.lineTo(x, padding.top + graphHeight); 354 ctx.stroke(); 355 } 356 357 // Draw axes 358 ctx.strokeStyle = lineColor; 359 ctx.lineWidth = 2; 360 ctx.beginPath(); 361 ctx.moveTo(padding.left, padding.top); 362 ctx.lineTo(padding.left, padding.top + graphHeight); 363 ctx.lineTo(padding.left + graphWidth, padding.top + graphHeight); 364 ctx.stroke(); 365 366 // Draw Y-axis labels (progress %) 367 ctx.fillStyle = textColor; 368 ctx.font = '10px "Courier New", monospace'; 369 ctx.textAlign = 'right'; 370 ctx.textBaseline = 'middle'; 371 for (let i = 0; i <= 5; i++) { 372 const progress = maxProgress - (progressRange * i / 5); 373 const y = padding.top + (graphHeight * i / 5); 374 ctx.fillText(progress.toFixed(1) + '%', padding.left - 5, y); 375 } 376 377 // Draw X-axis labels (time) 378 ctx.textAlign = 'center'; 379 ctx.textBaseline = 'top'; 380 const now = Date.now(); 381 const displayPoints = [0, 0.5, 1]; 382 displayPoints.forEach(fraction => { 383 const time = minTime + (timeRange * fraction); 384 const x = padding.left + (graphWidth * fraction); 385 const secondsAgo = Math.round((now - time) / 1000); 386 387 let label; 388 if (secondsAgo < 60) { 389 label = secondsAgo === 0 ? 'now' : `-${secondsAgo}s`; 390 } else if (secondsAgo < 3600) { 391 const minutesAgo = Math.round(secondsAgo / 60); 392 label = `-${minutesAgo}m`; 393 } else if (secondsAgo < 86400) { 394 const hoursAgo = Math.round(secondsAgo / 3600); 395 label = `-${hoursAgo}h`; 396 } else { 397 const daysAgo = Math.round(secondsAgo / 86400); 398 label = `-${daysAgo}d`; 399 } 400 401 ctx.fillText(label, x, padding.top + graphHeight + 5); 402 }); 403 404 // Draw the data line 405 ctx.strokeStyle = lineColor; 406 ctx.lineWidth = 2; 407 ctx.beginPath(); 408 409 dataPoints.forEach((point, index) => { 410 const x = padding.left + ((point.timestamp - minTime) / timeRange) * graphWidth; 411 const y = padding.top + graphHeight - ((point.progress - minProgress) / progressRange) * graphHeight; 412 413 if (index === 0) { 414 ctx.moveTo(x, y); 415 } else { 416 ctx.lineTo(x, y); 417 } 418 }); 419 420 ctx.stroke(); 421 422 // Draw data points 423 ctx.fillStyle = lineColor; 424 dataPoints.forEach((point) => { 425 const x = padding.left + ((point.timestamp - minTime) / timeRange) * graphWidth; 426 const y = padding.top + graphHeight - ((point.progress - minProgress) / progressRange) * graphHeight; 427 428 ctx.beginPath(); 429 ctx.arc(x, y, 2, 0, 2 * Math.PI); 430 ctx.fill(); 431 }); 432 433 // Draw trend line if we have estimation 434 const history = loadHistory(); 435 if (history.length >= MIN_DATA_POINTS) { 436 const estimation = calculateEstimation(history); 437 if (estimation && estimation.slope > 0) { 438 ctx.strokeStyle = trendColor; 439 ctx.lineWidth = 1; 440 ctx.setLineDash([5, 5]); 441 ctx.beginPath(); 442 443 // Draw from first point to projected end 444 const firstPoint = dataPoints[0]; 445 const lastPoint = dataPoints[dataPoints.length - 1]; 446 447 const x1 = padding.left; 448 const y1 = padding.top + graphHeight - ((firstPoint.progress - minProgress) / progressRange) * graphHeight; 449 450 // Project to 100% or edge of graph, whichever comes first 451 const timeToMax = (maxProgress - lastPoint.progress) / estimation.slope; 452 const projectedTime = Math.min(lastPoint.timestamp + timeToMax * 1000, maxTime + timeRange * 0.2); 453 const projectedProgress = Math.min(lastPoint.progress + estimation.slope * (projectedTime - lastPoint.timestamp) / 1000, maxProgress); 454 455 const x2 = padding.left + ((projectedTime - minTime) / timeRange) * graphWidth; 456 const y2 = padding.top + graphHeight - ((projectedProgress - minProgress) / progressRange) * graphHeight; 457 458 ctx.moveTo(x1, y1); 459 ctx.lineTo(x2, y2); 460 ctx.stroke(); 461 ctx.setLineDash([]); 462 } 463 } 464 } 465 466 // Update the UI with current estimation 467 function updateUI() { 468 const history = loadHistory(); 469 const statusEl = document.getElementById('awtyt-status'); 470 const progressEl = document.getElementById('awtyt-progress'); 471 const rateEl = document.getElementById('awtyt-rate'); 472 const etaEl = document.getElementById('awtyt-eta'); 473 const remainingEl = document.getElementById('awtyt-remaining'); 474 const datapointsEl = document.getElementById('awtyt-datapoints'); 475 476 datapointsEl.textContent = history.length; 477 478 // Create page graph if it doesn't exist 479 createPageGraph(); 480 481 // Draw both graphs 482 const fourHours = 4 * 60 * 60 * 1000; 483 const recentData = thinData(history, 100, fourHours); 484 const fullData = thinData(history, 150); 485 486 drawGraph('awtyt-graph-recent', recentData); 487 drawGraph('awtyt-graph-full', fullData); 488 489 // Update page graph info 490 const pageInfoEl = document.getElementById('awtyt-page-graph-info'); 491 if (pageInfoEl) { 492 if (history.length < 2) { 493 pageInfoEl.textContent = 'Collecting data...'; 494 } else { 495 const timeSpan = history[history.length - 1].timestamp - history[0].timestamp; 496 const hours = Math.floor(timeSpan / 3600000); 497 const minutes = Math.floor((timeSpan % 3600000) / 60000); 498 let timeText = ''; 499 if (hours > 0) timeText += `${hours}h `; 500 if (minutes > 0 || hours === 0) timeText += `${minutes}m`; 501 pageInfoEl.textContent = `${history.length} data points over ${timeText.trim()} • Showing ${fullData.length} points`; 502 } 503 } 504 505 if (history.length < MIN_DATA_POINTS) { 506 statusEl.textContent = `Need ${MIN_DATA_POINTS - history.length} more data points...`; 507 progressEl.textContent = history.length > 0 ? `Progress: ${history[history.length - 1].progress.toFixed(2)}%` : ''; 508 rateEl.textContent = ''; 509 etaEl.textContent = ''; 510 remainingEl.textContent = ''; 511 return; 512 } 513 514 const estimation = calculateEstimation(history); 515 if (!estimation) { 516 statusEl.textContent = 'Unable to estimate (no progress detected)'; 517 return; 518 } 519 520 statusEl.textContent = '📊 Estimation Active'; 521 progressEl.textContent = `Progress: ${estimation.currentProgress.toFixed(2)}%`; 522 rateEl.textContent = `Rate: ${(estimation.slope * 60).toFixed(4)}%/min`; 523 etaEl.textContent = `ETA: ${estimation.estimatedCompletionTime.toLocaleString()}`; 524 remainingEl.textContent = `Time Left: ${formatTimeRemaining(estimation.timeRemaining)}`; 525 } 526 527 let reloadTimer = null; 528 let reloadCountdown = null; 529 530 // Setup auto-reload 531 function setupAutoReload() { 532 clearAutoReload(); 533 const interval = parseInt(document.getElementById('awtyt-reload-interval').value) * 1000; 534 const reloadTime = Date.now() + interval; 535 GM_setValue(RELOAD_KEY + '_time', reloadTime); 536 537 reloadTimer = setTimeout(() => { 538 location.reload(); 539 }, interval); 540 541 // Update countdown every second 542 reloadCountdown = setInterval(() => { 543 updateReloadCountdown(); 544 }, 1000); 545 546 updateReloadCountdown(); 547 } 548 549 // Clear auto-reload timers 550 function clearAutoReload() { 551 if (reloadTimer) { 552 clearTimeout(reloadTimer); 553 reloadTimer = null; 554 } 555 if (reloadCountdown) { 556 clearInterval(reloadCountdown); 557 reloadCountdown = null; 558 } 559 document.getElementById('awtyt-next-reload').textContent = ''; 560 } 561 562 // Update reload countdown display 563 function updateReloadCountdown() { 564 const reloadTime = GM_getValue(RELOAD_KEY + '_time', 0); 565 const remaining = Math.max(0, Math.floor((reloadTime - Date.now()) / 1000)); 566 document.getElementById('awtyt-next-reload').textContent = 567 remaining > 0 ? `Next reload in ${remaining}s` : 'Reloading...'; 568 } 569 570 // Main tracking loop 571 function trackProgress() { 572 const progress = getCurrentProgress(); 573 if (progress !== null) { 574 const history = loadHistory(); 575 const now = Date.now(); 576 577 // Only add if progress has changed or it's been more than 1 minute 578 const shouldAdd = history.length === 0 || 579 history[history.length - 1].progress !== progress || 580 (now - history[history.length - 1].timestamp) > 60000; 581 582 if (shouldAdd) { 583 history.push({ 584 timestamp: now, 585 progress: progress 586 }); 587 588 // Keep all historical data - no limit! 589 saveHistory(history); 590 } 591 592 updateUI(); 593 } 594 } 595 596 // Initialize 597 createUI(); 598 trackProgress(); 599 setInterval(trackProgress, CHECK_INTERVAL); 600 601 // Start auto-reload if enabled 602 if (GM_getValue(RELOAD_KEY + '_enabled', false)) { 603 setupAutoReload(); 604 } 605 606})();