GameSentenceMiner 2.18.15__py3-none-any.whl → 2.18.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. GameSentenceMiner/anki.py +8 -53
  2. GameSentenceMiner/owocr/owocr/ocr.py +3 -2
  3. GameSentenceMiner/owocr/owocr/run.py +5 -1
  4. GameSentenceMiner/ui/anki_confirmation.py +16 -2
  5. GameSentenceMiner/util/configuration.py +6 -9
  6. GameSentenceMiner/util/db.py +11 -7
  7. GameSentenceMiner/util/games_table.py +320 -0
  8. GameSentenceMiner/web/anki_api_endpoints.py +506 -0
  9. GameSentenceMiner/web/database_api.py +239 -117
  10. GameSentenceMiner/web/static/css/loading-skeleton.css +41 -0
  11. GameSentenceMiner/web/static/css/search.css +54 -0
  12. GameSentenceMiner/web/static/css/stats.css +76 -0
  13. GameSentenceMiner/web/static/js/anki_stats.js +304 -50
  14. GameSentenceMiner/web/static/js/database.js +44 -7
  15. GameSentenceMiner/web/static/js/heatmap.js +326 -0
  16. GameSentenceMiner/web/static/js/overview.js +20 -224
  17. GameSentenceMiner/web/static/js/search.js +190 -23
  18. GameSentenceMiner/web/static/js/stats.js +371 -1
  19. GameSentenceMiner/web/stats.py +188 -0
  20. GameSentenceMiner/web/templates/anki_stats.html +145 -58
  21. GameSentenceMiner/web/templates/components/date-range.html +19 -0
  22. GameSentenceMiner/web/templates/components/html-head.html +45 -0
  23. GameSentenceMiner/web/templates/components/js-config.html +37 -0
  24. GameSentenceMiner/web/templates/components/popups.html +15 -0
  25. GameSentenceMiner/web/templates/components/settings-modal.html +233 -0
  26. GameSentenceMiner/web/templates/database.html +13 -3
  27. GameSentenceMiner/web/templates/goals.html +9 -31
  28. GameSentenceMiner/web/templates/overview.html +16 -223
  29. GameSentenceMiner/web/templates/search.html +46 -0
  30. GameSentenceMiner/web/templates/stats.html +49 -311
  31. GameSentenceMiner/web/texthooking_page.py +4 -66
  32. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/METADATA +1 -1
  33. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/RECORD +37 -28
  34. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/WHEEL +0 -0
  35. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/entry_points.txt +0 -0
  36. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/licenses/LICENSE +0 -0
  37. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Shared Heatmap Component for GSM
3
+ * Provides reusable GitHub-style heatmap visualization
4
+ */
5
+
6
+ class HeatmapRenderer {
7
+ constructor(options = {}) {
8
+ this.containerId = options.containerId || 'heatmapContainer';
9
+ this.metricName = options.metricName || 'characters';
10
+ this.metricLabel = options.metricLabel || 'characters';
11
+ this.calculateStreaks = options.calculateStreaks || this.defaultCalculateStreaks.bind(this);
12
+ }
13
+
14
+ /**
15
+ * Helper function to get week number of year (GitHub style - week starts on Sunday)
16
+ */
17
+ getWeekOfYear(date) {
18
+ const yearStart = new Date(date.getFullYear(), 0, 1);
19
+ const dayOfYear = Math.floor((date - yearStart) / (24 * 60 * 60 * 1000)) + 1;
20
+ const dayOfWeek = yearStart.getDay(); // 0 = Sunday
21
+
22
+ // Calculate week number (1-indexed)
23
+ const weekNum = Math.ceil((dayOfYear + dayOfWeek) / 7);
24
+ return Math.min(53, weekNum); // Cap at 53 weeks
25
+ }
26
+
27
+ /**
28
+ * Helper function to get the first Sunday of the year (or before)
29
+ */
30
+ getFirstSunday(year) {
31
+ const jan1 = new Date(year, 0, 1);
32
+ const dayOfWeek = jan1.getDay();
33
+ const firstSunday = new Date(year, 0, 1 - dayOfWeek);
34
+ return firstSunday;
35
+ }
36
+
37
+ /**
38
+ * Default streak calculation function
39
+ */
40
+ defaultCalculateStreaks(grid, yearData, allLinesForYear) {
41
+ const dates = [];
42
+
43
+ // Collect all dates in chronological order
44
+ for (let week = 0; week < 53; week++) {
45
+ for (let day = 0; day < 7; day++) {
46
+ const date = grid[day][week];
47
+ if (date) {
48
+ const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
49
+ const activity = yearData[dateStr] || 0;
50
+ dates.push({ date: dateStr, activity: activity });
51
+ }
52
+ }
53
+ }
54
+
55
+ // Sort dates chronologically
56
+ dates.sort((a, b) => new Date(a.date) - new Date(b.date));
57
+
58
+ let longestStreak = 0;
59
+ let tempStreak = 0;
60
+
61
+ // Calculate longest streak
62
+ for (let i = 0; i < dates.length; i++) {
63
+ if (dates[i].activity > 0) {
64
+ tempStreak++;
65
+ longestStreak = Math.max(longestStreak, tempStreak);
66
+ } else {
67
+ tempStreak = 0;
68
+ }
69
+ }
70
+
71
+ // Calculate current streak from today backwards
72
+ const date = new Date();
73
+ const today = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
74
+ const streakRequirement = window.statsConfig ? window.statsConfig.streakRequirementHours : 1.0;
75
+
76
+ // Find today's index or the most recent date before today
77
+ let todayIndex = -1;
78
+ for (let i = dates.length - 1; i >= 0; i--) {
79
+ if (dates[i].date <= today) {
80
+ todayIndex = i;
81
+ break;
82
+ }
83
+ }
84
+
85
+ // Count backwards from today (or most recent date)
86
+ let currentStreak = 0;
87
+ if (todayIndex >= 0) {
88
+ for (let i = todayIndex; i >= 0; i--) {
89
+ if (dates[i].activity >= streakRequirement) {
90
+ currentStreak++;
91
+ } else {
92
+ break;
93
+ }
94
+ }
95
+ }
96
+
97
+ // Calculate average metric (e.g., avg daily characters or avg daily mining)
98
+ let totalActivity = 0;
99
+ let activeDays = 0;
100
+ for (let i = 0; i < dates.length; i++) {
101
+ if (dates[i].activity > 0) {
102
+ totalActivity += dates[i].activity;
103
+ activeDays++;
104
+ }
105
+ }
106
+ const avgDaily = activeDays > 0 ? Math.round(totalActivity / activeDays) : 0;
107
+
108
+ return { longestStreak, currentStreak, avgDaily };
109
+ }
110
+
111
+ /**
112
+ * Create GitHub-style heatmap visualization
113
+ * @param {Object} heatmapData - Object with year keys containing date->value mappings
114
+ * @param {Array} allLinesData - Optional array of all line data for detailed calculations
115
+ */
116
+ render(heatmapData, allLinesData = []) {
117
+ const container = document.getElementById(this.containerId);
118
+ if (!container) {
119
+ console.error(`Heatmap container #${this.containerId} not found`);
120
+ return;
121
+ }
122
+
123
+ container.innerHTML = ''; // Clear existing content
124
+
125
+ if (!heatmapData || Object.keys(heatmapData).length === 0) {
126
+ container.innerHTML = `<p style="text-align: center; color: var(--text-tertiary); padding: 20px;">No ${this.metricLabel} data available for the selected date range.</p>`;
127
+ return;
128
+ }
129
+
130
+ Object.keys(heatmapData).sort().forEach(year => {
131
+ const yearData = heatmapData[year];
132
+ const yearDiv = document.createElement('div');
133
+ yearDiv.className = 'heatmap-year';
134
+
135
+ const yearTitle = document.createElement('h3');
136
+ yearTitle.textContent = year;
137
+ yearDiv.appendChild(yearTitle);
138
+
139
+ // Find maximum activity value for this year to scale colors
140
+ const maxActivity = Math.max(...Object.values(yearData));
141
+
142
+ // Create main wrapper to center everything
143
+ const mainWrapper = document.createElement('div');
144
+ mainWrapper.className = 'heatmap-wrapper';
145
+
146
+ // Create container wrapper for labels and grid
147
+ const containerWrapper = document.createElement('div');
148
+ containerWrapper.className = 'heatmap-container-wrapper';
149
+
150
+ // Create day labels (S, M, T, W, T, F, S)
151
+ const dayLabels = document.createElement('div');
152
+ dayLabels.className = 'heatmap-day-labels';
153
+ const dayNames = ['S', '', 'M', '', 'W', '', 'F']; // Only show some labels for space
154
+ dayNames.forEach(dayName => {
155
+ const dayLabel = document.createElement('div');
156
+ dayLabel.className = 'heatmap-day-label';
157
+ dayLabel.textContent = dayName;
158
+ dayLabels.appendChild(dayLabel);
159
+ });
160
+
161
+ // Create grid container
162
+ const gridContainer = document.createElement('div');
163
+
164
+ // Create month labels
165
+ const monthLabels = document.createElement('div');
166
+ monthLabels.className = 'heatmap-month-labels';
167
+
168
+ // Create the main grid
169
+ const gridDiv = document.createElement('div');
170
+ gridDiv.className = 'heatmap-grid';
171
+
172
+ // Initialize 7x53 grid with empty cells
173
+ const grid = Array(7).fill(null).map(() => Array(53).fill(null));
174
+
175
+ // Get the first Sunday of the year (start of week 1)
176
+ const firstSunday = this.getFirstSunday(parseInt(year));
177
+
178
+ // Populate grid with dates for the entire year
179
+ for (let week = 0; week < 53; week++) {
180
+ for (let day = 0; day < 7; day++) {
181
+ const currentDate = new Date(firstSunday);
182
+ currentDate.setDate(firstSunday.getDate() + (week * 7) + day);
183
+
184
+ // Only include dates that belong to the current year
185
+ if (currentDate.getFullYear() === parseInt(year)) {
186
+ grid[day][week] = currentDate;
187
+ }
188
+ }
189
+ }
190
+
191
+ // Create month labels based on grid positions
192
+ const monthTracker = new Set();
193
+ for (let week = 0; week < 53; week++) {
194
+ const dateInWeek = grid[0][week] || grid[1][week] || grid[2][week] ||
195
+ grid[3][week] || grid[4][week] || grid[5][week] || grid[6][week];
196
+
197
+ if (dateInWeek) {
198
+ const month = dateInWeek.getMonth();
199
+ const monthName = dateInWeek.toLocaleDateString('en', { month: 'short' });
200
+
201
+ // Add month label if it's the first week of the month
202
+ if (!monthTracker.has(month) && dateInWeek.getDate() <= 7) {
203
+ const monthLabel = document.createElement('div');
204
+ monthLabel.className = 'heatmap-month-label';
205
+ monthLabel.style.gridColumn = `${week + 1}`;
206
+ monthLabel.textContent = monthName;
207
+ monthLabels.appendChild(monthLabel);
208
+ monthTracker.add(month);
209
+ }
210
+ }
211
+ }
212
+
213
+ // Create cells for the grid
214
+ for (let day = 0; day < 7; day++) {
215
+ for (let week = 0; week < 53; week++) {
216
+ const cell = document.createElement('div');
217
+ cell.className = 'heatmap-cell';
218
+
219
+ const date = grid[day][week];
220
+ if (date) {
221
+ const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
222
+ const activity = yearData[dateStr] || 0;
223
+
224
+ if (activity > 0 && maxActivity > 0) {
225
+ // Calculate percentage of maximum activity
226
+ const percentage = (activity / maxActivity) * 100;
227
+
228
+ // Assign discrete color levels based on percentage thresholds
229
+ let colorLevel;
230
+ if (percentage <= 25) {
231
+ colorLevel = 1; // Light green
232
+ } else if (percentage <= 50) {
233
+ colorLevel = 2; // Medium green
234
+ } else if (percentage <= 75) {
235
+ colorLevel = 3; // Dark green
236
+ } else {
237
+ colorLevel = 4; // Darkest green
238
+ }
239
+
240
+ // Define discrete colors for each level
241
+ const colors = {
242
+ 1: '#c6e48b', // Light green (1-25%)
243
+ 2: '#7bc96f', // Medium green (26-50%)
244
+ 3: '#239a3b', // Dark green (51-75%)
245
+ 4: '#196127' // Darkest green (76-100%)
246
+ };
247
+
248
+ cell.style.backgroundColor = colors[colorLevel];
249
+ }
250
+
251
+ // Format tooltip based on metric type
252
+ const activityLabel = this.metricName === 'sentences'
253
+ ? `sentence${activity !== 1 ? 's' : ''} mined`
254
+ : `${this.metricLabel}`;
255
+ cell.title = `${dateStr}: ${activity} ${activityLabel}`;
256
+ } else {
257
+ // Empty cell for dates outside the year
258
+ cell.style.backgroundColor = 'transparent';
259
+ cell.style.cursor = 'default';
260
+ }
261
+
262
+ gridDiv.appendChild(cell);
263
+ }
264
+ }
265
+
266
+ gridContainer.appendChild(monthLabels);
267
+ gridContainer.appendChild(gridDiv);
268
+ containerWrapper.appendChild(dayLabels);
269
+ containerWrapper.appendChild(gridContainer);
270
+ mainWrapper.appendChild(containerWrapper);
271
+
272
+ // Filter allLinesData for this specific year
273
+ const yearLines = allLinesData ? allLinesData.filter(line => {
274
+ if (!line.timestamp) return false;
275
+ const lineYear = new Date(parseFloat(line.timestamp) * 1000).getFullYear();
276
+ return lineYear === parseInt(year);
277
+ }) : [];
278
+
279
+ // Calculate and display streaks with year-specific data
280
+ const streaks = this.calculateStreaks(grid, yearData, yearLines);
281
+ const streaksDiv = document.createElement('div');
282
+ streaksDiv.className = 'heatmap-streaks';
283
+
284
+ // Format the third metric label based on type
285
+ const thirdMetricLabel = this.metricName === 'sentences' ? 'Avg Daily Mining' : 'Avg Daily Time';
286
+
287
+ streaksDiv.innerHTML = `
288
+ <div class="heatmap-streak-item">
289
+ <div class="heatmap-streak-number">${streaks.longestStreak}</div>
290
+ <div class="heatmap-streak-label">Longest Streak</div>
291
+ </div>
292
+ <div class="heatmap-streak-item">
293
+ <div class="heatmap-streak-number">${streaks.currentStreak}</div>
294
+ <div class="heatmap-streak-label">Current Streak</div>
295
+ </div>
296
+ <div class="heatmap-streak-item">
297
+ <div class="heatmap-streak-number">${streaks.avgDaily}</div>
298
+ <div class="heatmap-streak-label">${thirdMetricLabel}</div>
299
+ </div>
300
+ `;
301
+ mainWrapper.appendChild(streaksDiv);
302
+ yearDiv.appendChild(mainWrapper);
303
+
304
+ // Add legend with discrete colors
305
+ const legend = document.createElement('div');
306
+ legend.className = 'heatmap-legend';
307
+ legend.innerHTML = `
308
+ <span>Less</span>
309
+ <div class="heatmap-legend-item" style="background-color: #ebedf0;" title="No activity"></div>
310
+ <div class="heatmap-legend-item" style="background-color: #c6e48b;" title="1-25% of max activity"></div>
311
+ <div class="heatmap-legend-item" style="background-color: #7bc96f;" title="26-50% of max activity"></div>
312
+ <div class="heatmap-legend-item" style="background-color: #239a3b;" title="51-75% of max activity"></div>
313
+ <div class="heatmap-legend-item" style="background-color: #196127;" title="76-100% of max activity"></div>
314
+ <span>More</span>
315
+ `;
316
+ yearDiv.appendChild(legend);
317
+
318
+ container.appendChild(yearDiv);
319
+ });
320
+ }
321
+ }
322
+
323
+ // Export for use in other scripts
324
+ if (typeof window !== 'undefined') {
325
+ window.HeatmapRenderer = HeatmapRenderer;
326
+ }
@@ -22,32 +22,8 @@ function getThemeTextColor() {
22
22
 
23
23
  document.addEventListener('DOMContentLoaded', function () {
24
24
 
25
- // Helper function to get week number of year (GitHub style - week starts on Sunday)
26
- function getWeekOfYear(date) {
27
- const yearStart = new Date(date.getFullYear(), 0, 1);
28
- const dayOfYear = Math.floor((date - yearStart) / (24 * 60 * 60 * 1000)) + 1;
29
- const dayOfWeek = yearStart.getDay(); // 0 = Sunday
30
-
31
- // Calculate week number (1-indexed)
32
- const weekNum = Math.ceil((dayOfYear + dayOfWeek) / 7);
33
- return Math.min(53, weekNum); // Cap at 53 weeks
34
- }
35
-
36
- // Helper function to get day of week (0 = Sunday, 6 = Saturday)
37
- function getDayOfWeek(date) {
38
- return date.getDay();
39
- }
40
-
41
- // Helper function to get the first Sunday of the year (or before)
42
- function getFirstSunday(year) {
43
- const jan1 = new Date(year, 0, 1);
44
- const dayOfWeek = jan1.getDay();
45
- const firstSunday = new Date(year, 0, 1 - dayOfWeek);
46
- return firstSunday;
47
- }
48
-
49
- // Function to calculate heatmap streaks and average daily time
50
- function calculateHeatmapStreaks(grid, yearData, allLinesForYear = []) {
25
+ // Custom streak calculation function for activity heatmap (includes average daily time)
26
+ function calculateActivityStreaks(grid, yearData, allLinesForYear = []) {
51
27
  const dates = [];
52
28
 
53
29
  // Collect all dates in chronological order
@@ -160,194 +136,20 @@ document.addEventListener('DOMContentLoaded', function () {
160
136
  }
161
137
  }
162
138
 
163
- return { longestStreak, currentStreak, avgDailyTime };
139
+ return { longestStreak, currentStreak, avgDaily: avgDailyTime };
164
140
  }
165
-
166
- // Function to create GitHub-style heatmap
141
+
142
+ // Initialize heatmap renderer with custom configuration for activity tracking
143
+ const activityHeatmapRenderer = new HeatmapRenderer({
144
+ containerId: 'heatmapContainer',
145
+ metricName: 'characters',
146
+ metricLabel: 'characters',
147
+ calculateStreaks: calculateActivityStreaks
148
+ });
149
+
150
+ // Function to create GitHub-style heatmap using shared component
167
151
  function createHeatmap(heatmapData) {
168
- const container = document.getElementById('heatmapContainer');
169
-
170
- Object.keys(heatmapData).sort().forEach(year => {
171
- const yearData = heatmapData[year];
172
- const yearDiv = document.createElement('div');
173
- yearDiv.className = 'heatmap-year';
174
-
175
- const yearTitle = document.createElement('h3');
176
- yearTitle.textContent = year;
177
- yearDiv.appendChild(yearTitle);
178
-
179
- // Find maximum activity value for this year to scale colors
180
- const maxActivity = Math.max(...Object.values(yearData));
181
-
182
- // Create main wrapper to center everything
183
- const mainWrapper = document.createElement('div');
184
- mainWrapper.className = 'heatmap-wrapper';
185
-
186
- // Create container wrapper for labels and grid
187
- const containerWrapper = document.createElement('div');
188
- containerWrapper.className = 'heatmap-container-wrapper';
189
-
190
- // Create day labels (S, M, T, W, T, F, S)
191
- const dayLabels = document.createElement('div');
192
- dayLabels.className = 'heatmap-day-labels';
193
- const dayNames = ['S', '', 'M', '', 'W', '', 'F']; // Only show some labels for space
194
- dayNames.forEach(dayName => {
195
- const dayLabel = document.createElement('div');
196
- dayLabel.className = 'heatmap-day-label';
197
- dayLabel.textContent = dayName;
198
- dayLabels.appendChild(dayLabel);
199
- });
200
-
201
- // Create grid container
202
- const gridContainer = document.createElement('div');
203
-
204
- // Create month labels
205
- const monthLabels = document.createElement('div');
206
- monthLabels.className = 'heatmap-month-labels';
207
-
208
- // Create the main grid
209
- const gridDiv = document.createElement('div');
210
- gridDiv.className = 'heatmap-grid';
211
-
212
- // Initialize 7x53 grid with empty cells
213
- const grid = Array(7).fill(null).map(() => Array(53).fill(null));
214
-
215
- // Get the first Sunday of the year (start of week 1)
216
- const firstSunday = getFirstSunday(parseInt(year));
217
-
218
- // Populate grid with dates for the entire year
219
- for (let week = 0; week < 53; week++) {
220
- for (let day = 0; day < 7; day++) {
221
- const currentDate = new Date(firstSunday);
222
- currentDate.setDate(firstSunday.getDate() + (week * 7) + day);
223
-
224
- // Only include dates that belong to the current year
225
- if (currentDate.getFullYear() === parseInt(year)) {
226
- grid[day][week] = currentDate;
227
- }
228
- }
229
- }
230
-
231
- // Create month labels based on grid positions
232
- const monthTracker = new Set();
233
- for (let week = 0; week < 53; week++) {
234
- const dateInWeek = grid[0][week] || grid[1][week] || grid[2][week] ||
235
- grid[3][week] || grid[4][week] || grid[5][week] || grid[6][week];
236
-
237
- if (dateInWeek) {
238
- const month = dateInWeek.getMonth();
239
- const monthName = dateInWeek.toLocaleDateString('en', { month: 'short' });
240
-
241
- // Add month label if it's the first week of the month
242
- if (!monthTracker.has(month) && dateInWeek.getDate() <= 7) {
243
- const monthLabel = document.createElement('div');
244
- monthLabel.className = 'heatmap-month-label';
245
- monthLabel.style.gridColumn = `${week + 1}`;
246
- monthLabel.textContent = monthName;
247
- monthLabels.appendChild(monthLabel);
248
- monthTracker.add(month);
249
- }
250
- }
251
- }
252
-
253
- // Create cells for the grid
254
- for (let day = 0; day < 7; day++) {
255
- for (let week = 0; week < 53; week++) {
256
- const cell = document.createElement('div');
257
- cell.className = 'heatmap-cell';
258
-
259
- const date = grid[day][week];
260
- if (date) {
261
- const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
262
- const activity = yearData[dateStr] || 0;
263
-
264
- if (activity > 0 && maxActivity > 0) {
265
- // Calculate percentage of maximum activity
266
- const percentage = (activity / maxActivity) * 100;
267
-
268
- // Assign discrete color levels based on percentage thresholds
269
- let colorLevel;
270
- if (percentage <= 25) {
271
- colorLevel = 1; // Light green
272
- } else if (percentage <= 50) {
273
- colorLevel = 2; // Medium green
274
- } else if (percentage <= 75) {
275
- colorLevel = 3; // Dark green
276
- } else {
277
- colorLevel = 4; // Darkest green
278
- }
279
-
280
- // Define discrete colors for each level
281
- const colors = {
282
- 1: '#c6e48b', // Light green (1-25%)
283
- 2: '#7bc96f', // Medium green (26-50%)
284
- 3: '#239a3b', // Dark green (51-75%)
285
- 4: '#196127' // Darkest green (76-100%)
286
- };
287
-
288
- cell.style.backgroundColor = colors[colorLevel];
289
- }
290
-
291
- cell.title = `${dateStr}: ${activity} characters`;
292
- } else {
293
- // Empty cell for dates outside the year
294
- cell.style.backgroundColor = 'transparent';
295
- cell.style.cursor = 'default';
296
- }
297
-
298
- gridDiv.appendChild(cell);
299
- }
300
- }
301
-
302
- gridContainer.appendChild(monthLabels);
303
- gridContainer.appendChild(gridDiv);
304
- containerWrapper.appendChild(dayLabels);
305
- containerWrapper.appendChild(gridContainer);
306
- mainWrapper.appendChild(containerWrapper);
307
-
308
- // Calculate and display streaks with average daily time
309
- const yearLines = window.allLinesData ? window.allLinesData.filter(line => {
310
- if (!line.timestamp) return false;
311
- const lineYear = new Date(parseFloat(line.timestamp) * 1000).getFullYear();
312
- return lineYear === parseInt(year);
313
- }) : [];
314
-
315
- const streaks = calculateHeatmapStreaks(grid, yearData, yearLines);
316
- const streaksDiv = document.createElement('div');
317
- streaksDiv.className = 'heatmap-streaks';
318
- streaksDiv.innerHTML = `
319
- <div class="heatmap-streak-item">
320
- <div class="heatmap-streak-number">${streaks.longestStreak}</div>
321
- <div class="heatmap-streak-label">Longest Streak</div>
322
- </div>
323
- <div class="heatmap-streak-item">
324
- <div class="heatmap-streak-number">${streaks.currentStreak}</div>
325
- <div class="heatmap-streak-label">Current Streak</div>
326
- </div>
327
- <div class="heatmap-streak-item">
328
- <div class="heatmap-streak-number">${streaks.avgDailyTime}</div>
329
- <div class="heatmap-streak-label">Avg Daily Time</div>
330
- </div>
331
- `;
332
- mainWrapper.appendChild(streaksDiv);
333
- yearDiv.appendChild(mainWrapper);
334
-
335
- // Add legend with discrete colors
336
- const legend = document.createElement('div');
337
- legend.className = 'heatmap-legend';
338
- legend.innerHTML = `
339
- <span>Less</span>
340
- <div class="heatmap-legend-item" style="background-color: #ebedf0;" title="No activity"></div>
341
- <div class="heatmap-legend-item" style="background-color: #c6e48b;" title="1-25% of max activity"></div>
342
- <div class="heatmap-legend-item" style="background-color: #7bc96f;" title="26-50% of max activity"></div>
343
- <div class="heatmap-legend-item" style="background-color: #239a3b;" title="51-75% of max activity"></div>
344
- <div class="heatmap-legend-item" style="background-color: #196127;" title="76-100% of max activity"></div>
345
- <span>More</span>
346
- `;
347
- yearDiv.appendChild(legend);
348
-
349
- container.appendChild(yearDiv);
350
- });
152
+ activityHeatmapRenderer.render(heatmapData, window.allLinesData || []);
351
153
  }
352
154
 
353
155
  function showNoDataPopup() {
@@ -911,11 +713,7 @@ document.addEventListener('DOMContentLoaded', function () {
911
713
  document.getElementById('currentSessionTotalHours').textContent = hoursDisplay;
912
714
  document.getElementById('currentSessionTotalChars').textContent = lastSession.totalChars.toLocaleString();
913
715
  document.getElementById('currentSessionStartTime').textContent = startTimeDisplay;
914
- if (index === sessionDetails.length - 1) {
915
- document.getElementById('currentSessionEndTime').textContent = 'Now';
916
- } else {
917
- document.getElementById('currentSessionEndTime').textContent = endTimeDisplay;
918
- }
716
+ document.getElementById('currentSessionEndTime').textContent = endTimeDisplay;
919
717
  document.getElementById('currentSessionCharsPerHour').textContent = lastSession.readSpeed !== '-' ? lastSession.readSpeed.toLocaleString() : '-';
920
718
  }
921
719
 
@@ -926,6 +724,7 @@ document.addEventListener('DOMContentLoaded', function () {
926
724
  const today = new Date();
927
725
  const pad = n => n.toString().padStart(2, '0');
928
726
  const todayStr = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
727
+ const afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
929
728
  document.getElementById('todayDate').textContent = todayStr;
930
729
 
931
730
  // Filter lines for today
@@ -992,11 +791,10 @@ document.addEventListener('DOMContentLoaded', function () {
992
791
  };
993
792
  } else {
994
793
  // Continue current session
995
- currentSession.endTime = ts;
794
+ currentSession.endTime = ts + afkTimerSeconds;
996
795
  currentSession.totalChars += chars;
997
796
  currentSession.lines.push(line);
998
797
  if (lastTimestamp !== null) {
999
- let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
1000
798
  currentSession.totalSeconds += Math.min(ts - lastTimestamp, afkTimerSeconds);
1001
799
  }
1002
800
  }
@@ -1022,7 +820,7 @@ document.addEventListener('DOMContentLoaded', function () {
1022
820
  }
1023
821
 
1024
822
  // Optionally, you can expose sessionDetails for debugging or further UI use:
1025
- console.log(sessionDetails);
823
+ // console.log(sessionDetails);
1026
824
  window.todaySessionDetails = sessionDetails;
1027
825
 
1028
826
  // Calculate total reading time (reuse AFK logic from calculateHeatmapStreaks)
@@ -1032,7 +830,6 @@ document.addEventListener('DOMContentLoaded', function () {
1032
830
  .filter(ts => !isNaN(ts))
1033
831
  .sort((a, b) => a - b);
1034
832
  // Get AFK timer from settings modal if available
1035
- let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
1036
833
  if (timestamps.length >= 2) {
1037
834
  for (let i = 1; i < timestamps.length; i++) {
1038
835
  const gap = timestamps[i] - timestamps[i-1];
@@ -1076,6 +873,7 @@ document.addEventListener('DOMContentLoaded', function () {
1076
873
  // Determine target date string (YYYY-MM-DD) from the end timestamp
1077
874
  const endDateObj = new Date(endTimestamp * 1000);
1078
875
  const targetDateStr = `${endDateObj.getFullYear()}-${pad(endDateObj.getMonth() + 1)}-${pad(endDateObj.getDate())}`;
876
+ const afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
1079
877
  document.getElementById('todayDate').textContent = targetDateStr;
1080
878
 
1081
879
  // Filter lines that fall on the target date
@@ -1141,7 +939,7 @@ document.addEventListener('DOMContentLoaded', function () {
1141
939
  };
1142
940
  } else {
1143
941
  // Continue current session
1144
- currentSession.endTime = ts;
942
+ currentSession.endTime = ts + afkTimerSeconds;
1145
943
  currentSession.totalChars += chars;
1146
944
  currentSession.lines.push(line);
1147
945
  if (lastTimestamp !== null) {
@@ -1184,8 +982,6 @@ document.addEventListener('DOMContentLoaded', function () {
1184
982
  .filter(ts => !isNaN(ts))
1185
983
  .sort((a, b) => a - b);
1186
984
 
1187
- let afkTimerSeconds = window.statsConfig?.afkTimerSeconds || 120;
1188
-
1189
985
  if (timestamps.length >= 2) {
1190
986
  for (let i = 1; i < timestamps.length; i++) {
1191
987
  const gap = timestamps[i] - timestamps[i - 1];