GameSentenceMiner 2.15.11__py3-none-any.whl → 2.15.12__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.
@@ -0,0 +1,1427 @@
1
+ // Helper function to detect the current theme based on the app's theme system
2
+ function getCurrentTheme() {
3
+ const dataTheme = document.documentElement.getAttribute('data-theme');
4
+ if (dataTheme === 'dark' || dataTheme === 'light') {
5
+ return dataTheme;
6
+ }
7
+
8
+ // Fallback to system preference if no manual theme is set
9
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
10
+ return 'dark';
11
+ }
12
+ return 'light';
13
+ }
14
+
15
+ // Helper function to get theme-appropriate text color
16
+ function getThemeTextColor() {
17
+ return getCurrentTheme() === 'dark' ? '#fff' : '#222';
18
+ }
19
+
20
+ // Ensure Chart.js uses white font in dark mode and black in light mode for all chart text
21
+ if (window.Chart) {
22
+ function setChartFontColor() {
23
+ Chart.defaults.color = getThemeTextColor();
24
+ }
25
+ setChartFontColor();
26
+
27
+ // Listen for theme changes from both manual toggle and system preference
28
+ if (window.matchMedia) {
29
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', setChartFontColor);
30
+ }
31
+
32
+ // Listen for manual theme changes via MutationObserver on data-theme attribute
33
+ const observer = new MutationObserver((mutations) => {
34
+ mutations.forEach((mutation) => {
35
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
36
+ setChartFontColor();
37
+ }
38
+ });
39
+ });
40
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
41
+ }
42
+
43
+ // Statistics Page JavaScript
44
+ // Dependencies: shared.js (provides utility functions like showElement, hideElement, escapeHtml)
45
+
46
+ document.addEventListener('DOMContentLoaded', function () {
47
+ // Helper function to create a chart to avoid repeating code
48
+ function createChart(canvasId, datasets, chartTitle) {
49
+ const ctx = document.getElementById(canvasId).getContext('2d');
50
+ new Chart(ctx, {
51
+ type: 'line',
52
+ data: {
53
+ labels: datasets.labels,
54
+ datasets: datasets.datasets
55
+ },
56
+ options: {
57
+ responsive: true,
58
+ plugins: {
59
+ legend: {
60
+ position: 'top',
61
+ labels: {
62
+ color: getThemeTextColor()
63
+ }
64
+ },
65
+ title: {
66
+ display: true,
67
+ text: chartTitle,
68
+ color: getThemeTextColor()
69
+ }
70
+ },
71
+ scales: {
72
+ y: {
73
+ beginAtZero: true,
74
+ title: {
75
+ display: true,
76
+ text: 'Cumulative Count',
77
+ color: getThemeTextColor()
78
+ },
79
+ ticks: {
80
+ color: getThemeTextColor()
81
+ }
82
+ },
83
+ x: {
84
+ title: {
85
+ display: true,
86
+ text: 'Date',
87
+ color: getThemeTextColor()
88
+ },
89
+ ticks: {
90
+ color: getThemeTextColor()
91
+ }
92
+ }
93
+ }
94
+ }
95
+ });
96
+ }
97
+
98
+ // Helper function to get week number of year (GitHub style - week starts on Sunday)
99
+ function getWeekOfYear(date) {
100
+ const yearStart = new Date(date.getFullYear(), 0, 1);
101
+ const dayOfYear = Math.floor((date - yearStart) / (24 * 60 * 60 * 1000)) + 1;
102
+ const dayOfWeek = yearStart.getDay(); // 0 = Sunday
103
+
104
+ // Calculate week number (1-indexed)
105
+ const weekNum = Math.ceil((dayOfYear + dayOfWeek) / 7);
106
+ return Math.min(53, weekNum); // Cap at 53 weeks
107
+ }
108
+
109
+ // Helper function to get day of week (0 = Sunday, 6 = Saturday)
110
+ function getDayOfWeek(date) {
111
+ return date.getDay();
112
+ }
113
+
114
+ // Helper function to get the first Sunday of the year (or before)
115
+ function getFirstSunday(year) {
116
+ const jan1 = new Date(year, 0, 1);
117
+ const dayOfWeek = jan1.getDay();
118
+ const firstSunday = new Date(year, 0, 1 - dayOfWeek);
119
+ return firstSunday;
120
+ }
121
+
122
+ // Function to calculate heatmap streaks and average daily time
123
+ function calculateHeatmapStreaks(grid, yearData, allLinesForYear = []) {
124
+ const dates = [];
125
+
126
+ // Collect all dates in chronological order
127
+ for (let week = 0; week < 53; week++) {
128
+ for (let day = 0; day < 7; day++) {
129
+ const date = grid[day][week];
130
+ if (date) {
131
+ const dateStr = date.toISOString().split('T')[0];
132
+ const activity = yearData[dateStr] || 0;
133
+ dates.push({ date: dateStr, activity: activity });
134
+ }
135
+ }
136
+ }
137
+
138
+ // Sort dates chronologically
139
+ dates.sort((a, b) => new Date(a.date) - new Date(b.date));
140
+
141
+
142
+ let longestStreak = 0;
143
+ let currentStreak = 0;
144
+ let tempStreak = 0;
145
+
146
+ // Calculate longest streak
147
+ for (let i = 0; i < dates.length; i++) {
148
+ if (dates[i].activity > 0) {
149
+ tempStreak++;
150
+ longestStreak = Math.max(longestStreak, tempStreak);
151
+ } else {
152
+ tempStreak = 0;
153
+ }
154
+ }
155
+
156
+ // Calculate current streak from today backwards
157
+ const today = new Date().toISOString().split('T')[0];
158
+
159
+ // Find today's index or the most recent date before today
160
+ let todayIndex = -1;
161
+ for (let i = dates.length - 1; i >= 0; i--) {
162
+ if (dates[i].date <= today) {
163
+ todayIndex = i;
164
+ break;
165
+ }
166
+ }
167
+
168
+ // Count backwards from today (or most recent date)
169
+ if (todayIndex >= 0) {
170
+ for (let i = todayIndex; i >= 0; i--) {
171
+ if (dates[i].activity > 0) {
172
+ currentStreak++;
173
+ } else {
174
+ break;
175
+ }
176
+ }
177
+ }
178
+
179
+ // Calculate average daily time for this year
180
+ let avgDailyTime = "-";
181
+ if (allLinesForYear && allLinesForYear.length > 0) {
182
+ // Group timestamps by day for this year
183
+ const dailyTimestamps = {};
184
+ for (const line of allLinesForYear) {
185
+ const dateStr = new Date(parseFloat(line.timestamp) * 1000).toISOString().split('T')[0];
186
+ if (!dailyTimestamps[dateStr]) {
187
+ dailyTimestamps[dateStr] = [];
188
+ }
189
+ dailyTimestamps[dateStr].push(parseFloat(line.timestamp));
190
+ }
191
+
192
+ // Calculate reading time for each day with activity
193
+ let totalHours = 0;
194
+ let activeDays = 0;
195
+ const afkTimerSeconds = 120; // Default AFK timer - should be fetched from settings
196
+
197
+ for (const [dateStr, timestamps] of Object.entries(dailyTimestamps)) {
198
+ if (timestamps.length >= 2) {
199
+ timestamps.sort((a, b) => a - b);
200
+ let dayReadingTime = 0;
201
+
202
+ for (let i = 1; i < timestamps.length; i++) {
203
+ const gap = timestamps[i] - timestamps[i-1];
204
+ dayReadingTime += Math.min(gap, afkTimerSeconds);
205
+ }
206
+
207
+ if (dayReadingTime > 0) {
208
+ totalHours += dayReadingTime / 3600;
209
+ activeDays++;
210
+ }
211
+ } else if (timestamps.length === 1) {
212
+ // Single timestamp - count as minimal activity (1 second)
213
+ totalHours += 1 / 3600;
214
+ activeDays++;
215
+ }
216
+ }
217
+
218
+ if (activeDays > 0) {
219
+ const avgHours = totalHours / activeDays;
220
+ if (avgHours < 1) {
221
+ const minutes = Math.round(avgHours * 60);
222
+ avgDailyTime = `${minutes}m`;
223
+ } else {
224
+ const hours = Math.floor(avgHours);
225
+ const minutes = Math.round((avgHours - hours) * 60);
226
+ avgDailyTime = minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
227
+ }
228
+ }
229
+ }
230
+
231
+ return { longestStreak, currentStreak, avgDailyTime };
232
+ }
233
+
234
+ // Function to create GitHub-style heatmap
235
+ function createHeatmap(heatmapData) {
236
+ const container = document.getElementById('heatmapContainer');
237
+
238
+ Object.keys(heatmapData).sort().forEach(year => {
239
+ const yearData = heatmapData[year];
240
+ const yearDiv = document.createElement('div');
241
+ yearDiv.className = 'heatmap-year';
242
+
243
+ const yearTitle = document.createElement('h3');
244
+ yearTitle.textContent = year;
245
+ yearDiv.appendChild(yearTitle);
246
+
247
+ // Find maximum activity value for this year to scale colors
248
+ const maxActivity = Math.max(...Object.values(yearData));
249
+
250
+ // Create main wrapper to center everything
251
+ const mainWrapper = document.createElement('div');
252
+ mainWrapper.className = 'heatmap-wrapper';
253
+
254
+ // Create container wrapper for labels and grid
255
+ const containerWrapper = document.createElement('div');
256
+ containerWrapper.className = 'heatmap-container-wrapper';
257
+
258
+ // Create day labels (S, M, T, W, T, F, S)
259
+ const dayLabels = document.createElement('div');
260
+ dayLabels.className = 'heatmap-day-labels';
261
+ const dayNames = ['S', '', 'M', '', 'W', '', 'F']; // Only show some labels for space
262
+ dayNames.forEach(dayName => {
263
+ const dayLabel = document.createElement('div');
264
+ dayLabel.className = 'heatmap-day-label';
265
+ dayLabel.textContent = dayName;
266
+ dayLabels.appendChild(dayLabel);
267
+ });
268
+
269
+ // Create grid container
270
+ const gridContainer = document.createElement('div');
271
+
272
+ // Create month labels
273
+ const monthLabels = document.createElement('div');
274
+ monthLabels.className = 'heatmap-month-labels';
275
+
276
+ // Create the main grid
277
+ const gridDiv = document.createElement('div');
278
+ gridDiv.className = 'heatmap-grid';
279
+
280
+ // Initialize 7x53 grid with empty cells
281
+ const grid = Array(7).fill(null).map(() => Array(53).fill(null));
282
+
283
+ // Get the first Sunday of the year (start of week 1)
284
+ const firstSunday = getFirstSunday(parseInt(year));
285
+
286
+ // Populate grid with dates for the entire year
287
+ for (let week = 0; week < 53; week++) {
288
+ for (let day = 0; day < 7; day++) {
289
+ const currentDate = new Date(firstSunday);
290
+ currentDate.setDate(firstSunday.getDate() + (week * 7) + day);
291
+
292
+ // Only include dates that belong to the current year
293
+ if (currentDate.getFullYear() === parseInt(year)) {
294
+ grid[day][week] = currentDate;
295
+ }
296
+ }
297
+ }
298
+
299
+ // Create month labels based on grid positions
300
+ const monthTracker = new Set();
301
+ for (let week = 0; week < 53; week++) {
302
+ const dateInWeek = grid[0][week] || grid[1][week] || grid[2][week] ||
303
+ grid[3][week] || grid[4][week] || grid[5][week] || grid[6][week];
304
+
305
+ if (dateInWeek) {
306
+ const month = dateInWeek.getMonth();
307
+ const monthName = dateInWeek.toLocaleDateString('en', { month: 'short' });
308
+
309
+ // Add month label if it's the first week of the month
310
+ if (!monthTracker.has(month) && dateInWeek.getDate() <= 7) {
311
+ const monthLabel = document.createElement('div');
312
+ monthLabel.className = 'heatmap-month-label';
313
+ monthLabel.style.gridColumn = `${week + 1}`;
314
+ monthLabel.textContent = monthName;
315
+ monthLabels.appendChild(monthLabel);
316
+ monthTracker.add(month);
317
+ }
318
+ }
319
+ }
320
+
321
+ // Create cells for the grid
322
+ for (let day = 0; day < 7; day++) {
323
+ for (let week = 0; week < 53; week++) {
324
+ const cell = document.createElement('div');
325
+ cell.className = 'heatmap-cell';
326
+
327
+ const date = grid[day][week];
328
+ if (date) {
329
+ const dateStr = date.toISOString().split('T')[0];
330
+ const activity = yearData[dateStr] || 0;
331
+
332
+ if (activity > 0 && maxActivity > 0) {
333
+ // Calculate percentage of maximum activity
334
+ const percentage = (activity / maxActivity) * 100;
335
+
336
+ // Assign discrete color levels based on percentage thresholds
337
+ let colorLevel;
338
+ if (percentage <= 25) {
339
+ colorLevel = 1; // Light green
340
+ } else if (percentage <= 50) {
341
+ colorLevel = 2; // Medium green
342
+ } else if (percentage <= 75) {
343
+ colorLevel = 3; // Dark green
344
+ } else {
345
+ colorLevel = 4; // Darkest green
346
+ }
347
+
348
+ // Define discrete colors for each level
349
+ const colors = {
350
+ 1: '#c6e48b', // Light green (1-25%)
351
+ 2: '#7bc96f', // Medium green (26-50%)
352
+ 3: '#239a3b', // Dark green (51-75%)
353
+ 4: '#196127' // Darkest green (76-100%)
354
+ };
355
+
356
+ cell.style.backgroundColor = colors[colorLevel];
357
+ }
358
+
359
+ cell.title = `${dateStr}: ${activity} characters`;
360
+ } else {
361
+ // Empty cell for dates outside the year
362
+ cell.style.backgroundColor = 'transparent';
363
+ cell.style.cursor = 'default';
364
+ }
365
+
366
+ gridDiv.appendChild(cell);
367
+ }
368
+ }
369
+
370
+ gridContainer.appendChild(monthLabels);
371
+ gridContainer.appendChild(gridDiv);
372
+ containerWrapper.appendChild(dayLabels);
373
+ containerWrapper.appendChild(gridContainer);
374
+ mainWrapper.appendChild(containerWrapper);
375
+
376
+ // Calculate and display streaks with average daily time
377
+ const yearLines = window.allLinesData ? window.allLinesData.filter(line => {
378
+ if (!line.timestamp) return false;
379
+ const lineYear = new Date(parseFloat(line.timestamp) * 1000).getFullYear();
380
+ return lineYear === parseInt(year);
381
+ }) : [];
382
+
383
+ const streaks = calculateHeatmapStreaks(grid, yearData, yearLines);
384
+ const streaksDiv = document.createElement('div');
385
+ streaksDiv.className = 'heatmap-streaks';
386
+ streaksDiv.innerHTML = `
387
+ <div class="heatmap-streak-item">
388
+ <div class="heatmap-streak-number">${streaks.longestStreak}</div>
389
+ <div class="heatmap-streak-label">Longest Streak</div>
390
+ </div>
391
+ <div class="heatmap-streak-item">
392
+ <div class="heatmap-streak-number">${streaks.currentStreak}</div>
393
+ <div class="heatmap-streak-label">Current Streak</div>
394
+ </div>
395
+ <div class="heatmap-streak-item">
396
+ <div class="heatmap-streak-number">${streaks.avgDailyTime}</div>
397
+ <div class="heatmap-streak-label">Avg Daily Time</div>
398
+ </div>
399
+ `;
400
+ mainWrapper.appendChild(streaksDiv);
401
+ yearDiv.appendChild(mainWrapper);
402
+
403
+ // Add legend with discrete colors
404
+ const legend = document.createElement('div');
405
+ legend.className = 'heatmap-legend';
406
+ legend.innerHTML = `
407
+ <span>Less</span>
408
+ <div class="heatmap-legend-item" style="background-color: #ebedf0;" title="No activity"></div>
409
+ <div class="heatmap-legend-item" style="background-color: #c6e48b;" title="1-25% of max activity"></div>
410
+ <div class="heatmap-legend-item" style="background-color: #7bc96f;" title="26-50% of max activity"></div>
411
+ <div class="heatmap-legend-item" style="background-color: #239a3b;" title="51-75% of max activity"></div>
412
+ <div class="heatmap-legend-item" style="background-color: #196127;" title="76-100% of max activity"></div>
413
+ <span>More</span>
414
+ `;
415
+ yearDiv.appendChild(legend);
416
+
417
+ container.appendChild(yearDiv);
418
+ });
419
+ }
420
+
421
+ // Function to generate distinct colors for games
422
+ function generateGameColors(gameCount) {
423
+ const colors = [];
424
+
425
+ // Predefined set of good colors for the first few games
426
+ const predefinedColors = [
427
+ '#3498db', '#e74c3c', '#2ecc71', '#f1c40f', '#9b59b6',
428
+ '#1abc9c', '#e67e22', '#34495e', '#16a085', '#27ae60',
429
+ '#2980b9', '#8e44ad', '#d35400', '#c0392b', '#7f8c8d'
430
+ ];
431
+
432
+ // Use predefined colors first
433
+ for (let i = 0; i < Math.min(gameCount, predefinedColors.length); i++) {
434
+ colors.push(predefinedColors[i]);
435
+ }
436
+
437
+ // Generate additional colors using HSL if needed
438
+ if (gameCount > predefinedColors.length) {
439
+ const remaining = gameCount - predefinedColors.length;
440
+ for (let i = 0; i < remaining; i++) {
441
+ // Distribute hue evenly across the color wheel
442
+ const hue = (i * 360 / remaining) % 360;
443
+ // Use varied saturation and lightness for visual distinction
444
+ const saturation = 65 + (i % 3) * 10; // 65%, 75%, 85%
445
+ const lightness = 45 + (i % 2) * 10; // 45%, 55%
446
+
447
+ colors.push(`hsl(${hue.toFixed(0)}, ${saturation}%, ${lightness}%)`);
448
+ }
449
+ }
450
+
451
+ return colors;
452
+ }
453
+
454
+ // Reusable function to create game bar charts with interactive legend
455
+ function createGameBarChart(canvasId, chartData, chartTitle, yAxisLabel) {
456
+ const ctx = document.getElementById(canvasId).getContext('2d');
457
+ const colors = generateGameColors(chartData.labels.length);
458
+
459
+ // Track which bars are hidden for toggle functionality
460
+ const hiddenBars = new Array(chartData.labels.length).fill(false);
461
+
462
+ new Chart(ctx, {
463
+ type: 'bar',
464
+ data: {
465
+ labels: chartData.labels, // Each game as a separate label
466
+ datasets: [{
467
+ label: chartTitle,
468
+ data: chartData.totals,
469
+ backgroundColor: colors.map(color => color + '99'), // Semi-transparent
470
+ borderColor: colors,
471
+ borderWidth: 2
472
+ }]
473
+ },
474
+ options: {
475
+ responsive: true,
476
+ interaction: {
477
+ intersect: false,
478
+ mode: 'nearest'
479
+ },
480
+ plugins: {
481
+ legend: {
482
+ position: 'right',
483
+ labels: {
484
+ color: getThemeTextColor(),
485
+ generateLabels: function(chart) {
486
+ // Create custom legend items for each game
487
+ return chartData.labels.map((gameName, index) => ({
488
+ text: gameName,
489
+ fillStyle: colors[index],
490
+ strokeStyle: colors[index],
491
+ lineWidth: 2,
492
+ hidden: hiddenBars[index],
493
+ index: index,
494
+ fontColor: getThemeTextColor()
495
+ }));
496
+ }
497
+ },
498
+ onClick: function(e, legendItem) {
499
+ const index = legendItem.index;
500
+ const chart = this.chart;
501
+ const meta = chart.getDatasetMeta(0);
502
+
503
+ // Toggle visibility for this specific bar
504
+ hiddenBars[index] = !hiddenBars[index];
505
+
506
+ // Update the dataset to hide/show this bar
507
+ if (hiddenBars[index]) {
508
+ meta.data[index].hidden = true;
509
+ } else {
510
+ meta.data[index].hidden = false;
511
+ }
512
+
513
+ chart.update();
514
+ }
515
+ },
516
+ title: {
517
+ display: true,
518
+ text: chartTitle,
519
+ color: getThemeTextColor()
520
+ },
521
+ tooltip: {
522
+ callbacks: {
523
+ title: function(context) {
524
+ // Show the game name as the main title
525
+ return context[0].label;
526
+ },
527
+ label: function(context) {
528
+ // Show only this game's data
529
+ const value = context.parsed.y;
530
+ return `Characters: ${value.toLocaleString()}`;
531
+ }
532
+ },
533
+ displayColors: true,
534
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
535
+ titleColor: 'white',
536
+ bodyColor: 'white',
537
+ borderColor: 'rgba(255, 255, 255, 0.1)',
538
+ borderWidth: 1
539
+ }
540
+ },
541
+ scales: {
542
+ y: {
543
+ beginAtZero: true,
544
+ title: {
545
+ display: true,
546
+ text: yAxisLabel,
547
+ color: getThemeTextColor()
548
+ },
549
+ ticks: {
550
+ color: getThemeTextColor()
551
+ }
552
+ },
553
+ x: {
554
+ title: {
555
+ display: false // Remove unhelpful "Game Titles" label
556
+ },
557
+ ticks: {
558
+ color: getThemeTextColor()
559
+ }
560
+ }
561
+ }
562
+ }
563
+ });
564
+ }
565
+
566
+ // Specialized function for charts with custom formatting (time/speed)
567
+ function createGameBarChartWithCustomFormat(canvasId, chartData, chartTitle, yAxisLabel, formatFunction) {
568
+ const ctx = document.getElementById(canvasId).getContext('2d');
569
+ const colors = generateGameColors(chartData.labels.length);
570
+
571
+ // Track which bars are hidden for toggle functionality
572
+ const hiddenBars = new Array(chartData.labels.length).fill(false);
573
+
574
+ new Chart(ctx, {
575
+ type: 'bar',
576
+ data: {
577
+ labels: chartData.labels, // Each game as a separate label
578
+ datasets: [{
579
+ label: chartTitle,
580
+ data: chartData.totals,
581
+ backgroundColor: colors.map(color => color + '99'), // Semi-transparent
582
+ borderColor: colors,
583
+ borderWidth: 2
584
+ }]
585
+ },
586
+ options: {
587
+ responsive: true,
588
+ interaction: {
589
+ intersect: false,
590
+ mode: 'nearest'
591
+ },
592
+ plugins: {
593
+ legend: {
594
+ position: 'right',
595
+ labels: {
596
+ color: getThemeTextColor(),
597
+ generateLabels: function(chart) {
598
+ // Create custom legend items for each game
599
+ return chartData.labels.map((gameName, index) => ({
600
+ text: gameName,
601
+ fillStyle: colors[index],
602
+ strokeStyle: colors[index],
603
+ lineWidth: 2,
604
+ hidden: hiddenBars[index],
605
+ index: index,
606
+ fontColor: getThemeTextColor()
607
+ }));
608
+ }
609
+ },
610
+ onClick: function(e, legendItem) {
611
+ const index = legendItem.index;
612
+ const chart = this.chart;
613
+ const meta = chart.getDatasetMeta(0);
614
+
615
+ // Toggle visibility for this specific bar
616
+ hiddenBars[index] = !hiddenBars[index];
617
+
618
+ // Update the dataset to hide/show this bar
619
+ if (hiddenBars[index]) {
620
+ meta.data[index].hidden = true;
621
+ } else {
622
+ meta.data[index].hidden = false;
623
+ }
624
+
625
+ chart.update();
626
+ }
627
+ },
628
+ title: {
629
+ display: true,
630
+ text: chartTitle,
631
+ color: getThemeTextColor()
632
+ },
633
+ tooltip: {
634
+ callbacks: {
635
+ title: function(context) {
636
+ // Show the game name as the main title
637
+ return context[0].label;
638
+ },
639
+ label: function(context) {
640
+ // Use custom format function
641
+ const value = context.parsed.y;
642
+ return formatFunction(value);
643
+ }
644
+ },
645
+ displayColors: true,
646
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
647
+ titleColor: 'white',
648
+ bodyColor: 'white',
649
+ borderColor: 'rgba(255, 255, 255, 0.1)',
650
+ borderWidth: 1
651
+ }
652
+ },
653
+ scales: {
654
+ y: {
655
+ beginAtZero: true,
656
+ title: {
657
+ display: true,
658
+ text: yAxisLabel,
659
+ color: getThemeTextColor()
660
+ },
661
+ ticks: {
662
+ color: getThemeTextColor()
663
+ }
664
+ },
665
+ x: {
666
+ title: {
667
+ display: false // Remove unhelpful axis labels
668
+ },
669
+ ticks: {
670
+ color: getThemeTextColor()
671
+ }
672
+ }
673
+ }
674
+ }
675
+ });
676
+ }
677
+
678
+ // Helper functions for formatting
679
+ function formatTime(hours) {
680
+ if (hours < 1) {
681
+ const minutes = (hours * 60).toFixed(0);
682
+ return `Time: ${minutes} minutes`;
683
+ } else {
684
+ return `Time: ${hours.toFixed(2)} hours`;
685
+ }
686
+ }
687
+
688
+ function formatSpeed(charsPerHour) {
689
+ return `Speed: ${charsPerHour.toLocaleString()} chars/hour`;
690
+ }
691
+
692
+ // Initialize Kanji Grid Renderer (using shared component)
693
+ const kanjiGridRenderer = new KanjiGridRenderer({
694
+ containerSelector: '#kanjiGrid',
695
+ counterSelector: '#kanjiCount',
696
+ colorMode: 'backend',
697
+ emptyMessage: 'No kanji data available'
698
+ });
699
+
700
+ // Function to create kanji grid (now using shared renderer)
701
+ function createKanjiGrid(kanjiData) {
702
+ kanjiGridRenderer.render(kanjiData);
703
+ }
704
+
705
+ // Function to load stats data with optional year filter
706
+ function loadStatsData(filterYear = null) {
707
+ const url = filterYear && filterYear !== 'all' ? `/api/stats?year=${filterYear}` : '/api/stats';
708
+
709
+ return fetch(url)
710
+ .then(response => response.json())
711
+ .then(data => {
712
+ // Store all lines data globally for heatmap calculations
713
+ if (data.allLinesData && Array.isArray(data.allLinesData)) {
714
+ window.allLinesData = data.allLinesData;
715
+ } else {
716
+ // If not provided by API, we'll work without it
717
+ window.allLinesData = [];
718
+ }
719
+
720
+ if (!data.labels || data.labels.length === 0) {
721
+ console.log("No data to display.");
722
+ return data;
723
+ }
724
+
725
+ // Filter datasets for each chart
726
+ const linesData = {
727
+ labels: data.labels,
728
+ datasets: data.datasets.filter(d => d.label.includes('Lines Received'))
729
+ };
730
+
731
+ const charsData = {
732
+ labels: data.labels,
733
+ datasets: data.datasets.filter(d => d.label.includes('Characters Read'))
734
+ };
735
+
736
+ // Remove the 'hidden' property so they appear on their own charts
737
+ [...charsData.datasets].forEach(d => delete d.hidden);
738
+
739
+ // Create the charts (only on initial load)
740
+ if (!window.chartsInitialized) {
741
+ createChart('linesChart', linesData, 'Cumulative Lines Received');
742
+ createChart('charsChart', charsData, 'Cumulative Characters Read');
743
+
744
+ // Create reading chars quantity chart if data exists
745
+ if (data.totalCharsPerGame) {
746
+ createGameBarChart('readingCharsChart', data.totalCharsPerGame, 'Reading Chars Quantity', 'Characters Read');
747
+ }
748
+
749
+ // Create reading time quantity chart if data exists
750
+ if (data.readingTimePerGame) {
751
+ createGameBarChartWithCustomFormat('readingTimeChart', data.readingTimePerGame, 'Reading Time Quantity', 'Time (hours)', formatTime);
752
+ }
753
+
754
+ // Create reading speed per game chart if data exists
755
+ if (data.readingSpeedPerGame) {
756
+ createGameBarChartWithCustomFormat('readingSpeedPerGameChart', data.readingSpeedPerGame, 'Reading Speed Improvement', 'Speed (chars/hour)', formatSpeed);
757
+ }
758
+
759
+ // Create kanji grid if data exists
760
+ if (data.kanjiGridData) {
761
+ createKanjiGrid(data.kanjiGridData);
762
+ }
763
+
764
+ window.chartsInitialized = true;
765
+ }
766
+
767
+ // Always update heatmap
768
+ if (data.heatmapData) {
769
+ const container = document.getElementById('heatmapContainer');
770
+ container.innerHTML = '';
771
+ createHeatmap(data.heatmapData);
772
+ }
773
+
774
+ // Load dashboard data (only on initial load)
775
+ if (!window.dashboardInitialized) {
776
+ loadDashboardData(data);
777
+ window.dashboardInitialized = true;
778
+ }
779
+
780
+ return data;
781
+ })
782
+ .catch(error => {
783
+ console.error('Error fetching chart data:', error);
784
+ showDashboardError();
785
+ throw error;
786
+ });
787
+ }
788
+
789
+ // Initial load with saved year preference
790
+ const savedYear = localStorage.getItem('selectedHeatmapYear') || 'all';
791
+ loadStatsData(savedYear);
792
+
793
+ // Make functions globally available
794
+ window.createHeatmap = createHeatmap;
795
+ window.loadStatsData = loadStatsData;
796
+
797
+ // Dashboard functionality
798
+ function loadDashboardData(data = null) {
799
+ function updateTodayOverview(allLinesData) {
800
+ // Get today's date string (YYYY-MM-DD)
801
+ const today = new Date();
802
+ const todayStr = today.toISOString().split('T')[0];
803
+ document.getElementById('todayDate').textContent = todayStr;
804
+
805
+ // Filter lines for today
806
+ const todayLines = (allLinesData || []).filter(line => {
807
+ if (!line.timestamp) return false;
808
+ const lineDate = new Date(parseFloat(line.timestamp) * 1000).toISOString().split('T')[0];
809
+ return lineDate === todayStr;
810
+ });
811
+
812
+ // Calculate total characters read today (only valid numbers)
813
+ const totalChars = todayLines.reduce((sum, line) => {
814
+ const chars = Number(line.characters);
815
+ return sum + (isNaN(chars) ? 0 : chars);
816
+ }, 0);
817
+
818
+ // Calculate sessions (count gaps > session threshold as new sessions)
819
+ let sessions = 0;
820
+ const sessionGapDefault = 3600; // 1 hour in seconds
821
+ // Try to get session gap from settings modal if available
822
+ let sessionGap = sessionGapDefault;
823
+ const sessionGapInput = document.getElementById('sessionGap');
824
+ if (sessionGapInput && sessionGapInput.value) {
825
+ const parsed = parseInt(sessionGapInput.value, 10);
826
+ if (!isNaN(parsed) && parsed > 0) sessionGap = parsed;
827
+ }
828
+ if (todayLines.length > 0 && todayLines[0].session_id !== undefined) {
829
+ const sessionSet = new Set(todayLines.map(l => l.session_id));
830
+ sessions = sessionSet.size;
831
+ } else {
832
+ // Use timestamp gap logic
833
+ const timestamps = todayLines
834
+ .map(l => parseFloat(l.timestamp))
835
+ .filter(ts => !isNaN(ts))
836
+ .sort((a, b) => a - b);
837
+ if (timestamps.length > 0) {
838
+ sessions = 1;
839
+ for (let i = 1; i < timestamps.length; i++) {
840
+ if (timestamps[i] - timestamps[i - 1] > sessionGap) {
841
+ sessions += 1;
842
+ }
843
+ }
844
+ } else {
845
+ sessions = 0;
846
+ }
847
+ }
848
+
849
+ // Calculate total reading time (reuse AFK logic from calculateHeatmapStreaks)
850
+ let totalSeconds = 0;
851
+ const timestamps = todayLines
852
+ .map(l => parseFloat(l.timestamp))
853
+ .filter(ts => !isNaN(ts))
854
+ .sort((a, b) => a - b);
855
+ const afkTimerSeconds = 120;
856
+ if (timestamps.length >= 2) {
857
+ for (let i = 1; i < timestamps.length; i++) {
858
+ const gap = timestamps[i] - timestamps[i-1];
859
+ totalSeconds += Math.min(gap, afkTimerSeconds);
860
+ }
861
+ } else if (timestamps.length === 1) {
862
+ totalSeconds = 1;
863
+ }
864
+ let totalHours = totalSeconds / 3600;
865
+
866
+ // Calculate chars/hour
867
+ let charsPerHour = '-';
868
+ if (totalChars > 0) {
869
+ // Avoid division by zero, set minimum time to 1 minute if activity exists
870
+ if (totalHours <= 0) totalHours = 1/60;
871
+ charsPerHour = Math.round(totalChars / totalHours).toLocaleString();
872
+ }
873
+
874
+ // Format hours for display
875
+ let hoursDisplay = '-';
876
+ if (totalHours > 0) {
877
+ const h = Math.floor(totalHours);
878
+ const m = Math.round((totalHours - h) * 60);
879
+ hoursDisplay = h > 0 ? `${h}h${m > 0 ? ' ' + m + 'm' : ''}` : `${m}m`;
880
+ }
881
+
882
+ document.getElementById('todayTotalHours').textContent = hoursDisplay;
883
+ document.getElementById('todayTotalChars').textContent = totalChars.toLocaleString();
884
+ document.getElementById('todaySessions').textContent = sessions;
885
+ document.getElementById('todayCharsPerHour').textContent = charsPerHour;
886
+ }
887
+
888
+ if (data && data.currentGameStats && data.allGamesStats) {
889
+ // Use existing data if available
890
+ updateCurrentGameDashboard(data.currentGameStats);
891
+ updateAllGamesDashboard(data.allGamesStats);
892
+ if (data.allLinesData) updateTodayOverview(data.allLinesData);
893
+ hideDashboardLoading();
894
+ } else {
895
+ // Fetch fresh data
896
+ showDashboardLoading();
897
+ fetch('/api/stats')
898
+ .then(response => response.json())
899
+ .then(data => {
900
+ if (data.currentGameStats && data.allGamesStats) {
901
+ updateCurrentGameDashboard(data.currentGameStats);
902
+ updateAllGamesDashboard(data.allGamesStats);
903
+ if (data.allLinesData) updateTodayOverview(data.allLinesData);
904
+ } else {
905
+ showDashboardError();
906
+ }
907
+ hideDashboardLoading();
908
+ })
909
+ .catch(error => {
910
+ console.error('Error fetching dashboard data:', error);
911
+ showDashboardError();
912
+ hideDashboardLoading();
913
+ });
914
+ }
915
+ }
916
+
917
+ function updateCurrentGameDashboard(stats) {
918
+ if (!stats) {
919
+ showNoDashboardData('currentGameCard', 'No current game data available');
920
+ return;
921
+ }
922
+
923
+ // Update game name and subtitle
924
+ document.getElementById('currentGameName').textContent = stats.game_name;
925
+
926
+ // Update main statistics
927
+ document.getElementById('currentTotalChars').textContent = stats.total_characters_formatted;
928
+ document.getElementById('currentTotalTime').textContent = stats.total_time_formatted;
929
+ document.getElementById('currentReadingSpeed').textContent = stats.reading_speed_formatted;
930
+ document.getElementById('currentSessions').textContent = stats.sessions.toLocaleString();
931
+
932
+ // Update progress section
933
+ document.getElementById('currentMonthlyChars').textContent = stats.monthly_characters_formatted;
934
+ document.getElementById('currentFirstDate').textContent = stats.first_date;
935
+ document.getElementById('currentLastDate').textContent = stats.last_date;
936
+
937
+ // Update streak indicator
938
+ const streakElement = document.getElementById('currentGameStreak');
939
+ const streakValue = document.getElementById('currentStreakValue');
940
+ if (stats.current_streak > 0) {
941
+ streakValue.textContent = stats.current_streak;
942
+ streakElement.style.display = 'inline-flex';
943
+ } else {
944
+ streakElement.style.display = 'none';
945
+ }
946
+
947
+ // Show the card
948
+ document.getElementById('currentGameCard').style.display = 'block';
949
+ }
950
+
951
+ function updateAllGamesDashboard(stats) {
952
+ if (!stats) {
953
+ showNoDashboardData('allGamesCard', 'No games data available');
954
+ return;
955
+ }
956
+
957
+ // Update subtitle
958
+ const gamesText = stats.unique_games === 1 ? '1 game played' : `${stats.unique_games} games played`;
959
+ document.getElementById('totalGamesCount').textContent = gamesText;
960
+
961
+ // Update main statistics
962
+ document.getElementById('allTotalChars').textContent = stats.total_characters_formatted;
963
+ document.getElementById('allTotalTime').textContent = stats.total_time_formatted;
964
+ document.getElementById('allReadingSpeed').textContent = stats.reading_speed_formatted;
965
+ document.getElementById('allSessions').textContent = stats.sessions.toLocaleString();
966
+
967
+ // Update progress section
968
+ document.getElementById('allMonthlyChars').textContent = stats.monthly_characters_formatted;
969
+ document.getElementById('allUniqueGames').textContent = stats.unique_games.toLocaleString();
970
+ document.getElementById('allTotalSentences').textContent = stats.total_sentences.toLocaleString();
971
+
972
+ // Update streak indicator
973
+ const streakElement = document.getElementById('allGamesStreak');
974
+ const streakValue = document.getElementById('allStreakValue');
975
+ if (stats.current_streak > 0) {
976
+ streakValue.textContent = stats.current_streak;
977
+ streakElement.style.display = 'inline-flex';
978
+ } else {
979
+ streakElement.style.display = 'none';
980
+ }
981
+
982
+
983
+ // Show the card
984
+ document.getElementById('allGamesCard').style.display = 'block';
985
+ }
986
+
987
+ function showDashboardLoading() {
988
+ document.getElementById('dashboardLoading').style.display = 'flex';
989
+ document.getElementById('dashboardError').style.display = 'none';
990
+ document.getElementById('currentGameCard').style.display = 'none';
991
+ document.getElementById('allGamesCard').style.display = 'none';
992
+ }
993
+
994
+ function hideDashboardLoading() {
995
+ document.getElementById('dashboardLoading').style.display = 'none';
996
+ }
997
+
998
+ function showDashboardError() {
999
+ document.getElementById('dashboardError').style.display = 'block';
1000
+ document.getElementById('dashboardLoading').style.display = 'none';
1001
+ document.getElementById('currentGameCard').style.display = 'none';
1002
+ document.getElementById('allGamesCard').style.display = 'none';
1003
+ }
1004
+
1005
+ function showNoDashboardData(cardId, message) {
1006
+ const card = document.getElementById(cardId);
1007
+ const statsGrid = card.querySelector('.dashboard-stats-grid');
1008
+ const progressSection = card.querySelector('.dashboard-progress-section');
1009
+
1010
+ // Hide stats and progress sections
1011
+ statsGrid.style.display = 'none';
1012
+ progressSection.style.display = 'none';
1013
+
1014
+ // Add no data message
1015
+ let noDataMsg = card.querySelector('.no-data-message');
1016
+ if (!noDataMsg) {
1017
+ noDataMsg = document.createElement('div');
1018
+ noDataMsg.className = 'no-data-message';
1019
+ noDataMsg.style.cssText = 'text-align: center; padding: 40px 20px; color: var(--text-tertiary); font-style: italic;';
1020
+ card.appendChild(noDataMsg);
1021
+ }
1022
+ noDataMsg.textContent = message;
1023
+
1024
+ card.style.display = 'block';
1025
+ }
1026
+
1027
+ // Add click animations for dashboard stat items
1028
+ const statItems = document.querySelectorAll('.dashboard-stat-item');
1029
+ statItems.forEach(item => {
1030
+ item.addEventListener('click', function() {
1031
+ // Add click animation
1032
+ this.style.transform = 'scale(0.95)';
1033
+ setTimeout(() => {
1034
+ this.style.transform = '';
1035
+ }, 150);
1036
+ });
1037
+ });
1038
+
1039
+ // Add accessibility improvements
1040
+ statItems.forEach(item => {
1041
+ item.setAttribute('tabindex', '0');
1042
+ item.setAttribute('role', 'button');
1043
+
1044
+ item.addEventListener('keydown', function(e) {
1045
+ if (e.key === 'Enter' || e.key === ' ') {
1046
+ e.preventDefault();
1047
+ this.click();
1048
+ }
1049
+ });
1050
+ });
1051
+
1052
+ // Global function to retry dashboard loading
1053
+ window.loadDashboardData = loadDashboardData;
1054
+
1055
+ // Delete Game Entry Functionality
1056
+ class GameDeletionManager {
1057
+ constructor() {
1058
+ this.games = [];
1059
+ this.selectedGames = new Set();
1060
+ this.isLoading = false;
1061
+
1062
+ this.initializeElements();
1063
+ this.attachEventListeners();
1064
+ this.loadGames();
1065
+ }
1066
+
1067
+ initializeElements() {
1068
+ // Control elements
1069
+ this.selectAllBtn = document.getElementById('selectAllBtn');
1070
+ this.selectNoneBtn = document.getElementById('selectNoneBtn');
1071
+ this.deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
1072
+ this.headerCheckbox = document.getElementById('headerCheckbox');
1073
+
1074
+ // Table elements
1075
+ this.gamesTableBody = document.getElementById('gamesTableBody');
1076
+ this.loadingIndicator = document.getElementById('loadingIndicator');
1077
+ this.noGamesMessage = document.getElementById('noGamesMessage');
1078
+ this.errorMessage = document.getElementById('errorMessage');
1079
+ this.retryBtn = document.getElementById('retryBtn');
1080
+
1081
+ // Modal elements
1082
+ this.confirmationModal = document.getElementById('confirmationModal');
1083
+ this.progressModal = document.getElementById('progressModal');
1084
+ this.resultModal = document.getElementById('resultModal');
1085
+
1086
+ // Modal content elements
1087
+ this.selectedGamesList = document.getElementById('selectedGamesList');
1088
+ this.totalGamesCount = document.getElementById('totalGamesCount');
1089
+ this.totalSentencesCount = document.getElementById('totalSentencesCount');
1090
+ this.totalCharactersCount = document.getElementById('totalCharactersCount');
1091
+ this.progressText = document.getElementById('progressText');
1092
+ this.resultContent = document.getElementById('resultContent');
1093
+ this.resultTitle = document.getElementById('resultTitle');
1094
+ }
1095
+
1096
+ attachEventListeners() {
1097
+ // Control buttons
1098
+ if (this.selectAllBtn) this.selectAllBtn.addEventListener('click', () => this.selectAll());
1099
+ if (this.selectNoneBtn) this.selectNoneBtn.addEventListener('click', () => this.selectNone());
1100
+ if (this.deleteSelectedBtn) this.deleteSelectedBtn.addEventListener('click', () => this.showConfirmation());
1101
+ if (this.headerCheckbox) this.headerCheckbox.addEventListener('change', (e) => this.toggleAll(e.target.checked));
1102
+
1103
+ // Modal controls
1104
+ const closeModalBtn = document.getElementById('closeModal');
1105
+ if (closeModalBtn) closeModalBtn.addEventListener('click', () => this.hideModal('confirmationModal'));
1106
+ const cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
1107
+ if (cancelDeleteBtn) cancelDeleteBtn.addEventListener('click', () => this.hideModal('confirmationModal'));
1108
+ const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
1109
+ if (confirmDeleteBtn) confirmDeleteBtn.addEventListener('click', () => this.performDeletion());
1110
+ const closeResultModalBtn = document.getElementById('closeResultModal');
1111
+ if (closeResultModalBtn) closeResultModalBtn.addEventListener('click', () => this.hideModal('resultModal'));
1112
+ const okBtn = document.getElementById('okBtn');
1113
+ if (okBtn) okBtn.addEventListener('click', () => this.hideModal('resultModal'));
1114
+
1115
+ // Retry button
1116
+ if (this.retryBtn) this.retryBtn.addEventListener('click', () => this.loadGames());
1117
+
1118
+ // Close modals when clicking outside
1119
+ [this.confirmationModal, this.progressModal, this.resultModal].forEach(modal => {
1120
+ modal.addEventListener('click', (e) => {
1121
+ if (e.target === modal) {
1122
+ this.hideModal(modal.id);
1123
+ }
1124
+ });
1125
+ });
1126
+ }
1127
+
1128
+ async loadGames() {
1129
+ this.showLoading(true);
1130
+ hideElement(this.errorMessage);
1131
+ hideElement(this.noGamesMessage);
1132
+
1133
+ try {
1134
+ const response = await fetch('/api/games-list');
1135
+ const data = await response.json();
1136
+
1137
+ if (!response.ok) {
1138
+ throw new Error(data.error || 'Failed to fetch games');
1139
+ }
1140
+
1141
+ this.games = data.games;
1142
+ this.selectedGames.clear();
1143
+ this.renderGamesTable();
1144
+ this.updateDeleteButton();
1145
+
1146
+ if (this.games.length === 0) {
1147
+ showElement(this.noGamesMessage);
1148
+ }
1149
+
1150
+ } catch (error) {
1151
+ console.error('Error loading games:', error);
1152
+ this.showError(error.message);
1153
+ } finally {
1154
+ this.showLoading(false);
1155
+ }
1156
+ }
1157
+
1158
+ renderGamesTable() {
1159
+ this.gamesTableBody.innerHTML = '';
1160
+
1161
+ this.games.forEach(game => {
1162
+ const row = document.createElement('tr');
1163
+ row.dataset.gameName = game.name;
1164
+
1165
+ row.innerHTML = `
1166
+ <td class="checkbox-cell">
1167
+ <input type="checkbox" class="game-checkbox" data-game="${game.name}">
1168
+ </td>
1169
+ <td><strong>${escapeHtml(game.name)}</strong></td>
1170
+ <td>${game.sentence_count.toLocaleString()}</td>
1171
+ <td>${game.total_characters.toLocaleString()}</td>
1172
+ <td>${game.date_range}</td>
1173
+ <td>${game.first_entry_date}</td>
1174
+ <td>${game.last_entry_date}</td>
1175
+ `;
1176
+
1177
+ // Add checkbox event listener
1178
+ const checkbox = row.querySelector('.game-checkbox');
1179
+ checkbox.addEventListener('change', (e) => {
1180
+ this.toggleGameSelection(game.name, e.target.checked);
1181
+ });
1182
+
1183
+ this.gamesTableBody.appendChild(row);
1184
+ });
1185
+ }
1186
+
1187
+ toggleGameSelection(gameName, isSelected) {
1188
+ if (isSelected) {
1189
+ this.selectedGames.add(gameName);
1190
+ } else {
1191
+ this.selectedGames.delete(gameName);
1192
+ }
1193
+
1194
+ this.updateRowSelection(gameName, isSelected);
1195
+ this.updateHeaderCheckbox();
1196
+ this.updateDeleteButton();
1197
+ }
1198
+
1199
+ updateRowSelection(gameName, isSelected) {
1200
+ const row = document.querySelector(`tr[data-game-name="${gameName}"]`);
1201
+ if (row) {
1202
+ if (isSelected) {
1203
+ row.classList.add('selected');
1204
+ } else {
1205
+ row.classList.remove('selected');
1206
+ }
1207
+ }
1208
+ }
1209
+
1210
+ selectAll() {
1211
+ this.games.forEach(game => {
1212
+ this.selectedGames.add(game.name);
1213
+ const checkbox = document.querySelector(`input[data-game="${game.name}"]`);
1214
+ if (checkbox) {
1215
+ checkbox.checked = true;
1216
+ this.updateRowSelection(game.name, true);
1217
+ }
1218
+ });
1219
+ this.updateHeaderCheckbox();
1220
+ this.updateDeleteButton();
1221
+ }
1222
+
1223
+ selectNone() {
1224
+ this.selectedGames.clear();
1225
+ document.querySelectorAll('.game-checkbox').forEach(checkbox => {
1226
+ checkbox.checked = false;
1227
+ });
1228
+ document.querySelectorAll('tr.selected').forEach(row => {
1229
+ row.classList.remove('selected');
1230
+ });
1231
+ this.updateHeaderCheckbox();
1232
+ this.updateDeleteButton();
1233
+ }
1234
+
1235
+ toggleAll(checked) {
1236
+ if (checked) {
1237
+ this.selectAll();
1238
+ } else {
1239
+ this.selectNone();
1240
+ }
1241
+ }
1242
+
1243
+ updateHeaderCheckbox() {
1244
+ const totalGames = this.games.length;
1245
+ const selectedCount = this.selectedGames.size;
1246
+
1247
+ if (selectedCount === 0) {
1248
+ this.headerCheckbox.checked = false;
1249
+ this.headerCheckbox.indeterminate = false;
1250
+ } else if (selectedCount === totalGames) {
1251
+ this.headerCheckbox.checked = true;
1252
+ this.headerCheckbox.indeterminate = false;
1253
+ } else {
1254
+ this.headerCheckbox.checked = false;
1255
+ this.headerCheckbox.indeterminate = true;
1256
+ }
1257
+ }
1258
+
1259
+ updateDeleteButton() {
1260
+ this.deleteSelectedBtn.disabled = this.selectedGames.size === 0;
1261
+ this.deleteSelectedBtn.textContent = this.selectedGames.size > 0
1262
+ ? `Delete Selected Games (${this.selectedGames.size})`
1263
+ : 'Delete Selected Games';
1264
+ }
1265
+
1266
+ showConfirmation() {
1267
+ if (this.selectedGames.size === 0) return;
1268
+
1269
+ // Populate confirmation modal
1270
+ this.populateConfirmationModal();
1271
+ this.showModal('confirmationModal');
1272
+ }
1273
+
1274
+ populateConfirmationModal() {
1275
+ const selectedGameData = this.games.filter(game => this.selectedGames.has(game.name));
1276
+
1277
+ // Populate games list
1278
+ this.selectedGamesList.innerHTML = '';
1279
+ selectedGameData.forEach(game => {
1280
+ const gameItem = document.createElement('div');
1281
+ gameItem.className = 'game-item';
1282
+ gameItem.innerHTML = `
1283
+ <div>
1284
+ <div class="game-name">${escapeHtml(game.name)}</div>
1285
+ <div class="game-stats">${game.date_range}</div>
1286
+ </div>
1287
+ <div class="game-stats">
1288
+ ${game.sentence_count} sentences, ${game.total_characters.toLocaleString()} chars
1289
+ </div>
1290
+ `;
1291
+ this.selectedGamesList.appendChild(gameItem);
1292
+ });
1293
+
1294
+ // Calculate totals
1295
+ const totalGames = selectedGameData.length;
1296
+ const totalSentences = selectedGameData.reduce((sum, game) => sum + game.sentence_count, 0);
1297
+ const totalCharacters = selectedGameData.reduce((sum, game) => sum + game.total_characters, 0);
1298
+
1299
+ this.totalGamesCount.textContent = totalGames;
1300
+ this.totalSentencesCount.textContent = totalSentences.toLocaleString();
1301
+ this.totalCharactersCount.textContent = totalCharacters.toLocaleString();
1302
+ }
1303
+
1304
+ async performDeletion() {
1305
+ this.hideModal('confirmationModal');
1306
+ this.showModal('progressModal');
1307
+
1308
+ // Show native confirmation as second stage
1309
+ const gameNames = Array.from(this.selectedGames);
1310
+ const confirmText = `Are you absolutely sure you want to delete ${gameNames.length} game(s)? This action cannot be undone.`;
1311
+
1312
+ if (!confirm(confirmText)) {
1313
+ this.hideModal('progressModal');
1314
+ return;
1315
+ }
1316
+
1317
+ try {
1318
+ this.progressText.textContent = `Deleting ${gameNames.length} games...`;
1319
+
1320
+ const response = await fetch('/api/delete-games', {
1321
+ method: 'POST',
1322
+ headers: {
1323
+ 'Content-Type': 'application/json',
1324
+ },
1325
+ body: JSON.stringify({ game_names: gameNames })
1326
+ });
1327
+
1328
+ const result = await response.json();
1329
+
1330
+ this.hideModal('progressModal');
1331
+ this.showResult(result, response.status);
1332
+
1333
+ } catch (error) {
1334
+ console.error('Error deleting games:', error);
1335
+ this.hideModal('progressModal');
1336
+ this.showResult({ error: error.message }, 500);
1337
+ }
1338
+ }
1339
+
1340
+ showResult(result, status) {
1341
+ let title, content, isSuccess = false;
1342
+
1343
+ if (status === 200) {
1344
+ // Complete success
1345
+ title = 'Deletion Successful';
1346
+ isSuccess = true;
1347
+ content = `
1348
+ <div class="success-message">
1349
+ <p>✅ Successfully deleted ${result.successful_games.length} games!</p>
1350
+ <p><strong>Total sentences deleted:</strong> ${result.total_sentences_deleted.toLocaleString()}</p>
1351
+ </div>
1352
+ `;
1353
+ } else if (status === 207) {
1354
+ // Partial success
1355
+ title = 'Deletion Partially Successful';
1356
+ content = `
1357
+ <div class="warning-result">
1358
+ <p>⚠️ ${result.successful_games.length} games deleted successfully</p>
1359
+ <p>${result.failed_games.length} games failed to delete</p>
1360
+ <p><strong>Total sentences deleted:</strong> ${result.total_sentences_deleted.toLocaleString()}</p>
1361
+ </div>
1362
+ <div style="margin-top: 15px;">
1363
+ <strong>Failed games:</strong>
1364
+ <ul style="margin: 10px 0; padding-left: 20px;">
1365
+ ${result.failed_games.map(game => `<li>${escapeHtml(game)}</li>`).join('')}
1366
+ </ul>
1367
+ </div>
1368
+ `;
1369
+ isSuccess = true; // Still refresh since some succeeded
1370
+ } else {
1371
+ // Complete failure
1372
+ title = 'Deletion Failed';
1373
+ content = `
1374
+ <div class="error-result">
1375
+ <p>❌ Failed to delete games</p>
1376
+ <p><strong>Error:</strong> ${escapeHtml(result.error || 'Unknown error occurred')}</p>
1377
+ </div>
1378
+ `;
1379
+ }
1380
+
1381
+ this.resultTitle.textContent = title;
1382
+ this.resultContent.innerHTML = content;
1383
+ this.showModal('resultModal');
1384
+
1385
+ // Auto-refresh if any deletions were successful
1386
+ if (isSuccess) {
1387
+ setTimeout(() => {
1388
+ this.hideModal('resultModal');
1389
+ window.location.reload();
1390
+ }, 3000);
1391
+ }
1392
+ }
1393
+
1394
+ showModal(modalId) {
1395
+ const modal = document.getElementById(modalId);
1396
+ modal.classList.add('show');
1397
+ modal.style.display = 'flex';
1398
+ }
1399
+
1400
+ hideModal(modalId) {
1401
+ const modal = document.getElementById(modalId);
1402
+ modal.classList.remove('show');
1403
+ modal.style.display = 'none';
1404
+ }
1405
+
1406
+ showLoading(show) {
1407
+ this.isLoading = show;
1408
+ if (show) {
1409
+ showElement(this.loadingIndicator);
1410
+ hideElement(this.gamesTableBody.parentElement);
1411
+ } else {
1412
+ hideElement(this.loadingIndicator);
1413
+ showElement(this.gamesTableBody.parentElement);
1414
+ }
1415
+ }
1416
+
1417
+ showError(message) {
1418
+ document.getElementById('errorText').textContent = message;
1419
+ showElement(this.errorMessage);
1420
+ }
1421
+ }
1422
+
1423
+ // Initialize the deletion manager
1424
+ if (document.getElementById('gamesTableBody')) {
1425
+ new GameDeletionManager();
1426
+ }
1427
+ });