GameSentenceMiner 2.18.15__py3-none-any.whl → 2.18.17__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.
Files changed (37) hide show
  1. GameSentenceMiner/anki.py +8 -53
  2. GameSentenceMiner/owocr/owocr/ocr.py +3 -2
  3. GameSentenceMiner/owocr/owocr/run.py +5 -1
  4. GameSentenceMiner/ui/anki_confirmation.py +16 -2
  5. GameSentenceMiner/util/configuration.py +6 -9
  6. GameSentenceMiner/util/db.py +11 -7
  7. GameSentenceMiner/util/games_table.py +320 -0
  8. GameSentenceMiner/web/anki_api_endpoints.py +506 -0
  9. GameSentenceMiner/web/database_api.py +239 -117
  10. GameSentenceMiner/web/static/css/loading-skeleton.css +41 -0
  11. GameSentenceMiner/web/static/css/search.css +54 -0
  12. GameSentenceMiner/web/static/css/stats.css +76 -0
  13. GameSentenceMiner/web/static/js/anki_stats.js +304 -50
  14. GameSentenceMiner/web/static/js/database.js +44 -7
  15. GameSentenceMiner/web/static/js/heatmap.js +326 -0
  16. GameSentenceMiner/web/static/js/overview.js +20 -224
  17. GameSentenceMiner/web/static/js/search.js +190 -23
  18. GameSentenceMiner/web/static/js/stats.js +371 -1
  19. GameSentenceMiner/web/stats.py +188 -0
  20. GameSentenceMiner/web/templates/anki_stats.html +145 -58
  21. GameSentenceMiner/web/templates/components/date-range.html +19 -0
  22. GameSentenceMiner/web/templates/components/html-head.html +45 -0
  23. GameSentenceMiner/web/templates/components/js-config.html +37 -0
  24. GameSentenceMiner/web/templates/components/popups.html +15 -0
  25. GameSentenceMiner/web/templates/components/settings-modal.html +233 -0
  26. GameSentenceMiner/web/templates/database.html +13 -3
  27. GameSentenceMiner/web/templates/goals.html +9 -31
  28. GameSentenceMiner/web/templates/overview.html +16 -223
  29. GameSentenceMiner/web/templates/search.html +46 -0
  30. GameSentenceMiner/web/templates/stats.html +49 -311
  31. GameSentenceMiner/web/texthooking_page.py +4 -66
  32. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/METADATA +1 -1
  33. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/RECORD +37 -28
  34. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/WHEEL +0 -0
  35. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/entry_points.txt +0 -0
  36. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/licenses/LICENSE +0 -0
  37. {gamesentenceminer-2.18.15.dist-info → gamesentenceminer-2.18.17.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,3 @@
1
- // Search Page JavaScript
2
- // Dependencies: shared.js (provides utility functions like escapeHtml, escapeRegex)
3
-
4
1
  class SentenceSearchApp {
5
2
  constructor() {
6
3
  this.searchInput = document.getElementById('searchInput');
@@ -14,6 +11,9 @@ class SentenceSearchApp {
14
11
  this.searchStats = document.getElementById('searchStats');
15
12
  this.searchTime = document.getElementById('searchTime');
16
13
  this.regexCheckbox = document.getElementById('regexCheckbox');
14
+ this.deleteLinesBtn = document.getElementById('deleteLinesBtn');
15
+ this.selectAllBtn = document.getElementById('selectAllBtn');
16
+ this.pageSizeFilter = document.getElementById('pageSizeFilter');
17
17
 
18
18
  this.currentPage = 1;
19
19
  this.pageSize = 20;
@@ -21,30 +21,31 @@ class SentenceSearchApp {
21
21
  this.currentQuery = '';
22
22
  this.totalResults = 0;
23
23
  this.currentUseRegex = false;
24
-
25
- // Move initialization logic to async method
26
24
  this.initialize();
27
25
  }
28
26
 
29
27
  async initialize() {
30
- // Check for ?q= parameter and pre-fill input
31
28
  const urlParams = new URLSearchParams(window.location.search);
32
29
  const qParam = urlParams.get('q');
33
30
  if (qParam) {
34
31
  this.searchInput.value = qParam;
35
32
  }
36
33
 
34
+ if (this.pageSizeFilter) {
35
+ this.pageSizeFilter.value = this.pageSize.toString();
36
+ } else {
37
+ console.error('Page size filter element not found!');
38
+ }
39
+
37
40
  this.initializeEventListeners();
38
41
  await this.loadGamesList();
39
42
 
40
- // Trigger search after games list loads if q param is present
41
43
  if (qParam) {
42
44
  this.performSearch();
43
45
  }
44
46
  }
45
47
 
46
48
  initializeEventListeners() {
47
- // Debounced search input
48
49
  this.searchInput.addEventListener('input', (e) => {
49
50
  clearTimeout(this.searchTimeout);
50
51
  this.searchTimeout = setTimeout(() => {
@@ -52,11 +53,17 @@ class SentenceSearchApp {
52
53
  }, 300);
53
54
  });
54
55
 
55
- // Filter changes
56
56
  this.gameFilter.addEventListener('change', () => this.performSearch());
57
57
  this.sortFilter.addEventListener('change', () => this.performSearch());
58
58
 
59
- // Pagination
59
+ if (this.pageSizeFilter) {
60
+ this.pageSizeFilter.addEventListener('change', () => {
61
+ this.pageSize = parseInt(this.pageSizeFilter.value);
62
+ this.currentPage = 1;
63
+ this.performSearch();
64
+ });
65
+ }
66
+
60
67
  document.getElementById('prevPage').addEventListener('click', () => {
61
68
  if (this.currentPage > 1) {
62
69
  this.currentPage--;
@@ -69,12 +76,23 @@ class SentenceSearchApp {
69
76
  this.performSearch();
70
77
  });
71
78
 
72
- // Regex checkbox toggle triggers search
73
79
  if (this.regexCheckbox) {
74
80
  this.regexCheckbox.addEventListener('change', () => {
75
81
  this.performSearch();
76
82
  });
77
83
  }
84
+
85
+ if (this.deleteLinesBtn) {
86
+ this.deleteLinesBtn.addEventListener('click', () => {
87
+ this.showDeleteConfirmation();
88
+ });
89
+ }
90
+
91
+ if (this.selectAllBtn) {
92
+ this.selectAllBtn.addEventListener('click', () => {
93
+ this.toggleSelectAll();
94
+ });
95
+ }
78
96
  }
79
97
 
80
98
  async loadGamesList() {
@@ -84,7 +102,6 @@ class SentenceSearchApp {
84
102
 
85
103
  if (response.ok && data.games) {
86
104
  const gameSelect = this.gameFilter;
87
- // Clear existing options except "All Games"
88
105
  gameSelect.innerHTML = '<option value="">All Games</option>';
89
106
 
90
107
  data.games.forEach(game => {
@@ -105,14 +122,12 @@ class SentenceSearchApp {
105
122
  const sortBy = this.sortFilter.value;
106
123
  const useRegex = this.regexCheckbox && this.regexCheckbox.checked;
107
124
 
108
- // Reset to first page for new searches or regex toggle
109
125
  if (query !== this.currentQuery || useRegex !== this.currentUseRegex) {
110
126
  this.currentPage = 1;
111
127
  }
112
128
  this.currentQuery = query;
113
129
  this.currentUseRegex = useRegex;
114
130
 
115
- // Show appropriate state
116
131
  if (!query) {
117
132
  this.showEmptyState();
118
133
  return;
@@ -156,7 +171,6 @@ class SentenceSearchApp {
156
171
  this.hideAllStates();
157
172
  this.totalResults = data.total;
158
173
 
159
- // Update stats
160
174
  const resultText = data.total === 1 ? 'result' : 'results';
161
175
  this.searchStats.textContent = `${data.total.toLocaleString()} ${resultText} found`;
162
176
  this.searchTime.textContent = `Search completed in ${searchTime}ms`;
@@ -165,8 +179,7 @@ class SentenceSearchApp {
165
179
  this.showNoResultsState();
166
180
  return;
167
181
  }
168
-
169
- // Render results
182
+
170
183
  this.searchResults.innerHTML = '';
171
184
  data.results.forEach(result => {
172
185
  const resultElement = this.createResultElement(result);
@@ -175,20 +188,39 @@ class SentenceSearchApp {
175
188
 
176
189
  this.updatePagination(data);
177
190
  this.searchResults.style.display = 'block';
191
+ this.updateDeleteButtonState();
178
192
  }
179
193
 
180
194
  createResultElement(result) {
181
195
  const div = document.createElement('div');
182
196
  div.className = 'search-result';
197
+ div.style.display = 'flex';
198
+ div.style.alignItems = 'flex-start';
199
+ div.style.gap = '12px';
200
+
201
+ const checkbox = document.createElement('input');
202
+ checkbox.type = 'checkbox';
203
+ checkbox.className = 'line-checkbox';
204
+ checkbox.dataset.lineId = result.id;
205
+ checkbox.checked = false;
206
+
207
+ checkbox.addEventListener('change', () => {
208
+ this.updateDeleteButtonState();
209
+ });
210
+
211
+ if (typeof result.sentence !== 'string') {
212
+ console.warn('Unexpected sentence format:', result.sentence);
213
+ result.sentence = JSON.stringify(result.sentence);
214
+ }
183
215
 
184
- // Highlight search terms
185
216
  const highlightedText = this.highlightSearchTerms(result.sentence, this.currentQuery);
186
217
 
187
- // Format timestamp to ISO format
188
218
  const date = new Date(result.timestamp * 1000);
189
219
  const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${date.toTimeString().split(' ')[0]}`;
190
220
 
191
- div.innerHTML = `
221
+ const contentDiv = document.createElement('div');
222
+ contentDiv.style.flex = '1';
223
+ contentDiv.innerHTML = `
192
224
  <div class="result-sentence">${highlightedText}</div>
193
225
  <div class="result-metadata">
194
226
  <div class="metadata-item">
@@ -207,6 +239,9 @@ class SentenceSearchApp {
207
239
  </div>
208
240
  `;
209
241
 
242
+ div.appendChild(checkbox);
243
+ div.appendChild(contentDiv);
244
+
210
245
  return div;
211
246
  }
212
247
 
@@ -221,7 +256,6 @@ class SentenceSearchApp {
221
256
  const pattern = new RegExp(query, 'gi');
222
257
  return escapedText.replace(pattern, '<span class="search-highlight">$&</span>');
223
258
  } catch (e) {
224
- // If invalid regex, just return escaped text
225
259
  return escapedText;
226
260
  }
227
261
  } else {
@@ -288,10 +322,143 @@ class SentenceSearchApp {
288
322
  this.errorMessage.style.display = 'none';
289
323
  this.searchResults.style.display = 'none';
290
324
  document.getElementById('pagination').style.display = 'none';
325
+
326
+ if (this.selectAllBtn) {
327
+ this.selectAllBtn.disabled = true;
328
+ this.selectAllBtn.textContent = 'Select All';
329
+ }
330
+ }
331
+
332
+ updateDeleteButtonState() {
333
+ const selectedCount = this.getSelectedCount();
334
+
335
+ if (this.deleteLinesBtn) {
336
+ this.deleteLinesBtn.disabled = selectedCount === 0;
337
+ this.deleteLinesBtn.textContent = selectedCount > 0
338
+ ? `Delete Selected (${selectedCount})`
339
+ : 'Delete Selected';
340
+ }
341
+
342
+ if (this.selectAllBtn) {
343
+ const totalVisible = document.querySelectorAll('.line-checkbox').length;
344
+
345
+ if (totalVisible === 0) {
346
+ this.selectAllBtn.disabled = true;
347
+ this.selectAllBtn.textContent = 'Select All';
348
+ } else {
349
+ this.selectAllBtn.disabled = false;
350
+ if (this.areAllVisibleSelected()) {
351
+ this.selectAllBtn.textContent = 'Deselect All';
352
+ } else {
353
+ this.selectAllBtn.textContent = 'Select All';
354
+ }
355
+ }
356
+ }
357
+ }
358
+
359
+ getSelectedLineIds() {
360
+ const selectedIds = [];
361
+ const checkboxes = document.querySelectorAll('.line-checkbox:checked');
362
+
363
+ checkboxes.forEach(checkbox => {
364
+ const lineId = checkbox.dataset.lineId;
365
+ selectedIds.push(lineId);
366
+ });
367
+
368
+ return selectedIds;
369
+ }
370
+
371
+ getSelectedCount() {
372
+ return document.querySelectorAll('.line-checkbox:checked').length;
373
+ }
374
+
375
+ areAllVisibleSelected() {
376
+ const allCheckboxes = document.querySelectorAll('.line-checkbox');
377
+ const selectedCheckboxes = document.querySelectorAll('.line-checkbox:checked');
378
+ return allCheckboxes.length > 0 && allCheckboxes.length === selectedCheckboxes.length;
379
+ }
380
+
381
+ toggleSelectAll() {
382
+ const visibleCheckboxes = document.querySelectorAll('.line-checkbox');
383
+ const shouldSelect = !this.areAllVisibleSelected();
384
+
385
+ visibleCheckboxes.forEach(checkbox => {
386
+ checkbox.checked = shouldSelect;
387
+ });
388
+
389
+ this.updateDeleteButtonState();
390
+ }
391
+
392
+ showDeleteConfirmation() {
393
+ const count = this.getSelectedCount();
394
+ if (count === 0) return;
395
+
396
+ const message = `Are you sure you want to delete ${count} selected sentence${count > 1 ? 's' : ''}? This action cannot be undone.`;
397
+
398
+ document.getElementById('deleteConfirmationMessage').textContent = message;
399
+ openModal('deleteConfirmationModal');
400
+ }
401
+
402
+ async deleteSelectedLines() {
403
+ const lineIds = this.getSelectedLineIds();
404
+
405
+ if (lineIds.length === 0) {
406
+ return;
407
+ }
408
+
409
+ try {
410
+ this.showLoadingState();
411
+
412
+ const response = await fetch('/api/delete-sentence-lines', {
413
+ method: 'POST',
414
+ headers: {
415
+ 'Content-Type': 'application/json'
416
+ },
417
+ body: JSON.stringify({ line_ids: lineIds })
418
+ });
419
+
420
+ const data = await response.json();
421
+
422
+ if (!response.ok) {
423
+ throw new Error(data.error || 'Failed to delete sentences');
424
+ }
425
+
426
+ document.querySelectorAll('.line-checkbox:checked').forEach(cb => cb.checked = false);
427
+ this.updateDeleteButtonState();
428
+
429
+ await this.performSearch();
430
+
431
+ this.showMessage('Success', `Successfully deleted ${data.deleted_count} sentence${data.deleted_count > 1 ? 's' : ''}`);
432
+
433
+ } catch (error) {
434
+ this.showErrorState(`Failed to delete sentences: ${error.message}`);
435
+ console.error('Delete error:', error);
436
+ }
437
+ }
438
+
439
+ showMessage(title, message) {
440
+ document.getElementById('messageModalTitle').textContent = title;
441
+ document.getElementById('messageModalText').textContent = message;
442
+ openModal('messageModal');
291
443
  }
292
444
  }
293
445
 
294
- // Initialize the app when DOM is loaded
295
446
  document.addEventListener('DOMContentLoaded', () => {
296
- new SentenceSearchApp();
447
+ const app = new SentenceSearchApp();
448
+
449
+ const closeButtons = document.querySelectorAll('[data-action="closeModal"]');
450
+ closeButtons.forEach(btn => {
451
+ const modalId = btn.getAttribute('data-modal');
452
+ if (modalId) {
453
+ btn.addEventListener('click', () => closeModal(modalId));
454
+ }
455
+ });
456
+
457
+ const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
458
+ if (confirmDeleteBtn) {
459
+ confirmDeleteBtn.addEventListener('click', () => {
460
+ closeModal('deleteConfirmationModal');
461
+ app.deleteSelectedLines();
462
+ });
463
+ }
297
464
  });