GameSentenceMiner 2.15.10__py3-none-any.whl → 2.15.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- GameSentenceMiner/anki.py +31 -0
- GameSentenceMiner/ocr/owocr_helper.py +5 -5
- GameSentenceMiner/web/static/css/kanji-grid.css +107 -0
- GameSentenceMiner/web/static/css/search.css +14 -0
- GameSentenceMiner/web/static/css/shared.css +932 -0
- GameSentenceMiner/web/static/css/stats.css +499 -0
- GameSentenceMiner/web/static/js/anki_stats.js +84 -0
- GameSentenceMiner/web/static/js/database.js +541 -0
- GameSentenceMiner/web/static/js/kanji-grid.js +203 -0
- GameSentenceMiner/web/static/js/search.js +273 -0
- GameSentenceMiner/web/static/js/shared.js +506 -0
- GameSentenceMiner/web/static/js/stats.js +1427 -0
- GameSentenceMiner/web/templates/anki_stats.html +205 -0
- GameSentenceMiner/web/templates/components/navigation.html +16 -0
- GameSentenceMiner/web/templates/components/theme-styles.html +128 -0
- GameSentenceMiner/web/templates/stats.html +4 -0
- GameSentenceMiner/web/texthooking_page.py +50 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/RECORD +23 -11
- GameSentenceMiner/web/templates/text_replacements.html +0 -449
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,506 @@
|
|
1
|
+
// Shared JavaScript functionality across all pages
|
2
|
+
|
3
|
+
// Modal Management Functions
|
4
|
+
function openModal(modalId) {
|
5
|
+
const modal = document.getElementById(modalId);
|
6
|
+
if (modal) {
|
7
|
+
modal.classList.add('show');
|
8
|
+
modal.style.display = 'flex';
|
9
|
+
}
|
10
|
+
}
|
11
|
+
|
12
|
+
function closeModal(modalId) {
|
13
|
+
const modal = document.getElementById(modalId);
|
14
|
+
if (modal) {
|
15
|
+
modal.classList.remove('show');
|
16
|
+
modal.style.display = 'none';
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
// Initialize modal close functionality (backdrop clicks and ESC key)
|
21
|
+
function initializeModalHandlers() {
|
22
|
+
// Close modals when clicking outside (backdrop)
|
23
|
+
document.querySelectorAll('.modal').forEach(modal => {
|
24
|
+
modal.addEventListener('click', (e) => {
|
25
|
+
if (e.target === modal) {
|
26
|
+
closeModal(modal.id);
|
27
|
+
}
|
28
|
+
});
|
29
|
+
});
|
30
|
+
|
31
|
+
// Close modals on ESC key press
|
32
|
+
document.addEventListener('keydown', (e) => {
|
33
|
+
if (e.key === 'Escape') {
|
34
|
+
const openModals = document.querySelectorAll('.modal.show');
|
35
|
+
openModals.forEach(modal => {
|
36
|
+
closeModal(modal.id);
|
37
|
+
});
|
38
|
+
}
|
39
|
+
});
|
40
|
+
}
|
41
|
+
|
42
|
+
// API Helper Functions
|
43
|
+
async function fetchWithErrorHandling(url, options = {}) {
|
44
|
+
try {
|
45
|
+
const response = await fetch(url, options);
|
46
|
+
const data = await response.json();
|
47
|
+
|
48
|
+
if (!response.ok) {
|
49
|
+
throw new Error(data.error || `HTTP ${response.status}: ${response.statusText}`);
|
50
|
+
}
|
51
|
+
|
52
|
+
return { success: true, data, status: response.status };
|
53
|
+
} catch (error) {
|
54
|
+
console.error(`API Error (${url}):`, error);
|
55
|
+
return { success: false, error: error.message, status: 0 };
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
async function loadGamesList() {
|
60
|
+
const result = await fetchWithErrorHandling('/api/games-list');
|
61
|
+
if (result.success) {
|
62
|
+
return result.data.games || [];
|
63
|
+
}
|
64
|
+
return [];
|
65
|
+
}
|
66
|
+
|
67
|
+
// UI Helper Functions
|
68
|
+
function showElement(element) {
|
69
|
+
if (element) {
|
70
|
+
element.style.display = '';
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
function hideElement(element) {
|
75
|
+
if (element) {
|
76
|
+
element.style.display = 'none';
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
function showElementFlex(element) {
|
81
|
+
if (element) {
|
82
|
+
element.style.display = 'flex';
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
function showElementBlock(element) {
|
87
|
+
if (element) {
|
88
|
+
element.style.display = 'block';
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
function toggleElement(element, show) {
|
93
|
+
if (element) {
|
94
|
+
element.style.display = show ? '' : 'none';
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
function showLoadingState(container) {
|
99
|
+
if (container) {
|
100
|
+
container.innerHTML = `
|
101
|
+
<div class="loading-indicator">
|
102
|
+
<div class="spinner"></div>
|
103
|
+
<span>Loading...</span>
|
104
|
+
</div>
|
105
|
+
`;
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
function showErrorState(container, message) {
|
110
|
+
if (container) {
|
111
|
+
container.innerHTML = `
|
112
|
+
<div class="error-message">
|
113
|
+
<strong>Error:</strong> ${escapeHtml(message)}
|
114
|
+
</div>
|
115
|
+
`;
|
116
|
+
}
|
117
|
+
}
|
118
|
+
|
119
|
+
// Form Validation Helpers
|
120
|
+
function validateRequired(value, fieldName) {
|
121
|
+
if (!value || value.trim() === '') {
|
122
|
+
throw new Error(`${fieldName} is required`);
|
123
|
+
}
|
124
|
+
return value.trim();
|
125
|
+
}
|
126
|
+
|
127
|
+
function validateNumber(value, fieldName, min = null, max = null) {
|
128
|
+
const num = Number(value);
|
129
|
+
if (isNaN(num)) {
|
130
|
+
throw new Error(`${fieldName} must be a valid number`);
|
131
|
+
}
|
132
|
+
if (min !== null && num < min) {
|
133
|
+
throw new Error(`${fieldName} must be at least ${min}`);
|
134
|
+
}
|
135
|
+
if (max !== null && num > max) {
|
136
|
+
throw new Error(`${fieldName} must be at most ${max}`);
|
137
|
+
}
|
138
|
+
return num;
|
139
|
+
}
|
140
|
+
|
141
|
+
// Dark mode toggle functionality
|
142
|
+
function initializeThemeToggle() {
|
143
|
+
const themeToggle = document.getElementById('themeToggle');
|
144
|
+
const themeIcon = document.getElementById('themeIcon');
|
145
|
+
const documentElement = document.documentElement;
|
146
|
+
|
147
|
+
if (!themeToggle || !themeIcon) {
|
148
|
+
console.warn('Theme toggle elements not found');
|
149
|
+
return;
|
150
|
+
}
|
151
|
+
|
152
|
+
// Check for saved theme preference or default to browser preference
|
153
|
+
function getPreferredTheme() {
|
154
|
+
const savedTheme = localStorage.getItem('theme');
|
155
|
+
if (savedTheme) {
|
156
|
+
return savedTheme;
|
157
|
+
}
|
158
|
+
|
159
|
+
// Check browser preference
|
160
|
+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
161
|
+
return 'dark';
|
162
|
+
}
|
163
|
+
|
164
|
+
return 'light';
|
165
|
+
}
|
166
|
+
|
167
|
+
// Apply theme
|
168
|
+
function applyTheme(theme) {
|
169
|
+
if (theme === 'dark') {
|
170
|
+
documentElement.setAttribute('data-theme', 'dark');
|
171
|
+
themeIcon.textContent = '☀️';
|
172
|
+
themeToggle.title = 'Switch to light mode';
|
173
|
+
} else {
|
174
|
+
documentElement.setAttribute('data-theme', 'light');
|
175
|
+
themeIcon.textContent = '🌙';
|
176
|
+
themeToggle.title = 'Switch to dark mode';
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
// Initialize theme
|
181
|
+
const currentTheme = getPreferredTheme();
|
182
|
+
applyTheme(currentTheme);
|
183
|
+
|
184
|
+
// Toggle theme on button click
|
185
|
+
themeToggle.addEventListener('click', () => {
|
186
|
+
const currentTheme = documentElement.getAttribute('data-theme');
|
187
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
188
|
+
|
189
|
+
applyTheme(newTheme);
|
190
|
+
localStorage.setItem('theme', newTheme);
|
191
|
+
location.reload();
|
192
|
+
});
|
193
|
+
|
194
|
+
// Listen for browser theme changes
|
195
|
+
if (window.matchMedia) {
|
196
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
197
|
+
mediaQuery.addEventListener('change', (e) => {
|
198
|
+
// Only auto-switch if user hasn't manually set a preference
|
199
|
+
if (!localStorage.getItem('theme')) {
|
200
|
+
applyTheme(e.matches ? 'dark' : 'light');
|
201
|
+
}
|
202
|
+
});
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
// Settings Modal Functionality (for pages that need it)
|
207
|
+
class SettingsManager {
|
208
|
+
constructor() {
|
209
|
+
this.initializeElements();
|
210
|
+
this.attachEventListeners();
|
211
|
+
}
|
212
|
+
|
213
|
+
initializeElements() {
|
214
|
+
this.settingsToggle = document.getElementById('settingsToggle');
|
215
|
+
this.settingsModal = document.getElementById('settingsModal');
|
216
|
+
this.closeSettingsModal = document.getElementById('closeSettingsModal');
|
217
|
+
this.cancelSettingsBtn = document.getElementById('cancelSettingsBtn');
|
218
|
+
this.saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
219
|
+
this.settingsError = document.getElementById('settingsError');
|
220
|
+
this.settingsSuccess = document.getElementById('settingsSuccess');
|
221
|
+
|
222
|
+
// Optional elements that may not exist on all pages
|
223
|
+
this.afkTimerInput = document.getElementById('afkTimer');
|
224
|
+
this.sessionGapInput = document.getElementById('sessionGap');
|
225
|
+
this.heatmapYearSelect = document.getElementById('heatmapYear');
|
226
|
+
this.streakRequirementInput = document.getElementById('streakRequirement');
|
227
|
+
}
|
228
|
+
|
229
|
+
attachEventListeners() {
|
230
|
+
if (!this.settingsToggle || !this.settingsModal) {
|
231
|
+
return; // Settings not available on this page
|
232
|
+
}
|
233
|
+
|
234
|
+
this.settingsToggle.addEventListener('click', () => this.openModal());
|
235
|
+
|
236
|
+
if (this.closeSettingsModal) {
|
237
|
+
this.closeSettingsModal.addEventListener('click', () => this.closeModal());
|
238
|
+
}
|
239
|
+
|
240
|
+
if (this.cancelSettingsBtn) {
|
241
|
+
this.cancelSettingsBtn.addEventListener('click', () => this.closeModal());
|
242
|
+
}
|
243
|
+
|
244
|
+
if (this.saveSettingsBtn) {
|
245
|
+
this.saveSettingsBtn.addEventListener('click', () => this.saveSettings());
|
246
|
+
}
|
247
|
+
|
248
|
+
// Close modal when clicking outside
|
249
|
+
if (this.settingsModal) {
|
250
|
+
this.settingsModal.addEventListener('click', (e) => {
|
251
|
+
if (e.target === this.settingsModal) {
|
252
|
+
this.closeModal();
|
253
|
+
}
|
254
|
+
});
|
255
|
+
}
|
256
|
+
|
257
|
+
// Clear messages when user starts typing
|
258
|
+
[this.afkTimerInput, this.sessionGapInput, this.heatmapYearSelect, this.streakRequirementInput]
|
259
|
+
.filter(Boolean)
|
260
|
+
.forEach(input => {
|
261
|
+
input.addEventListener('input', () => this.clearMessages());
|
262
|
+
});
|
263
|
+
|
264
|
+
// Handle year selection change
|
265
|
+
if (this.heatmapYearSelect) {
|
266
|
+
this.heatmapYearSelect.addEventListener('change', (e) => {
|
267
|
+
const selectedYear = e.target.value;
|
268
|
+
localStorage.setItem('selectedHeatmapYear', selectedYear);
|
269
|
+
this.refreshHeatmapData(selectedYear);
|
270
|
+
});
|
271
|
+
}
|
272
|
+
}
|
273
|
+
|
274
|
+
async openModal() {
|
275
|
+
try {
|
276
|
+
await this.loadCurrentSettings();
|
277
|
+
await this.loadAvailableYears();
|
278
|
+
this.showModal();
|
279
|
+
} catch (error) {
|
280
|
+
console.error('Error opening settings modal:', error);
|
281
|
+
this.showError('Failed to load current settings');
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
closeModal() {
|
286
|
+
this.hideModal();
|
287
|
+
this.clearMessages();
|
288
|
+
}
|
289
|
+
|
290
|
+
showModal() {
|
291
|
+
if (this.settingsModal) {
|
292
|
+
this.settingsModal.classList.add('show');
|
293
|
+
this.settingsModal.style.display = 'flex';
|
294
|
+
}
|
295
|
+
}
|
296
|
+
|
297
|
+
hideModal() {
|
298
|
+
if (this.settingsModal) {
|
299
|
+
this.settingsModal.classList.remove('show');
|
300
|
+
this.settingsModal.style.display = 'none';
|
301
|
+
}
|
302
|
+
}
|
303
|
+
|
304
|
+
async loadCurrentSettings() {
|
305
|
+
const response = await fetch('/api/settings');
|
306
|
+
if (!response.ok) {
|
307
|
+
throw new Error('Failed to fetch settings');
|
308
|
+
}
|
309
|
+
|
310
|
+
const settings = await response.json();
|
311
|
+
|
312
|
+
if (this.afkTimerInput) {
|
313
|
+
this.afkTimerInput.value = settings.afk_timer_seconds;
|
314
|
+
}
|
315
|
+
if (this.sessionGapInput) {
|
316
|
+
this.sessionGapInput.value = settings.session_gap_seconds;
|
317
|
+
}
|
318
|
+
if (this.streakRequirementInput) {
|
319
|
+
this.streakRequirementInput.value = settings.streak_requirement_hours || 1;
|
320
|
+
}
|
321
|
+
|
322
|
+
// Load saved year preference
|
323
|
+
const savedYear = localStorage.getItem('selectedHeatmapYear') || 'all';
|
324
|
+
if (this.heatmapYearSelect) {
|
325
|
+
this.heatmapYearSelect.value = savedYear;
|
326
|
+
}
|
327
|
+
}
|
328
|
+
|
329
|
+
async loadAvailableYears() {
|
330
|
+
if (!this.heatmapYearSelect) return;
|
331
|
+
|
332
|
+
try {
|
333
|
+
const response = await fetch('/api/stats');
|
334
|
+
if (!response.ok) throw new Error('Failed to fetch stats');
|
335
|
+
|
336
|
+
const data = await response.json();
|
337
|
+
const availableYears = Object.keys(data.heatmapData || {}).sort().reverse();
|
338
|
+
|
339
|
+
// Clear existing options except "All Years"
|
340
|
+
this.heatmapYearSelect.innerHTML = '<option value="all">All Years</option>';
|
341
|
+
|
342
|
+
// Add available years
|
343
|
+
availableYears.forEach(year => {
|
344
|
+
const option = document.createElement('option');
|
345
|
+
option.value = year;
|
346
|
+
option.textContent = year;
|
347
|
+
this.heatmapYearSelect.appendChild(option);
|
348
|
+
});
|
349
|
+
|
350
|
+
// Restore saved selection
|
351
|
+
const savedYear = localStorage.getItem('selectedHeatmapYear') || 'all';
|
352
|
+
this.heatmapYearSelect.value = savedYear;
|
353
|
+
|
354
|
+
} catch (error) {
|
355
|
+
console.error('Error loading available years:', error);
|
356
|
+
}
|
357
|
+
}
|
358
|
+
|
359
|
+
async refreshHeatmapData(selectedYear) {
|
360
|
+
try {
|
361
|
+
if (typeof loadStatsData === 'function') {
|
362
|
+
await loadStatsData(selectedYear);
|
363
|
+
}
|
364
|
+
} catch (error) {
|
365
|
+
console.error('Error refreshing heatmap data:', error);
|
366
|
+
}
|
367
|
+
}
|
368
|
+
|
369
|
+
async saveSettings() {
|
370
|
+
try {
|
371
|
+
this.clearMessages();
|
372
|
+
|
373
|
+
const settings = {};
|
374
|
+
|
375
|
+
if (this.afkTimerInput) {
|
376
|
+
const afkTimer = parseInt(this.afkTimerInput.value);
|
377
|
+
if (isNaN(afkTimer) || afkTimer < 30 || afkTimer > 600) {
|
378
|
+
this.showError('AFK timer must be between 30 and 600 seconds');
|
379
|
+
return;
|
380
|
+
}
|
381
|
+
settings.afk_timer_seconds = afkTimer;
|
382
|
+
}
|
383
|
+
|
384
|
+
if (this.sessionGapInput) {
|
385
|
+
const sessionGap = parseInt(this.sessionGapInput.value);
|
386
|
+
if (isNaN(sessionGap) || sessionGap < 300 || sessionGap > 7200) {
|
387
|
+
this.showError('Session gap must be between 300 and 7200 seconds');
|
388
|
+
return;
|
389
|
+
}
|
390
|
+
settings.session_gap_seconds = sessionGap;
|
391
|
+
}
|
392
|
+
|
393
|
+
if (this.streakRequirementInput) {
|
394
|
+
const streakRequirement = parseFloat(this.streakRequirementInput.value);
|
395
|
+
if (isNaN(streakRequirement) || streakRequirement < 0.01 || streakRequirement > 24) {
|
396
|
+
this.showError('Streak requirement must be between 0.01 and 24 hours');
|
397
|
+
return;
|
398
|
+
}
|
399
|
+
settings.streak_requirement_hours = streakRequirement;
|
400
|
+
}
|
401
|
+
|
402
|
+
// Show loading state
|
403
|
+
if (this.saveSettingsBtn) {
|
404
|
+
this.saveSettingsBtn.disabled = true;
|
405
|
+
this.saveSettingsBtn.textContent = 'Saving...';
|
406
|
+
}
|
407
|
+
|
408
|
+
const response = await fetch('/api/settings', {
|
409
|
+
method: 'POST',
|
410
|
+
headers: {
|
411
|
+
'Content-Type': 'application/json'
|
412
|
+
},
|
413
|
+
body: JSON.stringify(settings)
|
414
|
+
});
|
415
|
+
|
416
|
+
const result = await response.json();
|
417
|
+
|
418
|
+
if (!response.ok) {
|
419
|
+
throw new Error(result.error || 'Failed to save settings');
|
420
|
+
}
|
421
|
+
|
422
|
+
this.showSuccess('Settings saved successfully! Changes will apply to new calculations.');
|
423
|
+
|
424
|
+
// Auto-close modal after 2 seconds
|
425
|
+
setTimeout(() => {
|
426
|
+
this.closeModal();
|
427
|
+
}, 2000);
|
428
|
+
|
429
|
+
} catch (error) {
|
430
|
+
console.error('Error saving settings:', error);
|
431
|
+
this.showError(error.message || 'Failed to save settings');
|
432
|
+
} finally {
|
433
|
+
// Reset button state
|
434
|
+
if (this.saveSettingsBtn) {
|
435
|
+
this.saveSettingsBtn.disabled = false;
|
436
|
+
this.saveSettingsBtn.textContent = 'Save Settings';
|
437
|
+
}
|
438
|
+
}
|
439
|
+
}
|
440
|
+
|
441
|
+
showError(message) {
|
442
|
+
if (this.settingsError) {
|
443
|
+
this.settingsError.textContent = message;
|
444
|
+
this.settingsError.style.display = 'block';
|
445
|
+
}
|
446
|
+
if (this.settingsSuccess) {
|
447
|
+
this.settingsSuccess.style.display = 'none';
|
448
|
+
}
|
449
|
+
}
|
450
|
+
|
451
|
+
showSuccess(message) {
|
452
|
+
if (this.settingsSuccess) {
|
453
|
+
this.settingsSuccess.textContent = message;
|
454
|
+
this.settingsSuccess.style.display = 'block';
|
455
|
+
}
|
456
|
+
if (this.settingsError) {
|
457
|
+
this.settingsError.style.display = 'none';
|
458
|
+
}
|
459
|
+
}
|
460
|
+
|
461
|
+
clearMessages() {
|
462
|
+
if (this.settingsError) {
|
463
|
+
this.settingsError.style.display = 'none';
|
464
|
+
}
|
465
|
+
if (this.settingsSuccess) {
|
466
|
+
this.settingsSuccess.style.display = 'none';
|
467
|
+
}
|
468
|
+
}
|
469
|
+
}
|
470
|
+
|
471
|
+
// Utility functions
|
472
|
+
function formatLargeNumber(num) {
|
473
|
+
if (num >= 1000000) {
|
474
|
+
return (num / 1000000).toFixed(1) + 'M';
|
475
|
+
} else if (num >= 1000) {
|
476
|
+
return (num / 1000).toFixed(1) + 'K';
|
477
|
+
}
|
478
|
+
return num.toString();
|
479
|
+
}
|
480
|
+
|
481
|
+
function escapeHtml(unsafe) {
|
482
|
+
return unsafe
|
483
|
+
.replace(/&/g, "&")
|
484
|
+
.replace(/</g, "<")
|
485
|
+
.replace(/>/g, ">")
|
486
|
+
.replace(/"/g, """)
|
487
|
+
.replace(/'/g, "'");
|
488
|
+
}
|
489
|
+
|
490
|
+
function escapeRegex(string) {
|
491
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
492
|
+
}
|
493
|
+
|
494
|
+
// Initialize shared functionality when DOM loads
|
495
|
+
document.addEventListener('DOMContentLoaded', function() {
|
496
|
+
// Initialize theme toggle
|
497
|
+
initializeThemeToggle();
|
498
|
+
|
499
|
+
// Initialize modal handlers
|
500
|
+
initializeModalHandlers();
|
501
|
+
|
502
|
+
// Initialize settings manager if settings toggle exists
|
503
|
+
if (document.getElementById('settingsToggle')) {
|
504
|
+
new SettingsManager();
|
505
|
+
}
|
506
|
+
});
|