GameSentenceMiner 2.18.15__py3-none-any.whl → 2.18.16__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.

Files changed (34) hide show
  1. GameSentenceMiner/anki.py +8 -53
  2. GameSentenceMiner/ui/anki_confirmation.py +16 -2
  3. GameSentenceMiner/util/db.py +11 -7
  4. GameSentenceMiner/util/games_table.py +320 -0
  5. GameSentenceMiner/web/anki_api_endpoints.py +506 -0
  6. GameSentenceMiner/web/database_api.py +239 -117
  7. GameSentenceMiner/web/static/css/loading-skeleton.css +41 -0
  8. GameSentenceMiner/web/static/css/search.css +54 -0
  9. GameSentenceMiner/web/static/css/stats.css +76 -0
  10. GameSentenceMiner/web/static/js/anki_stats.js +304 -50
  11. GameSentenceMiner/web/static/js/database.js +44 -7
  12. GameSentenceMiner/web/static/js/heatmap.js +326 -0
  13. GameSentenceMiner/web/static/js/overview.js +20 -224
  14. GameSentenceMiner/web/static/js/search.js +190 -23
  15. GameSentenceMiner/web/static/js/stats.js +371 -1
  16. GameSentenceMiner/web/stats.py +188 -0
  17. GameSentenceMiner/web/templates/anki_stats.html +145 -58
  18. GameSentenceMiner/web/templates/components/date-range.html +19 -0
  19. GameSentenceMiner/web/templates/components/html-head.html +45 -0
  20. GameSentenceMiner/web/templates/components/js-config.html +37 -0
  21. GameSentenceMiner/web/templates/components/popups.html +15 -0
  22. GameSentenceMiner/web/templates/components/settings-modal.html +233 -0
  23. GameSentenceMiner/web/templates/database.html +13 -3
  24. GameSentenceMiner/web/templates/goals.html +9 -31
  25. GameSentenceMiner/web/templates/overview.html +16 -223
  26. GameSentenceMiner/web/templates/search.html +46 -0
  27. GameSentenceMiner/web/templates/stats.html +49 -311
  28. GameSentenceMiner/web/texthooking_page.py +4 -66
  29. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.16.dist-info}/METADATA +1 -1
  30. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.16.dist-info}/RECORD +34 -25
  31. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.16.dist-info}/WHEEL +0 -0
  32. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.16.dist-info}/entry_points.txt +0 -0
  33. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.16.dist-info}/licenses/LICENSE +0 -0
  34. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.16.dist-info}/top_level.txt +0 -0
@@ -1,32 +1,12 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Anki vs GSM Kanji Stats</title>
7
-
8
- <!-- Include html2canvas for screenshot functionality -->
9
- <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
10
-
11
- <!-- Include Chart.js from a CDN -->
12
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
13
-
14
- <!-- Include shared theme styles -->
15
- {% include 'components/theme-styles.html' %}
16
-
17
- <!-- Include shared CSS -->
18
- <link rel="stylesheet" href="/static/css/shared.css">
19
-
20
- <!-- Include shared dashboard and popup CSS -->
21
- <link rel="stylesheet" href="/static/css/dashboard-shared.css">
22
- <link rel="stylesheet" href="/static/css/popups-shared.css">
23
-
24
- <!-- Include stats-specific CSS -->
25
- <link rel="stylesheet" href="/static/css/stats.css">
26
-
27
- <!-- Include shared kanji grid CSS -->
28
- <link rel="stylesheet" href="/static/css/kanji-grid.css">
29
- </head>
3
+ {% set page_title = 'Anki and GSM Kanji Stats' %}
4
+ {% set page_specific_css = 'stats.css' %}
5
+ {% set include_kanji_grid_css = true %}
6
+ {% set include_chart_js = true %}
7
+ {% set include_html2canvas = true %}
8
+ {% include 'components/html-head.html' %}
9
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/loading-skeleton.css') }}">
30
10
  <body>
31
11
 
32
12
  <div class="container">
@@ -38,39 +18,137 @@
38
18
  <!-- Include shared navigation -->
39
19
  {% include 'components/navigation.html' %}
40
20
 
41
- <div class="dashboard-card date-range">
42
- <div class="dashboard-card-header">
43
- <h3 class="dashboard-card-title">
44
- <span class="dashboard-card-icon">📅</span>
45
- Date Range
46
- </h3>
21
+ <!-- Include shared date range component -->
22
+ {% include 'components/date-range.html' %}
23
+
24
+ <!-- Include shared popup components -->
25
+ {% include 'components/popups.html' %}
26
+
27
+ <!-- AnkiConnect Warning Banner -->
28
+ <div id="ankiConnectWarning" class="dashboard-card" style="display: none; background: var(--warning-bg, #fff3cd); border-left: 4px solid var(--warning-color, #f39c12); margin-bottom: 20px;">
29
+ <div style="padding: 16px; display: flex; align-items: center; gap: 12px;">
30
+ <span style="font-size: 24px;">⚠️</span>
31
+ <div style="flex: 1;">
32
+ <strong style="color: var(--warning-color, #f39c12); display: block; margin-bottom: 4px;">AnkiConnect Not Detected</strong>
33
+ <p style="margin: 0; color: var(--text-primary); font-size: 14px;">
34
+ Unable to connect to Anki. Please ensure Anki is running and <a href="https://ankiweb.net/shared/info/2055492159" target="_blank" style="color: var(--primary-color); text-decoration: underline;">AnkiConnect</a> is installed.
35
+ </p>
36
+ </div>
47
37
  </div>
38
+ </div>
48
39
 
49
- <div class="dashboard-date-range">
50
- <div class="dashboard-date-item tooltip" data-tooltip="Select the start date for your stats">
51
- <label for="fromDate">From</label>
52
- <input type="date" id="fromDate" class="dashboard-date-input">
40
+ <!-- Anki Game Stats Section -->
41
+ <div class="dashboard-container">
42
+ <div class="dashboard-card current-game" id="gameStatsCard">
43
+ <div class="dashboard-card-header">
44
+ <div>
45
+ <h3 class="dashboard-card-title">
46
+ <span class="dashboard-card-icon">🎮</span>
47
+ Anki Game Stats
48
+ </h3>
49
+ <p class="dashboard-card-subtitle">Review Anki statistics by game</p>
50
+ </div>
53
51
  </div>
54
- <div class="dashboard-date-item tooltip" data-tooltip="Select the end date for your stats">
55
- <label for="toDate">To</label>
56
- <input type="date" id="toDate" class="dashboard-date-input">
52
+
53
+ <div class="dashboard-progress-section">
54
+ <div id="gameStatsTableContainer">
55
+ <div id="gameStatsLoading" class="dashboard-loading" style="display: none;">
56
+ <div class="spinner"></div>
57
+ <span>Loading game statistics...</span>
58
+ </div>
59
+ <table id="gameStatsTable" class="stats-table">
60
+ <thead>
61
+ <tr>
62
+ <th>Game Name</th>
63
+ <th>Avg Time/Card</th>
64
+ <th>Retention</th>
65
+ <!-- <th>Mined Lines</th> -->
66
+ </tr>
67
+ </thead>
68
+ <tbody id="gameStatsTableBody">
69
+ <!-- Data will be populated by JavaScript -->
70
+ </tbody>
71
+ </table>
72
+ <div id="gameStatsEmpty" class="empty-message" style="display: none;">
73
+ No game statistics available for the selected date range.
74
+ </div>
75
+ </div>
57
76
  </div>
58
77
  </div>
59
78
  </div>
60
79
 
61
-
62
- <div id="dateErrorPopup" class="dashboard-popup hidden">
63
- <div class="dashboard-popup-content">
64
- <div class="dashboard-popup-icon">⚠️</div>
65
- <div class="dashboard-popup-message">"From" date cannot be later than "To" date.</div>
66
- <button id="closePopupBtn" class="dashboard-popup-btn">OK</button>
80
+ <!-- NSFW vs SFW Retention Section -->
81
+ <!--
82
+ <div class="dashboard-container">
83
+ <div class="dashboard-card current-game" id="nsfwSfwRetentionCard">
84
+ <div class="dashboard-card-header">
85
+ <div>
86
+ <h3 class="dashboard-card-title">
87
+ <span class="dashboard-card-icon">🔞</span>
88
+ Retention of NSFW vs SFW
89
+ </h3>
90
+ <p class="dashboard-card-subtitle">Compare retention rates between NSFW and SFW game content</p>
91
+ </div>
92
+ </div>
93
+
94
+ <div class="dashboard-stats-grid" id="nsfwSfwRetentionStats">
95
+ <div class="dashboard-stat-item tooltip" data-tooltip="Retention rate for NSFW tagged cards">
96
+ <span class="dashboard-stat-value" id="nsfwRetention" style="color: var(--text-secondary);">
97
+ <div class="loading-skeleton" style="width: 50px; height: 24px; display: inline-block;"></div>
98
+ </span>
99
+ <span class="dashboard-stat-label">NSFW Retention</span>
100
+ <span class="dashboard-stat-sublabel" id="nsfwReviews" style="font-size: 0.75rem; color: var(--text-tertiary);"></span>
101
+ </div>
102
+ <div class="dashboard-stat-item tooltip" data-tooltip="Retention rate for SFW tagged cards">
103
+ <span class="dashboard-stat-value" id="sfwRetention" style="color: var(--text-secondary);">
104
+ <div class="loading-skeleton" style="width: 50px; height: 24px; display: inline-block;"></div>
105
+ </span>
106
+ <span class="dashboard-stat-label">SFW Retention</span>
107
+ <span class="dashboard-stat-sublabel" id="sfwReviews" style="font-size: 0.75rem; color: var(--text-tertiary);"></span>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="dashboard-stats-grid" style="margin-top: 20px;">
112
+ <div class="dashboard-stat-item tooltip" data-tooltip="Average time per card for NSFW tagged cards">
113
+ <span class="dashboard-stat-value" id="nsfwAvgTime" style="color: var(--text-secondary);">
114
+ <div class="loading-skeleton" style="width: 40px; height: 24px; display: inline-block;"></div>
115
+ </span>
116
+ <span class="dashboard-stat-label">NSFW Avg Time</span>
117
+ </div>
118
+ <div class="dashboard-stat-item tooltip" data-tooltip="Average time per card for SFW tagged cards">
119
+ <span class="dashboard-stat-value" id="sfwAvgTime" style="color: var(--text-secondary);">
120
+ <div class="loading-skeleton" style="width: 40px; height: 24px; display: inline-block;"></div>
121
+ </span>
122
+ <span class="dashboard-stat-label">SFW Avg Time</span>
123
+ </div>
124
+ </div>
125
+
126
+ <div id="nsfwSfwRetentionLoading" class="dashboard-loading" style="display: none;">
127
+ <div class="spinner"></div>
128
+ <span>Loading retention statistics...</span>
129
+ </div>
130
+
131
+ <div id="nsfwSfwRetentionEmpty" class="empty-message" style="display: none;">
132
+ No NSFW/SFW retention data available for the selected date range.
133
+ </div>
67
134
  </div>
68
135
  </div>
136
+ -->
69
137
 
70
- <div id="noDataPopup" class="no-data-popup hidden">
71
- <div class="no-data-content">
72
- <p>No data found for the selected range.</p>
73
- <button id="closeNoDataPopup">OK</button>
138
+ <!-- Missing High-Frequency Kanji Section -->
139
+ <!-- Mining Heatmap Card (Full Width) -->
140
+ <div class="dashboard-card all-games" id="miningHeatmapCard" style="margin-bottom: 24px;">
141
+ <div class="dashboard-card-header">
142
+ <h3 class="dashboard-card-title">
143
+ <span class="dashboard-card-icon">📊</span>
144
+ Mining Heatmap
145
+ </h3>
146
+ <p class="dashboard-card-subtitle">Track your daily sentence mining progress</p>
147
+ </div>
148
+
149
+ <div class="dashboard-progress-section">
150
+ <div class="dashboard-progress-title">Activity Heatmap</div>
151
+ <div id="miningHeatmapContainer"></div>
74
152
  </div>
75
153
  </div>
76
154
 
@@ -94,19 +172,27 @@
94
172
 
95
173
  <div class="dashboard-stats-grid" id="missingKanjiStats">
96
174
  <div class="dashboard-stat-item tooltip" data-tooltip="Number of high-frequency kanji missing from Anki">
97
- <span class="dashboard-stat-value" id="missingKanjiCount">-</span>
175
+ <span class="dashboard-stat-value" id="missingKanjiCount">
176
+ <div class="loading-skeleton" style="width: 40px; height: 24px; display: inline-block;"></div>
177
+ </span>
98
178
  <span class="dashboard-stat-label">Missing Kanji</span>
99
179
  </div>
100
180
  <div class="dashboard-stat-item tooltip" data-tooltip="Total kanji in your Anki collection">
101
- <span class="dashboard-stat-value" id="ankiTotalKanji">-</span>
181
+ <span class="dashboard-stat-value" id="ankiTotalKanji">
182
+ <div class="loading-skeleton" style="width: 40px; height: 24px; display: inline-block;"></div>
183
+ </span>
102
184
  <span class="dashboard-stat-label">Kanji in Anki</span>
103
185
  </div>
104
186
  <div class="dashboard-stat-item tooltip" data-tooltip="Total unique kanji seen in GSM">
105
- <span class="dashboard-stat-value" id="gsmTotalKanji">-</span>
187
+ <span class="dashboard-stat-value" id="gsmTotalKanji">
188
+ <div class="loading-skeleton" style="width: 40px; height: 24px; display: inline-block;"></div>
189
+ </span>
106
190
  <span class="dashboard-stat-label">Kanji in GSM</span>
107
191
  </div>
108
192
  <div class="dashboard-stat-item tooltip" data-tooltip="Percentage of GSM kanji covered by Anki">
109
- <span class="dashboard-stat-value" id="ankiCoverage">-</span>
193
+ <span class="dashboard-stat-value" id="ankiCoverage">
194
+ <div class="loading-skeleton" style="width: 50px; height: 24px; display: inline-block;"></div>
195
+ </span>
110
196
  <span class="dashboard-stat-label">Coverage %</span>
111
197
  </div>
112
198
  </div>
@@ -241,10 +327,11 @@
241
327
  </div>
242
328
  </div>
243
329
 
244
- <!-- Include shared JavaScript first (required dependency for anki_stats.js) -->
245
- <script src="/static/js/shared.js"></script>
246
- <script src="/static/js/kanji-grid.js"></script>
247
- <script src="/static/js/anki_stats.js"></script>
330
+ <!-- Include shared JavaScript configuration -->
331
+ {% set include_kanji_grid_js = true %}
332
+ {% set include_heatmap_js = true %}
333
+ {% set page_specific_js = 'anki_stats.js' %}
334
+ {% include 'components/js-config.html' %}
248
335
 
249
336
  </body>
250
337
  </html>
@@ -0,0 +1,19 @@
1
+ <!-- Date Range Component -->
2
+ <div class="dashboard-card date-range">
3
+ <div class="dashboard-card-header">
4
+ <h3 class="dashboard-card-title">
5
+ <span class="dashboard-card-icon">📅</span>
6
+ Date Range
7
+ </h3>
8
+ </div>
9
+ <div class="dashboard-date-range">
10
+ <div class="dashboard-date-item tooltip" data-tooltip="{{ from_tooltip | default('Select the start date for your stats') }}">
11
+ <label for="{{ from_id | default('fromDate') }}">From</label>
12
+ <input type="date" id="{{ from_id | default('fromDate') }}" class="dashboard-date-input">
13
+ </div>
14
+ <div class="dashboard-date-item tooltip" data-tooltip="{{ to_tooltip | default('Select the end date for your stats') }}">
15
+ <label for="{{ to_id | default('toDate') }}">To</label>
16
+ <input type="date" id="{{ to_id | default('toDate') }}" class="dashboard-date-input">
17
+ </div>
18
+ </div>
19
+ </div>
@@ -0,0 +1,45 @@
1
+ <!-- HTML Head Component -->
2
+ <head>
3
+ <meta charset="UTF-8">
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
5
+ <title>{{ page_title | default('GSM Dashboard') }}</title>
6
+
7
+ {% if include_chartjs | default(true) %}
8
+ <!-- Include Chart.js from a CDN -->
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
10
+ {% endif %}
11
+
12
+ {% if include_html2canvas | default(true) %}
13
+ <!-- Include html2canvas for screenshot functionality -->
14
+ <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
15
+ {% endif %}
16
+
17
+ <!-- Include shared theme styles -->
18
+ {% include 'components/theme-styles.html' %}
19
+
20
+ <!-- Include shared CSS -->
21
+ <link rel="stylesheet" href="/static/css/shared.css">
22
+
23
+ {% if include_dashboard_css | default(true) %}
24
+ <!-- Include shared dashboard and popup CSS -->
25
+ <link rel="stylesheet" href="/static/css/dashboard-shared.css">
26
+ <link rel="stylesheet" href="/static/css/popups-shared.css">
27
+ {% endif %}
28
+
29
+ {% if page_specific_css %}
30
+ <!-- Include page-specific CSS -->
31
+ <link rel="stylesheet" href="/static/css/{{ page_specific_css }}">
32
+ {% endif %}
33
+
34
+ {% if include_kanji_grid_css | default(false) %}
35
+ <!-- Include shared kanji grid CSS -->
36
+ <link rel="stylesheet" href="/static/css/kanji-grid.css">
37
+ {% endif %}
38
+
39
+ {% if additional_css %}
40
+ <!-- Additional CSS files -->
41
+ {% for css_file in additional_css %}
42
+ <link rel="stylesheet" href="/static/css/{{ css_file }}">
43
+ {% endfor %}
44
+ {% endif %}
45
+ </head>
@@ -0,0 +1,37 @@
1
+ <!-- JavaScript Configuration Component -->
2
+ {% if config_vars %}
3
+ <!-- Inject config values for frontend -->
4
+ <script>
5
+ window.{{ config_object_name | default('statsConfig') }} = {{ config_vars | tojson }};
6
+ </script>
7
+ {% endif %}
8
+
9
+ <!-- Include shared JavaScript first (required dependency) -->
10
+ <script src="/static/js/shared.js"></script>
11
+
12
+ {% if include_kanji_grid_js | default(false) %}
13
+ <script src="/static/js/kanji-grid.js"></script>
14
+ {% endif %}
15
+
16
+ {% if include_heatmap_js | default(false) %}
17
+ <script src="/static/js/heatmap.js"></script>
18
+ {% endif %}
19
+
20
+ {% if page_specific_js %}
21
+ <!-- Include page-specific JavaScript -->
22
+ <script src="/static/js/{{ page_specific_js }}"></script>
23
+ {% endif %}
24
+
25
+ {% if additional_js %}
26
+ <!-- Additional JavaScript files -->
27
+ {% for js_file in additional_js %}
28
+ <script src="/static/js/{{ js_file }}"></script>
29
+ {% endfor %}
30
+ {% endif %}
31
+
32
+ {% if inline_js %}
33
+ <!-- Inline JavaScript -->
34
+ <script>
35
+ {{ inline_js | safe }}
36
+ </script>
37
+ {% endif %}
@@ -0,0 +1,15 @@
1
+ <!-- Shared Popup Components -->
2
+ <div id="{{ date_error_id | default('dateErrorPopup') }}" class="dashboard-popup hidden">
3
+ <div class="dashboard-popup-content">
4
+ <div class="dashboard-popup-icon">⚠️</div>
5
+ <div class="dashboard-popup-message">{{ date_error_message | default('"From" date cannot be later than "To" date.') }}</div>
6
+ <button id="{{ date_error_btn_id | default('closePopupBtn') }}" class="dashboard-popup-btn">OK</button>
7
+ </div>
8
+ </div>
9
+
10
+ <div id="{{ no_data_id | default('noDataPopup') }}" class="no-data-popup hidden">
11
+ <div class="no-data-content">
12
+ <p>{{ no_data_message | default('No data found for the selected range.') }}</p>
13
+ <button id="{{ no_data_btn_id | default('closeNoDataPopup') }}">OK</button>
14
+ </div>
15
+ </div>
@@ -0,0 +1,233 @@
1
+ <!-- Settings Modal Component -->
2
+ <div id="{{ modal_id | default('settingsModal') }}" class="modal">
3
+ <div class="modal-content">
4
+ <div class="modal-header">
5
+ <h3>{{ title | default('Statistics Settings') }}</h3>
6
+ <span class="close-btn" id="{{ close_btn_id | default('closeSettingsModal') }}">&times;</span>
7
+ </div>
8
+ <div class="modal-body">
9
+ <p style="color: var(--text-secondary); margin-bottom: 20px;">
10
+ {{ description | default('Configure how reading time and sessions are calculated for your statistics.') }}
11
+ </p>
12
+ <form id="{{ form_id | default('settingsForm') }}">
13
+ {% if show_timezone_section | default(false) %}
14
+ <!-- Timezone Section (commented out in original) -->
15
+ <!-- <div style="margin-bottom: 20px;">
16
+ <label for="timezone-select"
17
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
18
+ Timezone
19
+ </label>
20
+ <div style="display: flex; gap: 8px;">
21
+ <select id="timezone-select" name="timezone"
22
+ style="flex-grow: 1; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;">
23
+ </select>
24
+ <button id="set-local-timezone-btn"
25
+ style="padding: 10px; border: none; border-radius: 5px; background: var(--primary-color); color: white; font-size: 14px; cursor: pointer;">
26
+ Set to Local
27
+ </button>
28
+ </div>
29
+ <small style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
30
+ Select the timezone to base your statistics on
31
+ </small>
32
+ </div> -->
33
+ {% endif %}
34
+
35
+ {% if show_basic_settings | default(true) %}
36
+ <div style="margin-bottom: 20px;">
37
+ <label for="{{ afk_timer_id | default('afkTimer') }}"
38
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
39
+ AFK Timer (seconds)
40
+ </label>
41
+ <input type="number" id="{{ afk_timer_id | default('afkTimer') }}" name="afk_timer_seconds"
42
+ style="padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
43
+ placeholder="120">
44
+ <small
45
+ style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
46
+ Maximum time between activities that still counts as active reading
47
+ </small>
48
+ </div>
49
+
50
+ <div style="margin-bottom: 20px;">
51
+ <label for="{{ session_gap_id | default('sessionGap') }}"
52
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
53
+ Session Gap (seconds)
54
+ </label>
55
+ <input type="number" id="{{ session_gap_id | default('sessionGap') }}" name="session_gap_seconds"
56
+ style="padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
57
+ placeholder="3600">
58
+ <small
59
+ style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
60
+ Time gap that triggers a new reading session
61
+ </small>
62
+ </div>
63
+
64
+ <div style="margin-bottom: 20px;">
65
+ <label for="{{ streak_req_id | default('streakRequirement') }}"
66
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
67
+ Streak Requirement (hours)
68
+ </label>
69
+ <input type="number" id="{{ streak_req_id | default('streakRequirement') }}" name="streak_requirement_hours" min="0.01"
70
+ max="24" step="0.01"
71
+ style="padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
72
+ placeholder="1.0">
73
+ <small
74
+ style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
75
+ Minimum hours of reading activity required to maintain streak (0.01-24 hours)
76
+ </small>
77
+ </div>
78
+ {% endif %}
79
+
80
+ {% if show_goals_section | default(true) %}
81
+ <!-- Reading Goals Section -->
82
+ <div style="margin-bottom: 20px; padding-top: 20px; border-top: 1px solid var(--border-color);">
83
+ <label
84
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
85
+ Reading Goals Configuration
86
+ </label>
87
+ <p style="color: var(--text-secondary); margin-bottom: 20px; font-size: 14px;">
88
+ Set your long-term reading targets for tracking progress and projections.
89
+ </p>
90
+
91
+ <div style="margin-bottom: 15px;">
92
+ <label for="{{ hours_target_id | default('readingHoursTarget') }}"
93
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
94
+ Reading Hours Target
95
+ </label>
96
+ <input type="number" id="{{ hours_target_id | default('readingHoursTarget') }}" name="reading_hours_target" min="1"
97
+ max="10000"
98
+ style="padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
99
+ placeholder="1500">
100
+ <small
101
+ style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
102
+ Total reading hours goal (1-10,000 hours) - Default: 1500 hours based on TMW N1
103
+ achievement
104
+ </small>
105
+ </div>
106
+
107
+ <div style="margin-bottom: 15px;">
108
+ <label for="{{ chars_target_id | default('characterCountTarget') }}"
109
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
110
+ Character Count Target
111
+ </label>
112
+ <input type="number" id="{{ chars_target_id | default('characterCountTarget') }}" name="character_count_target" min="1000"
113
+ max="1000000000"
114
+ style="padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
115
+ placeholder="25000000">
116
+ <small
117
+ style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
118
+ Total characters read goal (1,000-1,000,000,000) - Default: 25 million characters
119
+ </small>
120
+ </div>
121
+
122
+ {% if show_target_dates | default(true) %}
123
+ <div style="margin-bottom: 15px;">
124
+ <label for="{{ hours_date_id | default('readingHoursTargetDate') }}"
125
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
126
+ Reading Hours Target Date
127
+ </label>
128
+ <input type="date" id="{{ hours_date_id | default('readingHoursTargetDate') }}" name="reading_hours_target_date"
129
+ style="width: 90%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
130
+ data-date-format="yyyy-mm-dd">
131
+ <small
132
+ style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
133
+ Target date to achieve your reading hours goal
134
+ </small>
135
+ </div>
136
+
137
+ <div style="margin-bottom: 15px;">
138
+ <label for="{{ chars_date_id | default('characterCountTargetDate') }}"
139
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
140
+ Character Count Target Date
141
+ </label>
142
+ <input type="date" id="{{ chars_date_id | default('characterCountTargetDate') }}" name="character_count_target_date"
143
+ style="width: 90%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
144
+ data-date-format="yyyy-mm-dd">
145
+ <small
146
+ style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
147
+ Target date to achieve your character count goal
148
+ </small>
149
+ </div>
150
+ {% endif %}
151
+
152
+ <div style="margin-bottom: 15px;">
153
+ <label for="{{ games_target_id | default('gamesTarget') }}"
154
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
155
+ Games/Visual Novels Target
156
+ </label>
157
+ <input type="number" id="{{ games_target_id | default('gamesTarget') }}" name="games_target" min="1" max="1000"
158
+ style="width: 90%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
159
+ placeholder="100">
160
+ <small
161
+ style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
162
+ Number of games/visual novels to complete (1-1,000) - Default: 100 based on Refold
163
+ standards
164
+ </small>
165
+ </div>
166
+
167
+ {% if show_target_dates | default(true) %}
168
+ <div style="margin-bottom: 15px;">
169
+ <label for="{{ games_date_id | default('gamesTargetDate') }}"
170
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
171
+ Games/Visual Novels Target Date
172
+ </label>
173
+ <input type="date" id="{{ games_date_id | default('gamesTargetDate') }}" name="games_target_date"
174
+ style="width: 90%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
175
+ data-date-format="yyyy-mm-dd">
176
+ <small
177
+ style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
178
+ Target date to achieve your games goal
179
+ </small>
180
+ </div>
181
+ {% endif %}
182
+ </div>
183
+ {% endif %}
184
+
185
+ {% if show_exstatic_section | default(true) %}
186
+ <!-- Import ExStatic Lines Section -->
187
+ <div style="margin-bottom: 20px; padding-top: 20px; border-top: 1px solid var(--border-color);">
188
+ <label
189
+ style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
190
+ Import ExStatic Lines
191
+ </label>
192
+ <div style="margin-bottom: 10px;">
193
+ <input type="file" id="{{ exstatic_file_id | default('exstaticFile') }}" accept=".csv"
194
+ style="width: 90%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;">
195
+ <small
196
+ style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
197
+ Select an ExStatic CSV file to import reading data into GSM
198
+ </small>
199
+ </div>
200
+ <button type="button" id="{{ import_btn_id | default('importExstaticBtn') }}"
201
+ style="width: 90%; padding: 10px; background: #666; color: white; border: none; border-radius: 5px; font-size: 14px; cursor: not-allowed; margin-bottom: 10px;"
202
+ disabled>
203
+ Import ExStatic Lines
204
+ </button>
205
+ <div id="{{ import_progress_id | default('importProgress') }}" style="display: none; margin-bottom: 10px;">
206
+ <div
207
+ style="background: var(--bg-tertiary); border-radius: 5px; overflow: hidden; height: 20px; position: relative;">
208
+ <div id="{{ import_progress_bar_id | default('importProgressBar') }}"
209
+ style="background: var(--primary-color); height: 100%; width: 0%; transition: width 0.3s ease;">
210
+ </div>
211
+ <span id="{{ import_progress_text_id | default('importProgressText') }}"
212
+ style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 12px; color: var(--text-primary);">0%</span>
213
+ </div>
214
+ </div>
215
+ <div id="{{ import_status_id | default('importStatus') }}"
216
+ style="display: none; padding: 10px; border-radius: 5px; font-size: 14px;"></div>
217
+ </div>
218
+ {% endif %}
219
+ </form>
220
+
221
+ <div id="{{ error_id | default('settingsError') }}"
222
+ style="display: none; background: var(--danger-color); color: white; padding: 10px; border-radius: 5px; margin-bottom: 15px; font-size: 14px;">
223
+ </div>
224
+ <div id="{{ success_id | default('settingsSuccess') }}"
225
+ style="display: none; background: var(--success-color); color: white; padding: 10px; border-radius: 5px; margin-bottom: 15px; font-size: 14px;">
226
+ </div>
227
+ </div>
228
+ <div class="modal-footer">
229
+ <button id="{{ cancel_btn_id | default('cancelSettingsBtn') }}" class="cancel-btn">Cancel</button>
230
+ <button id="{{ save_btn_id | default('saveSettingsBtn') }}" class="confirm-delete-btn">Save Settings</button>
231
+ </div>
232
+ </div>
233
+ </div>
@@ -231,11 +231,21 @@
231
231
  </small>
232
232
  </div>
233
233
 
234
- <div class="form-group">
234
+ <div class="form-group" id="timeWindowGroup">
235
235
  <label class="form-label">Time Window (minutes):</label>
236
- <input type="number" id="timeWindow" class="form-input" min="1" max="1440" value="5" placeholder="5">
236
+ <input type="number" id="timeWindow" class="form-input" min="1" value="5" placeholder="5">
237
237
  <small style="color: var(--text-tertiary); font-size: 12px;">
238
- Only remove duplicates that appear within this many minutes of each other (1-1440 minutes).
238
+ Only remove duplicates that appear within this many minutes of each other (minimum 1 minute, no maximum limit).
239
+ </small>
240
+ </div>
241
+
242
+ <div class="form-group">
243
+ <div class="checkbox-container">
244
+ <input type="checkbox" id="ignoreTimeWindow" class="checkbox-input">
245
+ <label for="ignoreTimeWindow" class="checkbox-label">Entire Game (ignore time window)</label>
246
+ </div>
247
+ <small style="color: var(--text-tertiary); font-size: 12px; margin-left: 25px; display: block;">
248
+ Remove ALL duplicates within each game regardless of when they occurred.
239
249
  </small>
240
250
  </div>
241
251