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,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
|
+
}
|