GameSentenceMiner 2.16.5__py3-none-any.whl → 2.16.7__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.
- GameSentenceMiner/anki.py +7 -2
- GameSentenceMiner/config_gui.py +8 -0
- GameSentenceMiner/locales/en_us.json +5 -1
- GameSentenceMiner/locales/ja_jp.json +4 -0
- GameSentenceMiner/locales/zh_cn.json +4 -0
- GameSentenceMiner/util/configuration.py +35 -0
- GameSentenceMiner/web/database_api.py +207 -93
- GameSentenceMiner/web/static/css/stats.css +243 -0
- GameSentenceMiner/web/static/js/search.js +46 -22
- GameSentenceMiner/web/static/js/shared.js +60 -12
- GameSentenceMiner/web/static/js/stats.js +363 -20
- GameSentenceMiner/web/stats.py +3 -3
- GameSentenceMiner/web/templates/search.html +6 -1
- GameSentenceMiner/web/templates/stats.html +163 -12
- GameSentenceMiner/web/texthooking_page.py +1 -1
- {gamesentenceminer-2.16.5.dist-info → gamesentenceminer-2.16.7.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.16.5.dist-info → gamesentenceminer-2.16.7.dist-info}/RECORD +22 -22
- /GameSentenceMiner/web/{websockets.py → gsm_websocket.py} +0 -0
- {gamesentenceminer-2.16.5.dist-info → gamesentenceminer-2.16.7.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.16.5.dist-info → gamesentenceminer-2.16.7.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.16.5.dist-info → gamesentenceminer-2.16.7.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.16.5.dist-info → gamesentenceminer-2.16.7.dist-info}/top_level.txt +0 -0
@@ -451,6 +451,28 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
451
451
|
return colors;
|
452
452
|
}
|
453
453
|
|
454
|
+
// Helper function to filter chart data for visible bars
|
455
|
+
function getFilteredChartData(originalData, hiddenBars, colors) {
|
456
|
+
// Filter data to only include visible bars
|
457
|
+
const visibleLabels = [];
|
458
|
+
const visibleTotals = [];
|
459
|
+
const visibleColors = [];
|
460
|
+
|
461
|
+
originalData.labels.forEach((label, index) => {
|
462
|
+
if (!hiddenBars[index]) {
|
463
|
+
visibleLabels.push(label);
|
464
|
+
visibleTotals.push(originalData.totals[index]);
|
465
|
+
visibleColors.push(colors[index]);
|
466
|
+
}
|
467
|
+
});
|
468
|
+
|
469
|
+
return {
|
470
|
+
labels: visibleLabels,
|
471
|
+
totals: visibleTotals,
|
472
|
+
colors: visibleColors
|
473
|
+
};
|
474
|
+
}
|
475
|
+
|
454
476
|
// Reusable function to create game bar charts with interactive legend
|
455
477
|
function createGameBarChart(canvasId, chartData, chartTitle, yAxisLabel) {
|
456
478
|
const ctx = document.getElementById(canvasId).getContext('2d');
|
@@ -459,6 +481,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
459
481
|
// Track which bars are hidden for toggle functionality
|
460
482
|
const hiddenBars = new Array(chartData.labels.length).fill(false);
|
461
483
|
|
484
|
+
// Store original data for filtering
|
485
|
+
const originalData = {
|
486
|
+
labels: [...chartData.labels],
|
487
|
+
totals: [...chartData.totals]
|
488
|
+
};
|
489
|
+
|
490
|
+
function updateChartData() {
|
491
|
+
return getFilteredChartData(originalData, hiddenBars, colors);
|
492
|
+
}
|
493
|
+
|
462
494
|
new Chart(ctx, {
|
463
495
|
type: 'bar',
|
464
496
|
data: {
|
@@ -483,8 +515,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
483
515
|
labels: {
|
484
516
|
color: getThemeTextColor(),
|
485
517
|
generateLabels: function(chart) {
|
486
|
-
// Create custom legend items for each game
|
487
|
-
return
|
518
|
+
// Create custom legend items for each game using original data
|
519
|
+
return originalData.labels.map((gameName, index) => ({
|
488
520
|
text: gameName,
|
489
521
|
fillStyle: colors[index],
|
490
522
|
strokeStyle: colors[index],
|
@@ -498,19 +530,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
498
530
|
onClick: function(e, legendItem) {
|
499
531
|
const index = legendItem.index;
|
500
532
|
const chart = this.chart;
|
501
|
-
const meta = chart.getDatasetMeta(0);
|
502
533
|
|
503
534
|
// Toggle visibility for this specific bar
|
504
535
|
hiddenBars[index] = !hiddenBars[index];
|
505
536
|
|
506
|
-
// Update
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
537
|
+
// Update chart with filtered data
|
538
|
+
const filteredData = updateChartData();
|
539
|
+
chart.data.labels = filteredData.labels;
|
540
|
+
chart.data.datasets[0].data = filteredData.totals;
|
541
|
+
chart.data.datasets[0].backgroundColor = filteredData.colors.map(color => color + '99');
|
542
|
+
chart.data.datasets[0].borderColor = filteredData.colors;
|
512
543
|
|
513
|
-
chart.update();
|
544
|
+
chart.update('resize');
|
514
545
|
}
|
515
546
|
},
|
516
547
|
title: {
|
@@ -571,6 +602,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
571
602
|
// Track which bars are hidden for toggle functionality
|
572
603
|
const hiddenBars = new Array(chartData.labels.length).fill(false);
|
573
604
|
|
605
|
+
// Store original data for filtering
|
606
|
+
const originalData = {
|
607
|
+
labels: [...chartData.labels],
|
608
|
+
totals: [...chartData.totals]
|
609
|
+
};
|
610
|
+
|
611
|
+
function updateChartData() {
|
612
|
+
return getFilteredChartData(originalData, hiddenBars, colors);
|
613
|
+
}
|
614
|
+
|
574
615
|
new Chart(ctx, {
|
575
616
|
type: 'bar',
|
576
617
|
data: {
|
@@ -595,8 +636,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
595
636
|
labels: {
|
596
637
|
color: getThemeTextColor(),
|
597
638
|
generateLabels: function(chart) {
|
598
|
-
// Create custom legend items for each game
|
599
|
-
return
|
639
|
+
// Create custom legend items for each game using original data
|
640
|
+
return originalData.labels.map((gameName, index) => ({
|
600
641
|
text: gameName,
|
601
642
|
fillStyle: colors[index],
|
602
643
|
strokeStyle: colors[index],
|
@@ -610,19 +651,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
610
651
|
onClick: function(e, legendItem) {
|
611
652
|
const index = legendItem.index;
|
612
653
|
const chart = this.chart;
|
613
|
-
const meta = chart.getDatasetMeta(0);
|
614
654
|
|
615
655
|
// Toggle visibility for this specific bar
|
616
656
|
hiddenBars[index] = !hiddenBars[index];
|
617
657
|
|
618
|
-
// Update
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
658
|
+
// Update chart with filtered data
|
659
|
+
const filteredData = updateChartData();
|
660
|
+
chart.data.labels = filteredData.labels;
|
661
|
+
chart.data.datasets[0].data = filteredData.totals;
|
662
|
+
chart.data.datasets[0].backgroundColor = filteredData.colors.map(color => color + '99');
|
663
|
+
chart.data.datasets[0].borderColor = filteredData.colors;
|
624
664
|
|
625
|
-
chart.update();
|
665
|
+
chart.update('resize');
|
626
666
|
}
|
627
667
|
},
|
628
668
|
title: {
|
@@ -777,6 +817,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
777
817
|
window.dashboardInitialized = true;
|
778
818
|
}
|
779
819
|
|
820
|
+
// Load goal progress chart (always refresh)
|
821
|
+
if (typeof loadGoalProgress === 'function') {
|
822
|
+
// Use the current data instead of making another API call
|
823
|
+
updateGoalProgressWithData(data);
|
824
|
+
}
|
825
|
+
|
780
826
|
return data;
|
781
827
|
})
|
782
828
|
.catch(error => {
|
@@ -786,13 +832,310 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
786
832
|
});
|
787
833
|
}
|
788
834
|
|
835
|
+
// Goal Progress Chart functionality
|
836
|
+
let goalSettings = {
|
837
|
+
reading_hours_target: 1500,
|
838
|
+
character_count_target: 25000000,
|
839
|
+
games_target: 100
|
840
|
+
};
|
841
|
+
|
842
|
+
// Function to load goal settings from API
|
843
|
+
async function loadGoalSettings() {
|
844
|
+
try {
|
845
|
+
const response = await fetch('/api/settings');
|
846
|
+
if (response.ok) {
|
847
|
+
const settings = await response.json();
|
848
|
+
goalSettings = {
|
849
|
+
reading_hours_target: settings.reading_hours_target || 1500,
|
850
|
+
character_count_target: settings.character_count_target || 25000000,
|
851
|
+
games_target: settings.games_target || 100
|
852
|
+
};
|
853
|
+
}
|
854
|
+
} catch (error) {
|
855
|
+
console.error('Error loading goal settings:', error);
|
856
|
+
}
|
857
|
+
}
|
858
|
+
|
859
|
+
// Function to calculate 90-day rolling average for projections
|
860
|
+
function calculate90DayAverage(allLinesData, metricType) {
|
861
|
+
if (!allLinesData || allLinesData.length === 0) {
|
862
|
+
return 0;
|
863
|
+
}
|
864
|
+
|
865
|
+
const today = new Date();
|
866
|
+
const ninetyDaysAgo = new Date(today.getTime() - (90 * 24 * 60 * 60 * 1000));
|
867
|
+
|
868
|
+
// Filter data to last 90 days
|
869
|
+
const recentData = allLinesData.filter(line => {
|
870
|
+
const lineDate = new Date(line.timestamp * 1000);
|
871
|
+
return lineDate >= ninetyDaysAgo && lineDate <= today;
|
872
|
+
});
|
873
|
+
|
874
|
+
if (recentData.length === 0) {
|
875
|
+
return 0;
|
876
|
+
}
|
877
|
+
|
878
|
+
let dailyTotals = {};
|
879
|
+
|
880
|
+
if (metricType === 'hours') {
|
881
|
+
// Group by day and calculate reading time using AFK timer logic
|
882
|
+
const dailyTimestamps = {};
|
883
|
+
for (const line of recentData) {
|
884
|
+
const dateStr = new Date(line.timestamp * 1000).toISOString().split('T')[0];
|
885
|
+
if (!dailyTimestamps[dateStr]) {
|
886
|
+
dailyTimestamps[dateStr] = [];
|
887
|
+
}
|
888
|
+
dailyTimestamps[dateStr].push(line.timestamp);
|
889
|
+
}
|
890
|
+
|
891
|
+
for (const [dateStr, timestamps] of Object.entries(dailyTimestamps)) {
|
892
|
+
if (timestamps.length >= 2) {
|
893
|
+
timestamps.sort((a, b) => a - b);
|
894
|
+
let dayHours = 0;
|
895
|
+
const afkTimerSeconds = 120; // Default AFK timer
|
896
|
+
|
897
|
+
for (let i = 1; i < timestamps.length; i++) {
|
898
|
+
const gap = timestamps[i] - timestamps[i-1];
|
899
|
+
dayHours += Math.min(gap, afkTimerSeconds) / 3600;
|
900
|
+
}
|
901
|
+
dailyTotals[dateStr] = dayHours;
|
902
|
+
} else if (timestamps.length === 1) {
|
903
|
+
dailyTotals[dateStr] = 1 / 3600; // Minimal activity
|
904
|
+
}
|
905
|
+
}
|
906
|
+
} else if (metricType === 'characters') {
|
907
|
+
// Group by day and sum characters
|
908
|
+
for (const line of recentData) {
|
909
|
+
const dateStr = new Date(line.timestamp * 1000).toISOString().split('T')[0];
|
910
|
+
dailyTotals[dateStr] = (dailyTotals[dateStr] || 0) + (line.characters || 0);
|
911
|
+
}
|
912
|
+
} else if (metricType === 'games') {
|
913
|
+
// Group by day and count unique games
|
914
|
+
const dailyGames = {};
|
915
|
+
for (const line of recentData) {
|
916
|
+
const dateStr = new Date(line.timestamp * 1000).toISOString().split('T')[0];
|
917
|
+
if (!dailyGames[dateStr]) {
|
918
|
+
dailyGames[dateStr] = new Set();
|
919
|
+
}
|
920
|
+
dailyGames[dateStr].add(line.game_name);
|
921
|
+
}
|
922
|
+
|
923
|
+
for (const [dateStr, gamesSet] of Object.entries(dailyGames)) {
|
924
|
+
dailyTotals[dateStr] = gamesSet.size;
|
925
|
+
}
|
926
|
+
}
|
927
|
+
|
928
|
+
const totalDays = Object.keys(dailyTotals).length;
|
929
|
+
if (totalDays === 0) {
|
930
|
+
return 0;
|
931
|
+
}
|
932
|
+
|
933
|
+
const totalValue = Object.values(dailyTotals).reduce((sum, value) => sum + value, 0);
|
934
|
+
return totalValue / totalDays;
|
935
|
+
}
|
936
|
+
|
937
|
+
// Function to format projection text
|
938
|
+
function formatProjection(currentValue, targetValue, dailyAverage, metricType) {
|
939
|
+
if (currentValue >= targetValue) {
|
940
|
+
return 'Goal achieved! 🎉';
|
941
|
+
}
|
942
|
+
|
943
|
+
if (dailyAverage <= 0) {
|
944
|
+
return 'No recent activity';
|
945
|
+
}
|
946
|
+
|
947
|
+
const remaining = targetValue - currentValue;
|
948
|
+
const daysToComplete = Math.ceil(remaining / dailyAverage);
|
949
|
+
|
950
|
+
if (daysToComplete <= 0) {
|
951
|
+
return 'Goal achieved! 🎉';
|
952
|
+
} else if (daysToComplete === 1) {
|
953
|
+
return '~1 day remaining';
|
954
|
+
} else if (daysToComplete <= 7) {
|
955
|
+
return `~${daysToComplete} days remaining`;
|
956
|
+
} else if (daysToComplete <= 30) {
|
957
|
+
const weeks = Math.ceil(daysToComplete / 7);
|
958
|
+
return `~${weeks} week${weeks > 1 ? 's' : ''} remaining`;
|
959
|
+
} else if (daysToComplete <= 365) {
|
960
|
+
const months = Math.ceil(daysToComplete / 30);
|
961
|
+
return `~${months} month${months > 1 ? 's' : ''} remaining`;
|
962
|
+
} else {
|
963
|
+
const years = Math.ceil(daysToComplete / 365);
|
964
|
+
return `~${years} year${years > 1 ? 's' : ''} remaining`;
|
965
|
+
}
|
966
|
+
}
|
967
|
+
|
968
|
+
// Function to format large numbers
|
969
|
+
function formatGoalNumber(num) {
|
970
|
+
if (num >= 1000000) {
|
971
|
+
return (num / 1000000).toFixed(1) + 'M';
|
972
|
+
} else if (num >= 1000) {
|
973
|
+
return (num / 1000).toFixed(1) + 'K';
|
974
|
+
}
|
975
|
+
return num.toString();
|
976
|
+
}
|
977
|
+
|
978
|
+
// Function to update progress bar color based on percentage
|
979
|
+
function updateProgressBarColor(progressElement, percentage) {
|
980
|
+
// Remove existing completion classes
|
981
|
+
progressElement.classList.remove('completion-0', 'completion-25', 'completion-50', 'completion-75', 'completion-100');
|
982
|
+
|
983
|
+
// Add appropriate class based on percentage
|
984
|
+
if (percentage >= 100) {
|
985
|
+
progressElement.classList.add('completion-100');
|
986
|
+
} else if (percentage >= 75) {
|
987
|
+
progressElement.classList.add('completion-75');
|
988
|
+
} else if (percentage >= 50) {
|
989
|
+
progressElement.classList.add('completion-50');
|
990
|
+
} else if (percentage >= 25) {
|
991
|
+
progressElement.classList.add('completion-25');
|
992
|
+
} else {
|
993
|
+
progressElement.classList.add('completion-0');
|
994
|
+
}
|
995
|
+
}
|
996
|
+
|
997
|
+
// Helper function to update goal progress UI with provided data
|
998
|
+
function updateGoalProgressUI(allGamesStats, allLinesData) {
|
999
|
+
if (!allGamesStats) {
|
1000
|
+
throw new Error('No stats data available');
|
1001
|
+
}
|
1002
|
+
|
1003
|
+
// Calculate current progress
|
1004
|
+
const currentHours = allGamesStats.total_time_hours || 0;
|
1005
|
+
const currentCharacters = allGamesStats.total_characters || 0;
|
1006
|
+
const currentGames = allGamesStats.unique_games || 0;
|
1007
|
+
|
1008
|
+
// Calculate 90-day averages for projections
|
1009
|
+
const dailyHoursAvg = calculate90DayAverage(allLinesData, 'hours');
|
1010
|
+
const dailyCharsAvg = calculate90DayAverage(allLinesData, 'characters');
|
1011
|
+
const dailyGamesAvg = calculate90DayAverage(allLinesData, 'games');
|
1012
|
+
|
1013
|
+
// Update Hours Goal
|
1014
|
+
const hoursPercentage = Math.min(100, (currentHours / goalSettings.reading_hours_target) * 100);
|
1015
|
+
document.getElementById('goalHoursCurrent').textContent = Math.floor(currentHours).toLocaleString();
|
1016
|
+
document.getElementById('goalHoursTarget').textContent = goalSettings.reading_hours_target.toLocaleString();
|
1017
|
+
document.getElementById('goalHoursPercentage').textContent = Math.floor(hoursPercentage) + '%';
|
1018
|
+
document.getElementById('goalHoursProjection').textContent =
|
1019
|
+
formatProjection(currentHours, goalSettings.reading_hours_target, dailyHoursAvg, 'hours');
|
1020
|
+
|
1021
|
+
const hoursProgressBar = document.getElementById('goalHoursProgress');
|
1022
|
+
hoursProgressBar.style.width = hoursPercentage + '%';
|
1023
|
+
hoursProgressBar.setAttribute('data-percentage', Math.floor(hoursPercentage / 25) * 25);
|
1024
|
+
updateProgressBarColor(hoursProgressBar, hoursPercentage);
|
1025
|
+
|
1026
|
+
// Update Characters Goal
|
1027
|
+
const charsPercentage = Math.min(100, (currentCharacters / goalSettings.character_count_target) * 100);
|
1028
|
+
document.getElementById('goalCharsCurrent').textContent = formatGoalNumber(currentCharacters);
|
1029
|
+
document.getElementById('goalCharsTarget').textContent = formatGoalNumber(goalSettings.character_count_target);
|
1030
|
+
document.getElementById('goalCharsPercentage').textContent = Math.floor(charsPercentage) + '%';
|
1031
|
+
document.getElementById('goalCharsProjection').textContent =
|
1032
|
+
formatProjection(currentCharacters, goalSettings.character_count_target, dailyCharsAvg, 'characters');
|
1033
|
+
|
1034
|
+
const charsProgressBar = document.getElementById('goalCharsProgress');
|
1035
|
+
charsProgressBar.style.width = charsPercentage + '%';
|
1036
|
+
charsProgressBar.setAttribute('data-percentage', Math.floor(charsPercentage / 25) * 25);
|
1037
|
+
updateProgressBarColor(charsProgressBar, charsPercentage);
|
1038
|
+
|
1039
|
+
// Update Games Goal
|
1040
|
+
const gamesPercentage = Math.min(100, (currentGames / goalSettings.games_target) * 100);
|
1041
|
+
document.getElementById('goalGamesCurrent').textContent = currentGames.toLocaleString();
|
1042
|
+
document.getElementById('goalGamesTarget').textContent = goalSettings.games_target.toLocaleString();
|
1043
|
+
document.getElementById('goalGamesPercentage').textContent = Math.floor(gamesPercentage) + '%';
|
1044
|
+
document.getElementById('goalGamesProjection').textContent =
|
1045
|
+
formatProjection(currentGames, goalSettings.games_target, dailyGamesAvg, 'games');
|
1046
|
+
|
1047
|
+
const gamesProgressBar = document.getElementById('goalGamesProgress');
|
1048
|
+
gamesProgressBar.style.width = gamesPercentage + '%';
|
1049
|
+
gamesProgressBar.setAttribute('data-percentage', Math.floor(gamesPercentage / 25) * 25);
|
1050
|
+
updateProgressBarColor(gamesProgressBar, gamesPercentage);
|
1051
|
+
}
|
1052
|
+
|
1053
|
+
// Main function to load and display goal progress
|
1054
|
+
async function loadGoalProgress() {
|
1055
|
+
const goalProgressChart = document.getElementById('goalProgressChart');
|
1056
|
+
const goalProgressLoading = document.getElementById('goalProgressLoading');
|
1057
|
+
const goalProgressError = document.getElementById('goalProgressError');
|
1058
|
+
|
1059
|
+
if (!goalProgressChart) return;
|
1060
|
+
|
1061
|
+
try {
|
1062
|
+
// Show loading state
|
1063
|
+
goalProgressLoading.style.display = 'flex';
|
1064
|
+
goalProgressError.style.display = 'none';
|
1065
|
+
|
1066
|
+
// Load goal settings and stats data
|
1067
|
+
await loadGoalSettings();
|
1068
|
+
const response = await fetch('/api/stats');
|
1069
|
+
if (!response.ok) throw new Error('Failed to fetch stats data');
|
1070
|
+
|
1071
|
+
const data = await response.json();
|
1072
|
+
const allGamesStats = data.allGamesStats;
|
1073
|
+
const allLinesData = data.allLinesData || [];
|
1074
|
+
|
1075
|
+
// Update the UI using the shared helper function
|
1076
|
+
updateGoalProgressUI(allGamesStats, allLinesData);
|
1077
|
+
|
1078
|
+
// Hide loading state
|
1079
|
+
goalProgressLoading.style.display = 'none';
|
1080
|
+
|
1081
|
+
} catch (error) {
|
1082
|
+
console.error('Error loading goal progress:', error);
|
1083
|
+
goalProgressLoading.style.display = 'none';
|
1084
|
+
goalProgressError.style.display = 'block';
|
1085
|
+
}
|
1086
|
+
}
|
1087
|
+
|
789
1088
|
// Initial load with saved year preference
|
790
1089
|
const savedYear = localStorage.getItem('selectedHeatmapYear') || 'all';
|
791
1090
|
loadStatsData(savedYear);
|
792
1091
|
|
1092
|
+
// Function to update goal progress using existing stats data
|
1093
|
+
async function updateGoalProgressWithData(statsData) {
|
1094
|
+
const goalProgressChart = document.getElementById('goalProgressChart');
|
1095
|
+
const goalProgressLoading = document.getElementById('goalProgressLoading');
|
1096
|
+
const goalProgressError = document.getElementById('goalProgressError');
|
1097
|
+
|
1098
|
+
if (!goalProgressChart) return;
|
1099
|
+
|
1100
|
+
try {
|
1101
|
+
// Load goal settings if not already loaded
|
1102
|
+
if (!goalSettings.reading_hours_target) {
|
1103
|
+
await loadGoalSettings();
|
1104
|
+
}
|
1105
|
+
|
1106
|
+
const allGamesStats = statsData.allGamesStats;
|
1107
|
+
const allLinesData = statsData.allLinesData || [];
|
1108
|
+
|
1109
|
+
// Update the UI using the shared helper function
|
1110
|
+
updateGoalProgressUI(allGamesStats, allLinesData);
|
1111
|
+
|
1112
|
+
// Hide loading and error states
|
1113
|
+
goalProgressLoading.style.display = 'none';
|
1114
|
+
goalProgressError.style.display = 'none';
|
1115
|
+
|
1116
|
+
} catch (error) {
|
1117
|
+
console.error('Error updating goal progress:', error);
|
1118
|
+
goalProgressLoading.style.display = 'none';
|
1119
|
+
goalProgressError.style.display = 'block';
|
1120
|
+
}
|
1121
|
+
}
|
1122
|
+
|
1123
|
+
// Load goal progress initially
|
1124
|
+
setTimeout(() => {
|
1125
|
+
loadGoalProgress();
|
1126
|
+
}, 1000);
|
1127
|
+
|
1128
|
+
// Refresh goal progress when settings are updated
|
1129
|
+
window.addEventListener('settingsUpdated', () => {
|
1130
|
+
setTimeout(() => {
|
1131
|
+
loadGoalProgress();
|
1132
|
+
}, 500);
|
1133
|
+
});
|
1134
|
+
|
793
1135
|
// Make functions globally available
|
794
1136
|
window.createHeatmap = createHeatmap;
|
795
1137
|
window.loadStatsData = loadStatsData;
|
1138
|
+
window.loadGoalProgress = loadGoalProgress;
|
796
1139
|
|
797
1140
|
// Dashboard functionality
|
798
1141
|
function loadDashboardData(data = null) {
|
GameSentenceMiner/web/stats.py
CHANGED
@@ -2,7 +2,7 @@ import datetime
|
|
2
2
|
from collections import defaultdict
|
3
3
|
|
4
4
|
from GameSentenceMiner.util.db import GameLinesTable
|
5
|
-
from GameSentenceMiner.util.configuration import logger, get_config
|
5
|
+
from GameSentenceMiner.util.configuration import get_stats_config, logger, get_config
|
6
6
|
|
7
7
|
|
8
8
|
def is_kanji(char):
|
@@ -286,7 +286,7 @@ def calculate_actual_reading_time(timestamps, afk_timer_seconds=None):
|
|
286
286
|
return 0.0
|
287
287
|
|
288
288
|
if afk_timer_seconds is None:
|
289
|
-
afk_timer_seconds =
|
289
|
+
afk_timer_seconds = get_stats_config().afk_timer_seconds
|
290
290
|
|
291
291
|
# Sort timestamps to ensure chronological order
|
292
292
|
sorted_timestamps = sorted(timestamps)
|
@@ -442,7 +442,7 @@ def calculate_current_game_stats(all_lines):
|
|
442
442
|
sessions = 1
|
443
443
|
for i in range(1, len(sorted_timestamps)):
|
444
444
|
time_gap = sorted_timestamps[i] - sorted_timestamps[i-1]
|
445
|
-
if time_gap >
|
445
|
+
if time_gap > get_stats_config().session_gap_seconds:
|
446
446
|
sessions += 1
|
447
447
|
|
448
448
|
# Calculate daily activity for progress trend
|
@@ -40,7 +40,12 @@
|
|
40
40
|
autocomplete="off"
|
41
41
|
/>
|
42
42
|
</div>
|
43
|
-
|
43
|
+
<div class="regex-checkbox-container" style="margin-top: 8px;">
|
44
|
+
<label>
|
45
|
+
<input type="checkbox" id="regexCheckbox" />
|
46
|
+
Regex search
|
47
|
+
</label>
|
48
|
+
</div>
|
44
49
|
<div class="search-filters">
|
45
50
|
<div class="filter-group">
|
46
51
|
<label class="filter-label">Game:</label>
|