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.
@@ -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 chartData.labels.map((gameName, index) => ({
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 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
- }
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 chartData.labels.map((gameName, index) => ({
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 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
- }
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) {
@@ -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 = get_config().advanced.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 > get_config().advanced.session_gap_seconds:
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>