GameSentenceMiner 2.15.11__py3-none-any.whl → 2.16.0__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.
@@ -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, "&amp;")
484
+ .replace(/</g, "&lt;")
485
+ .replace(/>/g, "&gt;")
486
+ .replace(/"/g, "&quot;")
487
+ .replace(/'/g, "&#039;");
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
+ });