// ==UserScript== // @name Are We There Yet - Time Estimator // @namespace http://tampermonkey.net/ // @version 2.3 // @description Estimates completion time based on progress rate // @author You // @match https://are-we-there-yet.hackclub.com/* // @grant GM_setValue // @grant GM_getValue // @run-at document-end // ==/UserScript== (function() { 'use strict'; const STORAGE_KEY = 'awtyt_progress_history'; const CHECK_INTERVAL = 5000; // Check every 5 seconds const MIN_DATA_POINTS = 3; // Minimum data points needed for estimation const RELOAD_INTERVAL = 60000; // Reload page every 60 seconds (1 minute) const RELOAD_KEY = 'awtyt_auto_reload'; // Get current progress percentage from the page function getCurrentProgress() { // Try the progress element first const progressEl = document.getElementById('progress'); if (progressEl && progressEl.value) { return parseFloat(progressEl.value); } // Fallback to parsing the label text const labelEl = document.querySelector('label[for="progress"]'); if (labelEl) { const match = labelEl.textContent.match(/(\d+\.?\d*)%/); if (match) { return parseFloat(match[1]); } } return null; } // Load progress history from storage function loadHistory() { const stored = GM_getValue(STORAGE_KEY, '[]'); return JSON.parse(stored); } // Save progress history to storage function saveHistory(history) { GM_setValue(STORAGE_KEY, JSON.stringify(history)); } // Calculate estimated completion time function calculateEstimation(history) { if (history.length < MIN_DATA_POINTS) { return null; } // Use linear regression to estimate rate of change const n = history.length; let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; // Use only recent data points (last 20) for better accuracy const recentHistory = history.slice(-20); const baseTime = recentHistory[0].timestamp; recentHistory.forEach((point) => { const x = (point.timestamp - baseTime) / 1000; // seconds since first point const y = point.progress; sumX += x; sumY += y; sumXY += x * y; sumX2 += x * x; }); const slope = (recentHistory.length * sumXY - sumX * sumY) / (recentHistory.length * sumX2 - sumX * sumX); const intercept = (sumY - slope * sumX) / recentHistory.length; // If slope is zero or negative, we can't estimate if (slope <= 0) { return null; } const currentProgress = recentHistory[recentHistory.length - 1].progress; const currentTime = recentHistory[recentHistory.length - 1].timestamp; const timeElapsed = (currentTime - baseTime) / 1000; // Calculate when we'll reach 100% const timeToCompletion = (100 - currentProgress) / slope; const estimatedCompletionTime = new Date(currentTime + timeToCompletion * 1000); return { slope: slope, currentProgress: currentProgress, estimatedCompletionTime: estimatedCompletionTime, timeRemaining: timeToCompletion }; } // Format time remaining function formatTimeRemaining(seconds) { if (seconds < 0) return "Completed!"; const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); const parts = []; if (days > 0) parts.push(`${days}d`); if (hours > 0) parts.push(`${hours}h`); if (minutes > 0) parts.push(`${minutes}m`); if (secs > 0 || parts.length === 0) parts.push(`${secs}s`); return parts.join(' '); } // Create and inject the full history graph into the page function createPageGraph() { // Check if already exists if (document.getElementById('awtyt-page-graph-container')) { return; } const progressBar = document.getElementById('progress'); if (!progressBar) { return; // Progress bar not found yet } const container = document.createElement('div'); container.id = 'awtyt-page-graph-container'; container.style.cssText = ` margin: 3rem 0 2rem 0; `; container.innerHTML = `
Progress History
Collecting data...
`; // Insert after the progress bar progressBar.parentNode.insertBefore(container, progressBar.nextSibling); } // Create and inject the UI function createUI() { const container = document.createElement('div'); container.id = 'awtyt-estimator'; container.style.cssText = ` position: fixed; top: 10px; right: 10px; background: rgba(0, 0, 0, 0.9); color: #00ff00; padding: 15px; border-radius: 8px; font-family: 'Courier New', monospace; font-size: 14px; z-index: 10000; min-width: 350px; max-width: 90vw; box-shadow: 0 0 10px rgba(0, 255, 0, 0.3); `; container.innerHTML = `
⏱️ Progress Tracker
Collecting data...
Recent (Last 4 Hours)
0 data points collected
`; document.body.appendChild(container); // Add reset button handler document.getElementById('awtyt-reset').addEventListener('click', () => { saveHistory([]); updateUI(); }); // Load auto-reload preference const autoReloadEnabled = GM_getValue(RELOAD_KEY + '_enabled', false); const reloadInterval = GM_getValue(RELOAD_KEY + '_interval', 60); document.getElementById('awtyt-autoreload').checked = autoReloadEnabled; document.getElementById('awtyt-reload-interval').value = reloadInterval; // Auto-reload checkbox handler document.getElementById('awtyt-autoreload').addEventListener('change', (e) => { GM_setValue(RELOAD_KEY + '_enabled', e.target.checked); if (e.target.checked) { setupAutoReload(); } else { clearAutoReload(); } }); // Reload interval input handler document.getElementById('awtyt-reload-interval').addEventListener('change', (e) => { const interval = parseInt(e.target.value); if (interval >= 30 && interval <= 600) { GM_setValue(RELOAD_KEY + '_interval', interval); if (document.getElementById('awtyt-autoreload').checked) { clearAutoReload(); setupAutoReload(); } } }); } // Thin out data points for display based on time window function thinData(history, maxPoints, timeWindow = null) { if (history.length <= maxPoints) { return history; } const now = Date.now(); let filtered = history; // Filter by time window if specified if (timeWindow) { filtered = history.filter(p => (now - p.timestamp) <= timeWindow); } if (filtered.length <= maxPoints) { return filtered; } // Thin data by taking every Nth point, but always keep first and last const step = Math.ceil(filtered.length / maxPoints); const thinned = []; for (let i = 0; i < filtered.length; i += step) { thinned.push(filtered[i]); } // Ensure we include the last point if (thinned[thinned.length - 1] !== filtered[filtered.length - 1]) { thinned.push(filtered[filtered.length - 1]); } return thinned; } // Draw a progress graph on a specific canvas function drawGraph(canvasId, dataPoints, title = '') { const canvas = document.getElementById(canvasId); if (!canvas) { console.log('[AWTYT] Canvas not found:', canvasId); return; } const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; const padding = { top: 15, right: 10, bottom: 25, left: 45 }; const graphWidth = width - padding.left - padding.right; const graphHeight = height - padding.top - padding.bottom; // Determine if this is the page graph or popup graph const isPageGraph = canvasId === 'awtyt-graph-full'; const bgColor = isPageGraph ? '#1a1a1a' : '#000'; const lineColor = isPageGraph ? '#60a5fa' : '#00ff00'; const gridColor = isPageGraph ? '#2a2a2a' : '#003300'; const textColor = isPageGraph ? '#e0e0e0' : '#00ff00'; const trendColor = isPageGraph ? '#60a5fa66' : '#00ff0066'; // Clear canvas ctx.fillStyle = bgColor; ctx.fillRect(0, 0, width, height); if (dataPoints.length < 2) { // Not enough data to draw ctx.fillStyle = textColor; ctx.font = '12px "Courier New", monospace'; ctx.textAlign = 'center'; ctx.fillText('Collecting data...', width / 2, height / 2); console.log('[AWTYT] Not enough data points:', dataPoints.length); return; } // Find min/max values for scaling const minTime = dataPoints[0].timestamp; const maxTime = dataPoints[dataPoints.length - 1].timestamp; const timeRange = maxTime - minTime || 1; const progressValues = dataPoints.map(h => h.progress); const minProgress = Math.max(0, Math.min(...progressValues) - 0.5); const maxProgress = Math.min(100, Math.max(...progressValues) + 0.5); const progressRange = maxProgress - minProgress || 1; // Draw grid lines ctx.strokeStyle = gridColor; ctx.lineWidth = 1; // Horizontal grid lines (progress) for (let i = 0; i <= 5; i++) { const y = padding.top + (graphHeight * i / 5); ctx.beginPath(); ctx.moveTo(padding.left, y); ctx.lineTo(padding.left + graphWidth, y); ctx.stroke(); } // Vertical grid lines (time) for (let i = 0; i <= 5; i++) { const x = padding.left + (graphWidth * i / 5); ctx.beginPath(); ctx.moveTo(x, padding.top); ctx.lineTo(x, padding.top + graphHeight); ctx.stroke(); } // Draw axes ctx.strokeStyle = lineColor; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(padding.left, padding.top); ctx.lineTo(padding.left, padding.top + graphHeight); ctx.lineTo(padding.left + graphWidth, padding.top + graphHeight); ctx.stroke(); // Draw Y-axis labels (progress %) ctx.fillStyle = textColor; ctx.font = '10px "Courier New", monospace'; ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; for (let i = 0; i <= 5; i++) { const progress = maxProgress - (progressRange * i / 5); const y = padding.top + (graphHeight * i / 5); ctx.fillText(progress.toFixed(1) + '%', padding.left - 5, y); } // Draw X-axis labels (time) ctx.textAlign = 'center'; ctx.textBaseline = 'top'; const now = Date.now(); const displayPoints = [0, 0.5, 1]; displayPoints.forEach(fraction => { const time = minTime + (timeRange * fraction); const x = padding.left + (graphWidth * fraction); const secondsAgo = Math.round((now - time) / 1000); let label; if (secondsAgo < 60) { label = secondsAgo === 0 ? 'now' : `-${secondsAgo}s`; } else if (secondsAgo < 3600) { const minutesAgo = Math.round(secondsAgo / 60); label = `-${minutesAgo}m`; } else if (secondsAgo < 86400) { const hoursAgo = Math.round(secondsAgo / 3600); label = `-${hoursAgo}h`; } else { const daysAgo = Math.round(secondsAgo / 86400); label = `-${daysAgo}d`; } ctx.fillText(label, x, padding.top + graphHeight + 5); }); // Draw the data line ctx.strokeStyle = lineColor; ctx.lineWidth = 2; ctx.beginPath(); dataPoints.forEach((point, index) => { const x = padding.left + ((point.timestamp - minTime) / timeRange) * graphWidth; const y = padding.top + graphHeight - ((point.progress - minProgress) / progressRange) * graphHeight; if (index === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } }); ctx.stroke(); // Draw data points ctx.fillStyle = lineColor; dataPoints.forEach((point) => { const x = padding.left + ((point.timestamp - minTime) / timeRange) * graphWidth; const y = padding.top + graphHeight - ((point.progress - minProgress) / progressRange) * graphHeight; ctx.beginPath(); ctx.arc(x, y, 2, 0, 2 * Math.PI); ctx.fill(); }); // Draw trend line if we have estimation const history = loadHistory(); if (history.length >= MIN_DATA_POINTS) { const estimation = calculateEstimation(history); if (estimation && estimation.slope > 0) { ctx.strokeStyle = trendColor; ctx.lineWidth = 1; ctx.setLineDash([5, 5]); ctx.beginPath(); // Draw from first point to projected end const firstPoint = dataPoints[0]; const lastPoint = dataPoints[dataPoints.length - 1]; const x1 = padding.left; const y1 = padding.top + graphHeight - ((firstPoint.progress - minProgress) / progressRange) * graphHeight; // Project to 100% or edge of graph, whichever comes first const timeToMax = (maxProgress - lastPoint.progress) / estimation.slope; const projectedTime = Math.min(lastPoint.timestamp + timeToMax * 1000, maxTime + timeRange * 0.2); const projectedProgress = Math.min(lastPoint.progress + estimation.slope * (projectedTime - lastPoint.timestamp) / 1000, maxProgress); const x2 = padding.left + ((projectedTime - minTime) / timeRange) * graphWidth; const y2 = padding.top + graphHeight - ((projectedProgress - minProgress) / progressRange) * graphHeight; ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.setLineDash([]); } } } // Update the UI with current estimation function updateUI() { const history = loadHistory(); const statusEl = document.getElementById('awtyt-status'); const progressEl = document.getElementById('awtyt-progress'); const rateEl = document.getElementById('awtyt-rate'); const etaEl = document.getElementById('awtyt-eta'); const remainingEl = document.getElementById('awtyt-remaining'); const datapointsEl = document.getElementById('awtyt-datapoints'); datapointsEl.textContent = history.length; // Create page graph if it doesn't exist createPageGraph(); // Draw both graphs const fourHours = 4 * 60 * 60 * 1000; const recentData = thinData(history, 100, fourHours); const fullData = thinData(history, 150); drawGraph('awtyt-graph-recent', recentData); drawGraph('awtyt-graph-full', fullData); // Update page graph info const pageInfoEl = document.getElementById('awtyt-page-graph-info'); if (pageInfoEl) { if (history.length < 2) { pageInfoEl.textContent = 'Collecting data...'; } else { const timeSpan = history[history.length - 1].timestamp - history[0].timestamp; const hours = Math.floor(timeSpan / 3600000); const minutes = Math.floor((timeSpan % 3600000) / 60000); let timeText = ''; if (hours > 0) timeText += `${hours}h `; if (minutes > 0 || hours === 0) timeText += `${minutes}m`; pageInfoEl.textContent = `${history.length} data points over ${timeText.trim()} • Showing ${fullData.length} points`; } } if (history.length < MIN_DATA_POINTS) { statusEl.textContent = `Need ${MIN_DATA_POINTS - history.length} more data points...`; progressEl.textContent = history.length > 0 ? `Progress: ${history[history.length - 1].progress.toFixed(2)}%` : ''; rateEl.textContent = ''; etaEl.textContent = ''; remainingEl.textContent = ''; return; } const estimation = calculateEstimation(history); if (!estimation) { statusEl.textContent = 'Unable to estimate (no progress detected)'; return; } statusEl.textContent = '📊 Estimation Active'; progressEl.textContent = `Progress: ${estimation.currentProgress.toFixed(2)}%`; rateEl.textContent = `Rate: ${(estimation.slope * 60).toFixed(4)}%/min`; etaEl.textContent = `ETA: ${estimation.estimatedCompletionTime.toLocaleString()}`; remainingEl.textContent = `Time Left: ${formatTimeRemaining(estimation.timeRemaining)}`; } let reloadTimer = null; let reloadCountdown = null; // Setup auto-reload function setupAutoReload() { clearAutoReload(); const interval = parseInt(document.getElementById('awtyt-reload-interval').value) * 1000; const reloadTime = Date.now() + interval; GM_setValue(RELOAD_KEY + '_time', reloadTime); reloadTimer = setTimeout(() => { location.reload(); }, interval); // Update countdown every second reloadCountdown = setInterval(() => { updateReloadCountdown(); }, 1000); updateReloadCountdown(); } // Clear auto-reload timers function clearAutoReload() { if (reloadTimer) { clearTimeout(reloadTimer); reloadTimer = null; } if (reloadCountdown) { clearInterval(reloadCountdown); reloadCountdown = null; } document.getElementById('awtyt-next-reload').textContent = ''; } // Update reload countdown display function updateReloadCountdown() { const reloadTime = GM_getValue(RELOAD_KEY + '_time', 0); const remaining = Math.max(0, Math.floor((reloadTime - Date.now()) / 1000)); document.getElementById('awtyt-next-reload').textContent = remaining > 0 ? `Next reload in ${remaining}s` : 'Reloading...'; } // Main tracking loop function trackProgress() { const progress = getCurrentProgress(); if (progress !== null) { const history = loadHistory(); const now = Date.now(); // Only add if progress has changed or it's been more than 1 minute const shouldAdd = history.length === 0 || history[history.length - 1].progress !== progress || (now - history[history.length - 1].timestamp) > 60000; if (shouldAdd) { history.push({ timestamp: now, progress: progress }); // Keep all historical data - no limit! saveHistory(history); } updateUI(); } } // Initialize createUI(); trackProgress(); setInterval(trackProgress, CHECK_INTERVAL); // Start auto-reload if enabled if (GM_getValue(RELOAD_KEY + '_enabled', false)) { setupAutoReload(); } })();