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,750 @@
|
|
|
1
|
+
// Database Jiten.moe Integration Functions
|
|
2
|
+
// Dependencies: shared.js (provides escapeHtml, openModal, closeModal, safeJoinArray, logApiResponse), database-popups.js, database-helpers.js
|
|
3
|
+
|
|
4
|
+
// Global flag to prevent concurrent link operations
|
|
5
|
+
let isLinkingInProgress = false;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Open jiten.moe search modal for a specific game
|
|
9
|
+
* @param {string} gameId - Game ID to search for
|
|
10
|
+
* @param {string} gameTitle - Game title to search for
|
|
11
|
+
*/
|
|
12
|
+
function openJitenSearch(gameId, gameTitle) {
|
|
13
|
+
// Validate gameId
|
|
14
|
+
if (!gameId || gameId === 'undefined' || gameId === 'null') {
|
|
15
|
+
showDatabaseErrorPopup(`Cannot link game: Invalid game ID. Please refresh the page and try again.`);
|
|
16
|
+
console.error(`Invalid gameId provided to openJitenSearch: ${gameId}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
currentGameForSearch = currentGames.find(game => game.id === gameId);
|
|
21
|
+
if (!currentGameForSearch) {
|
|
22
|
+
showDatabaseErrorPopup(`Cannot find game with ID: ${gameId}. Please refresh the page and try again.`);
|
|
23
|
+
console.error(`Game not found in currentGames: ${gameId}`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Additional validation
|
|
28
|
+
if (!currentGameForSearch.id) {
|
|
29
|
+
showDatabaseErrorPopup(`Game data is incomplete (missing ID). Please refresh the page and try again.`);
|
|
30
|
+
console.error(`Game found but has no ID:`, currentGameForSearch);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
document.getElementById('searchingForGame').textContent = gameTitle;
|
|
35
|
+
document.getElementById('jitenSearchInput').value = gameTitle;
|
|
36
|
+
document.getElementById('jitenSearchResults').style.display = 'none';
|
|
37
|
+
document.getElementById('jitenSearchError').style.display = 'none';
|
|
38
|
+
|
|
39
|
+
openModal('jitenSearchModal');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Search jiten.moe database
|
|
44
|
+
*/
|
|
45
|
+
async function searchJitenMoe() {
|
|
46
|
+
const searchInput = document.getElementById('jitenSearchInput');
|
|
47
|
+
const resultsDiv = document.getElementById('jitenSearchResults');
|
|
48
|
+
const resultsListDiv = document.getElementById('jitenResultsList');
|
|
49
|
+
const errorDiv = document.getElementById('jitenSearchError');
|
|
50
|
+
const loadingDiv = document.getElementById('jitenSearchLoading');
|
|
51
|
+
|
|
52
|
+
const searchTerm = searchInput.value.trim();
|
|
53
|
+
if (!searchTerm) {
|
|
54
|
+
errorDiv.textContent = 'Please enter a search term';
|
|
55
|
+
errorDiv.style.display = 'block';
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Remove all punctuation from the search term before sending to API
|
|
60
|
+
const searchTermNoPunctuation = searchTerm.replace(/[^\p{L}\p{N}\s]/gu, '');
|
|
61
|
+
|
|
62
|
+
errorDiv.style.display = 'none';
|
|
63
|
+
resultsDiv.style.display = 'none';
|
|
64
|
+
loadingDiv.style.display = 'flex';
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`/api/jiten-search?title=${encodeURIComponent(searchTermNoPunctuation)}`);
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
|
|
70
|
+
if (response.ok) {
|
|
71
|
+
if (data.results && data.results.length > 0) {
|
|
72
|
+
renderJitenResults(data.results);
|
|
73
|
+
resultsDiv.style.display = 'block';
|
|
74
|
+
} else {
|
|
75
|
+
errorDiv.textContent = 'No results found. Try a different search term.';
|
|
76
|
+
errorDiv.style.display = 'block';
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
errorDiv.textContent = data.error || 'Search failed';
|
|
80
|
+
errorDiv.style.display = 'block';
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error searching jiten.moe:', error);
|
|
84
|
+
errorDiv.textContent = 'Search failed. Please try again.';
|
|
85
|
+
errorDiv.style.display = 'block';
|
|
86
|
+
} finally {
|
|
87
|
+
loadingDiv.style.display = 'none';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Render jiten.moe search results
|
|
93
|
+
* @param {Array} results - Search results from jiten.moe
|
|
94
|
+
*/
|
|
95
|
+
function renderJitenResults(results) {
|
|
96
|
+
const resultsListDiv = document.getElementById('jitenResultsList');
|
|
97
|
+
|
|
98
|
+
// Store results globally for easy access
|
|
99
|
+
jitenSearchResults = results;
|
|
100
|
+
|
|
101
|
+
resultsListDiv.innerHTML = '';
|
|
102
|
+
|
|
103
|
+
results.forEach((result, index) => {
|
|
104
|
+
const resultItem = document.createElement('div');
|
|
105
|
+
resultItem.className = 'jiten-result-item';
|
|
106
|
+
|
|
107
|
+
const mediaTypeMap = {1: 'Anime', 7: 'Visual Novel', 2: 'Manga'};
|
|
108
|
+
const mediaTypeText = mediaTypeMap[result.media_type] || 'Unknown';
|
|
109
|
+
|
|
110
|
+
resultItem.innerHTML = `
|
|
111
|
+
<div class="jiten-result-header">
|
|
112
|
+
${result.cover_name ? `<img src="${result.cover_name}" class="jiten-thumbnail" alt="Cover">` : '<div class="jiten-thumbnail-placeholder">🎮</div>'}
|
|
113
|
+
<div class="jiten-info">
|
|
114
|
+
<h5 class="jiten-title">${escapeHtml(result.title_original)}</h5>
|
|
115
|
+
${result.title_english ? `<p class="jiten-title-en">${escapeHtml(result.title_english)}</p>` : ''}
|
|
116
|
+
${result.title_romaji ? `<p class="jiten-title-rom">${escapeHtml(result.title_romaji)}</p>` : ''}
|
|
117
|
+
<div class="jiten-meta">
|
|
118
|
+
<span class="jiten-type">${mediaTypeText}</span>
|
|
119
|
+
${result.difficulty ? `<span class="jiten-difficulty">Difficulty: ${result.difficulty}</span>` : ''}
|
|
120
|
+
<span class="jiten-chars">Total: ${result.character_count.toLocaleString()} chars</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="jiten-actions">
|
|
124
|
+
<button class="action-btn primary" onclick="selectJitenGame(${index})">Select</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
${result.description ? `<div class="jiten-description">${escapeHtml(result.description.substring(0, 200))}${result.description.length > 200 ? '...' : ''}</div>` : ''}
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
resultsListDiv.appendChild(resultItem);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Select a jiten.moe game result
|
|
136
|
+
* @param {number} resultIndex - Index of the selected result
|
|
137
|
+
*/
|
|
138
|
+
function selectJitenGame(resultIndex) {
|
|
139
|
+
selectedJitenGame = jitenSearchResults[resultIndex];
|
|
140
|
+
|
|
141
|
+
// Check if we're linking an existing game or creating from potential
|
|
142
|
+
if (window.currentPotentialGame) {
|
|
143
|
+
if (typeof showPotentialGameLinkConfirmation === 'function') {
|
|
144
|
+
showPotentialGameLinkConfirmation();
|
|
145
|
+
} else {
|
|
146
|
+
showLinkConfirmation();
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
showLinkConfirmation();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Show link confirmation modal
|
|
155
|
+
*/
|
|
156
|
+
function showLinkConfirmation() {
|
|
157
|
+
if (!currentGameForSearch || !selectedJitenGame) return;
|
|
158
|
+
|
|
159
|
+
// Populate current game preview
|
|
160
|
+
const currentGamePreview = document.getElementById('currentGamePreview');
|
|
161
|
+
currentGamePreview.innerHTML = `
|
|
162
|
+
<div class="preview-header">
|
|
163
|
+
<h5>${escapeHtml(currentGameForSearch.title_original)}</h5>
|
|
164
|
+
<div class="preview-stats">
|
|
165
|
+
${currentGameForSearch.line_count.toLocaleString()} lines,
|
|
166
|
+
${currentGameForSearch.mined_character_count.toLocaleString()} mined characters
|
|
167
|
+
${currentGameForSearch.jiten_character_count > 0 ? `<br>Game Total: ${currentGameForSearch.jiten_character_count.toLocaleString()} chars` : ''}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
// Populate jiten game preview
|
|
173
|
+
const jitenGamePreview = document.getElementById('jitenGamePreview');
|
|
174
|
+
const mediaTypeMap = {1: 'Anime', 7: 'Visual Novel', 2: 'Manga'};
|
|
175
|
+
jitenGamePreview.innerHTML = `
|
|
176
|
+
<div class="preview-header">
|
|
177
|
+
${selectedJitenGame.cover_name ? `<img src="${selectedJitenGame.cover_name}" style="width: 60px; height: 60px; object-fit: cover; border-radius: 4px; margin-right: 10px;">` : ''}
|
|
178
|
+
<div>
|
|
179
|
+
<h5>${escapeHtml(selectedJitenGame.title_original)}</h5>
|
|
180
|
+
${selectedJitenGame.title_english ? `<p>${escapeHtml(selectedJitenGame.title_english)}</p>` : ''}
|
|
181
|
+
<div class="preview-stats">
|
|
182
|
+
${mediaTypeMap[selectedJitenGame.media_type] || 'Unknown'} |
|
|
183
|
+
Deck ID: ${selectedJitenGame.deck_id} |
|
|
184
|
+
Difficulty: ${selectedJitenGame.difficulty}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
${selectedJitenGame.description ? `<div style="margin-top: 10px; color: var(--text-secondary); font-size: 14px;">${escapeHtml(selectedJitenGame.description.substring(0, 150))}${selectedJitenGame.description.length > 150 ? '...' : ''}</div>` : ''}
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
// Show manual overrides warning if any
|
|
192
|
+
const warningDiv = document.getElementById('manualOverridesWarning');
|
|
193
|
+
const overriddenFieldsList = document.getElementById('overriddenFieldsList');
|
|
194
|
+
|
|
195
|
+
if (currentGameForSearch.has_manual_overrides && currentGameForSearch.manual_overrides) {
|
|
196
|
+
const overridesStr = safeJoinArray(currentGameForSearch.manual_overrides, ', ');
|
|
197
|
+
if (overridesStr) {
|
|
198
|
+
overriddenFieldsList.innerHTML = `<div>Fields: ${overridesStr}</div>`;
|
|
199
|
+
warningDiv.style.display = 'block';
|
|
200
|
+
} else {
|
|
201
|
+
warningDiv.style.display = 'none';
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
warningDiv.style.display = 'none';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Close search modal and open confirmation modal
|
|
208
|
+
closeModal('jitenSearchModal');
|
|
209
|
+
openModal('gameLinkConfirmModal');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Confirm and execute game linking to jiten.moe
|
|
214
|
+
*/
|
|
215
|
+
async function confirmLinkGame() {
|
|
216
|
+
if (!currentGameForSearch || !selectedJitenGame) {
|
|
217
|
+
showDatabaseErrorPopup('Missing game or jiten data. Please try again.');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Prevent concurrent link operations
|
|
222
|
+
if (isLinkingInProgress) {
|
|
223
|
+
console.log('Link operation already in progress, ignoring request');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Validate game ID before making API call
|
|
228
|
+
if (!currentGameForSearch.id || currentGameForSearch.id === 'undefined' || currentGameForSearch.id === 'null') {
|
|
229
|
+
showDatabaseErrorPopup(`Cannot link game: Invalid game ID (${currentGameForSearch.id}). Please refresh the page and try again.`);
|
|
230
|
+
console.error('Invalid game ID in confirmLinkGame:', currentGameForSearch);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const errorDiv = document.getElementById('linkConfirmError');
|
|
235
|
+
const loadingDiv = document.getElementById('linkConfirmLoading');
|
|
236
|
+
const confirmBtn = document.getElementById('confirmLinkBtn');
|
|
237
|
+
|
|
238
|
+
// Set global lock
|
|
239
|
+
isLinkingInProgress = true;
|
|
240
|
+
|
|
241
|
+
errorDiv.style.display = 'none';
|
|
242
|
+
loadingDiv.style.display = 'flex';
|
|
243
|
+
confirmBtn.disabled = true;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const apiUrl = `/api/games/${currentGameForSearch.id}/link-jiten`;
|
|
247
|
+
console.log(`Linking game to jiten.moe: ${apiUrl}`);
|
|
248
|
+
|
|
249
|
+
const response = await fetch(apiUrl, {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: { 'Content-Type': 'application/json' },
|
|
252
|
+
body: JSON.stringify({
|
|
253
|
+
deck_id: selectedJitenGame.deck_id,
|
|
254
|
+
jiten_data: selectedJitenGame
|
|
255
|
+
})
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const result = await response.json();
|
|
259
|
+
|
|
260
|
+
if (response.ok) {
|
|
261
|
+
// Success! Close modal
|
|
262
|
+
closeModal('gameLinkConfirmModal');
|
|
263
|
+
|
|
264
|
+
// Log the complete API response for debugging
|
|
265
|
+
logApiResponse('Link Game to Jiten', response, result);
|
|
266
|
+
|
|
267
|
+
// Show success message with line count
|
|
268
|
+
const lineCount = result.lines_linked || currentGameForSearch.line_count || 0;
|
|
269
|
+
console.log(`✅ Game linking successful: ${lineCount} lines linked`);
|
|
270
|
+
showDatabaseSuccessPopup(`Successfully linked "${currentGameForSearch.title_original}" to jiten.moe! ${lineCount} lines linked.`);
|
|
271
|
+
|
|
272
|
+
// Refresh data without page reload
|
|
273
|
+
await refreshAfterLinking();
|
|
274
|
+
|
|
275
|
+
// Reset state for next operation
|
|
276
|
+
confirmBtn.disabled = false;
|
|
277
|
+
} else {
|
|
278
|
+
const errorMessage = result.error || 'Failed to link game';
|
|
279
|
+
errorDiv.textContent = errorMessage;
|
|
280
|
+
errorDiv.style.display = 'block';
|
|
281
|
+
confirmBtn.disabled = false;
|
|
282
|
+
console.error('Link game API error:', result);
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error('Error linking game:', error);
|
|
286
|
+
errorDiv.textContent = `Network error: ${error.message || 'Failed to connect to server'}`;
|
|
287
|
+
errorDiv.style.display = 'block';
|
|
288
|
+
confirmBtn.disabled = false;
|
|
289
|
+
} finally {
|
|
290
|
+
loadingDiv.style.display = 'none';
|
|
291
|
+
// Release global lock
|
|
292
|
+
isLinkingInProgress = false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Refresh data after successful game linking without page reload
|
|
298
|
+
*/
|
|
299
|
+
async function refreshAfterLinking() {
|
|
300
|
+
console.log('🔄 Refreshing data after game linking...');
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Fetch updated game data from API
|
|
304
|
+
const gamesResponse = await fetch('/api/games-management');
|
|
305
|
+
const gamesData = await gamesResponse.json();
|
|
306
|
+
|
|
307
|
+
if (gamesResponse.ok && gamesData.games) {
|
|
308
|
+
// Update the currentGames array with fresh data
|
|
309
|
+
currentGames = gamesData.games;
|
|
310
|
+
console.log(`✅ Updated currentGames array with ${currentGames.length} games`);
|
|
311
|
+
|
|
312
|
+
// Get the current filter state
|
|
313
|
+
const activeFilterBtn = document.querySelector('.game-data-filters button.primary');
|
|
314
|
+
const currentFilter = activeFilterBtn ? activeFilterBtn.dataset.filter : 'all';
|
|
315
|
+
|
|
316
|
+
// Re-render the games list with the current filter (this is smooth, no loading indicator)
|
|
317
|
+
renderGamesList(currentGames, currentFilter);
|
|
318
|
+
|
|
319
|
+
// Silently update dashboard stats in the background
|
|
320
|
+
if (typeof databaseManager !== 'undefined' && databaseManager.loadGameManagementStats) {
|
|
321
|
+
await databaseManager.loadGameManagementStats();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.log('✅ Data refresh completed successfully');
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error('Error refreshing data after linking:', error);
|
|
328
|
+
// Don't show error to user since the link operation itself succeeded
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Mark a game as completed
|
|
334
|
+
* @param {string} gameId - Game ID to mark as completed
|
|
335
|
+
*/
|
|
336
|
+
async function markGameCompleted(gameId) {
|
|
337
|
+
showDatabaseConfirmPopup('Mark this game as completed?', async () => {
|
|
338
|
+
try {
|
|
339
|
+
const response = await fetch(`/api/games/${gameId}`, {
|
|
340
|
+
method: 'PUT',
|
|
341
|
+
headers: { 'Content-Type': 'application/json' },
|
|
342
|
+
body: JSON.stringify({ completed: true })
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const result = await response.json();
|
|
346
|
+
|
|
347
|
+
if (response.ok) {
|
|
348
|
+
await loadGamesForDataManagement();
|
|
349
|
+
showDatabaseSuccessPopup('Game marked as completed!');
|
|
350
|
+
} else {
|
|
351
|
+
showDatabaseErrorPopup(`Error: ${result.error}`);
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error('Error marking game as completed:', error);
|
|
355
|
+
showDatabaseErrorPopup('Failed to mark game as completed');
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Edit a game's information
|
|
362
|
+
* @param {string} gameId - Game ID to edit
|
|
363
|
+
*/
|
|
364
|
+
function editGame(gameId) {
|
|
365
|
+
const game = currentGames.find(g => g.id === gameId);
|
|
366
|
+
if (!game) {
|
|
367
|
+
showDatabaseErrorPopup('Game not found');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
openEditGameModal(game);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Open edit game modal with game data
|
|
376
|
+
* @param {Object} game - Game object to edit
|
|
377
|
+
*/
|
|
378
|
+
function openEditGameModal(game) {
|
|
379
|
+
// Populate form fields with current game data
|
|
380
|
+
document.getElementById('editGameId').value = game.id;
|
|
381
|
+
document.getElementById('editTitleOriginal').value = game.title_original || '';
|
|
382
|
+
document.getElementById('editTitleRomaji').value = game.title_romaji || '';
|
|
383
|
+
document.getElementById('editTitleEnglish').value = game.title_english || '';
|
|
384
|
+
document.getElementById('editType').value = game.type || '';
|
|
385
|
+
document.getElementById('editDescription').value = game.description || '';
|
|
386
|
+
document.getElementById('editDifficulty').value = game.difficulty || '';
|
|
387
|
+
document.getElementById('editDeckId').value = game.deck_id || '';
|
|
388
|
+
document.getElementById('editCharacterCount').value = game.jiten_character_count || '';
|
|
389
|
+
document.getElementById('editCompleted').checked = !!game.completed;
|
|
390
|
+
|
|
391
|
+
// Handle release date - convert ISO format to date input format (YYYY-MM-DD)
|
|
392
|
+
if (game.release_date) {
|
|
393
|
+
try {
|
|
394
|
+
const date = new Date(game.release_date);
|
|
395
|
+
if (!isNaN(date.getTime())) {
|
|
396
|
+
document.getElementById('editReleaseDate').value = date.toISOString().split('T')[0];
|
|
397
|
+
} else {
|
|
398
|
+
document.getElementById('editReleaseDate').value = '';
|
|
399
|
+
}
|
|
400
|
+
} catch (error) {
|
|
401
|
+
console.warn('Error parsing release date:', game.release_date, error);
|
|
402
|
+
document.getElementById('editReleaseDate').value = '';
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
document.getElementById('editReleaseDate').value = '';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Handle links JSON - keep the hidden field updated for compatibility
|
|
409
|
+
if (game.links && game.links.length > 0) {
|
|
410
|
+
document.getElementById('editLinks').value = JSON.stringify(game.links, null, 2);
|
|
411
|
+
|
|
412
|
+
// Extract URLs from links array and populate the list textarea
|
|
413
|
+
// Handle both array of objects and array of strings
|
|
414
|
+
const urls = game.links.map(link => {
|
|
415
|
+
if (typeof link === 'string') {
|
|
416
|
+
return link;
|
|
417
|
+
} else if (link && link.url) {
|
|
418
|
+
return link.url;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}).filter(url => url);
|
|
422
|
+
|
|
423
|
+
document.getElementById('editLinksList').value = urls.join('\n');
|
|
424
|
+
} else {
|
|
425
|
+
document.getElementById('editLinks').value = '';
|
|
426
|
+
document.getElementById('editLinksList').value = '';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Handle image preview
|
|
430
|
+
const imagePreview = document.getElementById('editImagePreview');
|
|
431
|
+
const imagePreviewImg = document.getElementById('editImagePreviewImg');
|
|
432
|
+
if (game.image) {
|
|
433
|
+
imagePreviewImg.src = game.image.startsWith('data:') ? game.image : `data:image/png;base64,${game.image}`;
|
|
434
|
+
imagePreview.style.display = 'block';
|
|
435
|
+
} else {
|
|
436
|
+
imagePreview.style.display = 'none';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Reset file input
|
|
440
|
+
document.getElementById('editImageUpload').value = '';
|
|
441
|
+
|
|
442
|
+
// Reset error display
|
|
443
|
+
document.getElementById('editGameError').style.display = 'none';
|
|
444
|
+
|
|
445
|
+
// Open the modal
|
|
446
|
+
openModal('editGameModal');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Convert any image file to PNG format using Canvas API
|
|
451
|
+
* @param {File} file - The image file to convert
|
|
452
|
+
* @returns {Promise<string>} Base64 PNG data (without data URI prefix)
|
|
453
|
+
*/
|
|
454
|
+
async function convertImageToPNG(file) {
|
|
455
|
+
return new Promise((resolve, reject) => {
|
|
456
|
+
const img = new Image();
|
|
457
|
+
const reader = new FileReader();
|
|
458
|
+
|
|
459
|
+
reader.onload = (e) => {
|
|
460
|
+
img.onload = () => {
|
|
461
|
+
try {
|
|
462
|
+
// Create canvas and draw image
|
|
463
|
+
const canvas = document.createElement('canvas');
|
|
464
|
+
canvas.width = img.width;
|
|
465
|
+
canvas.height = img.height;
|
|
466
|
+
const ctx = canvas.getContext('2d');
|
|
467
|
+
ctx.drawImage(img, 0, 0);
|
|
468
|
+
|
|
469
|
+
// Convert to PNG base64 (remove data:image/png;base64, prefix)
|
|
470
|
+
const pngDataUrl = canvas.toDataURL('image/png');
|
|
471
|
+
const pngBase64 = pngDataUrl.split(',')[1];
|
|
472
|
+
resolve(pngBase64);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
reject(new Error(`Failed to convert image to PNG: ${error.message}`));
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
img.onerror = () => reject(new Error('Failed to load image for conversion'));
|
|
478
|
+
img.src = e.target.result;
|
|
479
|
+
};
|
|
480
|
+
reader.onerror = () => reject(new Error('Failed to read image file'));
|
|
481
|
+
reader.readAsDataURL(file);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Save game edits
|
|
487
|
+
*/
|
|
488
|
+
async function saveGameEdits() {
|
|
489
|
+
const gameId = document.getElementById('editGameId').value;
|
|
490
|
+
const errorDiv = document.getElementById('editGameError');
|
|
491
|
+
const loadingDiv = document.getElementById('editGameLoading');
|
|
492
|
+
const saveBtn = document.getElementById('saveGameEditsBtn');
|
|
493
|
+
|
|
494
|
+
// Reset error display
|
|
495
|
+
errorDiv.style.display = 'none';
|
|
496
|
+
|
|
497
|
+
// Validate required fields
|
|
498
|
+
const titleOriginal = document.getElementById('editTitleOriginal').value.trim();
|
|
499
|
+
if (!titleOriginal) {
|
|
500
|
+
errorDiv.textContent = 'Original title is required';
|
|
501
|
+
errorDiv.style.display = 'block';
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Convert links list to JSON array
|
|
506
|
+
const linksListText = document.getElementById('editLinksList').value.trim();
|
|
507
|
+
let linksArray = [];
|
|
508
|
+
if (linksListText) {
|
|
509
|
+
// Split by newlines and filter out empty lines
|
|
510
|
+
const urls = linksListText.split('\n')
|
|
511
|
+
.map(url => url.trim())
|
|
512
|
+
.filter(url => url.length > 0);
|
|
513
|
+
|
|
514
|
+
// Convert each URL to the required JSON format
|
|
515
|
+
linksArray = urls.map(url => ({
|
|
516
|
+
deckId: 1,
|
|
517
|
+
linkId: 1,
|
|
518
|
+
linkType: 2,
|
|
519
|
+
url: url
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Validate difficulty
|
|
524
|
+
const difficulty = document.getElementById('editDifficulty').value;
|
|
525
|
+
if (difficulty && (parseInt(difficulty) < 1 || parseInt(difficulty) > 5)) {
|
|
526
|
+
errorDiv.textContent = 'Difficulty must be between 1 and 5';
|
|
527
|
+
errorDiv.style.display = 'block';
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Show loading state
|
|
532
|
+
loadingDiv.style.display = 'flex';
|
|
533
|
+
saveBtn.disabled = true;
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
// Prepare update data
|
|
537
|
+
const updateData = {
|
|
538
|
+
title_original: titleOriginal,
|
|
539
|
+
title_romaji: document.getElementById('editTitleRomaji').value.trim(),
|
|
540
|
+
title_english: document.getElementById('editTitleEnglish').value.trim(),
|
|
541
|
+
type: document.getElementById('editType').value,
|
|
542
|
+
description: document.getElementById('editDescription').value.trim(),
|
|
543
|
+
completed: document.getElementById('editCompleted').checked
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// Add release date if provided
|
|
547
|
+
const releaseDate = document.getElementById('editReleaseDate').value;
|
|
548
|
+
if (releaseDate) {
|
|
549
|
+
// Convert date input (YYYY-MM-DD) to ISO format for storage
|
|
550
|
+
updateData.release_date = releaseDate + 'T00:00:00';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Add optional numeric fields
|
|
554
|
+
const deckId = document.getElementById('editDeckId').value;
|
|
555
|
+
if (deckId) {
|
|
556
|
+
updateData.deck_id = parseInt(deckId);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (difficulty) {
|
|
560
|
+
updateData.difficulty = parseInt(difficulty);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const characterCount = document.getElementById('editCharacterCount').value;
|
|
564
|
+
if (characterCount) {
|
|
565
|
+
updateData.character_count = parseInt(characterCount);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Add links if provided
|
|
569
|
+
if (linksArray.length > 0) {
|
|
570
|
+
updateData.links = linksArray;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Handle image upload - convert to PNG format
|
|
574
|
+
const imageFile = document.getElementById('editImageUpload').files[0];
|
|
575
|
+
if (imageFile) {
|
|
576
|
+
try {
|
|
577
|
+
const pngBase64 = await convertImageToPNG(imageFile);
|
|
578
|
+
updateData.image = pngBase64;
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.error('Error converting image:', error);
|
|
581
|
+
errorDiv.textContent = `Failed to process image: ${error.message}`;
|
|
582
|
+
errorDiv.style.display = 'block';
|
|
583
|
+
saveBtn.disabled = false;
|
|
584
|
+
loadingDiv.style.display = 'none';
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Send update request
|
|
590
|
+
const response = await fetch(`/api/games/${gameId}`, {
|
|
591
|
+
method: 'PUT',
|
|
592
|
+
headers: { 'Content-Type': 'application/json' },
|
|
593
|
+
body: JSON.stringify(updateData)
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const result = await response.json();
|
|
597
|
+
|
|
598
|
+
if (response.ok) {
|
|
599
|
+
// Success! Close modal and refresh
|
|
600
|
+
closeModal('editGameModal');
|
|
601
|
+
await loadGamesForDataManagement();
|
|
602
|
+
showDatabaseSuccessPopup('Game updated successfully! All edited fields marked as manual overrides.');
|
|
603
|
+
} else {
|
|
604
|
+
errorDiv.textContent = result.error || 'Failed to update game';
|
|
605
|
+
errorDiv.style.display = 'block';
|
|
606
|
+
saveBtn.disabled = false;
|
|
607
|
+
}
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.error('Error saving game edits:', error);
|
|
610
|
+
errorDiv.textContent = `Error: ${error.message}`;
|
|
611
|
+
errorDiv.style.display = 'block';
|
|
612
|
+
saveBtn.disabled = false;
|
|
613
|
+
} finally {
|
|
614
|
+
loadingDiv.style.display = 'none';
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Repull data from jiten.moe for a game
|
|
620
|
+
* @param {string} gameId - Game ID to repull data for
|
|
621
|
+
* @param {string} gameName - Game name for display
|
|
622
|
+
*/
|
|
623
|
+
async function repullJitenData(gameId, gameName) {
|
|
624
|
+
console.log(`🔄 Starting repull operation for game: ${gameName} (ID: ${gameId})`);
|
|
625
|
+
|
|
626
|
+
showDatabaseConfirmPopup(
|
|
627
|
+
`Repull data from jiten.moe for "${gameName}"? This will update all non-manually edited fields with fresh data from jiten.moe.`,
|
|
628
|
+
async () => {
|
|
629
|
+
console.log(`✅ User confirmed repull for ${gameName}`);
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
console.log(`📡 Making API request to /api/games/${gameId}/repull-jiten`);
|
|
633
|
+
|
|
634
|
+
const response = await fetch(`/api/games/${gameId}/repull-jiten`, {
|
|
635
|
+
method: 'POST',
|
|
636
|
+
headers: { 'Content-Type': 'application/json' }
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
console.log(`📥 Received response:`, {
|
|
640
|
+
status: response.status,
|
|
641
|
+
statusText: response.statusText,
|
|
642
|
+
ok: response.ok,
|
|
643
|
+
headers: Object.fromEntries(response.headers.entries())
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const result = await response.json();
|
|
647
|
+
|
|
648
|
+
// Log the complete API response for debugging
|
|
649
|
+
logApiResponse('Repull Jiten Data', response, result);
|
|
650
|
+
|
|
651
|
+
if (response.ok) {
|
|
652
|
+
console.log(`✅ Repull operation successful for ${gameName}`);
|
|
653
|
+
|
|
654
|
+
let message = result.message || 'Repull completed successfully';
|
|
655
|
+
|
|
656
|
+
// Safe handling of updated_fields
|
|
657
|
+
if (result.updated_fields) {
|
|
658
|
+
const updatedFieldsStr = safeJoinArray(result.updated_fields, ', ');
|
|
659
|
+
if (updatedFieldsStr) {
|
|
660
|
+
message += ` Updated fields: ${updatedFieldsStr}.`;
|
|
661
|
+
console.log(`📝 Updated fields: ${updatedFieldsStr}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Safe handling of skipped_fields
|
|
666
|
+
if (result.skipped_fields) {
|
|
667
|
+
const skippedFieldsStr = safeJoinArray(result.skipped_fields, ', ');
|
|
668
|
+
if (skippedFieldsStr) {
|
|
669
|
+
message += ` Skipped (manually edited): ${skippedFieldsStr}.`;
|
|
670
|
+
console.log(`⏭️ Skipped fields: ${skippedFieldsStr}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
console.log(`📢 Final success message: ${message}`);
|
|
675
|
+
showDatabaseSuccessPopup(message);
|
|
676
|
+
|
|
677
|
+
// Refresh the current tab to show updated data
|
|
678
|
+
console.log(`🔄 Refreshing current tab to show updated data`);
|
|
679
|
+
const activeTab = document.querySelector('.tab-btn.active');
|
|
680
|
+
if (activeTab) {
|
|
681
|
+
console.log(`🔄 Switching to tab: ${activeTab.dataset.tab}`);
|
|
682
|
+
switchTab(activeTab.dataset.tab);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Update dashboard stats
|
|
686
|
+
console.log(`📊 Updating dashboard stats`);
|
|
687
|
+
if (typeof databaseManager !== 'undefined') {
|
|
688
|
+
await databaseManager.loadDashboardStats();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
console.log(`✅ Repull operation completed successfully for ${gameName}`);
|
|
692
|
+
} else {
|
|
693
|
+
console.error(`❌ Repull operation failed for ${gameName}:`, result);
|
|
694
|
+
const errorMessage = result.error || 'Unknown error occurred';
|
|
695
|
+
showDatabaseErrorPopup(`Error: ${errorMessage}`);
|
|
696
|
+
}
|
|
697
|
+
} catch (error) {
|
|
698
|
+
console.error(`💥 Exception during repull operation for ${gameName}:`, error);
|
|
699
|
+
console.error('Error stack:', error.stack);
|
|
700
|
+
showDatabaseErrorPopup('Failed to repull data from jiten.moe');
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Initialize jiten integration event handlers
|
|
708
|
+
*/
|
|
709
|
+
function initializeJitenIntegration() {
|
|
710
|
+
const jitenSearchBtn = document.getElementById('jitenSearchBtn');
|
|
711
|
+
if (jitenSearchBtn) {
|
|
712
|
+
jitenSearchBtn.addEventListener('click', searchJitenMoe);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Add Enter key support for jiten search input
|
|
716
|
+
const jitenSearchInput = document.getElementById('jitenSearchInput');
|
|
717
|
+
if (jitenSearchInput) {
|
|
718
|
+
jitenSearchInput.addEventListener('keypress', function(e) {
|
|
719
|
+
if (e.key === 'Enter') {
|
|
720
|
+
e.preventDefault();
|
|
721
|
+
searchJitenMoe();
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const confirmLinkBtn = document.getElementById('confirmLinkBtn');
|
|
727
|
+
if (confirmLinkBtn) {
|
|
728
|
+
confirmLinkBtn.addEventListener('click', confirmLinkGame);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Handle image upload preview - convert to PNG for preview
|
|
732
|
+
const imageUpload = document.getElementById('editImageUpload');
|
|
733
|
+
if (imageUpload) {
|
|
734
|
+
imageUpload.addEventListener('change', async function(e) {
|
|
735
|
+
const file = e.target.files[0];
|
|
736
|
+
if (file) {
|
|
737
|
+
try {
|
|
738
|
+
const pngBase64 = await convertImageToPNG(file);
|
|
739
|
+
const imagePreview = document.getElementById('editImagePreview');
|
|
740
|
+
const imagePreviewImg = document.getElementById('editImagePreviewImg');
|
|
741
|
+
imagePreviewImg.src = `data:image/png;base64,${pngBase64}`;
|
|
742
|
+
imagePreview.style.display = 'block';
|
|
743
|
+
} catch (error) {
|
|
744
|
+
console.error('Error previewing image:', error);
|
|
745
|
+
alert(`Failed to preview image: ${error.message}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|