GameSentenceMiner 2.17.0__py3-none-any.whl → 2.17.2__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.
Potentially problematic release.
This version of GameSentenceMiner might be problematic. Click here for more details.
- GameSentenceMiner/anki.py +31 -3
- GameSentenceMiner/config_gui.py +26 -2
- GameSentenceMiner/gametext.py +4 -3
- GameSentenceMiner/gsm.py +19 -23
- GameSentenceMiner/obs.py +17 -7
- GameSentenceMiner/ocr/owocr_helper.py +11 -8
- GameSentenceMiner/owocr/owocr/run.py +11 -5
- GameSentenceMiner/util/configuration.py +7 -5
- GameSentenceMiner/util/db.py +176 -8
- GameSentenceMiner/util/downloader/download_tools.py +57 -24
- GameSentenceMiner/util/ffmpeg.py +5 -2
- GameSentenceMiner/util/get_overlay_coords.py +3 -0
- GameSentenceMiner/util/gsm_utils.py +0 -54
- GameSentenceMiner/vad.py +5 -2
- GameSentenceMiner/web/database_api.py +12 -1
- GameSentenceMiner/web/gsm_websocket.py +1 -1
- GameSentenceMiner/web/static/css/shared.css +20 -0
- GameSentenceMiner/web/static/css/stats.css +496 -1
- GameSentenceMiner/web/static/js/anki_stats.js +87 -3
- GameSentenceMiner/web/static/js/shared.js +2 -49
- GameSentenceMiner/web/static/js/stats.js +274 -39
- GameSentenceMiner/web/templates/anki_stats.html +36 -0
- GameSentenceMiner/web/templates/index.html +1 -1
- GameSentenceMiner/web/templates/stats.html +35 -15
- GameSentenceMiner/web/texthooking_page.py +31 -8
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/RECORD +31 -31
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/top_level.txt +0 -0
|
@@ -44,10 +44,20 @@ if (window.Chart) {
|
|
|
44
44
|
// Dependencies: shared.js (provides utility functions like showElement, hideElement, escapeHtml)
|
|
45
45
|
|
|
46
46
|
document.addEventListener('DOMContentLoaded', function () {
|
|
47
|
+
|
|
48
|
+
// Global object to store chart instances
|
|
49
|
+
window.myCharts = window.myCharts || {};
|
|
50
|
+
|
|
47
51
|
// Helper function to create a chart to avoid repeating code
|
|
48
52
|
function createChart(canvasId, datasets, chartTitle) {
|
|
49
53
|
const ctx = document.getElementById(canvasId).getContext('2d');
|
|
50
|
-
|
|
54
|
+
|
|
55
|
+
// Destroy existing chart on this canvas if it exists
|
|
56
|
+
if (window.myCharts[canvasId]) {
|
|
57
|
+
window.myCharts[canvasId].destroy();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
51
61
|
type: 'line',
|
|
52
62
|
data: {
|
|
53
63
|
labels: datasets.labels,
|
|
@@ -93,6 +103,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
});
|
|
106
|
+
return window.myCharts[canvasId];
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
// Helper function to get week number of year (GitHub style - week starts on Sunday)
|
|
@@ -502,7 +513,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
502
513
|
return getFilteredChartData(originalData, hiddenBars, colors);
|
|
503
514
|
}
|
|
504
515
|
|
|
505
|
-
|
|
516
|
+
// Destroy existing chart on this canvas if it exists
|
|
517
|
+
if (window.myCharts[canvasId]) {
|
|
518
|
+
window.myCharts[canvasId].destroy();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
506
522
|
type: 'bar',
|
|
507
523
|
data: {
|
|
508
524
|
labels: chartData.labels, // Each game as a separate label
|
|
@@ -603,6 +619,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
603
619
|
}
|
|
604
620
|
}
|
|
605
621
|
});
|
|
622
|
+
|
|
623
|
+
return window.myCharts[canvasId];
|
|
606
624
|
}
|
|
607
625
|
|
|
608
626
|
// Specialized function for charts with custom formatting (time/speed)
|
|
@@ -623,7 +641,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
623
641
|
return getFilteredChartData(originalData, hiddenBars, colors);
|
|
624
642
|
}
|
|
625
643
|
|
|
626
|
-
|
|
644
|
+
// Destroy existing chart if it exists
|
|
645
|
+
if (window.myCharts[canvasId]) {
|
|
646
|
+
window.myCharts[canvasId].destroy();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Create new chart and store globally
|
|
650
|
+
window.myCharts[canvasId] = new Chart(ctx, {
|
|
627
651
|
type: 'bar',
|
|
628
652
|
data: {
|
|
629
653
|
labels: chartData.labels, // Each game as a separate label
|
|
@@ -724,6 +748,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
724
748
|
}
|
|
725
749
|
}
|
|
726
750
|
});
|
|
751
|
+
return window.myCharts[canvasId];
|
|
727
752
|
}
|
|
728
753
|
|
|
729
754
|
// Helper functions for formatting
|
|
@@ -753,9 +778,29 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
753
778
|
kanjiGridRenderer.render(kanjiData);
|
|
754
779
|
}
|
|
755
780
|
|
|
781
|
+
function showNoDataPopup() {
|
|
782
|
+
document.getElementById("noDataPopup").classList.remove("hidden");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
document.getElementById("closeNoDataPopup").addEventListener("click", () => {
|
|
786
|
+
document.getElementById("noDataPopup").classList.add("hidden");
|
|
787
|
+
});
|
|
788
|
+
|
|
756
789
|
// Function to load stats data with optional year filter
|
|
757
|
-
function loadStatsData(
|
|
758
|
-
|
|
790
|
+
function loadStatsData(start_timestamp = null, end_timestamp = null) {
|
|
791
|
+
let url = '/api/stats';
|
|
792
|
+
const params = new URLSearchParams();
|
|
793
|
+
|
|
794
|
+
if (start_timestamp && end_timestamp) {
|
|
795
|
+
// Only filter by timestamps
|
|
796
|
+
params.append('start', start_timestamp);
|
|
797
|
+
params.append('end', end_timestamp);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const queryString = params.toString();
|
|
801
|
+
if (queryString) {
|
|
802
|
+
url += `?${queryString}`;
|
|
803
|
+
}
|
|
759
804
|
|
|
760
805
|
return fetch(url)
|
|
761
806
|
.then(response => response.json())
|
|
@@ -770,6 +815,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
770
815
|
|
|
771
816
|
if (!data.labels || data.labels.length === 0) {
|
|
772
817
|
console.log("No data to display.");
|
|
818
|
+
showNoDataPopup();
|
|
773
819
|
return data;
|
|
774
820
|
}
|
|
775
821
|
|
|
@@ -787,34 +833,31 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
787
833
|
// Remove the 'hidden' property so they appear on their own charts
|
|
788
834
|
[...charsData.datasets].forEach(d => delete d.hidden);
|
|
789
835
|
|
|
790
|
-
//
|
|
791
|
-
if (!window.chartsInitialized) {
|
|
836
|
+
// Charts are re-created with the new data
|
|
792
837
|
createChart('linesChart', linesData, 'Cumulative Lines Received');
|
|
793
838
|
createChart('charsChart', charsData, 'Cumulative Characters Read');
|
|
794
839
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
// Create reading time quantity chart if data exists
|
|
801
|
-
if (data.readingTimePerGame) {
|
|
802
|
-
createGameBarChartWithCustomFormat('readingTimeChart', data.readingTimePerGame, 'Reading Time Quantity', 'Time (hours)', formatTime);
|
|
803
|
-
}
|
|
840
|
+
// Create reading chars quantity chart if data exists
|
|
841
|
+
if (data.totalCharsPerGame) {
|
|
842
|
+
createGameBarChart('readingCharsChart', data.totalCharsPerGame, 'Reading Chars Quantity', 'Characters Read');
|
|
843
|
+
}
|
|
804
844
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
845
|
+
// Create reading time quantity chart if data exists
|
|
846
|
+
if (data.readingTimePerGame) {
|
|
847
|
+
createGameBarChartWithCustomFormat('readingTimeChart', data.readingTimePerGame, 'Reading Time Quantity', 'Time (hours)', formatTime);
|
|
848
|
+
}
|
|
809
849
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
850
|
+
// Create reading speed per game chart if data exists
|
|
851
|
+
if (data.readingSpeedPerGame) {
|
|
852
|
+
createGameBarChartWithCustomFormat('readingSpeedPerGameChart', data.readingSpeedPerGame, 'Reading Speed Improvement', 'Speed (chars/hour)', formatSpeed);
|
|
853
|
+
}
|
|
814
854
|
|
|
815
|
-
|
|
855
|
+
// Create kanji grid if data exists
|
|
856
|
+
if (data.kanjiGridData) {
|
|
857
|
+
createKanjiGrid(data.kanjiGridData);
|
|
816
858
|
}
|
|
817
859
|
|
|
860
|
+
|
|
818
861
|
// Always update heatmap
|
|
819
862
|
if (data.heatmapData) {
|
|
820
863
|
const container = document.getElementById('heatmapContainer');
|
|
@@ -822,11 +865,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
822
865
|
createHeatmap(data.heatmapData);
|
|
823
866
|
}
|
|
824
867
|
|
|
825
|
-
// Load dashboard data
|
|
826
|
-
|
|
827
|
-
loadDashboardData(data);
|
|
828
|
-
window.dashboardInitialized = true;
|
|
829
|
-
}
|
|
868
|
+
// Load dashboard data
|
|
869
|
+
loadDashboardData(data, end_timestamp);
|
|
830
870
|
|
|
831
871
|
// Load goal progress chart (always refresh)
|
|
832
872
|
if (typeof loadGoalProgress === 'function') {
|
|
@@ -1117,9 +1157,100 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1117
1157
|
}
|
|
1118
1158
|
}
|
|
1119
1159
|
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1122
|
-
|
|
1160
|
+
// ================================
|
|
1161
|
+
// Utility to convert date strings to Unix timestamps
|
|
1162
|
+
// Returns start of day for startDate and end of day for endDate
|
|
1163
|
+
// ================================
|
|
1164
|
+
function getUnixTimestamps(startDate, endDate) {
|
|
1165
|
+
const start = new Date(startDate + 'T00:00:00');
|
|
1166
|
+
const startTimestamp = Math.floor(start.getTime() / 1000); // convert ms to s
|
|
1167
|
+
|
|
1168
|
+
const end = new Date(endDate + 'T23:59:59.999');
|
|
1169
|
+
const endTimestamp = Math.floor(end.getTime() / 1000); // convert ms to s
|
|
1170
|
+
|
|
1171
|
+
return { startTimestamp, endTimestamp };
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// ================================
|
|
1175
|
+
// Initialize date inputs with sessionStorage or fetch initial values
|
|
1176
|
+
// Dispatches "datesSet" event once dates are set
|
|
1177
|
+
// ================================
|
|
1178
|
+
function initializeDates() {
|
|
1179
|
+
const fromDateInput = document.getElementById('fromDate');
|
|
1180
|
+
const toDateInput = document.getElementById('toDate');
|
|
1181
|
+
|
|
1182
|
+
const fromDate = sessionStorage.getItem("fromDate");
|
|
1183
|
+
const toDate = sessionStorage.getItem("toDate");
|
|
1184
|
+
|
|
1185
|
+
if (!(fromDate && toDate)) {
|
|
1186
|
+
fetch('/api/stats')
|
|
1187
|
+
.then(response => response.json())
|
|
1188
|
+
.then(response_json => {
|
|
1189
|
+
// Get first date from API
|
|
1190
|
+
const firstDate = response_json.allGamesStats.first_date;
|
|
1191
|
+
fromDateInput.value = firstDate;
|
|
1192
|
+
|
|
1193
|
+
// Get today's date
|
|
1194
|
+
const today = new Date();
|
|
1195
|
+
toDateInput.value = today.toISOString().split("T")[0];
|
|
1196
|
+
|
|
1197
|
+
// Save in sessionStorage
|
|
1198
|
+
sessionStorage.setItem("fromDate", firstDate);
|
|
1199
|
+
sessionStorage.setItem("toDate", today.toISOString().split("T")[0]);
|
|
1200
|
+
|
|
1201
|
+
document.dispatchEvent(new Event("datesSet"));
|
|
1202
|
+
});
|
|
1203
|
+
} else {
|
|
1204
|
+
// If values already in sessionStorage, set inputs from there
|
|
1205
|
+
fromDateInput.value = fromDate;
|
|
1206
|
+
toDateInput.value = toDate;
|
|
1207
|
+
|
|
1208
|
+
document.dispatchEvent(new Event("datesSet"));
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const fromDateInput = document.getElementById('fromDate');
|
|
1213
|
+
const toDateInput = document.getElementById('toDate');
|
|
1214
|
+
const popup = document.getElementById('dateErrorPopup');
|
|
1215
|
+
const closePopupBtn = document.getElementById('closePopupBtn');
|
|
1216
|
+
|
|
1217
|
+
document.addEventListener("datesSet", () => {
|
|
1218
|
+
const fromDate = sessionStorage.getItem("fromDate");
|
|
1219
|
+
const toDate = sessionStorage.getItem("toDate");
|
|
1220
|
+
const { startTimestamp, endTimestamp } = getUnixTimestamps(fromDate, toDate);
|
|
1221
|
+
|
|
1222
|
+
loadStatsData(startTimestamp, endTimestamp);
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
function handleDateChange() {
|
|
1227
|
+
const fromDateStr = fromDateInput.value;
|
|
1228
|
+
const toDateStr = toDateInput.value;
|
|
1229
|
+
|
|
1230
|
+
sessionStorage.setItem("fromDate", fromDateStr);
|
|
1231
|
+
sessionStorage.setItem("toDate", toDateStr);
|
|
1232
|
+
|
|
1233
|
+
// Validate date order
|
|
1234
|
+
if (fromDateStr && toDateStr && new Date(fromDateStr) > new Date(toDateStr)) {
|
|
1235
|
+
popup.classList.remove("hidden");
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const { startTimestamp, endTimestamp } = getUnixTimestamps(fromDateStr, toDateStr);
|
|
1240
|
+
|
|
1241
|
+
loadStatsData(startTimestamp, endTimestamp);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Attach listeners to both date inputs
|
|
1245
|
+
fromDateInput.addEventListener("change", handleDateChange);
|
|
1246
|
+
toDateInput.addEventListener("change", handleDateChange);
|
|
1247
|
+
|
|
1248
|
+
initializeDates();
|
|
1249
|
+
|
|
1250
|
+
// Popup close button
|
|
1251
|
+
closePopupBtn.addEventListener("click", () => {
|
|
1252
|
+
popup.classList.add("hidden");
|
|
1253
|
+
});
|
|
1123
1254
|
|
|
1124
1255
|
// Populate settings modal with global config values on load
|
|
1125
1256
|
if (window.statsConfig) {
|
|
@@ -1129,9 +1260,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1129
1260
|
const streakReqInput = document.getElementById('streakRequirement');
|
|
1130
1261
|
if (streakReqInput) streakReqInput.value = window.statsConfig.streakRequirementHours || 1.0;
|
|
1131
1262
|
|
|
1132
|
-
const heatmapYearSelect = document.getElementById('heatmapYear');
|
|
1133
|
-
if (heatmapYearSelect) heatmapYearSelect.value = window.statsConfig.heatmapDisplayYear || 'all';
|
|
1134
|
-
|
|
1135
1263
|
const hoursTargetInput = document.getElementById('readingHoursTarget');
|
|
1136
1264
|
if (hoursTargetInput) hoursTargetInput.value = window.statsConfig.readingHoursTarget || 1500;
|
|
1137
1265
|
|
|
@@ -1191,7 +1319,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1191
1319
|
window.loadGoalProgress = loadGoalProgress;
|
|
1192
1320
|
|
|
1193
1321
|
// Dashboard functionality
|
|
1194
|
-
function loadDashboardData(data = null) {
|
|
1322
|
+
function loadDashboardData(data = null, end_timestamp = null) {
|
|
1195
1323
|
function updateTodayOverview(allLinesData) {
|
|
1196
1324
|
// Get today's date string (YYYY-MM-DD)
|
|
1197
1325
|
// Get today's date string (YYYY-MM-DD), timezone aware (local time)
|
|
@@ -1291,11 +1419,116 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1291
1419
|
document.getElementById('todayCharsPerHour').textContent = charsPerHour;
|
|
1292
1420
|
}
|
|
1293
1421
|
|
|
1422
|
+
function updateOverviewForEndDay(allLinesData, endTimestamp) {
|
|
1423
|
+
if (!endTimestamp) return;
|
|
1424
|
+
|
|
1425
|
+
const pad = n => n.toString().padStart(2, '0');
|
|
1426
|
+
|
|
1427
|
+
// Determine target date string (YYYY-MM-DD) from the end timestamp
|
|
1428
|
+
const endDateObj = new Date(endTimestamp * 1000);
|
|
1429
|
+
const targetDateStr = `${endDateObj.getFullYear()}-${pad(endDateObj.getMonth() + 1)}-${pad(endDateObj.getDate())}`;
|
|
1430
|
+
document.getElementById('todayDate').textContent = targetDateStr;
|
|
1431
|
+
|
|
1432
|
+
// Filter lines that fall on the target date
|
|
1433
|
+
const targetLines = (allLinesData || []).filter(line => {
|
|
1434
|
+
if (!line.timestamp) return false;
|
|
1435
|
+
const ts = parseFloat(line.timestamp);
|
|
1436
|
+
if (isNaN(ts)) return false;
|
|
1437
|
+
const dateObj = new Date(ts * 1000);
|
|
1438
|
+
const lineDate = `${dateObj.getFullYear()}-${pad(dateObj.getMonth() + 1)}-${pad(dateObj.getDate())}`;
|
|
1439
|
+
return lineDate === targetDateStr;
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// Calculate total characters
|
|
1443
|
+
const totalChars = targetLines.reduce((sum, line) => {
|
|
1444
|
+
const chars = Number(line.characters);
|
|
1445
|
+
return sum + (isNaN(chars) ? 0 : chars);
|
|
1446
|
+
}, 0);
|
|
1447
|
+
|
|
1448
|
+
// Determine session gap (from settings or default)
|
|
1449
|
+
let sessionGap = window.statsConfig?.sessionGapSeconds || 3600;
|
|
1450
|
+
const sessionGapInput = document.getElementById('sessionGap');
|
|
1451
|
+
if (sessionGapInput?.value) {
|
|
1452
|
+
const parsed = parseInt(sessionGapInput.value, 10);
|
|
1453
|
+
if (!isNaN(parsed) && parsed > 0) sessionGap = parsed;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Calculate sessions
|
|
1457
|
+
let sessions = 0;
|
|
1458
|
+
if (targetLines.length > 0 && targetLines[0].session_id !== undefined) {
|
|
1459
|
+
const sessionSet = new Set(targetLines.map(l => l.session_id));
|
|
1460
|
+
sessions = sessionSet.size;
|
|
1461
|
+
} else {
|
|
1462
|
+
const timestamps = targetLines
|
|
1463
|
+
.map(l => parseFloat(l.timestamp))
|
|
1464
|
+
.filter(ts => !isNaN(ts))
|
|
1465
|
+
.sort((a, b) => a - b);
|
|
1466
|
+
if (timestamps.length > 0) {
|
|
1467
|
+
sessions = 1;
|
|
1468
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
1469
|
+
if (timestamps[i] - timestamps[i - 1] > sessionGap) {
|
|
1470
|
+
sessions += 1;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// Calculate total reading time
|
|
1477
|
+
let totalSeconds = 0;
|
|
1478
|
+
const timestamps = targetLines
|
|
1479
|
+
.map(l => parseFloat(l.timestamp))
|
|
1480
|
+
.filter(ts => !isNaN(ts))
|
|
1481
|
+
.sort((a, b) => a - b);
|
|
1482
|
+
|
|
1483
|
+
let afkTimerSeconds = window.statsConfig?.afkTimerSeconds || 120;
|
|
1484
|
+
const afkTimerInput = document.getElementById('afkTimer');
|
|
1485
|
+
if (afkTimerInput?.value) {
|
|
1486
|
+
const parsed = parseInt(afkTimerInput.value, 10);
|
|
1487
|
+
if (!isNaN(parsed) && parsed > 0) afkTimerSeconds = parsed;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (timestamps.length >= 2) {
|
|
1491
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
1492
|
+
const gap = timestamps[i] - timestamps[i - 1];
|
|
1493
|
+
totalSeconds += Math.min(gap, afkTimerSeconds);
|
|
1494
|
+
}
|
|
1495
|
+
} else if (timestamps.length === 1) {
|
|
1496
|
+
totalSeconds = 1;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
let totalHours = totalSeconds / 3600;
|
|
1500
|
+
|
|
1501
|
+
// Calculate chars/hour
|
|
1502
|
+
let charsPerHour = '-';
|
|
1503
|
+
if (totalChars > 0) {
|
|
1504
|
+
if (totalHours <= 0) totalHours = 1/60; // Minimum 1 minute
|
|
1505
|
+
charsPerHour = Math.round(totalChars / totalHours).toLocaleString();
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Format hours for display
|
|
1509
|
+
let hoursDisplay = '-';
|
|
1510
|
+
if (totalHours > 0) {
|
|
1511
|
+
const h = Math.floor(totalHours);
|
|
1512
|
+
const m = Math.round((totalHours - h) * 60);
|
|
1513
|
+
hoursDisplay = h > 0 ? `${h}h${m > 0 ? ' ' + m + 'm' : ''}` : `${m}m`;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Update DOM
|
|
1517
|
+
document.getElementById('todayTotalHours').textContent = hoursDisplay;
|
|
1518
|
+
document.getElementById('todayTotalChars').textContent = totalChars.toLocaleString();
|
|
1519
|
+
document.getElementById('todaySessions').textContent = sessions;
|
|
1520
|
+
document.getElementById('todayCharsPerHour').textContent = charsPerHour;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1294
1523
|
if (data && data.currentGameStats && data.allGamesStats) {
|
|
1295
1524
|
// Use existing data if available
|
|
1296
1525
|
updateCurrentGameDashboard(data.currentGameStats);
|
|
1297
1526
|
updateAllGamesDashboard(data.allGamesStats);
|
|
1298
|
-
|
|
1527
|
+
|
|
1528
|
+
if (data.allLinesData) {
|
|
1529
|
+
end_timestamp == null ? updateTodayOverview(data.allLinesData) : updateOverviewForEndDay(data.allLinesData, end_timestamp)
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1299
1532
|
hideDashboardLoading();
|
|
1300
1533
|
} else {
|
|
1301
1534
|
// Fetch fresh data
|
|
@@ -1306,7 +1539,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1306
1539
|
if (data.currentGameStats && data.allGamesStats) {
|
|
1307
1540
|
updateCurrentGameDashboard(data.currentGameStats);
|
|
1308
1541
|
updateAllGamesDashboard(data.allGamesStats);
|
|
1309
|
-
if (data.allLinesData)
|
|
1542
|
+
if (data.allLinesData) {
|
|
1543
|
+
end_timestamp == null ? updateTodayOverview(data.allLinesData) : updateOverviewForEndDay(data.allLinesData, end_timestamp)
|
|
1544
|
+
}
|
|
1310
1545
|
} else {
|
|
1311
1546
|
showDashboardError();
|
|
1312
1547
|
}
|
|
@@ -34,6 +34,42 @@
|
|
|
34
34
|
<!-- Include shared navigation -->
|
|
35
35
|
{% include 'components/navigation.html' %}
|
|
36
36
|
|
|
37
|
+
<div class="dashboard-card date-range">
|
|
38
|
+
<div class="dashboard-card-header">
|
|
39
|
+
<h3 class="dashboard-card-title">
|
|
40
|
+
<span class="dashboard-card-icon">📅</span>
|
|
41
|
+
Date Range
|
|
42
|
+
</h3>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="dashboard-date-range">
|
|
46
|
+
<div class="dashboard-date-item tooltip" data-tooltip="Select the start date for your stats">
|
|
47
|
+
<label for="fromDate">From</label>
|
|
48
|
+
<input type="date" id="fromDate" class="dashboard-date-input">
|
|
49
|
+
</div>
|
|
50
|
+
<div class="dashboard-date-item tooltip" data-tooltip="Select the end date for your stats">
|
|
51
|
+
<label for="toDate">To</label>
|
|
52
|
+
<input type="date" id="toDate" class="dashboard-date-input">
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
<div id="dateErrorPopup" class="dashboard-popup hidden">
|
|
59
|
+
<div class="dashboard-popup-content">
|
|
60
|
+
<div class="dashboard-popup-icon">⚠️</div>
|
|
61
|
+
<div class="dashboard-popup-message">"From" date cannot be later than "To" date.</div>
|
|
62
|
+
<button id="closePopupBtn" class="dashboard-popup-btn">OK</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div id="noDataPopup" class="no-data-popup hidden">
|
|
67
|
+
<div class="no-data-content">
|
|
68
|
+
<p>No data found for the selected range.</p>
|
|
69
|
+
<button id="closeNoDataPopup">OK</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
37
73
|
<!-- Dashboard Statistics Sections -->
|
|
38
74
|
<div class="dashboard-container">
|
|
39
75
|
<!-- Missing High-Frequency Kanji Card -->
|