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.

Files changed (70) hide show
  1. GameSentenceMiner/__init__.py +39 -0
  2. GameSentenceMiner/anki.py +6 -3
  3. GameSentenceMiner/gametext.py +13 -2
  4. GameSentenceMiner/gsm.py +40 -3
  5. GameSentenceMiner/locales/en_us.json +4 -0
  6. GameSentenceMiner/locales/ja_jp.json +4 -0
  7. GameSentenceMiner/locales/zh_cn.json +4 -0
  8. GameSentenceMiner/obs.py +4 -1
  9. GameSentenceMiner/owocr/owocr/ocr.py +304 -134
  10. GameSentenceMiner/owocr/owocr/run.py +1 -1
  11. GameSentenceMiner/ui/anki_confirmation.py +4 -2
  12. GameSentenceMiner/ui/config_gui.py +12 -0
  13. GameSentenceMiner/util/configuration.py +6 -2
  14. GameSentenceMiner/util/cron/__init__.py +12 -0
  15. GameSentenceMiner/util/cron/daily_rollup.py +613 -0
  16. GameSentenceMiner/util/cron/jiten_update.py +397 -0
  17. GameSentenceMiner/util/cron/populate_games.py +154 -0
  18. GameSentenceMiner/util/cron/run_crons.py +148 -0
  19. GameSentenceMiner/util/cron/setup_populate_games_cron.py +118 -0
  20. GameSentenceMiner/util/cron_table.py +334 -0
  21. GameSentenceMiner/util/db.py +236 -49
  22. GameSentenceMiner/util/ffmpeg.py +23 -4
  23. GameSentenceMiner/util/games_table.py +340 -93
  24. GameSentenceMiner/util/jiten_api_client.py +188 -0
  25. GameSentenceMiner/util/stats_rollup_table.py +216 -0
  26. GameSentenceMiner/web/anki_api_endpoints.py +438 -220
  27. GameSentenceMiner/web/database_api.py +955 -1259
  28. GameSentenceMiner/web/jiten_database_api.py +1015 -0
  29. GameSentenceMiner/web/rollup_stats.py +672 -0
  30. GameSentenceMiner/web/static/css/dashboard-shared.css +75 -13
  31. GameSentenceMiner/web/static/css/overview.css +604 -47
  32. GameSentenceMiner/web/static/css/search.css +226 -0
  33. GameSentenceMiner/web/static/css/shared.css +762 -0
  34. GameSentenceMiner/web/static/css/stats.css +221 -0
  35. GameSentenceMiner/web/static/js/components/bar-chart.js +339 -0
  36. GameSentenceMiner/web/static/js/database-bulk-operations.js +320 -0
  37. GameSentenceMiner/web/static/js/database-game-data.js +390 -0
  38. GameSentenceMiner/web/static/js/database-game-operations.js +213 -0
  39. GameSentenceMiner/web/static/js/database-helpers.js +44 -0
  40. GameSentenceMiner/web/static/js/database-jiten-integration.js +750 -0
  41. GameSentenceMiner/web/static/js/database-popups.js +89 -0
  42. GameSentenceMiner/web/static/js/database-tabs.js +64 -0
  43. GameSentenceMiner/web/static/js/database-text-management.js +371 -0
  44. GameSentenceMiner/web/static/js/database.js +86 -718
  45. GameSentenceMiner/web/static/js/goals.js +79 -18
  46. GameSentenceMiner/web/static/js/heatmap.js +29 -23
  47. GameSentenceMiner/web/static/js/overview.js +1205 -339
  48. GameSentenceMiner/web/static/js/regex-patterns.js +100 -0
  49. GameSentenceMiner/web/static/js/search.js +215 -18
  50. GameSentenceMiner/web/static/js/shared.js +193 -39
  51. GameSentenceMiner/web/static/js/stats.js +1536 -179
  52. GameSentenceMiner/web/stats.py +1142 -269
  53. GameSentenceMiner/web/stats_api.py +2104 -0
  54. GameSentenceMiner/web/templates/anki_stats.html +4 -18
  55. GameSentenceMiner/web/templates/components/date-range.html +118 -3
  56. GameSentenceMiner/web/templates/components/html-head.html +40 -6
  57. GameSentenceMiner/web/templates/components/js-config.html +8 -8
  58. GameSentenceMiner/web/templates/components/regex-input.html +160 -0
  59. GameSentenceMiner/web/templates/database.html +564 -117
  60. GameSentenceMiner/web/templates/goals.html +41 -5
  61. GameSentenceMiner/web/templates/overview.html +159 -129
  62. GameSentenceMiner/web/templates/search.html +78 -9
  63. GameSentenceMiner/web/templates/stats.html +159 -5
  64. GameSentenceMiner/web/texthooking_page.py +280 -111
  65. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/METADATA +43 -2
  66. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/RECORD +70 -47
  67. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/WHEEL +0 -0
  68. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/entry_points.txt +0 -0
  69. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/licenses/LICENSE +0 -0
  70. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,390 @@
1
+ // Database Game Data Management Functions
2
+ // Dependencies: shared.js (provides escapeHtml, openModal, closeModal), database-helpers.js (provides formatReleaseDate), database-popups.js
3
+
4
+ // Global variables for game data management
5
+ let currentGames = [];
6
+ let currentGameForSearch = null;
7
+ let selectedJitenGame = null;
8
+ let jitenSearchResults = []; // Global storage for search results
9
+
10
+ /**
11
+ * Load games for the Link Games tab (data management)
12
+ */
13
+ async function loadGamesForDataManagement() {
14
+ const loadingIndicator = document.getElementById('gameDataLoadingIndicator');
15
+ const content = document.getElementById('gameDataContent');
16
+ const gamesList = document.getElementById('gameDataList');
17
+
18
+ loadingIndicator.style.display = 'flex';
19
+ content.style.display = 'none';
20
+
21
+ try {
22
+ const gamesResponse = await fetch('/api/games-management');
23
+ const gamesData = await gamesResponse.json();
24
+
25
+ if (gamesResponse.ok) {
26
+ currentGames = gamesData.games || [];
27
+
28
+ // Validate that all games have IDs
29
+ const gamesWithoutIds = currentGames.filter(game => !game.id);
30
+ if (gamesWithoutIds.length > 0) {
31
+ console.error(`Found ${gamesWithoutIds.length} games without IDs:`, gamesWithoutIds);
32
+ showDatabaseErrorPopup(`Warning: ${gamesWithoutIds.length} games are missing IDs. Please refresh the page.`);
33
+ }
34
+
35
+ console.log(`Loaded ${currentGames.length} games`);
36
+
37
+ renderGamesList(currentGames);
38
+ content.style.display = 'block';
39
+ } else {
40
+ const errorMsg = gamesData.error || 'Failed to load games';
41
+ gamesList.innerHTML = `<p class="error-text">${escapeHtml(errorMsg)}</p>`;
42
+ content.style.display = 'block';
43
+ console.error('Failed to load games:', gamesData);
44
+ }
45
+ } catch (error) {
46
+ console.error('Error loading games for data management:', error);
47
+ gamesList.innerHTML = `<p class="error-text">Network error: ${escapeHtml(error.message)}</p>`;
48
+ content.style.display = 'block';
49
+ } finally {
50
+ loadingIndicator.style.display = 'none';
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Render games list with filtering
56
+ * @param {Array} games - Array of game objects
57
+ * @param {string} filter - Filter type ('all', 'linked', 'unlinked')
58
+ */
59
+ function renderGamesList(games, filter = 'all') {
60
+ const gamesList = document.getElementById('gameDataList');
61
+
62
+ // Filter games based on selection
63
+ let filteredGames = games;
64
+ if (filter === 'linked') {
65
+ filteredGames = games.filter(game => game.is_linked);
66
+ } else if (filter === 'unlinked') {
67
+ filteredGames = games.filter(game => !game.is_linked);
68
+ }
69
+
70
+ // Update filter button states
71
+ document.querySelectorAll('.game-data-filters button').forEach(btn => {
72
+ btn.classList.remove('primary');
73
+ btn.classList.add('action-btn');
74
+ });
75
+ const activeBtn = document.getElementById(`filter${filter.charAt(0).toUpperCase() + filter.slice(1)}`);
76
+ if (activeBtn) {
77
+ activeBtn.classList.add('primary');
78
+ activeBtn.classList.remove('action-btn');
79
+ }
80
+
81
+ gamesList.innerHTML = '';
82
+
83
+ // Render existing games first
84
+ if (filteredGames.length > 0) {
85
+ filteredGames.forEach(game => {
86
+ const gameItem = document.createElement('div');
87
+ gameItem.className = 'game-data-item';
88
+
89
+ // Create status indicators
90
+ const statusIndicators = [];
91
+ if (game.is_linked) {
92
+ statusIndicators.push('<span class="status-badge linked">✅ Linked</span>');
93
+ } else {
94
+ statusIndicators.push('<span class="status-badge unlinked">🔍 Not Linked</span>');
95
+ }
96
+
97
+ if (game.has_manual_overrides) {
98
+ statusIndicators.push('<span class="status-badge manual">📝 Manual Edits</span>');
99
+ }
100
+
101
+ if (game.completed) {
102
+ statusIndicators.push('<span class="status-badge completed">🏁 Completed</span>');
103
+ }
104
+
105
+ // Format dates
106
+ const startDate = game.start_date ? new Date(game.start_date * 1000).toLocaleDateString() : 'Unknown';
107
+ const lastPlayed = game.last_played ? new Date(game.last_played * 1000).toLocaleDateString() : 'Unknown';
108
+
109
+ gameItem.innerHTML = `
110
+ <div class="game-header">
111
+ ${game.image ? `<img src="${game.image.startsWith('data:') ? game.image : 'data:image/png;base64,' + game.image}" class="game-thumbnail" alt="Game cover">` : '<div class="game-thumbnail-placeholder">🎮</div>'}
112
+ <div class="game-info">
113
+ <h4 class="game-title">${escapeHtml(game.title_original)}</h4>
114
+ ${game.title_english ? `<p class="game-title-en">${escapeHtml(game.title_english)}</p>` : ''}
115
+ ${game.title_romaji ? `<p class="game-title-rom">${escapeHtml(game.title_romaji)}</p>` : ''}
116
+ <div class="game-type-difficulty">
117
+ ${game.type ? `<span class="game-type">${escapeHtml(game.type)}</span>` : ''}
118
+ ${game.difficulty ? `<span class="game-difficulty">Difficulty: ${game.difficulty}</span>` : ''}
119
+ </div>
120
+ </div>
121
+ <div class="game-status">
122
+ ${statusIndicators.join('')}
123
+ </div>
124
+ </div>
125
+ ${game.line_count > 0 ? `
126
+ <div class="game-stats">
127
+ <span class="stat-item">${game.line_count.toLocaleString()} lines</span>
128
+ <span class="stat-item">${game.mined_character_count.toLocaleString()} read</span>
129
+ ${game.jiten_character_count > 0 ? `<span class="stat-item">Total: ${game.jiten_character_count.toLocaleString()} chars (${((game.mined_character_count / game.jiten_character_count) * 100).toFixed(1)}%)</span>` : ''}
130
+ <span class="stat-item">Started: ${startDate}</span>
131
+ <span class="stat-item">Last: ${lastPlayed}</span>
132
+ ${game.release_date ? `<span class="stat-item">Released: ${formatReleaseDate(game.release_date)}</span>` : ''}
133
+ </div>
134
+ ` : ''}
135
+ <div class="game-actions">
136
+ ${!game.is_linked ? `<button class="action-btn primary jiten-search-btn" data-game-id="${game.id}" data-title="${escapeHtml(game.title_original)}">🔍 Search jiten.moe</button>` : ''}
137
+ ${game.is_linked ? `<button class="action-btn warning repull-jiten-btn" data-game-id="${game.id}" data-title="${escapeHtml(game.title_original)}">🔄 Repull from Jiten</button>` : ''}
138
+ <button class="action-btn edit-game-btn" data-game-id="${game.id}">📝 Edit</button>
139
+ ${!game.completed ? `<button class="action-btn success mark-complete-btn" data-game-id="${game.id}">🏁 Mark Complete</button>` : ''}
140
+ </div>
141
+ ${game.description ? `<div class="game-description">${escapeHtml(game.description)}</div>` : ''}
142
+ `;
143
+
144
+ gamesList.appendChild(gameItem);
145
+ });
146
+
147
+ // Attach event listeners to action buttons
148
+ gamesList.querySelectorAll('.jiten-search-btn').forEach(btn => {
149
+ btn.addEventListener('click', function() {
150
+ openJitenSearch(btn.getAttribute('data-game-id'), btn.getAttribute('data-title'));
151
+ });
152
+ });
153
+ gamesList.querySelectorAll('.repull-jiten-btn').forEach(btn => {
154
+ btn.addEventListener('click', function() {
155
+ repullJitenData(btn.getAttribute('data-game-id'), btn.getAttribute('data-title'));
156
+ });
157
+ });
158
+ gamesList.querySelectorAll('.edit-game-btn').forEach(btn => {
159
+ btn.addEventListener('click', function() {
160
+ editGame(btn.getAttribute('data-game-id'));
161
+ });
162
+ });
163
+ gamesList.querySelectorAll('.mark-complete-btn').forEach(btn => {
164
+ btn.addEventListener('click', function() {
165
+ markGameCompleted(btn.getAttribute('data-game-id'));
166
+ });
167
+ });
168
+ } else {
169
+ // Show empty state if no games
170
+ gamesList.innerHTML = `
171
+ <div style="text-align: center; color: var(--text-secondary); padding: 40px;">
172
+ <p>No games found.</p>
173
+ <p>Start playing games to see them appear here!</p>
174
+ </div>
175
+ `;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Filter games by type
181
+ * @param {string} filter - Filter type ('all', 'linked', 'unlinked')
182
+ */
183
+ function filterGames(filter) {
184
+ renderGamesList(currentGames, filter);
185
+ }
186
+
187
+ /**
188
+ * Load games for the Manage Games tab
189
+ */
190
+ async function loadGamesForManagement() {
191
+ const loadingIndicator = document.getElementById('manageGamesLoadingIndicator');
192
+ const content = document.getElementById('manageGamesContent');
193
+ const gamesList = document.getElementById('manageGamesList');
194
+
195
+ loadingIndicator.style.display = 'flex';
196
+ content.style.display = 'none';
197
+
198
+ try {
199
+ const gamesResponse = await fetch('/api/games-management');
200
+ const gamesData = await gamesResponse.json();
201
+
202
+ if (gamesResponse.ok) {
203
+ const games = gamesData.games || [];
204
+
205
+ // Sort games alphabetically by title_original
206
+ games.sort((a, b) => {
207
+ const titleA = (a.title_original || '').toLowerCase();
208
+ const titleB = (b.title_original || '').toLowerCase();
209
+ return titleA.localeCompare(titleB);
210
+ });
211
+
212
+ gamesList.innerHTML = '';
213
+
214
+ games.forEach(game => {
215
+ const gameItem = document.createElement('div');
216
+ gameItem.className = 'manage-game-item';
217
+
218
+ // Create status indicators
219
+ const statusIndicators = [];
220
+ if (game.is_linked) {
221
+ statusIndicators.push('<span class="status-badge linked">✅ Linked</span>');
222
+ } else {
223
+ statusIndicators.push('<span class="status-badge unlinked">🔍 Not Linked</span>');
224
+ }
225
+
226
+ if (game.has_manual_overrides) {
227
+ statusIndicators.push('<span class="status-badge manual">📝 Manual Edits</span>');
228
+ }
229
+
230
+ if (game.completed) {
231
+ statusIndicators.push('<span class="status-badge completed">🏁 Completed</span>');
232
+ }
233
+
234
+ // Format dates
235
+ const startDate = game.start_date ? new Date(game.start_date * 1000).toLocaleDateString() : 'Unknown';
236
+ const lastPlayed = game.last_played ? new Date(game.last_played * 1000).toLocaleDateString() : 'Unknown';
237
+
238
+ gameItem.innerHTML = `
239
+ <div class="game-header">
240
+ ${game.image ? `<img src="${game.image.startsWith('data:') ? game.image : 'data:image/png;base64,' + game.image}" class="game-thumbnail" alt="Game cover">` : '<div class="game-thumbnail-placeholder">🎮</div>'}
241
+ <div class="game-info">
242
+ <h4 class="game-title">${escapeHtml(game.title_original)}</h4>
243
+ ${game.title_english ? `<p class="game-title-en">${escapeHtml(game.title_english)}</p>` : ''}
244
+ ${game.title_romaji ? `<p class="game-title-rom">${escapeHtml(game.title_romaji)}</p>` : ''}
245
+ <div class="game-type-difficulty">
246
+ ${game.type ? `<span class="game-type">${escapeHtml(game.type)}</span>` : ''}
247
+ ${game.difficulty ? `<span class="game-difficulty">Difficulty: ${game.difficulty}</span>` : ''}
248
+ </div>
249
+ </div>
250
+ <div class="game-status">
251
+ ${statusIndicators.join('')}
252
+ </div>
253
+ </div>
254
+ ${game.line_count > 0 ? `
255
+ <div class="game-stats">
256
+ <span class="stat-item">${game.line_count.toLocaleString()} lines</span>
257
+ <span class="stat-item">${game.mined_character_count.toLocaleString()} read</span>
258
+ ${game.jiten_character_count > 0 ? `<span class="stat-item">Total: ${game.jiten_character_count.toLocaleString()} chars (${((game.mined_character_count / game.jiten_character_count) * 100).toFixed(1)}%)</span>` : ''}
259
+ <span class="stat-item">Started: ${startDate}</span>
260
+ <span class="stat-item">Last: ${lastPlayed}</span>
261
+ ${game.release_date ? `<span class="stat-item">Released: ${formatReleaseDate(game.release_date)}</span>` : ''}
262
+ </div>
263
+ ` : ''}
264
+ <div class="individual-game-actions">
265
+ ${game.is_linked ? `<button class="action-btn unlink-btn" onclick="openIndividualGameUnlinkModal('${game.id}', '${escapeHtml(game.title_original)}', ${game.line_count}, ${game.mined_character_count})">🔗 Unlink Game</button>` : ''}
266
+ <button class="action-btn delete-lines-btn" onclick="openIndividualGameDeleteModal('${game.id}', '${escapeHtml(game.title_original)}', ${game.line_count}, ${game.mined_character_count})">🗑️ Delete Game Lines</button>
267
+ ${!game.is_linked ? `<button class="action-btn primary" onclick="openJitenSearch('${game.id}', '${escapeHtml(game.title_original)}')">🔍 Search jiten.moe</button>` : ''}
268
+ ${game.is_linked ? `<button class="action-btn warning" onclick="repullJitenData('${game.id}', '${escapeHtml(game.title_original)}')">🔄 Repull from Jiten</button>` : ''}
269
+ <button class="action-btn" onclick="editGame('${game.id}')">📝 Edit</button>
270
+ ${!game.completed ? `<button class="action-btn success" onclick="markGameCompleted('${game.id}')">🏁 Mark Complete</button>` : ''}
271
+ </div>
272
+ ${game.description ? `<div class="game-description">${escapeHtml(game.description)}</div>` : ''}
273
+ `;
274
+
275
+ gamesList.appendChild(gameItem);
276
+ });
277
+
278
+ content.style.display = 'block';
279
+ } else {
280
+ const errorMsg = gamesData.error || 'Failed to load games';
281
+ gamesList.innerHTML = `<p class="error-text">${escapeHtml(errorMsg)}</p>`;
282
+ content.style.display = 'block';
283
+ }
284
+ } catch (error) {
285
+ console.error('Error loading games for management:', error);
286
+ gamesList.innerHTML = `<p class="error-text">Network error: ${escapeHtml(error.message)}</p>`;
287
+ content.style.display = 'block';
288
+ } finally {
289
+ loadingIndicator.style.display = 'none';
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Load games for bulk operations tab
295
+ */
296
+ async function loadGamesForBulkOperations() {
297
+ const loadingIndicator = document.getElementById('bulkGamesLoadingIndicator');
298
+ const content = document.getElementById('bulkGamesContent');
299
+ const gamesList = document.getElementById('bulkGamesList');
300
+
301
+ loadingIndicator.style.display = 'flex';
302
+ content.style.display = 'none';
303
+
304
+ try {
305
+ const response = await fetch('/api/games-list');
306
+ const data = await response.json();
307
+
308
+ if (response.ok && data.games) {
309
+ // Sort games alphabetically by name
310
+ data.games.sort((a, b) => {
311
+ const nameA = (a.name || '').toLowerCase();
312
+ const nameB = (b.name || '').toLowerCase();
313
+ return nameA.localeCompare(nameB);
314
+ });
315
+
316
+ gamesList.innerHTML = '';
317
+
318
+ data.games.forEach(game => {
319
+ const gameItem = document.createElement('div');
320
+ gameItem.className = 'checkbox-container';
321
+ gameItem.innerHTML = `
322
+ <input type="checkbox" class="checkbox-input game-checkbox" data-game="${escapeHtml(game.name)}">
323
+ <label class="checkbox-label">
324
+ <strong>${escapeHtml(game.name)}</strong><br>
325
+ <small style="color: var(--text-tertiary);">
326
+ ${game.sentence_count} sentences, ${game.total_characters.toLocaleString()} characters
327
+ </small>
328
+ </label>
329
+ `;
330
+
331
+ // Add event listener for the checkbox
332
+ const checkbox = gameItem.querySelector('.game-checkbox');
333
+ checkbox.addEventListener('change', (event) => handleGameSelectionChange(event));
334
+
335
+ gamesList.appendChild(gameItem);
336
+ });
337
+
338
+ content.style.display = 'block';
339
+ }
340
+ } catch (error) {
341
+ console.error('Error loading games:', error);
342
+ gamesList.innerHTML = '<p class="error-text">Failed to load games</p>';
343
+ content.style.display = 'block';
344
+ } finally {
345
+ loadingIndicator.style.display = 'none';
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Load games for deduplication
351
+ */
352
+ async function loadGamesForDeduplication() {
353
+ try {
354
+ const response = await fetch('/api/games-list');
355
+ const data = await response.json();
356
+
357
+ if (response.ok && data.games) {
358
+ // Sort games alphabetically by name
359
+ data.games.sort((a, b) => {
360
+ const nameA = (a.name || '').toLowerCase();
361
+ const nameB = (b.name || '').toLowerCase();
362
+ return nameA.localeCompare(nameB);
363
+ });
364
+
365
+ const gameSelect = document.getElementById('gameSelection');
366
+ // Keep "All Games" option and add individual games
367
+ gameSelect.innerHTML = '<option value="all">All Games</option>';
368
+
369
+ data.games.forEach(game => {
370
+ const option = document.createElement('option');
371
+ option.value = game.name;
372
+ option.textContent = `${game.name} (${game.sentence_count} sentences)`;
373
+ gameSelect.appendChild(option);
374
+ });
375
+ }
376
+ } catch (error) {
377
+ console.error('Error loading games for deduplication:', error);
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Initialize game data filter buttons
383
+ */
384
+ function initializeGameDataFilters() {
385
+ // Game data filter buttons
386
+ const filterButtons = document.querySelectorAll('.game-data-filters button');
387
+ filterButtons.forEach(btn => {
388
+ btn.addEventListener('click', (event) => filterGames(event.target.dataset.filter));
389
+ });
390
+ }
@@ -0,0 +1,213 @@
1
+ // Database Individual Game Operations Functions
2
+ // Dependencies: shared.js (provides escapeHtml, openModal, closeModal), database-popups.js, database-helpers.js
3
+
4
+ // Global variables for individual game operations
5
+ let currentGameToUnlink = null;
6
+ let currentGameToDelete = null;
7
+
8
+ /**
9
+ * Open individual game unlink confirmation modal
10
+ * @param {string} gameId - Game ID to unlink
11
+ * @param {string} gameName - Game name for display
12
+ * @param {number} sentenceCount - Number of sentences
13
+ * @param {number} characterCount - Number of characters
14
+ */
15
+ function openIndividualGameUnlinkModal(gameId, gameName, sentenceCount, characterCount) {
16
+ // Find the game in currentGames to get release_date
17
+ const game = currentGames.find(g => g.id === gameId);
18
+
19
+ currentGameToUnlink = {
20
+ id: gameId,
21
+ name: gameName,
22
+ sentenceCount: sentenceCount,
23
+ characterCount: characterCount,
24
+ releaseDate: game ? game.release_date : null
25
+ };
26
+
27
+ // Populate modal with game information
28
+ document.getElementById('unlinkGameName').textContent = gameName;
29
+ document.getElementById('unlinkGameSentences').textContent = sentenceCount.toLocaleString();
30
+ document.getElementById('unlinkGameCharacters').textContent = characterCount.toLocaleString();
31
+ document.getElementById('unlinkGameReleaseDate').textContent = formatReleaseDate(currentGameToUnlink.releaseDate);
32
+
33
+ // Reset modal state
34
+ document.getElementById('individualUnlinkError').style.display = 'none';
35
+ document.getElementById('individualUnlinkLoading').style.display = 'none';
36
+ document.getElementById('confirmIndividualUnlinkBtn').disabled = false;
37
+
38
+ // Open the modal
39
+ openModal('individualGameUnlinkModal');
40
+ }
41
+
42
+ /**
43
+ * Confirm and execute individual game unlink operation
44
+ */
45
+ async function confirmIndividualGameUnlink() {
46
+ if (!currentGameToUnlink) {
47
+ showDatabaseErrorPopup('No game selected for unlinking');
48
+ return;
49
+ }
50
+
51
+ const errorDiv = document.getElementById('individualUnlinkError');
52
+ const loadingDiv = document.getElementById('individualUnlinkLoading');
53
+ const confirmBtn = document.getElementById('confirmIndividualUnlinkBtn');
54
+
55
+ // Reset state
56
+ errorDiv.style.display = 'none';
57
+
58
+ // Show loading state
59
+ loadingDiv.style.display = 'flex';
60
+ confirmBtn.disabled = true;
61
+
62
+ try {
63
+ // Call the unlink API (DELETE removes jiten.moe link but preserves sentences)
64
+ const response = await fetch(`/api/games/${currentGameToUnlink.id}`, {
65
+ method: 'DELETE',
66
+ headers: { 'Content-Type': 'application/json' }
67
+ });
68
+
69
+ const result = await response.json();
70
+
71
+ if (response.ok) {
72
+ // Success! Close modal and show success message
73
+ closeModal('individualGameUnlinkModal');
74
+ showDatabaseSuccessPopup(`Game "${result.game_name}" has been unlinked successfully. ${result.unlinked_lines} sentences preserved.`);
75
+
76
+ // Refresh the current tab
77
+ const activeTab = document.querySelector('.tab-btn.active');
78
+ if (activeTab) {
79
+ switchTab(activeTab.dataset.tab);
80
+ }
81
+
82
+ // Update dashboard stats
83
+ if (typeof databaseManager !== 'undefined') {
84
+ await databaseManager.loadDashboardStats();
85
+ }
86
+
87
+ // Clear the current game
88
+ currentGameToUnlink = null;
89
+ } else {
90
+ // Show error message
91
+ errorDiv.textContent = result.error || 'Failed to unlink game';
92
+ errorDiv.style.display = 'block';
93
+ confirmBtn.disabled = false;
94
+ }
95
+ } catch (error) {
96
+ console.error('Error unlinking game:', error);
97
+ errorDiv.textContent = `Error: ${error.message}`;
98
+ errorDiv.style.display = 'block';
99
+ confirmBtn.disabled = false;
100
+ } finally {
101
+ loadingDiv.style.display = 'none';
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Open individual game delete lines confirmation modal
107
+ * @param {string} gameId - Game ID to delete lines for
108
+ * @param {string} gameName - Game name for display
109
+ * @param {number} sentenceCount - Number of sentences
110
+ * @param {number} characterCount - Number of characters
111
+ */
112
+ function openIndividualGameDeleteModal(gameId, gameName, sentenceCount, characterCount) {
113
+ currentGameToDelete = {
114
+ id: gameId,
115
+ name: gameName,
116
+ sentenceCount: sentenceCount,
117
+ characterCount: characterCount
118
+ };
119
+
120
+ // Populate modal with game information
121
+ document.getElementById('deleteGameName').textContent = gameName;
122
+ document.getElementById('deleteGameSentences').textContent = sentenceCount.toLocaleString();
123
+ document.getElementById('deleteGameCharacters').textContent = characterCount.toLocaleString();
124
+
125
+ // Reset modal state
126
+ document.getElementById('individualDeleteError').style.display = 'none';
127
+ document.getElementById('individualDeleteLoading').style.display = 'none';
128
+ document.getElementById('confirmIndividualDeleteBtn').disabled = false;
129
+
130
+ // Open the modal
131
+ openModal('individualGameDeleteModal');
132
+ }
133
+
134
+ /**
135
+ * Confirm and execute individual game delete lines operation
136
+ */
137
+ async function confirmIndividualGameDelete() {
138
+ if (!currentGameToDelete) {
139
+ showDatabaseErrorPopup('No game selected for deletion');
140
+ return;
141
+ }
142
+
143
+ const errorDiv = document.getElementById('individualDeleteError');
144
+ const loadingDiv = document.getElementById('individualDeleteLoading');
145
+ const confirmBtn = document.getElementById('confirmIndividualDeleteBtn');
146
+
147
+ // Reset state
148
+ errorDiv.style.display = 'none';
149
+
150
+ // Show loading state
151
+ loadingDiv.style.display = 'flex';
152
+ confirmBtn.disabled = true;
153
+
154
+ try {
155
+ // Call the delete lines API - this should be a different endpoint that actually deletes sentences
156
+ // For now, we'll use the same endpoint but add a parameter to indicate permanent deletion
157
+ const response = await fetch(`/api/games/${currentGameToDelete.id}/delete-lines`, {
158
+ method: 'DELETE',
159
+ headers: { 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({ permanent: true })
161
+ });
162
+
163
+ const result = await response.json();
164
+
165
+ if (response.ok) {
166
+ // Success! Close modal and show success message
167
+ closeModal('individualGameDeleteModal');
168
+ showDatabaseSuccessPopup(`Game lines for "${result.game_name}" have been PERMANENTLY DELETED. ${result.deleted_lines} sentences removed forever.`);
169
+
170
+ // Refresh the current tab
171
+ const activeTab = document.querySelector('.tab-btn.active');
172
+ if (activeTab) {
173
+ switchTab(activeTab.dataset.tab);
174
+ }
175
+
176
+ // Update dashboard stats
177
+ if (typeof databaseManager !== 'undefined') {
178
+ await databaseManager.loadDashboardStats();
179
+ }
180
+
181
+ // Clear the current game
182
+ currentGameToDelete = null;
183
+ } else {
184
+ // Show error message
185
+ errorDiv.textContent = result.error || 'Failed to delete game lines';
186
+ errorDiv.style.display = 'block';
187
+ confirmBtn.disabled = false;
188
+ }
189
+ } catch (error) {
190
+ console.error('Error deleting game lines:', error);
191
+ errorDiv.textContent = `Error: ${error.message}`;
192
+ errorDiv.style.display = 'block';
193
+ confirmBtn.disabled = false;
194
+ } finally {
195
+ loadingDiv.style.display = 'none';
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Initialize individual game operations event handlers
201
+ */
202
+ function initializeGameOperations() {
203
+ // Individual game operation confirmation buttons
204
+ const confirmIndividualUnlinkBtn = document.getElementById('confirmIndividualUnlinkBtn');
205
+ if (confirmIndividualUnlinkBtn) {
206
+ confirmIndividualUnlinkBtn.addEventListener('click', confirmIndividualGameUnlink);
207
+ }
208
+
209
+ const confirmIndividualDeleteBtn = document.getElementById('confirmIndividualDeleteBtn');
210
+ if (confirmIndividualDeleteBtn) {
211
+ confirmIndividualDeleteBtn.addEventListener('click', confirmIndividualGameDelete);
212
+ }
213
+ }
@@ -0,0 +1,44 @@
1
+ // Database Helper Functions
2
+ // Dependencies: shared.js (provides escapeHtml and other utility functions)
3
+
4
+ /**
5
+ * Format release date for display
6
+ * @param {string} releaseDate - ISO date string or null
7
+ * @returns {string} Formatted date string
8
+ */
9
+ function formatReleaseDate(releaseDate) {
10
+ if (!releaseDate) return 'Unknown';
11
+
12
+ try {
13
+ // Handle ISO format like "2009-10-15T00:00:00"
14
+ const date = new Date(releaseDate);
15
+ if (isNaN(date.getTime())) return 'Invalid Date';
16
+
17
+ return date.toLocaleDateString('en-US', {
18
+ year: 'numeric',
19
+ month: 'short',
20
+ day: 'numeric'
21
+ });
22
+ } catch (error) {
23
+ console.warn('Error formatting release date:', releaseDate, error);
24
+ return 'Invalid Date';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Toggle visibility of time window controls based on checkbox state
30
+ */
31
+ function toggleTimeWindowVisibility() {
32
+ const ignoreTimeWindow = document.getElementById('ignoreTimeWindow').checked;
33
+ const timeWindowGroup = document.getElementById('timeWindowGroup');
34
+
35
+ if (ignoreTimeWindow) {
36
+ timeWindowGroup.style.opacity = '0.5';
37
+ timeWindowGroup.style.pointerEvents = 'none';
38
+ document.getElementById('timeWindow').disabled = true;
39
+ } else {
40
+ timeWindowGroup.style.opacity = '1';
41
+ timeWindowGroup.style.pointerEvents = 'auto';
42
+ document.getElementById('timeWindow').disabled = false;
43
+ }
44
+ }