GameSentenceMiner 2.19.16__py3-none-any.whl → 2.20.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.
Potentially problematic release.
This version of GameSentenceMiner might be problematic. Click here for more details.
- GameSentenceMiner/__init__.py +39 -0
- GameSentenceMiner/anki.py +6 -3
- GameSentenceMiner/gametext.py +13 -2
- GameSentenceMiner/gsm.py +40 -3
- GameSentenceMiner/locales/en_us.json +4 -0
- GameSentenceMiner/locales/ja_jp.json +4 -0
- GameSentenceMiner/locales/zh_cn.json +4 -0
- GameSentenceMiner/obs.py +4 -1
- GameSentenceMiner/owocr/owocr/ocr.py +304 -134
- GameSentenceMiner/owocr/owocr/run.py +1 -1
- GameSentenceMiner/ui/anki_confirmation.py +4 -2
- GameSentenceMiner/ui/config_gui.py +12 -0
- GameSentenceMiner/util/configuration.py +6 -2
- GameSentenceMiner/util/cron/__init__.py +12 -0
- GameSentenceMiner/util/cron/daily_rollup.py +613 -0
- GameSentenceMiner/util/cron/jiten_update.py +397 -0
- GameSentenceMiner/util/cron/populate_games.py +154 -0
- GameSentenceMiner/util/cron/run_crons.py +148 -0
- GameSentenceMiner/util/cron/setup_populate_games_cron.py +118 -0
- GameSentenceMiner/util/cron_table.py +334 -0
- GameSentenceMiner/util/db.py +236 -49
- GameSentenceMiner/util/ffmpeg.py +23 -4
- GameSentenceMiner/util/games_table.py +340 -93
- GameSentenceMiner/util/jiten_api_client.py +188 -0
- GameSentenceMiner/util/stats_rollup_table.py +216 -0
- GameSentenceMiner/web/anki_api_endpoints.py +438 -220
- GameSentenceMiner/web/database_api.py +955 -1259
- GameSentenceMiner/web/jiten_database_api.py +1015 -0
- GameSentenceMiner/web/rollup_stats.py +672 -0
- GameSentenceMiner/web/static/css/dashboard-shared.css +75 -13
- GameSentenceMiner/web/static/css/overview.css +604 -47
- GameSentenceMiner/web/static/css/search.css +226 -0
- GameSentenceMiner/web/static/css/shared.css +762 -0
- GameSentenceMiner/web/static/css/stats.css +221 -0
- GameSentenceMiner/web/static/js/components/bar-chart.js +339 -0
- GameSentenceMiner/web/static/js/database-bulk-operations.js +320 -0
- GameSentenceMiner/web/static/js/database-game-data.js +390 -0
- GameSentenceMiner/web/static/js/database-game-operations.js +213 -0
- GameSentenceMiner/web/static/js/database-helpers.js +44 -0
- GameSentenceMiner/web/static/js/database-jiten-integration.js +750 -0
- GameSentenceMiner/web/static/js/database-popups.js +89 -0
- GameSentenceMiner/web/static/js/database-tabs.js +64 -0
- GameSentenceMiner/web/static/js/database-text-management.js +371 -0
- GameSentenceMiner/web/static/js/database.js +86 -718
- GameSentenceMiner/web/static/js/goals.js +79 -18
- GameSentenceMiner/web/static/js/heatmap.js +29 -23
- GameSentenceMiner/web/static/js/overview.js +1205 -339
- GameSentenceMiner/web/static/js/regex-patterns.js +100 -0
- GameSentenceMiner/web/static/js/search.js +215 -18
- GameSentenceMiner/web/static/js/shared.js +193 -39
- GameSentenceMiner/web/static/js/stats.js +1536 -179
- GameSentenceMiner/web/stats.py +1142 -269
- GameSentenceMiner/web/stats_api.py +2104 -0
- GameSentenceMiner/web/templates/anki_stats.html +4 -18
- GameSentenceMiner/web/templates/components/date-range.html +118 -3
- GameSentenceMiner/web/templates/components/html-head.html +40 -6
- GameSentenceMiner/web/templates/components/js-config.html +8 -8
- GameSentenceMiner/web/templates/components/regex-input.html +160 -0
- GameSentenceMiner/web/templates/database.html +564 -117
- GameSentenceMiner/web/templates/goals.html +41 -5
- GameSentenceMiner/web/templates/overview.html +159 -129
- GameSentenceMiner/web/templates/search.html +78 -9
- GameSentenceMiner/web/templates/stats.html +159 -5
- GameSentenceMiner/web/texthooking_page.py +280 -111
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/METADATA +43 -2
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/RECORD +70 -47
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Shared regex patterns for search and deletion functionality
|
|
2
|
+
// This module provides preset regex patterns and helper functions
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Preset regex patterns with descriptions
|
|
6
|
+
* Each pattern includes:
|
|
7
|
+
* - value: unique identifier
|
|
8
|
+
* - label: user-friendly description
|
|
9
|
+
* - pattern: the actual regex pattern
|
|
10
|
+
*/
|
|
11
|
+
export const PRESET_PATTERNS = {
|
|
12
|
+
'lines_over_50': {
|
|
13
|
+
label: 'Lines over 50 characters',
|
|
14
|
+
pattern: '.{51,}'
|
|
15
|
+
},
|
|
16
|
+
'lines_over_100': {
|
|
17
|
+
label: 'Lines over 100 characters',
|
|
18
|
+
pattern: '.{101,}'
|
|
19
|
+
},
|
|
20
|
+
'non_japanese': {
|
|
21
|
+
label: 'Non-Japanese text',
|
|
22
|
+
pattern: '^[^\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]*$'
|
|
23
|
+
},
|
|
24
|
+
'ascii_only': {
|
|
25
|
+
label: 'ASCII-only lines',
|
|
26
|
+
pattern: '^[\x00-\x7F]*$'
|
|
27
|
+
},
|
|
28
|
+
'empty_lines': {
|
|
29
|
+
label: 'Empty or whitespace-only lines',
|
|
30
|
+
pattern: '^\s*$'
|
|
31
|
+
},
|
|
32
|
+
'numbers_only': {
|
|
33
|
+
label: 'Lines with numbers only',
|
|
34
|
+
pattern: '^\d+$'
|
|
35
|
+
},
|
|
36
|
+
'single_char': {
|
|
37
|
+
label: 'Single character lines',
|
|
38
|
+
pattern: '^.{1}$'
|
|
39
|
+
},
|
|
40
|
+
'repeated_chars': {
|
|
41
|
+
label: 'Lines with repeated characters (3+ times)',
|
|
42
|
+
pattern: '(.)\\1{2,}'
|
|
43
|
+
},
|
|
44
|
+
'everything': {
|
|
45
|
+
label: 'Everything',
|
|
46
|
+
pattern: '.*'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get pattern by key
|
|
52
|
+
* @param {string} key - Pattern key
|
|
53
|
+
* @returns {string|null} - Pattern string or null if not found
|
|
54
|
+
*/
|
|
55
|
+
export function getPattern(key) {
|
|
56
|
+
return PRESET_PATTERNS[key]?.pattern || null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get all pattern options for dropdown
|
|
61
|
+
* @returns {Array} - Array of {value, label} objects
|
|
62
|
+
*/
|
|
63
|
+
export function getPatternOptions() {
|
|
64
|
+
return Object.entries(PRESET_PATTERNS).map(([key, data]) => ({
|
|
65
|
+
value: key,
|
|
66
|
+
label: data.label
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate regex pattern
|
|
72
|
+
* @param {string} pattern - Regex pattern to validate
|
|
73
|
+
* @returns {Object} - {valid: boolean, error: string|null}
|
|
74
|
+
*/
|
|
75
|
+
export function validateRegex(pattern) {
|
|
76
|
+
try {
|
|
77
|
+
new RegExp(pattern);
|
|
78
|
+
return { valid: true, error: null };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return { valid: false, error: e.message };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Apply regex pattern to text
|
|
86
|
+
* @param {string} text - Text to test
|
|
87
|
+
* @param {string} pattern - Regex pattern
|
|
88
|
+
* @param {boolean} caseSensitive - Case sensitivity flag
|
|
89
|
+
* @returns {boolean} - True if pattern matches
|
|
90
|
+
*/
|
|
91
|
+
export function testPattern(text, pattern, caseSensitive = false) {
|
|
92
|
+
try {
|
|
93
|
+
const flags = caseSensitive ? 'g' : 'gi';
|
|
94
|
+
const regex = new RegExp(pattern, flags);
|
|
95
|
+
return regex.test(text);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error('Regex test error:', e);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -3,6 +3,8 @@ class SentenceSearchApp {
|
|
|
3
3
|
this.searchInput = document.getElementById('searchInput');
|
|
4
4
|
this.gameFilter = document.getElementById('gameFilter');
|
|
5
5
|
this.sortFilter = document.getElementById('sortFilter');
|
|
6
|
+
this.fromDateFilter = document.getElementById('searchFromDate');
|
|
7
|
+
this.toDateFilter = document.getElementById('searchToDate');
|
|
6
8
|
this.searchResults = document.getElementById('searchResults');
|
|
7
9
|
this.loadingIndicator = document.getElementById('loadingIndicator');
|
|
8
10
|
this.noResults = document.getElementById('noResults');
|
|
@@ -10,10 +12,27 @@ class SentenceSearchApp {
|
|
|
10
12
|
this.errorMessage = document.getElementById('errorMessage');
|
|
11
13
|
this.searchStats = document.getElementById('searchStats');
|
|
12
14
|
this.searchTime = document.getElementById('searchTime');
|
|
13
|
-
|
|
15
|
+
|
|
16
|
+
// Regex component elements
|
|
17
|
+
this.regexPresetSelect = document.querySelector('.regex-preset-select');
|
|
18
|
+
this.regexCustomInput = document.querySelector('.regex-custom-input');
|
|
19
|
+
this.regexCaseCheckbox = document.querySelector('.regex-case-checkbox');
|
|
20
|
+
this.regexModeCheckbox = document.querySelector('.regex-mode-checkbox');
|
|
21
|
+
|
|
22
|
+
// Duplicate detection elements
|
|
23
|
+
this.toggleDuplicateBtn = document.getElementById('toggleDuplicateDetection');
|
|
24
|
+
this.duplicateSection = document.getElementById('duplicateDetectionSection');
|
|
25
|
+
this.duplicateTimeWindow = document.getElementById('duplicateTimeWindow');
|
|
26
|
+
this.duplicateIgnoreTimeWindow = document.getElementById('duplicateIgnoreTimeWindow');
|
|
27
|
+
this.duplicateCaseSensitive = document.getElementById('duplicateCaseSensitive');
|
|
28
|
+
this.searchDuplicatesBtn = document.getElementById('searchDuplicatesBtn');
|
|
29
|
+
this.duplicateTimeWindowGroup = document.getElementById('duplicateTimeWindowGroup');
|
|
30
|
+
|
|
14
31
|
this.deleteLinesBtn = document.getElementById('deleteLinesBtn');
|
|
15
32
|
this.selectAllBtn = document.getElementById('selectAllBtn');
|
|
16
33
|
this.pageSizeFilter = document.getElementById('pageSizeFilter');
|
|
34
|
+
this.toggleAdvancedBtn = document.getElementById('toggleAdvancedSearch');
|
|
35
|
+
this.advancedSearchSection = document.getElementById('advancedSearchSection');
|
|
17
36
|
|
|
18
37
|
this.currentPage = 1;
|
|
19
38
|
this.pageSize = 20;
|
|
@@ -21,6 +40,7 @@ class SentenceSearchApp {
|
|
|
21
40
|
this.currentQuery = '';
|
|
22
41
|
this.totalResults = 0;
|
|
23
42
|
this.currentUseRegex = false;
|
|
43
|
+
this.isDuplicateSearch = false;
|
|
24
44
|
this.initialize();
|
|
25
45
|
}
|
|
26
46
|
|
|
@@ -56,6 +76,8 @@ class SentenceSearchApp {
|
|
|
56
76
|
this.gameFilter.addEventListener('change', () => this.performSearch());
|
|
57
77
|
this.sortFilter.addEventListener('change', () => this.performSearch());
|
|
58
78
|
|
|
79
|
+
// Date range filters do NOT auto-trigger search - user must click Search button
|
|
80
|
+
|
|
59
81
|
if (this.pageSizeFilter) {
|
|
60
82
|
this.pageSizeFilter.addEventListener('change', () => {
|
|
61
83
|
this.pageSize = parseInt(this.pageSizeFilter.value);
|
|
@@ -76,8 +98,24 @@ class SentenceSearchApp {
|
|
|
76
98
|
this.performSearch();
|
|
77
99
|
});
|
|
78
100
|
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
// Regex component event listeners
|
|
102
|
+
if (this.regexCustomInput) {
|
|
103
|
+
this.regexCustomInput.addEventListener('input', () => {
|
|
104
|
+
clearTimeout(this.searchTimeout);
|
|
105
|
+
this.searchTimeout = setTimeout(() => {
|
|
106
|
+
this.performSearch();
|
|
107
|
+
}, 300);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.regexModeCheckbox) {
|
|
112
|
+
this.regexModeCheckbox.addEventListener('change', () => {
|
|
113
|
+
this.performSearch();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.regexCaseCheckbox) {
|
|
118
|
+
this.regexCaseCheckbox.addEventListener('change', () => {
|
|
81
119
|
this.performSearch();
|
|
82
120
|
});
|
|
83
121
|
}
|
|
@@ -93,6 +131,53 @@ class SentenceSearchApp {
|
|
|
93
131
|
this.toggleSelectAll();
|
|
94
132
|
});
|
|
95
133
|
}
|
|
134
|
+
|
|
135
|
+
if (this.toggleAdvancedBtn) {
|
|
136
|
+
this.toggleAdvancedBtn.addEventListener('click', () => {
|
|
137
|
+
this.toggleAdvancedSearch();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Duplicate detection event listeners
|
|
142
|
+
if (this.toggleDuplicateBtn) {
|
|
143
|
+
this.toggleDuplicateBtn.addEventListener('click', () => {
|
|
144
|
+
this.toggleDuplicateDetection();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (this.duplicateIgnoreTimeWindow) {
|
|
149
|
+
this.duplicateIgnoreTimeWindow.addEventListener('change', () => {
|
|
150
|
+
this.toggleDuplicateTimeWindow();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (this.searchDuplicatesBtn) {
|
|
155
|
+
this.searchDuplicatesBtn.addEventListener('click', () => {
|
|
156
|
+
this.searchForDuplicates();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Manual search button for date filtering
|
|
161
|
+
const manualSearchBtn = document.getElementById('manualSearchBtn');
|
|
162
|
+
if (manualSearchBtn) {
|
|
163
|
+
manualSearchBtn.addEventListener('click', () => {
|
|
164
|
+
this.performSearch();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
toggleAdvancedSearch() {
|
|
170
|
+
if (!this.advancedSearchSection || !this.toggleAdvancedBtn) return;
|
|
171
|
+
|
|
172
|
+
const isHidden = this.advancedSearchSection.style.display === 'none';
|
|
173
|
+
|
|
174
|
+
if (isHidden) {
|
|
175
|
+
this.advancedSearchSection.style.display = 'block';
|
|
176
|
+
this.toggleAdvancedBtn.innerHTML = '<span id="toggleIcon">▲</span> Hide Advanced Search';
|
|
177
|
+
} else {
|
|
178
|
+
this.advancedSearchSection.style.display = 'none';
|
|
179
|
+
this.toggleAdvancedBtn.innerHTML = '<span id="toggleIcon">▼</span> Show Advanced Search';
|
|
180
|
+
}
|
|
96
181
|
}
|
|
97
182
|
|
|
98
183
|
async loadGamesList() {
|
|
@@ -120,25 +205,36 @@ class SentenceSearchApp {
|
|
|
120
205
|
const query = this.searchInput.value.trim();
|
|
121
206
|
const gameFilter = this.gameFilter.value;
|
|
122
207
|
const sortBy = this.sortFilter.value;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
208
|
+
const fromDate = this.fromDateFilter ? this.fromDateFilter.value : '';
|
|
209
|
+
const toDate = this.toDateFilter ? this.toDateFilter.value : '';
|
|
210
|
+
|
|
211
|
+
// Get regex settings from component
|
|
212
|
+
const customRegex = this.regexCustomInput ? this.regexCustomInput.value.trim() : '';
|
|
213
|
+
const useRegex = this.regexModeCheckbox ? this.regexModeCheckbox.checked : false;
|
|
214
|
+
const caseSensitive = this.regexCaseCheckbox ? this.regexCaseCheckbox.checked : false;
|
|
215
|
+
|
|
216
|
+
// Use custom regex if provided and regex mode is enabled, otherwise use search query
|
|
217
|
+
// If search is empty, default to .* regex pattern
|
|
218
|
+
let searchPattern = (useRegex && customRegex) ? customRegex : query;
|
|
219
|
+
let effectiveUseRegex = useRegex;
|
|
220
|
+
|
|
221
|
+
if (!searchPattern && !query) {
|
|
222
|
+
searchPattern = '.*';
|
|
223
|
+
effectiveUseRegex = true; // Force regex mode for .* pattern
|
|
127
224
|
}
|
|
128
|
-
this.currentQuery = query;
|
|
129
|
-
this.currentUseRegex = useRegex;
|
|
130
225
|
|
|
131
|
-
if (
|
|
132
|
-
this.
|
|
133
|
-
return;
|
|
226
|
+
if (searchPattern !== this.currentQuery || effectiveUseRegex !== this.currentUseRegex) {
|
|
227
|
+
this.currentPage = 1;
|
|
134
228
|
}
|
|
229
|
+
this.currentQuery = searchPattern;
|
|
230
|
+
this.currentUseRegex = effectiveUseRegex;
|
|
135
231
|
|
|
136
232
|
this.showLoadingState();
|
|
137
233
|
const startTime = Date.now();
|
|
138
234
|
|
|
139
235
|
try {
|
|
140
236
|
const params = new URLSearchParams({
|
|
141
|
-
q:
|
|
237
|
+
q: searchPattern,
|
|
142
238
|
page: this.currentPage,
|
|
143
239
|
page_size: this.pageSize,
|
|
144
240
|
sort: sortBy
|
|
@@ -147,9 +243,18 @@ class SentenceSearchApp {
|
|
|
147
243
|
if (gameFilter) {
|
|
148
244
|
params.append('game', gameFilter);
|
|
149
245
|
}
|
|
150
|
-
if (
|
|
246
|
+
if (fromDate) {
|
|
247
|
+
params.append('from_date', fromDate);
|
|
248
|
+
}
|
|
249
|
+
if (toDate) {
|
|
250
|
+
params.append('to_date', toDate);
|
|
251
|
+
}
|
|
252
|
+
if (effectiveUseRegex) {
|
|
151
253
|
params.append('use_regex', 'true');
|
|
152
254
|
}
|
|
255
|
+
if (caseSensitive) {
|
|
256
|
+
params.append('case_sensitive', 'true');
|
|
257
|
+
}
|
|
153
258
|
|
|
154
259
|
const response = await fetch(`/api/search-sentences?${params}`);
|
|
155
260
|
const data = await response.json();
|
|
@@ -248,12 +353,15 @@ class SentenceSearchApp {
|
|
|
248
353
|
highlightSearchTerms(text, query) {
|
|
249
354
|
if (!query) return escapeHtml(text);
|
|
250
355
|
|
|
251
|
-
const useRegex = this.
|
|
356
|
+
const useRegex = this.regexModeCheckbox ? this.regexModeCheckbox.checked : false;
|
|
357
|
+
const customRegex = this.regexCustomInput ? this.regexCustomInput.value.trim() : '';
|
|
358
|
+
const caseSensitive = this.regexCaseCheckbox ? this.regexCaseCheckbox.checked : false;
|
|
252
359
|
const escapedText = escapeHtml(text);
|
|
253
360
|
|
|
254
|
-
if (useRegex) {
|
|
361
|
+
if (useRegex && customRegex) {
|
|
255
362
|
try {
|
|
256
|
-
const
|
|
363
|
+
const flags = caseSensitive ? 'g' : 'gi';
|
|
364
|
+
const pattern = new RegExp(customRegex, flags);
|
|
257
365
|
return escapedText.replace(pattern, '<span class="search-highlight">$&</span>');
|
|
258
366
|
} catch (e) {
|
|
259
367
|
return escapedText;
|
|
@@ -262,7 +370,8 @@ class SentenceSearchApp {
|
|
|
262
370
|
const searchTerms = query.split(' ').filter(term => term.length > 0);
|
|
263
371
|
let result = escapedText;
|
|
264
372
|
searchTerms.forEach(term => {
|
|
265
|
-
const
|
|
373
|
+
const flags = caseSensitive ? 'g' : 'gi';
|
|
374
|
+
const regex = new RegExp(`(${escapeRegex(term)})`, flags);
|
|
266
375
|
result = result.replace(regex, '<span class="search-highlight">$1</span>');
|
|
267
376
|
});
|
|
268
377
|
return result;
|
|
@@ -436,6 +545,94 @@ class SentenceSearchApp {
|
|
|
436
545
|
}
|
|
437
546
|
}
|
|
438
547
|
|
|
548
|
+
toggleDuplicateDetection() {
|
|
549
|
+
if (!this.duplicateSection || !this.toggleDuplicateBtn) return;
|
|
550
|
+
|
|
551
|
+
const isHidden = this.duplicateSection.style.display === 'none';
|
|
552
|
+
const icon = document.getElementById('duplicateToggleIcon');
|
|
553
|
+
|
|
554
|
+
if (isHidden) {
|
|
555
|
+
this.duplicateSection.style.display = 'block';
|
|
556
|
+
if (icon) icon.textContent = '▲';
|
|
557
|
+
} else {
|
|
558
|
+
this.duplicateSection.style.display = 'none';
|
|
559
|
+
if (icon) icon.textContent = '▼';
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
toggleDuplicateTimeWindow() {
|
|
564
|
+
if (!this.duplicateIgnoreTimeWindow || !this.duplicateTimeWindowGroup) return;
|
|
565
|
+
|
|
566
|
+
const isIgnored = this.duplicateIgnoreTimeWindow.checked;
|
|
567
|
+
|
|
568
|
+
if (isIgnored) {
|
|
569
|
+
this.duplicateTimeWindowGroup.style.opacity = '0.5';
|
|
570
|
+
this.duplicateTimeWindowGroup.style.pointerEvents = 'none';
|
|
571
|
+
if (this.duplicateTimeWindow) {
|
|
572
|
+
this.duplicateTimeWindow.disabled = true;
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
this.duplicateTimeWindowGroup.style.opacity = '1';
|
|
576
|
+
this.duplicateTimeWindowGroup.style.pointerEvents = 'auto';
|
|
577
|
+
if (this.duplicateTimeWindow) {
|
|
578
|
+
this.duplicateTimeWindow.disabled = false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async searchForDuplicates() {
|
|
584
|
+
const gameFilter = this.gameFilter.value;
|
|
585
|
+
const timeWindow = parseInt(this.duplicateTimeWindow.value);
|
|
586
|
+
const ignoreTimeWindow = this.duplicateIgnoreTimeWindow.checked;
|
|
587
|
+
const caseSensitive = this.duplicateCaseSensitive.checked;
|
|
588
|
+
|
|
589
|
+
// Validate input
|
|
590
|
+
if (!ignoreTimeWindow && (isNaN(timeWindow) || timeWindow < 1)) {
|
|
591
|
+
this.showErrorState('Time window must be at least 1 minute');
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
this.showLoadingState();
|
|
596
|
+
this.isDuplicateSearch = true;
|
|
597
|
+
const startTime = Date.now();
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
const requestData = {
|
|
601
|
+
game: gameFilter,
|
|
602
|
+
time_window_minutes: timeWindow,
|
|
603
|
+
ignore_time_window: ignoreTimeWindow,
|
|
604
|
+
case_sensitive: caseSensitive
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const response = await fetch('/api/search-duplicates', {
|
|
608
|
+
method: 'POST',
|
|
609
|
+
headers: { 'Content-Type': 'application/json' },
|
|
610
|
+
body: JSON.stringify(requestData)
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const data = await response.json();
|
|
614
|
+
const searchTime = Date.now() - startTime;
|
|
615
|
+
|
|
616
|
+
if (!response.ok) {
|
|
617
|
+
throw new Error(data.error || 'Duplicate search failed');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Display results using existing display method
|
|
621
|
+
this.displayResults(data, searchTime);
|
|
622
|
+
|
|
623
|
+
// Update stats text to indicate duplicate search
|
|
624
|
+
if (data.total > 0) {
|
|
625
|
+
const modeText = ignoreTimeWindow ? 'across entire game' : `within ${timeWindow} minute window`;
|
|
626
|
+
const gameText = gameFilter ? ` in ${gameFilter}` : '';
|
|
627
|
+
this.searchStats.textContent = `Found ${data.total.toLocaleString()} duplicate sentences ${modeText}${gameText}`;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
} catch (error) {
|
|
631
|
+
this.showErrorState(error.message);
|
|
632
|
+
this.isDuplicateSearch = false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
439
636
|
showMessage(title, message) {
|
|
440
637
|
document.getElementById('messageModalTitle').textContent = title;
|
|
441
638
|
document.getElementById('messageModalText').textContent = message;
|
|
@@ -517,6 +517,62 @@ function escapeRegex(string) {
|
|
|
517
517
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
function safeJoinArray(arr, separator = ', ') {
|
|
521
|
+
/**
|
|
522
|
+
* Safely join an array with proper type checking and fallbacks.
|
|
523
|
+
* Handles various data types that might be returned from API responses.
|
|
524
|
+
*
|
|
525
|
+
* @param {*} arr - The value to join (should be an array, but handles other types)
|
|
526
|
+
* @param {string} separator - The separator to use for joining
|
|
527
|
+
* @returns {string} - The joined string or appropriate fallback
|
|
528
|
+
*/
|
|
529
|
+
if (!arr) {
|
|
530
|
+
return '';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (Array.isArray(arr)) {
|
|
534
|
+
return arr.join(separator);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (typeof arr === 'string') {
|
|
538
|
+
return arr;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Handle other types by converting to string
|
|
542
|
+
return String(arr);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function logApiResponse(operation, response, result) {
|
|
546
|
+
/**
|
|
547
|
+
* Log API response details for debugging purposes.
|
|
548
|
+
*
|
|
549
|
+
* @param {string} operation - The operation being performed
|
|
550
|
+
* @param {Response} response - The fetch response object
|
|
551
|
+
* @param {*} result - The parsed JSON result
|
|
552
|
+
*/
|
|
553
|
+
console.group(`🔍 API Response Debug: ${operation}`);
|
|
554
|
+
console.log('Response status:', response.status, response.statusText);
|
|
555
|
+
console.log('Response OK:', response.ok);
|
|
556
|
+
console.log('Result object:', result);
|
|
557
|
+
|
|
558
|
+
// Print FULL API response as formatted JSON
|
|
559
|
+
console.log('%c📋 FULL API RESPONSE (JSON):', 'color: #00ff00; font-weight: bold; font-size: 14px;');
|
|
560
|
+
console.log(JSON.stringify(result, null, 2));
|
|
561
|
+
|
|
562
|
+
if (result && typeof result === 'object') {
|
|
563
|
+
Object.keys(result).forEach(key => {
|
|
564
|
+
const value = result[key];
|
|
565
|
+
console.log(`${key}:`, {
|
|
566
|
+
value,
|
|
567
|
+
type: typeof value,
|
|
568
|
+
isArray: Array.isArray(value),
|
|
569
|
+
length: Array.isArray(value) ? value.length : 'N/A'
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
console.groupEnd();
|
|
574
|
+
}
|
|
575
|
+
|
|
520
576
|
// Screenshot functionality
|
|
521
577
|
function initializeScreenshotButton() {
|
|
522
578
|
const screenshotButton = document.getElementById('screenshotToggle');
|
|
@@ -525,62 +581,160 @@ function initializeScreenshotButton() {
|
|
|
525
581
|
return; // Screenshot button not available on this page
|
|
526
582
|
}
|
|
527
583
|
|
|
528
|
-
screenshotButton.addEventListener('click',
|
|
584
|
+
screenshotButton.addEventListener('click', exportPageToPDF);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Lazy-load libraries
|
|
588
|
+
let html2canvasLoaded = false;
|
|
589
|
+
let html2canvasLoading = false;
|
|
590
|
+
let jsPDFLoaded = false;
|
|
591
|
+
let jsPDFLoading = false;
|
|
592
|
+
|
|
593
|
+
// Lazy-load html2canvas
|
|
594
|
+
async function loadHtml2Canvas() {
|
|
595
|
+
if (html2canvasLoaded) return true;
|
|
596
|
+
if (html2canvasLoading) {
|
|
597
|
+
return new Promise(resolve => {
|
|
598
|
+
const check = setInterval(() => {
|
|
599
|
+
if (html2canvasLoaded) {
|
|
600
|
+
clearInterval(check);
|
|
601
|
+
resolve(true);
|
|
602
|
+
}
|
|
603
|
+
}, 100);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
html2canvasLoading = true;
|
|
608
|
+
return new Promise((resolve, reject) => {
|
|
609
|
+
const script = document.createElement('script');
|
|
610
|
+
script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
|
|
611
|
+
script.onload = () => {
|
|
612
|
+
html2canvasLoaded = true;
|
|
613
|
+
html2canvasLoading = false;
|
|
614
|
+
resolve(true);
|
|
615
|
+
};
|
|
616
|
+
script.onerror = () => {
|
|
617
|
+
html2canvasLoading = false;
|
|
618
|
+
reject(new Error('Failed to load html2canvas'));
|
|
619
|
+
};
|
|
620
|
+
document.head.appendChild(script);
|
|
621
|
+
});
|
|
529
622
|
}
|
|
530
623
|
|
|
531
|
-
|
|
624
|
+
// Lazy-load jsPDF
|
|
625
|
+
async function loadJsPDF() {
|
|
626
|
+
if (jsPDFLoaded) return true;
|
|
627
|
+
if (jsPDFLoading) {
|
|
628
|
+
return new Promise(resolve => {
|
|
629
|
+
const check = setInterval(() => {
|
|
630
|
+
if (jsPDFLoaded) {
|
|
631
|
+
clearInterval(check);
|
|
632
|
+
resolve(true);
|
|
633
|
+
}
|
|
634
|
+
}, 100);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
jsPDFLoading = true;
|
|
639
|
+
return new Promise((resolve, reject) => {
|
|
640
|
+
const script = document.createElement('script');
|
|
641
|
+
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
|
|
642
|
+
script.onload = () => {
|
|
643
|
+
jsPDFLoaded = true;
|
|
644
|
+
jsPDFLoading = false;
|
|
645
|
+
resolve(true);
|
|
646
|
+
};
|
|
647
|
+
script.onerror = () => {
|
|
648
|
+
jsPDFLoading = false;
|
|
649
|
+
reject(new Error('Failed to load jsPDF'));
|
|
650
|
+
};
|
|
651
|
+
document.head.appendChild(script);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Capture page and export as PDF
|
|
656
|
+
async function exportPageToPDF() {
|
|
532
657
|
try {
|
|
533
|
-
|
|
658
|
+
console.log('Starting PDF export...');
|
|
659
|
+
|
|
660
|
+
// Load libraries
|
|
534
661
|
if (typeof html2canvas === 'undefined') {
|
|
535
|
-
console.
|
|
536
|
-
|
|
662
|
+
console.log('Loading html2canvas...');
|
|
663
|
+
await loadHtml2Canvas();
|
|
537
664
|
}
|
|
538
|
-
|
|
539
|
-
|
|
665
|
+
if (typeof window.jspdf === 'undefined') {
|
|
666
|
+
console.log('Loading jsPDF...');
|
|
667
|
+
await loadJsPDF();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const { jsPDF } = window.jspdf;
|
|
671
|
+
|
|
672
|
+
// Create timestamped filename
|
|
540
673
|
const now = new Date();
|
|
541
|
-
const timestamp =
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const filename = `
|
|
549
|
-
|
|
550
|
-
|
|
674
|
+
const timestamp =
|
|
675
|
+
now.getFullYear() + '-' +
|
|
676
|
+
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
|
677
|
+
String(now.getDate()).padStart(2, '0') + '_' +
|
|
678
|
+
String(now.getHours()).padStart(2, '0') + '-' +
|
|
679
|
+
String(now.getMinutes()).padStart(2, '0') + '-' +
|
|
680
|
+
String(now.getSeconds()).padStart(2, '0');
|
|
681
|
+
const filename = `GSM_STATS_${timestamp}.pdf`;
|
|
682
|
+
|
|
683
|
+
console.log('Capturing page screenshot...');
|
|
684
|
+
// Take a screenshot of the full page with high quality for sharp text
|
|
551
685
|
const canvas = await html2canvas(document.body, {
|
|
552
686
|
useCORS: true,
|
|
553
687
|
allowTaint: true,
|
|
554
|
-
scale:
|
|
688
|
+
scale: 2.5, // Higher scale for sharper text (2.5x resolution)
|
|
555
689
|
scrollX: 0,
|
|
556
690
|
scrollY: 0,
|
|
557
691
|
width: document.body.scrollWidth,
|
|
558
|
-
height: document.body.scrollHeight
|
|
692
|
+
height: document.body.scrollHeight,
|
|
693
|
+
logging: false,
|
|
694
|
+
imageTimeout: 0,
|
|
695
|
+
removeContainer: true
|
|
559
696
|
});
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
697
|
+
|
|
698
|
+
console.log('Converting to image...');
|
|
699
|
+
// Use JPEG with high quality for better file size vs quality balance
|
|
700
|
+
const imgData = canvas.toDataURL('image/jpeg', 0.80);
|
|
701
|
+
|
|
702
|
+
console.log('Creating PDF...');
|
|
703
|
+
// PDF setup (A4)
|
|
704
|
+
const pdf = new jsPDF('p', 'pt', 'a4');
|
|
705
|
+
const pageWidth = pdf.internal.pageSize.getWidth();
|
|
706
|
+
const pageHeight = pdf.internal.pageSize.getHeight();
|
|
707
|
+
|
|
708
|
+
const imgWidth = pageWidth;
|
|
709
|
+
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
|
710
|
+
|
|
711
|
+
let heightLeft = imgHeight;
|
|
712
|
+
let position = 0;
|
|
713
|
+
|
|
714
|
+
// Add first page with SLOW compression for better quality
|
|
715
|
+
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'SLOW');
|
|
716
|
+
heightLeft -= pageHeight;
|
|
717
|
+
|
|
718
|
+
// Add more pages if needed
|
|
719
|
+
while (heightLeft > 0) {
|
|
720
|
+
position = heightLeft - imgHeight;
|
|
721
|
+
pdf.addPage();
|
|
722
|
+
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'SLOW');
|
|
723
|
+
heightLeft -= pageHeight;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
console.log('Saving PDF...');
|
|
727
|
+
// Download
|
|
728
|
+
pdf.save(filename);
|
|
729
|
+
console.log('PDF export complete!');
|
|
730
|
+
|
|
579
731
|
} catch (error) {
|
|
580
|
-
console.error('
|
|
732
|
+
console.error('PDF export failed:', error);
|
|
733
|
+
alert('Failed to export PDF: ' + error.message);
|
|
581
734
|
}
|
|
582
735
|
}
|
|
583
736
|
|
|
737
|
+
|
|
584
738
|
// Initialize shared functionality when DOM loads
|
|
585
739
|
document.addEventListener('DOMContentLoaded', function() {
|
|
586
740
|
// Initialize theme toggle
|