From 047380966960c653062fd90f41188d63ea0cba26 Mon Sep 17 00:00:00 2001 From: Sever Manolescu Date: Thu, 16 Oct 2025 20:24:15 +0300 Subject: [PATCH 1/2] Improve App Details page --- src/main/ipc/apps.js | 17 +++- src/renderer/app-details.html | 84 ++++++++++--------- src/renderer/js/app-details/app-details.js | 45 ++++++---- .../js/app-details/usage-over-time-chart.js | 72 ++++++++-------- src/renderer/styles/analytics/analytics.css | 1 - .../styles/app-details/app-details.css | 23 ++++- src/renderer/styles/app-details/chart.css | 51 +++++++++-- .../category-insights/category-insights.css | 1 - src/renderer/styles/goals/goals.css | 1 - .../styles/index-page/home-toolbar.css | 1 - .../styles/productivity/productivity.css | 1 - 11 files changed, 190 insertions(+), 107 deletions(-) diff --git a/src/main/ipc/apps.js b/src/main/ipc/apps.js index 12e0f1c..d999aab 100644 --- a/src/main/ipc/apps.js +++ b/src/main/ipc/apps.js @@ -320,7 +320,10 @@ function initializeAppHandlers() { `).all([appId]); // Get today's activity by hour - const todayStart = new Date().setHours(0, 0, 0, 0); + // Create a Date object for today at midnight in local time + const todayDate = new Date(); + todayDate.setHours(0, 0, 0, 0); + const todayStart = todayDate.getTime(); // Convert to milliseconds timestamp const todayActivity = db.prepare(` SELECT CAST(strftime('%H', start_time / 1000, 'unixepoch', 'localtime') AS INTEGER) as hour, @@ -449,8 +452,16 @@ function calculateStreak(db, appId) { if (sessions.length === 0) return 0; let streak = 0; - const today = new Date().toISOString().split('T')[0]; - const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]; + // Get today's date in local time (YYYY-MM-DD format) to match SQL 'localtime' + const todayDate = new Date(); + const today = todayDate.getFullYear() + '-' + + String(todayDate.getMonth() + 1).padStart(2, '0') + '-' + + String(todayDate.getDate()).padStart(2, '0'); + + const yesterdayDate = new Date(Date.now() - 86400000); + const yesterday = yesterdayDate.getFullYear() + '-' + + String(yesterdayDate.getMonth() + 1).padStart(2, '0') + '-' + + String(yesterdayDate.getDate()).padStart(2, '0'); // Check if there's activity today or yesterday if (sessions[0].date !== today && sessions[0].date !== yesterday) { diff --git a/src/renderer/app-details.html b/src/renderer/app-details.html index c9747c2..57769a9 100644 --- a/src/renderer/app-details.html +++ b/src/renderer/app-details.html @@ -9,7 +9,9 @@ + + @@ -79,50 +81,56 @@ - -
Overview
-
-
-
⏱️
-
-
Total Time
-
0h
+ +
+
+
+ + + + +
-
-
-
📊
-
-
This Week
-
0h
+ +
+ Custom: + + to +
-
-
-
🔥
-
-
Current Streak
-
0 days
+ +
+ 30 days of data available
-
-
📅
-
-
Total Sessions
-
0
-
+
+ + +
Overview
+
+
+
Total Time
+
0h
-
-
📆
-
-
First Used
-
-
-
+
+
This Week
+
0h
-
-
📈
-
-
Peak Day
-
0h
-
+
+
Current Streak
+
0h
+
+
+
Total Sessions
+
0h
+
+
+
First Used
+
0h
+
+
+
Peak Day
+
0h
diff --git a/src/renderer/js/app-details/app-details.js b/src/renderer/js/app-details/app-details.js index b9dda52..e2e62fd 100644 --- a/src/renderer/js/app-details/app-details.js +++ b/src/renderer/js/app-details/app-details.js @@ -46,19 +46,32 @@ async function loadAppDetails() { style="width: 120px; height: 120px; object-fit: contain;">`; } - // Update quick stats - document.querySelectorAll('.quick-stat-value')[0].textContent = formatTime(details.stats.totalTime); - document.querySelectorAll('.quick-stat-value')[1].textContent = formatTime(details.stats.thisWeek); - document.querySelectorAll('.quick-stat-value')[2].textContent = `${details.stats.streak} days`; - document.querySelectorAll('.quick-stat-value')[3].textContent = details.stats.sessionCount; - document.querySelectorAll('.quick-stat-value')[4].textContent = - details.stats.firstUsed ? new Date(details.stats.firstUsed).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'Unknown'; - - // Find peak day from weekly usage - const peakDay = details.weeklyUsage.reduce((max, day) => - day.total_duration > (max?.total_duration || 0) ? day : max, null); - document.querySelectorAll('.quick-stat-value')[5].textContent = - peakDay ? formatTime(peakDay.total_duration) : '0m'; + // Get all stat elements once + const statValues = document.querySelectorAll('.stat-value'); + const { stats, weeklyUsage } = details; + + if (statValues.length === 6){ + // Update quick stats + statValues[0].textContent = formatTime(stats.totalTime); + statValues[1].textContent = formatTime(stats.thisWeek); + statValues[2].textContent = `${stats.streak} days`; + statValues[3].textContent = stats.sessionCount; + statValues[4].textContent = stats.firstUsed + ? new Date(stats.firstUsed).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }) + : 'Unknown'; + + // Find and update peak day + const peakDay = weeklyUsage.reduce( + (max, day) => (day.total_duration > (max?.total_duration || 0) ? day : max), + null + ); + statValues[5].textContent = peakDay ? formatTime(peakDay.total_duration) : '0m'; + } + // Update usage chart updateUsageChart(details, currentChartPeriod); @@ -244,7 +257,11 @@ function updateMonthlyCalendar(details) { for (let i = 29; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); - days.push(date.toISOString().split('T')[0]); + // Format date in local time (YYYY-MM-DD) to match SQL 'localtime' + const dateStr = date.getFullYear() + '-' + + String(date.getMonth() + 1).padStart(2, '0') + '-' + + String(date.getDate()).padStart(2, '0'); + days.push(dateStr); } // Build HTML array diff --git a/src/renderer/js/app-details/usage-over-time-chart.js b/src/renderer/js/app-details/usage-over-time-chart.js index b8c1165..2abccf8 100644 --- a/src/renderer/js/app-details/usage-over-time-chart.js +++ b/src/renderer/js/app-details/usage-over-time-chart.js @@ -1,5 +1,3 @@ -let resizeTimeout; - function setupChartTabs(details) { const tabs = document.querySelectorAll('.chart-tab'); tabs.forEach(tab => { @@ -13,14 +11,6 @@ function setupChartTabs(details) { updateUsageChart(details, currentChartPeriod); }); }); - - // Add resize listener to redraw chart on window resize - window.addEventListener('resize', () => { - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(() => { - updateUsageChart(details, currentChartPeriod); - }, 100); - }); } function updateUsageChart(details, period) { @@ -62,7 +52,10 @@ function getLast7DaysData(weeklyData) { for (let i = 6; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); - const dateStr = date.toISOString().split('T')[0]; + // Format date in local time (YYYY-MM-DD) to match SQL 'localtime' + const dateStr = date.getFullYear() + '-' + + String(date.getMonth() + 1).padStart(2, '0') + '-' + + String(date.getDate()).padStart(2, '0'); const dayData = weeklyData.find(d => d.date === dateStr); last7Days.push({ date: dateStr, @@ -84,14 +77,21 @@ function getLast12WeeksData(details) { let weekDuration = 0; if (details.monthlyUsage) { for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { - const dateStr = d.toISOString().split('T')[0]; + // Format date in local time (YYYY-MM-DD) to match SQL 'localtime' + const dateStr = d.getFullYear() + '-' + + String(d.getMonth() + 1).padStart(2, '0') + '-' + + String(d.getDate()).padStart(2, '0'); const dayData = details.monthlyUsage.find(m => m.date === dateStr); if (dayData) weekDuration += dayData.total_duration; } } + // Format end date in local time + const endDateStr = endDate.getFullYear() + '-' + + String(endDate.getMonth() + 1).padStart(2, '0') + '-' + + String(endDate.getDate()).padStart(2, '0'); weeks.push({ - date: endDate.toISOString().split('T')[0], + date: endDateStr, duration: weekDuration }); } @@ -112,15 +112,21 @@ function getLast12MonthsData(details) { let monthDuration = 0; if (details.monthlyUsage) { details.monthlyUsage.forEach(dayData => { - const dayDate = new Date(dayData.date); + // Parse the date string (YYYY-MM-DD) correctly in local time + const dateParts = dayData.date.split('-'); + const dayDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2])); if (dayDate.getFullYear() === year && dayDate.getMonth() === month) { monthDuration += dayData.total_duration; } }); } + // Format date in local time (YYYY-MM-DD) + const dateStr = date.getFullYear() + '-' + + String(date.getMonth() + 1).padStart(2, '0') + '-' + + String(date.getDate()).padStart(2, '0'); months.push({ - date: date.toISOString().split('T')[0], + date: dateStr, duration: monthDuration }); } @@ -140,11 +146,9 @@ function drawChart(chartData, labelFormat) { const ctx = canvas.getContext('2d'); - // Set canvas size to match container - const container = canvas.parentElement; - const containerWidth = container.clientWidth; - canvas.width = containerWidth; - canvas.height = containerWidth * 0.4; // Maintain 2.5:1 aspect ratio + // Use fixed dimensions like analytics chart for consistent appearance + canvas.width = 800; + canvas.height = 350; // Prepare data points const dataPoints = chartData.map(item => ({ @@ -158,15 +162,15 @@ function drawChart(chartData, labelFormat) { // Canvas dimensions const width = canvas.width; const height = canvas.height; - const padding = { top: 30, right: 20, bottom: 40, left: 50 }; + const padding = { top: 30, right: 20, bottom: 40, left: 70 }; const chartWidth = width - padding.left - padding.right; const chartHeight = height - padding.top - padding.bottom; // Clear canvas ctx.clearRect(0, 0, width, height); - // Draw background - ctx.fillStyle = '#0a1e2f'; + // Draw background (match analytics chart color) + ctx.fillStyle = '#16202d'; ctx.fillRect(0, 0, width, height); // Draw grid lines @@ -222,23 +226,23 @@ function drawChart(chartData, labelFormat) { points.forEach(point => ctx.lineTo(point.x, point.y)); ctx.stroke(); - // Draw points and hover areas + // Draw points (match analytics chart sizes) points.forEach((point, index) => { // Draw point ctx.fillStyle = '#66c0f4'; ctx.beginPath(); - ctx.arc(point.x, point.y, 5, 0, Math.PI * 2); + ctx.arc(point.x, point.y, 7, 0, Math.PI * 2); ctx.fill(); // Draw inner dot - ctx.fillStyle = '#0a1e2f'; + ctx.fillStyle = '#16202d'; ctx.beginPath(); - ctx.arc(point.x, point.y, 2, 0, Math.PI * 2); + ctx.arc(point.x, point.y, 3, 0, Math.PI * 2); ctx.fill(); // Draw X-axis label ctx.fillStyle = '#8f98a0'; - ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto'; + ctx.font = '11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText(point.label, point.x, height - padding.bottom + 10); @@ -275,7 +279,7 @@ function drawChart(chartData, labelFormat) { ctx.clearRect(0, 0, width, height); // Draw background - ctx.fillStyle = '#0a1e2f'; + ctx.fillStyle = '#16202d'; ctx.fillRect(0, 0, width, height); // Draw grid lines @@ -331,10 +335,10 @@ function drawChart(chartData, labelFormat) { points.forEach(point => ctx.lineTo(point.x, point.y)); ctx.stroke(); - // Draw points + // Draw points (match analytics chart) points.forEach((point) => { const isHovered = point === closestPoint; - const radius = isHovered ? 7 : 5; + const radius = isHovered ? 9 : 7; // Outer circle ctx.fillStyle = isHovered ? '#ffffff' : '#66c0f4'; @@ -343,14 +347,14 @@ function drawChart(chartData, labelFormat) { ctx.fill(); // Inner dot - ctx.fillStyle = isHovered ? '#66c0f4' : '#0a1e2f'; + ctx.fillStyle = isHovered ? '#66c0f4' : '#16202d'; ctx.beginPath(); - ctx.arc(point.x, point.y, isHovered ? 3 : 2, 0, Math.PI * 2); + ctx.arc(point.x, point.y, isHovered ? 4 : 3, 0, Math.PI * 2); ctx.fill(); // Draw X-axis label (highlight if hovered) ctx.fillStyle = isHovered ? '#ffffff' : '#8f98a0'; - ctx.font = isHovered ? 'bold 13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto' : '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto'; + ctx.font = isHovered ? 'bold 12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto' : '11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText(point.label, point.x, height - padding.bottom + 10); diff --git a/src/renderer/styles/analytics/analytics.css b/src/renderer/styles/analytics/analytics.css index 82f83f2..360a003 100644 --- a/src/renderer/styles/analytics/analytics.css +++ b/src/renderer/styles/analytics/analytics.css @@ -8,7 +8,6 @@ } .analytics-header { - background: rgba(27, 40, 56, 0.95); backdrop-filter: blur(8px); box-shadow: 0 1px 0 rgba(102, 192, 244, 0.3); margin-bottom: 20px; diff --git a/src/renderer/styles/app-details/app-details.css b/src/renderer/styles/app-details/app-details.css index a9a3ab0..fee72a0 100644 --- a/src/renderer/styles/app-details/app-details.css +++ b/src/renderer/styles/app-details/app-details.css @@ -3,15 +3,16 @@ /* Override body for app-details (different background) */ body { - background: #171d25; + background: linear-gradient(to bottom, #1b2838 0%, #171d25 100%); + background-attachment: fixed; padding: 0px; } .details-container { - max-width: 100%; - margin: 0; + max-width: 1400px; + margin: 0 auto; padding: 10px 20px 20px 20px; - background: linear-gradient(to bottom, #1b2838 0%, #171d25 100%); + background: transparent; } /* Hero Section */ @@ -23,6 +24,10 @@ body { overflow: hidden; margin-bottom: 16px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + /* Break out of container to span full width */ + margin-left: calc(-50vw + 50%); + margin-right: calc(-50vw + 50%); + width: 100vw; } .app-hero-bg { @@ -239,3 +244,13 @@ body { .export-btn span { font-size: 14px; } + +.app-details-header { + backdrop-filter: blur(8px); + box-shadow: 0 1px 0 rgba(102, 192, 244, 0.3); + margin-bottom: 20px; + position: sticky; + top: 0; + z-index: 100; +} + diff --git a/src/renderer/styles/app-details/chart.css b/src/renderer/styles/app-details/chart.css index 575282b..6c2f9ac 100644 --- a/src/renderer/styles/app-details/chart.css +++ b/src/renderer/styles/app-details/chart.css @@ -28,17 +28,9 @@ border-color: rgba(102, 192, 244, 0.3); } -/* Usage Chart */ -.usage-chart-container { - padding: 10px 0; - display: flex; - justify-content: center; - align-items: center; -} - #usage-line-chart { display: block; - width: 100%; + max-width: 100%; height: auto; } @@ -79,3 +71,44 @@ font-weight: 500; white-space: nowrap; } + .chart-card { + background: transparent; + border: none; + padding: 0; + } + + .chart-card .section-header { + margin-bottom: 16px; + } + + .daily-usage-chart { + height: 350px; + position: relative; + padding: 20px 10px 30px 10px; + } + + .charts-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 10px; + margin-bottom: 20px; + } + + /* Responsive layout for charts */ + @media (max-width: 1400px) { + .charts-grid { + grid-template-columns: 1.5fr 1fr; + } + } + + @media (max-width: 1100px) { + .charts-grid { + grid-template-columns: 1fr; + } + } + + @media (max-width: 800px) { + .charts-grid { + grid-template-columns: 1fr; + } + } \ No newline at end of file diff --git a/src/renderer/styles/category-insights/category-insights.css b/src/renderer/styles/category-insights/category-insights.css index 9cfb418..5844634 100644 --- a/src/renderer/styles/category-insights/category-insights.css +++ b/src/renderer/styles/category-insights/category-insights.css @@ -16,7 +16,6 @@ body { /* Category Header with Date Controls */ .category-header { - background: rgba(27, 40, 56, 0.95); backdrop-filter: blur(8px); box-shadow: 0 1px 0 rgba(102, 192, 244, 0.3); margin-bottom: 20px; diff --git a/src/renderer/styles/goals/goals.css b/src/renderer/styles/goals/goals.css index cc0acb1..b79ae79 100644 --- a/src/renderer/styles/goals/goals.css +++ b/src/renderer/styles/goals/goals.css @@ -13,7 +13,6 @@ /* Header */ .goals-header { - background: rgba(27, 40, 56, 0.95); backdrop-filter: blur(8px); box-shadow: 0 1px 0 rgba(102, 192, 244, 0.3); margin-bottom: 20px; diff --git a/src/renderer/styles/index-page/home-toolbar.css b/src/renderer/styles/index-page/home-toolbar.css index d3ec20f..5fe6d1e 100644 --- a/src/renderer/styles/index-page/home-toolbar.css +++ b/src/renderer/styles/index-page/home-toolbar.css @@ -5,7 +5,6 @@ justify-content: space-between; gap: 20px; padding: 16px 20px; - background: linear-gradient(135deg, #1e2328 0%, #1b2838 100%); backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255, 255, 255, 0.1); margin-bottom: 20px; diff --git a/src/renderer/styles/productivity/productivity.css b/src/renderer/styles/productivity/productivity.css index 78bd3d0..eda5afa 100644 --- a/src/renderer/styles/productivity/productivity.css +++ b/src/renderer/styles/productivity/productivity.css @@ -8,7 +8,6 @@ /* Header */ .productivity-header { - background: rgba(27, 40, 56, 0.95); backdrop-filter: blur(8px); box-shadow: 0 1px 0 rgba(102, 192, 244, 0.3); margin-bottom: 20px; From fcd6cc64bcecc87b043ea72603eb777bdb4ba823 Mon Sep 17 00:00:00 2001 From: Sever Manolescu Date: Thu, 16 Oct 2025 21:13:31 +0300 Subject: [PATCH 2/2] Improve the chart, remove the daily, weekly buttons, add functionality to the data range buttons, improve the overall functionality and style --- src/main/ipc/apps.js | 188 +++++++++++++ src/preload/preload.js | 1 + src/renderer/app-details.html | 93 ++++--- src/renderer/js/app-details/app-details.js | 249 +++++++++++++----- src/renderer/js/app-details/listeners.js | 7 + src/renderer/js/app-details/usage-insights.js | 40 +++ .../js/app-details/usage-over-time-chart.js | 249 ++++++++---------- src/renderer/js/index-page/details.js | 27 ++ src/renderer/js/index-page/ui-components.js | 8 +- .../styles/app-details/app-details.css | 66 ++++- src/renderer/styles/app-details/chart.css | 10 +- .../styles/app-details/usage-trend.css | 63 +++-- 12 files changed, 714 insertions(+), 287 deletions(-) diff --git a/src/main/ipc/apps.js b/src/main/ipc/apps.js index d999aab..dfd6e23 100644 --- a/src/main/ipc/apps.js +++ b/src/main/ipc/apps.js @@ -262,6 +262,194 @@ function initializeAppHandlers() { } }); + ipcMain.handle('get-app-details-by-date-range', async (event, appId, startDate, endDate) => { + const db = getDb(); + + try { + // Get app info + const app = db.prepare(` + SELECT * FROM apps WHERE id = ? + `).get([appId]); + + if (!app) { + throw new Error('App not found'); + } + + // Convert date strings to timestamps + const startTimestamp = new Date(startDate).setHours(0, 0, 0, 0); + const endTimestamp = new Date(endDate).setHours(23, 59, 59, 999); + + // Get total sessions count for the date range + const sessionCount = db.prepare(` + SELECT COUNT(*) as count FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + `).get([appId, startTimestamp, endTimestamp]); + + // Get usage data for the date range (this replaces weeklyUsage) + const weeklyUsage = db.prepare(` + SELECT + DATE(start_time / 1000, 'unixepoch', 'localtime') as date, + SUM(duration) as total_duration + FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + GROUP BY date + ORDER BY date ASC + `).all([appId, startTimestamp, endTimestamp]); + + // Get this week's total time (last 7 days from end date) + const sevenDaysBeforeEnd = new Date(endDate); + sevenDaysBeforeEnd.setDate(sevenDaysBeforeEnd.getDate() - 6); + const sevenDaysBeforeEndTimestamp = sevenDaysBeforeEnd.setHours(0, 0, 0, 0); + + const thisWeek = db.prepare(` + SELECT SUM(duration) as total FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + `).get([appId, sevenDaysBeforeEndTimestamp, endTimestamp]); + + // Get longest session in date range + const longestSession = db.prepare(` + SELECT MAX(duration) as longest FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + `).get([appId, startTimestamp, endTimestamp]); + + // Get average session in date range + const avgSession = db.prepare(` + SELECT AVG(duration) as average FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + `).get([appId, startTimestamp, endTimestamp]); + + // Get current streak (consecutive days) + const streak = calculateStreak(db, appId); + + // Get recent sessions (last 10 in date range) + const recentSessions = db.prepare(` + SELECT * FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + ORDER BY start_time DESC + LIMIT 10 + `).all([appId, startTimestamp, endTimestamp]); + + // Get today's activity by hour (or end date's activity) + const todayDate = new Date(endDate); + todayDate.setHours(0, 0, 0, 0); + const todayStart = todayDate.getTime(); + const todayEnd = new Date(endDate).setHours(23, 59, 59, 999); + + const todayActivity = db.prepare(` + SELECT + CAST(strftime('%H', start_time / 1000, 'unixepoch', 'localtime') AS INTEGER) as hour, + SUM(duration) as total_duration + FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + GROUP BY hour + ORDER BY hour + `).all([appId, todayStart, todayEnd]); + + // Get monthly usage (30 days worth within the date range) + const monthlyUsage = db.prepare(` + SELECT + DATE(start_time / 1000, 'unixepoch', 'localtime') as date, + SUM(duration) as total_duration, + COUNT(*) as session_count + FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + GROUP BY date + ORDER BY date ASC + `).all([appId, startTimestamp, endTimestamp]); + + // Get usage by day of week (filtered by date range) + const dayOfWeekUsage = db.prepare(` + SELECT + CAST(strftime('%w', start_time / 1000, 'unixepoch', 'localtime') AS INTEGER) as day_of_week, + SUM(duration) as total_duration, + COUNT(*) as session_count + FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + GROUP BY day_of_week + ORDER BY day_of_week + `).all([appId, startTimestamp, endTimestamp]); + + // Get session duration distribution (filtered by date range) + const sessionDurations = db.prepare(` + SELECT duration FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + `).all([appId, startTimestamp, endTimestamp]); + + // Get all sessions for heatmap (within date range, max 90 days) + const heatmapData = db.prepare(` + SELECT + CAST(strftime('%w', start_time / 1000, 'unixepoch', 'localtime') AS INTEGER) as day_of_week, + CAST(strftime('%H', start_time / 1000, 'unixepoch', 'localtime') AS INTEGER) as hour, + SUM(duration) as total_duration + FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time <= ? + GROUP BY day_of_week, hour + `).all([appId, startTimestamp, endTimestamp]); + + // Get category ranking + const categoryRanking = db.prepare(` + SELECT id, name, total_time + FROM apps + WHERE category = ? AND hidden = 0 + ORDER BY total_time DESC + `).all([app.category]); + + const appRankInCategory = categoryRanking.findIndex(a => a.id === appId) + 1; + + // Get total time for all apps + const totalAllApps = db.prepare(` + SELECT SUM(total_time) as total FROM apps WHERE hidden = 0 + `).get(); + + const usagePercentage = totalAllApps?.total > 0 + ? (app.total_time / totalAllApps.total) * 100 + : 0; + + // Get last week's time for comparison (7 days before the selected range) + const fourteenDaysBeforeEnd = new Date(endDate); + fourteenDaysBeforeEnd.setDate(fourteenDaysBeforeEnd.getDate() - 13); + const fourteenDaysBeforeEndTimestamp = fourteenDaysBeforeEnd.setHours(0, 0, 0, 0); + + const lastWeek = db.prepare(` + SELECT SUM(duration) as total FROM sessions + WHERE app_id = ? AND start_time >= ? AND start_time < ? + `).get([appId, fourteenDaysBeforeEndTimestamp, sevenDaysBeforeEndTimestamp]); + + // Get streak history (all streaks) + const streakHistory = calculateStreakHistory(db, appId); + + const result = { + app, + stats: { + totalTime: app.total_time || 0, + thisWeek: thisWeek?.total || 0, + lastWeek: lastWeek?.total || 0, + sessionCount: sessionCount?.count || 0, + streak: streak, + longestSession: longestSession?.longest || 0, + avgSession: avgSession?.average || 0, + firstUsed: app.first_used, + categoryRank: appRankInCategory, + totalInCategory: categoryRanking.length, + usagePercentage: usagePercentage + }, + weeklyUsage, + monthlyUsage, + dayOfWeekUsage, + sessionDurations: sessionDurations ? sessionDurations.map(s => s.duration) : [], + heatmapData, + streakHistory, + recentSessions, + todayActivity + }; + + return result; + } catch (error) { + console.error('Error fetching app details by date range:', error); + throw error; + } + }); + ipcMain.handle('get-app-details', async (event, appId) => { const db = getDb(); diff --git a/src/preload/preload.js b/src/preload/preload.js index 72ffb32..7e2ae01 100644 --- a/src/preload/preload.js +++ b/src/preload/preload.js @@ -48,6 +48,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getTodayStats: () => ipcRenderer.invoke('get-today-stats'), getAppDetails: (appId) => ipcRenderer.invoke('get-app-details', appId), + getAppDetailsByDateRange: (appId, startDate, endDate) => ipcRenderer.invoke('get-app-details-by-date-range', appId, startDate, endDate), getAppById: (appId) => ipcRenderer.invoke('get-app-by-id', appId), getAnalyticsData: (startDate, endDate) => ipcRenderer.invoke('get-analytics-data', startDate, endDate), diff --git a/src/renderer/app-details.html b/src/renderer/app-details.html index 57769a9..c38b7b1 100644 --- a/src/renderer/app-details.html +++ b/src/renderer/app-details.html @@ -106,7 +106,7 @@
-
Overview
+

Overview

Total Time
@@ -143,45 +143,57 @@
-
-
-
-
Usage Over Time
-
- - - -
-
+
+
+

Usage Over Time

- +
-
-
Usage Trends
-