GameSentenceMiner 2.17.7__py3-none-any.whl → 2.18.1__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/ai/ai_prompting.py +6 -6
- GameSentenceMiner/anki.py +236 -152
- GameSentenceMiner/gametext.py +7 -4
- GameSentenceMiner/gsm.py +49 -10
- GameSentenceMiner/locales/en_us.json +7 -3
- GameSentenceMiner/locales/ja_jp.json +8 -4
- GameSentenceMiner/locales/zh_cn.json +8 -4
- GameSentenceMiner/obs.py +238 -59
- GameSentenceMiner/ocr/owocr_helper.py +1 -1
- GameSentenceMiner/tools/ss_selector.py +7 -8
- GameSentenceMiner/ui/__init__.py +0 -0
- GameSentenceMiner/ui/anki_confirmation.py +187 -0
- GameSentenceMiner/{config_gui.py → ui/config_gui.py} +100 -35
- GameSentenceMiner/ui/screenshot_selector.py +215 -0
- GameSentenceMiner/util/configuration.py +124 -22
- GameSentenceMiner/util/db.py +22 -13
- GameSentenceMiner/util/downloader/download_tools.py +2 -2
- GameSentenceMiner/util/ffmpeg.py +24 -30
- GameSentenceMiner/util/get_overlay_coords.py +34 -34
- GameSentenceMiner/util/gsm_utils.py +31 -1
- GameSentenceMiner/util/text_log.py +11 -9
- GameSentenceMiner/vad.py +31 -12
- GameSentenceMiner/web/database_api.py +742 -123
- GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
- GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
- GameSentenceMiner/web/static/css/overview.css +850 -0
- GameSentenceMiner/web/static/css/popups-shared.css +126 -0
- GameSentenceMiner/web/static/css/shared.css +97 -0
- GameSentenceMiner/web/static/css/stats.css +192 -597
- GameSentenceMiner/web/static/js/anki_stats.js +6 -4
- GameSentenceMiner/web/static/js/database.js +209 -5
- GameSentenceMiner/web/static/js/goals.js +610 -0
- GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
- GameSentenceMiner/web/static/js/overview.js +1176 -0
- GameSentenceMiner/web/static/js/shared.js +25 -0
- GameSentenceMiner/web/static/js/stats.js +154 -1459
- GameSentenceMiner/web/stats.py +2 -2
- GameSentenceMiner/web/templates/anki_stats.html +5 -0
- GameSentenceMiner/web/templates/components/kanji_grid/basic_kanji_book_bkb_v1_v2.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/duolingo_kanji.json +29 -0
- GameSentenceMiner/web/templates/components/kanji_grid/grade.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hk_primary_learning.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hkscs2016.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hsk_levels.json +33 -0
- GameSentenceMiner/web/templates/components/kanji_grid/humanum_frequency_list.json +41 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jis_levels.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jlpt_level.json +29 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jpdb_kanji_frequency_list.json +37 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jpdbv2_kanji_frequency_list.json +161 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jun_das_modern_chinese_character_frequency_list.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/kanji_in_context_revised_edition.json +37 -0
- GameSentenceMiner/web/templates/components/kanji_grid/kanji_kentei_level.json +61 -0
- GameSentenceMiner/web/templates/components/kanji_grid/mainland_china_elementary_textbook_characters.json +33 -0
- GameSentenceMiner/web/templates/components/kanji_grid/moe_way_quiz.json +47 -0
- GameSentenceMiner/web/templates/components/kanji_grid/official_kanji.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/remembering_the_kanji.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/standard_form_of_national_characters.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/table_of_general_standard_chinese_characters.json +21 -0
- GameSentenceMiner/web/templates/components/kanji_grid/the_kodansha_kanji_learners_course_klc.json +45 -0
- GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json +249 -0
- GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json +33 -0
- GameSentenceMiner/web/templates/components/navigation.html +3 -1
- GameSentenceMiner/web/templates/database.html +73 -1
- GameSentenceMiner/web/templates/goals.html +376 -0
- GameSentenceMiner/web/templates/index.html +13 -11
- GameSentenceMiner/web/templates/overview.html +416 -0
- GameSentenceMiner/web/templates/stats.html +46 -251
- GameSentenceMiner/web/texthooking_page.py +18 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/METADATA +5 -1
- gamesentenceminer-2.18.1.dist-info/RECORD +132 -0
- gamesentenceminer-2.17.7.dist-info/RECORD +0 -98
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/top_level.txt +0 -0
|
@@ -50,14 +50,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
50
50
|
|
|
51
51
|
// Helper function to create a chart to avoid repeating code
|
|
52
52
|
function createChart(canvasId, datasets, chartTitle) {
|
|
53
|
-
const ctx = document.getElementById(canvasId)
|
|
53
|
+
const ctx = document.getElementById(canvasId);
|
|
54
|
+
if (!ctx) return null; // Add null check
|
|
55
|
+
|
|
56
|
+
const context = ctx.getContext('2d');
|
|
54
57
|
|
|
55
58
|
// Destroy existing chart on this canvas if it exists
|
|
56
59
|
if (window.myCharts[canvasId]) {
|
|
57
60
|
window.myCharts[canvasId].destroy();
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
window.myCharts[canvasId] = new Chart(
|
|
63
|
+
window.myCharts[canvasId] = new Chart(context, {
|
|
61
64
|
type: 'line',
|
|
62
65
|
data: {
|
|
63
66
|
labels: datasets.labels,
|
|
@@ -106,339 +109,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
106
109
|
return window.myCharts[canvasId];
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
// Helper function to get week number of year (GitHub style - week starts on Sunday)
|
|
110
|
-
function getWeekOfYear(date) {
|
|
111
|
-
const yearStart = new Date(date.getFullYear(), 0, 1);
|
|
112
|
-
const dayOfYear = Math.floor((date - yearStart) / (24 * 60 * 60 * 1000)) + 1;
|
|
113
|
-
const dayOfWeek = yearStart.getDay(); // 0 = Sunday
|
|
114
|
-
|
|
115
|
-
// Calculate week number (1-indexed)
|
|
116
|
-
const weekNum = Math.ceil((dayOfYear + dayOfWeek) / 7);
|
|
117
|
-
return Math.min(53, weekNum); // Cap at 53 weeks
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Helper function to get day of week (0 = Sunday, 6 = Saturday)
|
|
121
|
-
function getDayOfWeek(date) {
|
|
122
|
-
return date.getDay();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Helper function to get the first Sunday of the year (or before)
|
|
126
|
-
function getFirstSunday(year) {
|
|
127
|
-
const jan1 = new Date(year, 0, 1);
|
|
128
|
-
const dayOfWeek = jan1.getDay();
|
|
129
|
-
const firstSunday = new Date(year, 0, 1 - dayOfWeek);
|
|
130
|
-
return firstSunday;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Function to calculate heatmap streaks and average daily time
|
|
134
|
-
function calculateHeatmapStreaks(grid, yearData, allLinesForYear = []) {
|
|
135
|
-
const dates = [];
|
|
136
|
-
|
|
137
|
-
// Collect all dates in chronological order
|
|
138
|
-
for (let week = 0; week < 53; week++) {
|
|
139
|
-
for (let day = 0; day < 7; day++) {
|
|
140
|
-
const date = grid[day][week];
|
|
141
|
-
if (date) {
|
|
142
|
-
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
143
|
-
const activity = yearData[dateStr] || 0;
|
|
144
|
-
dates.push({ date: dateStr, activity: activity });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Sort dates chronologically
|
|
150
|
-
dates.sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
let longestStreak = 0;
|
|
154
|
-
let currentStreak = 0;
|
|
155
|
-
let tempStreak = 0;
|
|
156
|
-
|
|
157
|
-
// Calculate longest streak
|
|
158
|
-
for (let i = 0; i < dates.length; i++) {
|
|
159
|
-
if (dates[i].activity > 0) {
|
|
160
|
-
tempStreak++;
|
|
161
|
-
longestStreak = Math.max(longestStreak, tempStreak);
|
|
162
|
-
} else {
|
|
163
|
-
tempStreak = 0;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Calculate current streak from today backwards, using streak requirement hours from config
|
|
168
|
-
const date = new Date();
|
|
169
|
-
const today = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
170
|
-
const streakRequirement = window.statsConfig ? window.statsConfig.streakRequirementHours : 1.0;
|
|
171
|
-
|
|
172
|
-
// Find today's index or the most recent date before today
|
|
173
|
-
let todayIndex = -1;
|
|
174
|
-
for (let i = dates.length - 1; i >= 0; i--) {
|
|
175
|
-
if (dates[i].date <= today) {
|
|
176
|
-
todayIndex = i;
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Count backwards from today (or most recent date)
|
|
182
|
-
if (todayIndex >= 0) {
|
|
183
|
-
for (let i = todayIndex; i >= 0; i--) {
|
|
184
|
-
if (dates[i].activity >= streakRequirement) {
|
|
185
|
-
currentStreak++;
|
|
186
|
-
} else {
|
|
187
|
-
break;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Calculate average daily time for this year
|
|
193
|
-
let avgDailyTime = "-";
|
|
194
|
-
if (allLinesForYear && allLinesForYear.length > 0) {
|
|
195
|
-
// Group timestamps by day for this year
|
|
196
|
-
const dailyTimestamps = {};
|
|
197
|
-
for (const line of allLinesForYear) {
|
|
198
|
-
const ts = parseFloat(line.timestamp);
|
|
199
|
-
if (isNaN(ts)) continue;
|
|
200
|
-
const dateObj = new Date(ts * 1000);
|
|
201
|
-
const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
|
|
202
|
-
if (!dailyTimestamps[dateStr]) {
|
|
203
|
-
dailyTimestamps[dateStr] = [];
|
|
204
|
-
}
|
|
205
|
-
dailyTimestamps[dateStr].push(parseFloat(line.timestamp));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Calculate reading time for each day with activity
|
|
209
|
-
let totalHours = 0;
|
|
210
|
-
let activeDays = 0;
|
|
211
|
-
let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
212
|
-
// Try to get AFK timer from settings modal if available and valid
|
|
213
|
-
const afkTimerInput = document.getElementById('afkTimer');
|
|
214
|
-
if (afkTimerInput && afkTimerInput.value) {
|
|
215
|
-
const parsed = parseInt(afkTimerInput.value, 10);
|
|
216
|
-
if (!isNaN(parsed) && parsed > 0) afkTimerSeconds = parsed;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
for (const [dateStr, timestamps] of Object.entries(dailyTimestamps)) {
|
|
220
|
-
if (timestamps.length >= 2) {
|
|
221
|
-
timestamps.sort((a, b) => a - b);
|
|
222
|
-
let dayReadingTime = 0;
|
|
223
|
-
|
|
224
|
-
for (let i = 1; i < timestamps.length; i++) {
|
|
225
|
-
const gap = timestamps[i] - timestamps[i-1];
|
|
226
|
-
dayReadingTime += Math.min(gap, afkTimerSeconds);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (dayReadingTime > 0) {
|
|
230
|
-
totalHours += dayReadingTime / 3600;
|
|
231
|
-
activeDays++;
|
|
232
|
-
}
|
|
233
|
-
} else if (timestamps.length === 1) {
|
|
234
|
-
// Single timestamp - count as minimal activity (1 second)
|
|
235
|
-
totalHours += 1 / 3600;
|
|
236
|
-
activeDays++;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (activeDays > 0) {
|
|
241
|
-
const avgHours = totalHours / activeDays;
|
|
242
|
-
if (avgHours < 1) {
|
|
243
|
-
const minutes = Math.round(avgHours * 60);
|
|
244
|
-
avgDailyTime = `${minutes}m`;
|
|
245
|
-
} else {
|
|
246
|
-
const hours = Math.floor(avgHours);
|
|
247
|
-
const minutes = Math.round((avgHours - hours) * 60);
|
|
248
|
-
avgDailyTime = minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return { longestStreak, currentStreak, avgDailyTime };
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Function to create GitHub-style heatmap
|
|
257
|
-
function createHeatmap(heatmapData) {
|
|
258
|
-
const container = document.getElementById('heatmapContainer');
|
|
259
|
-
|
|
260
|
-
Object.keys(heatmapData).sort().forEach(year => {
|
|
261
|
-
const yearData = heatmapData[year];
|
|
262
|
-
const yearDiv = document.createElement('div');
|
|
263
|
-
yearDiv.className = 'heatmap-year';
|
|
264
|
-
|
|
265
|
-
const yearTitle = document.createElement('h3');
|
|
266
|
-
yearTitle.textContent = year;
|
|
267
|
-
yearDiv.appendChild(yearTitle);
|
|
268
|
-
|
|
269
|
-
// Find maximum activity value for this year to scale colors
|
|
270
|
-
const maxActivity = Math.max(...Object.values(yearData));
|
|
271
|
-
|
|
272
|
-
// Create main wrapper to center everything
|
|
273
|
-
const mainWrapper = document.createElement('div');
|
|
274
|
-
mainWrapper.className = 'heatmap-wrapper';
|
|
275
|
-
|
|
276
|
-
// Create container wrapper for labels and grid
|
|
277
|
-
const containerWrapper = document.createElement('div');
|
|
278
|
-
containerWrapper.className = 'heatmap-container-wrapper';
|
|
279
|
-
|
|
280
|
-
// Create day labels (S, M, T, W, T, F, S)
|
|
281
|
-
const dayLabels = document.createElement('div');
|
|
282
|
-
dayLabels.className = 'heatmap-day-labels';
|
|
283
|
-
const dayNames = ['S', '', 'M', '', 'W', '', 'F']; // Only show some labels for space
|
|
284
|
-
dayNames.forEach(dayName => {
|
|
285
|
-
const dayLabel = document.createElement('div');
|
|
286
|
-
dayLabel.className = 'heatmap-day-label';
|
|
287
|
-
dayLabel.textContent = dayName;
|
|
288
|
-
dayLabels.appendChild(dayLabel);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// Create grid container
|
|
292
|
-
const gridContainer = document.createElement('div');
|
|
293
|
-
|
|
294
|
-
// Create month labels
|
|
295
|
-
const monthLabels = document.createElement('div');
|
|
296
|
-
monthLabels.className = 'heatmap-month-labels';
|
|
297
|
-
|
|
298
|
-
// Create the main grid
|
|
299
|
-
const gridDiv = document.createElement('div');
|
|
300
|
-
gridDiv.className = 'heatmap-grid';
|
|
301
|
-
|
|
302
|
-
// Initialize 7x53 grid with empty cells
|
|
303
|
-
const grid = Array(7).fill(null).map(() => Array(53).fill(null));
|
|
304
|
-
|
|
305
|
-
// Get the first Sunday of the year (start of week 1)
|
|
306
|
-
const firstSunday = getFirstSunday(parseInt(year));
|
|
307
|
-
|
|
308
|
-
// Populate grid with dates for the entire year
|
|
309
|
-
for (let week = 0; week < 53; week++) {
|
|
310
|
-
for (let day = 0; day < 7; day++) {
|
|
311
|
-
const currentDate = new Date(firstSunday);
|
|
312
|
-
currentDate.setDate(firstSunday.getDate() + (week * 7) + day);
|
|
313
|
-
|
|
314
|
-
// Only include dates that belong to the current year
|
|
315
|
-
if (currentDate.getFullYear() === parseInt(year)) {
|
|
316
|
-
grid[day][week] = currentDate;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Create month labels based on grid positions
|
|
322
|
-
const monthTracker = new Set();
|
|
323
|
-
for (let week = 0; week < 53; week++) {
|
|
324
|
-
const dateInWeek = grid[0][week] || grid[1][week] || grid[2][week] ||
|
|
325
|
-
grid[3][week] || grid[4][week] || grid[5][week] || grid[6][week];
|
|
326
|
-
|
|
327
|
-
if (dateInWeek) {
|
|
328
|
-
const month = dateInWeek.getMonth();
|
|
329
|
-
const monthName = dateInWeek.toLocaleDateString('en', { month: 'short' });
|
|
330
|
-
|
|
331
|
-
// Add month label if it's the first week of the month
|
|
332
|
-
if (!monthTracker.has(month) && dateInWeek.getDate() <= 7) {
|
|
333
|
-
const monthLabel = document.createElement('div');
|
|
334
|
-
monthLabel.className = 'heatmap-month-label';
|
|
335
|
-
monthLabel.style.gridColumn = `${week + 1}`;
|
|
336
|
-
monthLabel.textContent = monthName;
|
|
337
|
-
monthLabels.appendChild(monthLabel);
|
|
338
|
-
monthTracker.add(month);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Create cells for the grid
|
|
344
|
-
for (let day = 0; day < 7; day++) {
|
|
345
|
-
for (let week = 0; week < 53; week++) {
|
|
346
|
-
const cell = document.createElement('div');
|
|
347
|
-
cell.className = 'heatmap-cell';
|
|
348
|
-
|
|
349
|
-
const date = grid[day][week];
|
|
350
|
-
if (date) {
|
|
351
|
-
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
352
|
-
const activity = yearData[dateStr] || 0;
|
|
353
|
-
|
|
354
|
-
if (activity > 0 && maxActivity > 0) {
|
|
355
|
-
// Calculate percentage of maximum activity
|
|
356
|
-
const percentage = (activity / maxActivity) * 100;
|
|
357
|
-
|
|
358
|
-
// Assign discrete color levels based on percentage thresholds
|
|
359
|
-
let colorLevel;
|
|
360
|
-
if (percentage <= 25) {
|
|
361
|
-
colorLevel = 1; // Light green
|
|
362
|
-
} else if (percentage <= 50) {
|
|
363
|
-
colorLevel = 2; // Medium green
|
|
364
|
-
} else if (percentage <= 75) {
|
|
365
|
-
colorLevel = 3; // Dark green
|
|
366
|
-
} else {
|
|
367
|
-
colorLevel = 4; // Darkest green
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Define discrete colors for each level
|
|
371
|
-
const colors = {
|
|
372
|
-
1: '#c6e48b', // Light green (1-25%)
|
|
373
|
-
2: '#7bc96f', // Medium green (26-50%)
|
|
374
|
-
3: '#239a3b', // Dark green (51-75%)
|
|
375
|
-
4: '#196127' // Darkest green (76-100%)
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
cell.style.backgroundColor = colors[colorLevel];
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
cell.title = `${dateStr}: ${activity} characters`;
|
|
382
|
-
} else {
|
|
383
|
-
// Empty cell for dates outside the year
|
|
384
|
-
cell.style.backgroundColor = 'transparent';
|
|
385
|
-
cell.style.cursor = 'default';
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
gridDiv.appendChild(cell);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
gridContainer.appendChild(monthLabels);
|
|
393
|
-
gridContainer.appendChild(gridDiv);
|
|
394
|
-
containerWrapper.appendChild(dayLabels);
|
|
395
|
-
containerWrapper.appendChild(gridContainer);
|
|
396
|
-
mainWrapper.appendChild(containerWrapper);
|
|
397
|
-
|
|
398
|
-
// Calculate and display streaks with average daily time
|
|
399
|
-
const yearLines = window.allLinesData ? window.allLinesData.filter(line => {
|
|
400
|
-
if (!line.timestamp) return false;
|
|
401
|
-
const lineYear = new Date(parseFloat(line.timestamp) * 1000).getFullYear();
|
|
402
|
-
return lineYear === parseInt(year);
|
|
403
|
-
}) : [];
|
|
404
|
-
|
|
405
|
-
const streaks = calculateHeatmapStreaks(grid, yearData, yearLines);
|
|
406
|
-
const streaksDiv = document.createElement('div');
|
|
407
|
-
streaksDiv.className = 'heatmap-streaks';
|
|
408
|
-
streaksDiv.innerHTML = `
|
|
409
|
-
<div class="heatmap-streak-item">
|
|
410
|
-
<div class="heatmap-streak-number">${streaks.longestStreak}</div>
|
|
411
|
-
<div class="heatmap-streak-label">Longest Streak</div>
|
|
412
|
-
</div>
|
|
413
|
-
<div class="heatmap-streak-item">
|
|
414
|
-
<div class="heatmap-streak-number">${streaks.currentStreak}</div>
|
|
415
|
-
<div class="heatmap-streak-label">Current Streak</div>
|
|
416
|
-
</div>
|
|
417
|
-
<div class="heatmap-streak-item">
|
|
418
|
-
<div class="heatmap-streak-number">${streaks.avgDailyTime}</div>
|
|
419
|
-
<div class="heatmap-streak-label">Avg Daily Time</div>
|
|
420
|
-
</div>
|
|
421
|
-
`;
|
|
422
|
-
mainWrapper.appendChild(streaksDiv);
|
|
423
|
-
yearDiv.appendChild(mainWrapper);
|
|
424
|
-
|
|
425
|
-
// Add legend with discrete colors
|
|
426
|
-
const legend = document.createElement('div');
|
|
427
|
-
legend.className = 'heatmap-legend';
|
|
428
|
-
legend.innerHTML = `
|
|
429
|
-
<span>Less</span>
|
|
430
|
-
<div class="heatmap-legend-item" style="background-color: #ebedf0;" title="No activity"></div>
|
|
431
|
-
<div class="heatmap-legend-item" style="background-color: #c6e48b;" title="1-25% of max activity"></div>
|
|
432
|
-
<div class="heatmap-legend-item" style="background-color: #7bc96f;" title="26-50% of max activity"></div>
|
|
433
|
-
<div class="heatmap-legend-item" style="background-color: #239a3b;" title="51-75% of max activity"></div>
|
|
434
|
-
<div class="heatmap-legend-item" style="background-color: #196127;" title="76-100% of max activity"></div>
|
|
435
|
-
<span>More</span>
|
|
436
|
-
`;
|
|
437
|
-
yearDiv.appendChild(legend);
|
|
438
|
-
|
|
439
|
-
container.appendChild(yearDiv);
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
112
|
|
|
443
113
|
// Function to generate distinct colors for games
|
|
444
114
|
function generateGameColors(gameCount) {
|
|
@@ -496,8 +166,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
496
166
|
}
|
|
497
167
|
|
|
498
168
|
// Reusable function to create game bar charts with interactive legend
|
|
499
|
-
function createGameBarChart(canvasId, chartData, chartTitle, yAxisLabel) {
|
|
500
|
-
const
|
|
169
|
+
function createGameBarChart(canvasId, chartData, chartTitle, yAxisLabel, showTrendline = false) {
|
|
170
|
+
const canvas = document.getElementById(canvasId);
|
|
171
|
+
if (!canvas) return null; // Add null check
|
|
172
|
+
|
|
173
|
+
const ctx = canvas.getContext('2d');
|
|
501
174
|
const colors = generateGameColors(chartData.labels.length);
|
|
502
175
|
|
|
503
176
|
// Track which bars are hidden for toggle functionality
|
|
@@ -513,22 +186,49 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
513
186
|
return getFilteredChartData(originalData, hiddenBars, colors);
|
|
514
187
|
}
|
|
515
188
|
|
|
189
|
+
// Calculate trendline if requested
|
|
190
|
+
let trendlineData = null;
|
|
191
|
+
if (showTrendline && chartData.totals.length > 1) {
|
|
192
|
+
const trendline = calculateTrendline(chartData.totals);
|
|
193
|
+
trendlineData = trendline.points;
|
|
194
|
+
}
|
|
195
|
+
|
|
516
196
|
// Destroy existing chart on this canvas if it exists
|
|
517
197
|
if (window.myCharts[canvasId]) {
|
|
518
198
|
window.myCharts[canvasId].destroy();
|
|
519
199
|
}
|
|
520
200
|
|
|
201
|
+
// Build datasets array
|
|
202
|
+
const datasets = [{
|
|
203
|
+
label: chartTitle,
|
|
204
|
+
data: chartData.totals,
|
|
205
|
+
backgroundColor: colors.map(color => color + '99'), // Semi-transparent
|
|
206
|
+
borderColor: colors,
|
|
207
|
+
borderWidth: 2,
|
|
208
|
+
order: 2
|
|
209
|
+
}];
|
|
210
|
+
|
|
211
|
+
// Add trendline dataset if available
|
|
212
|
+
if (trendlineData) {
|
|
213
|
+
datasets.push({
|
|
214
|
+
label: 'Trendline',
|
|
215
|
+
data: trendlineData,
|
|
216
|
+
type: 'line',
|
|
217
|
+
borderColor: '#ff6384',
|
|
218
|
+
borderWidth: 3,
|
|
219
|
+
borderDash: [5, 5],
|
|
220
|
+
fill: false,
|
|
221
|
+
pointRadius: 0,
|
|
222
|
+
pointHoverRadius: 0,
|
|
223
|
+
order: 1
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
521
227
|
window.myCharts[canvasId] = new Chart(ctx, {
|
|
522
228
|
type: 'bar',
|
|
523
229
|
data: {
|
|
524
230
|
labels: chartData.labels, // Each game as a separate label
|
|
525
|
-
datasets:
|
|
526
|
-
label: chartTitle,
|
|
527
|
-
data: chartData.totals,
|
|
528
|
-
backgroundColor: colors.map(color => color + '99'), // Semi-transparent
|
|
529
|
-
borderColor: colors,
|
|
530
|
-
borderWidth: 2
|
|
531
|
-
}]
|
|
231
|
+
datasets: datasets
|
|
532
232
|
},
|
|
533
233
|
options: {
|
|
534
234
|
responsive: true,
|
|
@@ -624,8 +324,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
624
324
|
}
|
|
625
325
|
|
|
626
326
|
// Specialized function for charts with custom formatting (time/speed)
|
|
627
|
-
function createGameBarChartWithCustomFormat(canvasId, chartData, chartTitle, yAxisLabel, formatFunction) {
|
|
628
|
-
const
|
|
327
|
+
function createGameBarChartWithCustomFormat(canvasId, chartData, chartTitle, yAxisLabel, formatFunction, showTrendline = false) {
|
|
328
|
+
const canvas = document.getElementById(canvasId);
|
|
329
|
+
if (!canvas) return null; // Add null check
|
|
330
|
+
|
|
331
|
+
const ctx = canvas.getContext('2d');
|
|
629
332
|
const colors = generateGameColors(chartData.labels.length);
|
|
630
333
|
|
|
631
334
|
// Track which bars are hidden for toggle functionality
|
|
@@ -641,23 +344,50 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
641
344
|
return getFilteredChartData(originalData, hiddenBars, colors);
|
|
642
345
|
}
|
|
643
346
|
|
|
347
|
+
// Calculate trendline if requested
|
|
348
|
+
let trendlineData = null;
|
|
349
|
+
if (showTrendline && chartData.totals.length > 1) {
|
|
350
|
+
const trendline = calculateTrendline(chartData.totals);
|
|
351
|
+
trendlineData = trendline.points;
|
|
352
|
+
}
|
|
353
|
+
|
|
644
354
|
// Destroy existing chart if it exists
|
|
645
355
|
if (window.myCharts[canvasId]) {
|
|
646
356
|
window.myCharts[canvasId].destroy();
|
|
647
357
|
}
|
|
648
358
|
|
|
359
|
+
// Build datasets array
|
|
360
|
+
const datasets = [{
|
|
361
|
+
label: chartTitle,
|
|
362
|
+
data: chartData.totals,
|
|
363
|
+
backgroundColor: colors.map(color => color + '99'), // Semi-transparent
|
|
364
|
+
borderColor: colors,
|
|
365
|
+
borderWidth: 2,
|
|
366
|
+
order: 2
|
|
367
|
+
}];
|
|
368
|
+
|
|
369
|
+
// Add trendline dataset if available
|
|
370
|
+
if (trendlineData) {
|
|
371
|
+
datasets.push({
|
|
372
|
+
label: 'Trendline',
|
|
373
|
+
data: trendlineData,
|
|
374
|
+
type: 'line',
|
|
375
|
+
borderColor: '#ff6384',
|
|
376
|
+
borderWidth: 3,
|
|
377
|
+
borderDash: [5, 5],
|
|
378
|
+
fill: false,
|
|
379
|
+
pointRadius: 0,
|
|
380
|
+
pointHoverRadius: 0,
|
|
381
|
+
order: 1
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
649
385
|
// Create new chart and store globally
|
|
650
386
|
window.myCharts[canvasId] = new Chart(ctx, {
|
|
651
387
|
type: 'bar',
|
|
652
388
|
data: {
|
|
653
389
|
labels: chartData.labels, // Each game as a separate label
|
|
654
|
-
datasets:
|
|
655
|
-
label: chartTitle,
|
|
656
|
-
data: chartData.totals,
|
|
657
|
-
backgroundColor: colors.map(color => color + '99'), // Semi-transparent
|
|
658
|
-
borderColor: colors,
|
|
659
|
-
borderWidth: 2
|
|
660
|
-
}]
|
|
390
|
+
datasets: datasets
|
|
661
391
|
},
|
|
662
392
|
options: {
|
|
663
393
|
responsive: true,
|
|
@@ -751,6 +481,38 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
751
481
|
return window.myCharts[canvasId];
|
|
752
482
|
}
|
|
753
483
|
|
|
484
|
+
// Helper function to calculate linear regression trendline
|
|
485
|
+
function calculateTrendline(data) {
|
|
486
|
+
const n = data.length;
|
|
487
|
+
if (n === 0) return { slope: 0, intercept: 0, points: [] };
|
|
488
|
+
|
|
489
|
+
// Calculate means
|
|
490
|
+
let sumX = 0, sumY = 0;
|
|
491
|
+
for (let i = 0; i < n; i++) {
|
|
492
|
+
sumX += i;
|
|
493
|
+
sumY += data[i];
|
|
494
|
+
}
|
|
495
|
+
const meanX = sumX / n;
|
|
496
|
+
const meanY = sumY / n;
|
|
497
|
+
|
|
498
|
+
// Calculate slope
|
|
499
|
+
let numerator = 0, denominator = 0;
|
|
500
|
+
for (let i = 0; i < n; i++) {
|
|
501
|
+
numerator += (i - meanX) * (data[i] - meanY);
|
|
502
|
+
denominator += (i - meanX) * (i - meanX);
|
|
503
|
+
}
|
|
504
|
+
const slope = denominator !== 0 ? numerator / denominator : 0;
|
|
505
|
+
const intercept = meanY - slope * meanX;
|
|
506
|
+
|
|
507
|
+
// Generate trendline points
|
|
508
|
+
const points = [];
|
|
509
|
+
for (let i = 0; i < n; i++) {
|
|
510
|
+
points.push(slope * i + intercept);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return { slope, intercept, points };
|
|
514
|
+
}
|
|
515
|
+
|
|
754
516
|
// Helper functions for formatting
|
|
755
517
|
function formatTime(hours) {
|
|
756
518
|
if (hours < 1) {
|
|
@@ -779,12 +541,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
779
541
|
}
|
|
780
542
|
|
|
781
543
|
function showNoDataPopup() {
|
|
782
|
-
document.getElementById("noDataPopup")
|
|
544
|
+
const popup = document.getElementById("noDataPopup");
|
|
545
|
+
if (popup) popup.classList.remove("hidden");
|
|
783
546
|
}
|
|
784
547
|
|
|
785
|
-
document.getElementById("closeNoDataPopup")
|
|
786
|
-
|
|
787
|
-
|
|
548
|
+
const closeNoDataPopup = document.getElementById("closeNoDataPopup");
|
|
549
|
+
if (closeNoDataPopup) {
|
|
550
|
+
closeNoDataPopup.addEventListener("click", () => {
|
|
551
|
+
const popup = document.getElementById("noDataPopup");
|
|
552
|
+
if (popup) popup.classList.add("hidden");
|
|
553
|
+
});
|
|
554
|
+
}
|
|
788
555
|
|
|
789
556
|
// Function to load stats data with optional year filter
|
|
790
557
|
function loadStatsData(start_timestamp = null, end_timestamp = null) {
|
|
@@ -805,11 +572,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
805
572
|
return fetch(url)
|
|
806
573
|
.then(response => response.json())
|
|
807
574
|
.then(data => {
|
|
808
|
-
// Store all lines data globally for
|
|
575
|
+
// Store all lines data globally for potential future use
|
|
809
576
|
if (data.allLinesData && Array.isArray(data.allLinesData)) {
|
|
810
577
|
window.allLinesData = data.allLinesData;
|
|
811
578
|
} else {
|
|
812
|
-
// If not provided by API, we'll work without it
|
|
813
579
|
window.allLinesData = [];
|
|
814
580
|
}
|
|
815
581
|
|
|
@@ -834,22 +600,22 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
834
600
|
[...charsData.datasets].forEach(d => delete d.hidden);
|
|
835
601
|
|
|
836
602
|
// Charts are re-created with the new data
|
|
837
|
-
|
|
838
|
-
|
|
603
|
+
createChart('linesChart', linesData, 'Cumulative Lines Received');
|
|
604
|
+
createChart('charsChart', charsData, 'Cumulative Characters Read');
|
|
839
605
|
|
|
840
|
-
// Create reading chars quantity chart if data exists
|
|
606
|
+
// Create reading chars quantity chart if data exists (with trendline)
|
|
841
607
|
if (data.totalCharsPerGame) {
|
|
842
|
-
createGameBarChart('readingCharsChart', data.totalCharsPerGame, 'Reading Chars Quantity', 'Characters Read');
|
|
608
|
+
createGameBarChart('readingCharsChart', data.totalCharsPerGame, 'Reading Chars Quantity', 'Characters Read', true);
|
|
843
609
|
}
|
|
844
610
|
|
|
845
|
-
// Create reading time quantity chart if data exists
|
|
611
|
+
// Create reading time quantity chart if data exists (with trendline)
|
|
846
612
|
if (data.readingTimePerGame) {
|
|
847
|
-
createGameBarChartWithCustomFormat('readingTimeChart', data.readingTimePerGame, 'Reading Time Quantity', 'Time (hours)', formatTime);
|
|
613
|
+
createGameBarChartWithCustomFormat('readingTimeChart', data.readingTimePerGame, 'Reading Time Quantity', 'Time (hours)', formatTime, true);
|
|
848
614
|
}
|
|
849
615
|
|
|
850
|
-
// Create reading speed per game chart if data exists
|
|
616
|
+
// Create reading speed per game chart if data exists (with trendline)
|
|
851
617
|
if (data.readingSpeedPerGame) {
|
|
852
|
-
createGameBarChartWithCustomFormat('readingSpeedPerGameChart', data.readingSpeedPerGame, 'Reading Speed Improvement', 'Speed (chars/hour)', formatSpeed);
|
|
618
|
+
createGameBarChartWithCustomFormat('readingSpeedPerGameChart', data.readingSpeedPerGame, 'Reading Speed Improvement', 'Speed (chars/hour)', formatSpeed, true);
|
|
853
619
|
}
|
|
854
620
|
|
|
855
621
|
// Create kanji grid if data exists
|
|
@@ -857,306 +623,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
857
623
|
createKanjiGrid(data.kanjiGridData);
|
|
858
624
|
}
|
|
859
625
|
|
|
860
|
-
|
|
861
|
-
// Always update heatmap
|
|
862
|
-
if (data.heatmapData) {
|
|
863
|
-
const container = document.getElementById('heatmapContainer');
|
|
864
|
-
container.innerHTML = '';
|
|
865
|
-
createHeatmap(data.heatmapData);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// Load dashboard data
|
|
869
|
-
loadDashboardData(data, end_timestamp);
|
|
870
|
-
|
|
871
|
-
// Load goal progress chart (always refresh)
|
|
872
|
-
if (typeof loadGoalProgress === 'function') {
|
|
873
|
-
// Use the current data instead of making another API call
|
|
874
|
-
updateGoalProgressWithData(data);
|
|
875
|
-
}
|
|
876
|
-
|
|
877
626
|
return data;
|
|
878
627
|
})
|
|
879
628
|
.catch(error => {
|
|
880
629
|
console.error('Error fetching chart data:', error);
|
|
881
|
-
showDashboardError();
|
|
882
630
|
throw error;
|
|
883
631
|
});
|
|
884
632
|
}
|
|
885
633
|
|
|
886
|
-
// Goal Progress Chart functionality
|
|
887
|
-
let goalSettings = window.statsConfig || {};
|
|
888
|
-
if (!goalSettings.reading_hours_target) goalSettings.reading_hours_target = 1500;
|
|
889
|
-
if (!goalSettings.character_count_target) goalSettings.character_count_target = 25000000;
|
|
890
|
-
if (!goalSettings.games_target) goalSettings.games_target = 100;
|
|
891
|
-
|
|
892
|
-
// Function to load goal settings from API (fallback)
|
|
893
|
-
async function loadGoalSettings() {
|
|
894
|
-
// Use global config if available, otherwise fetch
|
|
895
|
-
if (window.statsConfig) {
|
|
896
|
-
goalSettings.reading_hours_target = window.statsConfig.readingHoursTarget || 1500;
|
|
897
|
-
goalSettings.character_count_target = window.statsConfig.characterCountTarget || 25000000;
|
|
898
|
-
goalSettings.games_target = window.statsConfig.gamesTarget || 100;
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
try {
|
|
902
|
-
const response = await fetch('/api/settings');
|
|
903
|
-
if (response.ok) {
|
|
904
|
-
const settings = await response.json();
|
|
905
|
-
goalSettings = {
|
|
906
|
-
reading_hours_target: settings.reading_hours_target || 1500,
|
|
907
|
-
character_count_target: settings.character_count_target || 25000000,
|
|
908
|
-
games_target: settings.games_target || 100
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
} catch (error) {
|
|
912
|
-
console.error('Error loading goal settings:', error);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// Function to calculate 90-day rolling average for projections
|
|
917
|
-
function calculate90DayAverage(allLinesData, metricType) {
|
|
918
|
-
if (!allLinesData || allLinesData.length === 0) {
|
|
919
|
-
return 0;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const today = new Date();
|
|
923
|
-
const ninetyDaysAgo = new Date(today.getTime() - (90 * 24 * 60 * 60 * 1000));
|
|
924
|
-
|
|
925
|
-
// Filter data to last 90 days
|
|
926
|
-
const recentData = allLinesData.filter(line => {
|
|
927
|
-
const lineDate = new Date(line.timestamp * 1000);
|
|
928
|
-
return lineDate >= ninetyDaysAgo && lineDate <= today;
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
if (recentData.length === 0) {
|
|
932
|
-
return 0;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
let dailyTotals = {};
|
|
936
|
-
|
|
937
|
-
if (metricType === 'hours') {
|
|
938
|
-
// Group by day and calculate reading time using AFK timer logic
|
|
939
|
-
const dailyTimestamps = {};
|
|
940
|
-
for (const line of recentData) {
|
|
941
|
-
const ts = parseFloat(line.timestamp);
|
|
942
|
-
if (isNaN(ts)) continue;
|
|
943
|
-
const dateObj = new Date(ts * 1000);
|
|
944
|
-
const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
|
|
945
|
-
if (!dailyTimestamps[dateStr]) {
|
|
946
|
-
dailyTimestamps[dateStr] = [];
|
|
947
|
-
}
|
|
948
|
-
dailyTimestamps[dateStr].push(ts);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
for (const [dateStr, timestamps] of Object.entries(dailyTimestamps)) {
|
|
952
|
-
if (timestamps.length >= 2) {
|
|
953
|
-
timestamps.sort((a, b) => a - b);
|
|
954
|
-
let dayHours = 0;
|
|
955
|
-
let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
956
|
-
// Try to get AFK timer from settings modal if available and valid
|
|
957
|
-
const afkTimerInput = document.getElementById('afkTimer');
|
|
958
|
-
if (afkTimerInput && afkTimerInput.value) {
|
|
959
|
-
const parsed = parseInt(afkTimerInput.value, 10);
|
|
960
|
-
if (!isNaN(parsed) && parsed > 0) afkTimerSeconds = parsed;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
for (let i = 1; i < timestamps.length; i++) {
|
|
964
|
-
const gap = timestamps[i] - timestamps[i-1];
|
|
965
|
-
dayHours += Math.min(gap, afkTimerSeconds) / 3600;
|
|
966
|
-
}
|
|
967
|
-
dailyTotals[dateStr] = dayHours;
|
|
968
|
-
} else if (timestamps.length === 1) {
|
|
969
|
-
dailyTotals[dateStr] = 1 / 3600; // Minimal activity
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
} else if (metricType === 'characters') {
|
|
973
|
-
// Group by day and sum characters
|
|
974
|
-
for (const line of recentData) {
|
|
975
|
-
const ts = parseFloat(line.timestamp);
|
|
976
|
-
if (isNaN(ts)) continue;
|
|
977
|
-
const dateObj = new Date(ts * 1000);
|
|
978
|
-
const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
|
|
979
|
-
dailyTotals[dateStr] = (dailyTotals[dateStr] || 0) + (line.characters || 0);
|
|
980
|
-
}
|
|
981
|
-
} else if (metricType === 'games') {
|
|
982
|
-
// Group by day and count unique games
|
|
983
|
-
const dailyGames = {};
|
|
984
|
-
for (const line of recentData) {
|
|
985
|
-
const ts = parseFloat(line.timestamp);
|
|
986
|
-
if (isNaN(ts)) continue;
|
|
987
|
-
const dateObj = new Date(ts * 1000);
|
|
988
|
-
const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
|
|
989
|
-
if (!dailyGames[dateStr]) {
|
|
990
|
-
dailyGames[dateStr] = new Set();
|
|
991
|
-
}
|
|
992
|
-
dailyGames[dateStr].add(line.game_name);
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
for (const [dateStr, gamesSet] of Object.entries(dailyGames)) {
|
|
996
|
-
dailyTotals[dateStr] = gamesSet.size;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const totalDays = Object.keys(dailyTotals).length;
|
|
1001
|
-
if (totalDays === 0) {
|
|
1002
|
-
return 0;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const totalValue = Object.values(dailyTotals).reduce((sum, value) => sum + value, 0);
|
|
1006
|
-
return totalValue / totalDays;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
// Function to format projection text
|
|
1010
|
-
function formatProjection(currentValue, targetValue, dailyAverage, metricType) {
|
|
1011
|
-
if (currentValue >= targetValue) {
|
|
1012
|
-
return 'Goal achieved! 🎉';
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (dailyAverage <= 0) {
|
|
1016
|
-
return 'No recent activity';
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
const remaining = targetValue - currentValue;
|
|
1020
|
-
const daysToComplete = Math.ceil(remaining / dailyAverage);
|
|
1021
|
-
|
|
1022
|
-
if (daysToComplete <= 0) {
|
|
1023
|
-
return 'Goal achieved! 🎉';
|
|
1024
|
-
} else if (daysToComplete === 1) {
|
|
1025
|
-
return '~1 day remaining';
|
|
1026
|
-
} else if (daysToComplete <= 7) {
|
|
1027
|
-
return `~${daysToComplete} days remaining`;
|
|
1028
|
-
} else if (daysToComplete <= 30) {
|
|
1029
|
-
const weeks = Math.ceil(daysToComplete / 7);
|
|
1030
|
-
return `~${weeks} week${weeks > 1 ? 's' : ''} remaining`;
|
|
1031
|
-
} else if (daysToComplete <= 365) {
|
|
1032
|
-
const months = Math.ceil(daysToComplete / 30);
|
|
1033
|
-
return `~${months} month${months > 1 ? 's' : ''} remaining`;
|
|
1034
|
-
} else {
|
|
1035
|
-
const years = Math.ceil(daysToComplete / 365);
|
|
1036
|
-
return `~${years} year${years > 1 ? 's' : ''} remaining`;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// Function to format large numbers
|
|
1041
|
-
function formatGoalNumber(num) {
|
|
1042
|
-
if (num >= 1000000) {
|
|
1043
|
-
return (num / 1000000).toFixed(1) + 'M';
|
|
1044
|
-
} else if (num >= 1000) {
|
|
1045
|
-
return (num / 1000).toFixed(1) + 'K';
|
|
1046
|
-
}
|
|
1047
|
-
return num.toString();
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// Function to update progress bar color based on percentage
|
|
1051
|
-
function updateProgressBarColor(progressElement, percentage) {
|
|
1052
|
-
// Remove existing completion classes
|
|
1053
|
-
progressElement.classList.remove('completion-0', 'completion-25', 'completion-50', 'completion-75', 'completion-100');
|
|
1054
|
-
|
|
1055
|
-
// Add appropriate class based on percentage
|
|
1056
|
-
if (percentage >= 100) {
|
|
1057
|
-
progressElement.classList.add('completion-100');
|
|
1058
|
-
} else if (percentage >= 75) {
|
|
1059
|
-
progressElement.classList.add('completion-75');
|
|
1060
|
-
} else if (percentage >= 50) {
|
|
1061
|
-
progressElement.classList.add('completion-50');
|
|
1062
|
-
} else if (percentage >= 25) {
|
|
1063
|
-
progressElement.classList.add('completion-25');
|
|
1064
|
-
} else {
|
|
1065
|
-
progressElement.classList.add('completion-0');
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// Helper function to update goal progress UI with provided data
|
|
1070
|
-
function updateGoalProgressUI(allGamesStats, allLinesData) {
|
|
1071
|
-
if (!allGamesStats) {
|
|
1072
|
-
throw new Error('No stats data available');
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
// Calculate current progress
|
|
1076
|
-
const currentHours = allGamesStats.total_time_hours || 0;
|
|
1077
|
-
const currentCharacters = allGamesStats.total_characters || 0;
|
|
1078
|
-
const currentGames = allGamesStats.unique_games || 0;
|
|
1079
|
-
|
|
1080
|
-
// Calculate 90-day averages for projections
|
|
1081
|
-
const dailyHoursAvg = calculate90DayAverage(allLinesData, 'hours');
|
|
1082
|
-
const dailyCharsAvg = calculate90DayAverage(allLinesData, 'characters');
|
|
1083
|
-
const dailyGamesAvg = calculate90DayAverage(allLinesData, 'games');
|
|
1084
|
-
|
|
1085
|
-
// Update Hours Goal
|
|
1086
|
-
const hoursPercentage = Math.min(100, (currentHours / goalSettings.reading_hours_target) * 100);
|
|
1087
|
-
document.getElementById('goalHoursCurrent').textContent = Math.floor(currentHours).toLocaleString();
|
|
1088
|
-
document.getElementById('goalHoursTarget').textContent = goalSettings.reading_hours_target.toLocaleString();
|
|
1089
|
-
document.getElementById('goalHoursPercentage').textContent = Math.floor(hoursPercentage) + '%';
|
|
1090
|
-
document.getElementById('goalHoursProjection').textContent =
|
|
1091
|
-
formatProjection(currentHours, goalSettings.reading_hours_target, dailyHoursAvg, 'hours');
|
|
1092
|
-
|
|
1093
|
-
const hoursProgressBar = document.getElementById('goalHoursProgress');
|
|
1094
|
-
hoursProgressBar.style.width = hoursPercentage + '%';
|
|
1095
|
-
hoursProgressBar.setAttribute('data-percentage', Math.floor(hoursPercentage / 25) * 25);
|
|
1096
|
-
updateProgressBarColor(hoursProgressBar, hoursPercentage);
|
|
1097
|
-
|
|
1098
|
-
// Update Characters Goal
|
|
1099
|
-
const charsPercentage = Math.min(100, (currentCharacters / goalSettings.character_count_target) * 100);
|
|
1100
|
-
document.getElementById('goalCharsCurrent').textContent = formatGoalNumber(currentCharacters);
|
|
1101
|
-
document.getElementById('goalCharsTarget').textContent = formatGoalNumber(goalSettings.character_count_target);
|
|
1102
|
-
document.getElementById('goalCharsPercentage').textContent = Math.floor(charsPercentage) + '%';
|
|
1103
|
-
document.getElementById('goalCharsProjection').textContent =
|
|
1104
|
-
formatProjection(currentCharacters, goalSettings.character_count_target, dailyCharsAvg, 'characters');
|
|
1105
|
-
|
|
1106
|
-
const charsProgressBar = document.getElementById('goalCharsProgress');
|
|
1107
|
-
charsProgressBar.style.width = charsPercentage + '%';
|
|
1108
|
-
charsProgressBar.setAttribute('data-percentage', Math.floor(charsPercentage / 25) * 25);
|
|
1109
|
-
updateProgressBarColor(charsProgressBar, charsPercentage);
|
|
1110
|
-
|
|
1111
|
-
// Update Games Goal
|
|
1112
|
-
const gamesPercentage = Math.min(100, (currentGames / goalSettings.games_target) * 100);
|
|
1113
|
-
document.getElementById('goalGamesCurrent').textContent = currentGames.toLocaleString();
|
|
1114
|
-
document.getElementById('goalGamesTarget').textContent = goalSettings.games_target.toLocaleString();
|
|
1115
|
-
document.getElementById('goalGamesPercentage').textContent = Math.floor(gamesPercentage) + '%';
|
|
1116
|
-
document.getElementById('goalGamesProjection').textContent =
|
|
1117
|
-
formatProjection(currentGames, goalSettings.games_target, dailyGamesAvg, 'games');
|
|
1118
|
-
|
|
1119
|
-
const gamesProgressBar = document.getElementById('goalGamesProgress');
|
|
1120
|
-
gamesProgressBar.style.width = gamesPercentage + '%';
|
|
1121
|
-
gamesProgressBar.setAttribute('data-percentage', Math.floor(gamesPercentage / 25) * 25);
|
|
1122
|
-
updateProgressBarColor(gamesProgressBar, gamesPercentage);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// Main function to load and display goal progress
|
|
1126
|
-
async function loadGoalProgress() {
|
|
1127
|
-
const goalProgressChart = document.getElementById('goalProgressChart');
|
|
1128
|
-
const goalProgressLoading = document.getElementById('goalProgressLoading');
|
|
1129
|
-
const goalProgressError = document.getElementById('goalProgressError');
|
|
1130
|
-
|
|
1131
|
-
if (!goalProgressChart) return;
|
|
1132
|
-
|
|
1133
|
-
try {
|
|
1134
|
-
// Show loading state
|
|
1135
|
-
goalProgressLoading.style.display = 'flex';
|
|
1136
|
-
goalProgressError.style.display = 'none';
|
|
1137
|
-
|
|
1138
|
-
// Load goal settings and stats data
|
|
1139
|
-
await loadGoalSettings();
|
|
1140
|
-
const response = await fetch('/api/stats');
|
|
1141
|
-
if (!response.ok) throw new Error('Failed to fetch stats data');
|
|
1142
|
-
|
|
1143
|
-
const data = await response.json();
|
|
1144
|
-
const allGamesStats = data.allGamesStats;
|
|
1145
|
-
const allLinesData = data.allLinesData || [];
|
|
1146
|
-
|
|
1147
|
-
// Update the UI using the shared helper function
|
|
1148
|
-
updateGoalProgressUI(allGamesStats, allLinesData);
|
|
1149
|
-
|
|
1150
|
-
// Hide loading state
|
|
1151
|
-
goalProgressLoading.style.display = 'none';
|
|
1152
|
-
|
|
1153
|
-
} catch (error) {
|
|
1154
|
-
console.error('Error loading goal progress:', error);
|
|
1155
|
-
goalProgressLoading.style.display = 'none';
|
|
1156
|
-
goalProgressError.style.display = 'block';
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
634
|
// ================================
|
|
1161
635
|
// Utility to convert date strings to Unix timestamps
|
|
1162
636
|
// Returns start of day for startDate and end of day for endDate
|
|
@@ -1179,6 +653,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1179
653
|
const fromDateInput = document.getElementById('fromDate');
|
|
1180
654
|
const toDateInput = document.getElementById('toDate');
|
|
1181
655
|
|
|
656
|
+
if (!fromDateInput || !toDateInput) return; // Null check
|
|
657
|
+
|
|
1182
658
|
const fromDate = sessionStorage.getItem("fromDate");
|
|
1183
659
|
const toDate = sessionStorage.getItem("toDate");
|
|
1184
660
|
|
|
@@ -1192,11 +668,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1192
668
|
|
|
1193
669
|
// Get today's date
|
|
1194
670
|
const today = new Date();
|
|
1195
|
-
|
|
671
|
+
const toDate = today.toLocaleDateString('en-CA');
|
|
672
|
+
toDateInput.value = toDate;
|
|
1196
673
|
|
|
1197
674
|
// Save in sessionStorage
|
|
1198
675
|
sessionStorage.setItem("fromDate", firstDate);
|
|
1199
|
-
sessionStorage.setItem("toDate",
|
|
676
|
+
sessionStorage.setItem("toDate", toDate);
|
|
1200
677
|
|
|
1201
678
|
document.dispatchEvent(new Event("datesSet"));
|
|
1202
679
|
});
|
|
@@ -1232,7 +709,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1232
709
|
|
|
1233
710
|
// Validate date order
|
|
1234
711
|
if (fromDateStr && toDateStr && new Date(fromDateStr) > new Date(toDateStr)) {
|
|
1235
|
-
popup.classList.remove("hidden");
|
|
712
|
+
if (popup) popup.classList.remove("hidden");
|
|
1236
713
|
return;
|
|
1237
714
|
}
|
|
1238
715
|
|
|
@@ -1242,15 +719,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1242
719
|
}
|
|
1243
720
|
|
|
1244
721
|
// Attach listeners to both date inputs
|
|
1245
|
-
fromDateInput.addEventListener("change", handleDateChange);
|
|
1246
|
-
toDateInput.addEventListener("change", handleDateChange);
|
|
722
|
+
if (fromDateInput) fromDateInput.addEventListener("change", handleDateChange);
|
|
723
|
+
if (toDateInput) toDateInput.addEventListener("change", handleDateChange);
|
|
1247
724
|
|
|
1248
725
|
initializeDates();
|
|
1249
726
|
|
|
1250
727
|
// Popup close button
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
728
|
+
if (closePopupBtn) {
|
|
729
|
+
closePopupBtn.addEventListener("click", () => {
|
|
730
|
+
if (popup) popup.classList.add("hidden");
|
|
731
|
+
});
|
|
732
|
+
}
|
|
1254
733
|
|
|
1255
734
|
// Populate settings modal with global config values on load
|
|
1256
735
|
if (window.statsConfig) {
|
|
@@ -1268,804 +747,20 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1268
747
|
|
|
1269
748
|
const gamesTargetInput = document.getElementById('gamesTarget');
|
|
1270
749
|
if (gamesTargetInput) gamesTargetInput.value = window.statsConfig.gamesTarget || 100;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
// Function to update goal progress using existing stats data
|
|
1274
|
-
async function updateGoalProgressWithData(statsData) {
|
|
1275
|
-
const goalProgressChart = document.getElementById('goalProgressChart');
|
|
1276
|
-
const goalProgressLoading = document.getElementById('goalProgressLoading');
|
|
1277
|
-
const goalProgressError = document.getElementById('goalProgressError');
|
|
1278
|
-
|
|
1279
|
-
if (!goalProgressChart) return;
|
|
1280
|
-
|
|
1281
|
-
try {
|
|
1282
|
-
// Load goal settings if not already loaded
|
|
1283
|
-
if (!goalSettings.reading_hours_target) {
|
|
1284
|
-
await loadGoalSettings();
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
const allGamesStats = statsData.allGamesStats;
|
|
1288
|
-
const allLinesData = statsData.allLinesData || [];
|
|
1289
|
-
|
|
1290
|
-
// Update the UI using the shared helper function
|
|
1291
|
-
updateGoalProgressUI(allGamesStats, allLinesData);
|
|
1292
|
-
|
|
1293
|
-
// Hide loading and error states
|
|
1294
|
-
goalProgressLoading.style.display = 'none';
|
|
1295
|
-
goalProgressError.style.display = 'none';
|
|
1296
|
-
|
|
1297
|
-
} catch (error) {
|
|
1298
|
-
console.error('Error updating goal progress:', error);
|
|
1299
|
-
goalProgressLoading.style.display = 'none';
|
|
1300
|
-
goalProgressError.style.display = 'block';
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// Load goal progress initially
|
|
1305
|
-
setTimeout(() => {
|
|
1306
|
-
loadGoalProgress();
|
|
1307
|
-
}, 1000);
|
|
1308
|
-
|
|
1309
|
-
// Refresh goal progress when settings are updated
|
|
1310
|
-
window.addEventListener('settingsUpdated', () => {
|
|
1311
|
-
setTimeout(() => {
|
|
1312
|
-
loadGoalProgress();
|
|
1313
|
-
}, 500);
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
// Make functions globally available
|
|
1317
|
-
window.createHeatmap = createHeatmap;
|
|
1318
|
-
window.loadStatsData = loadStatsData;
|
|
1319
|
-
window.loadGoalProgress = loadGoalProgress;
|
|
1320
|
-
|
|
1321
|
-
// Dashboard functionality
|
|
1322
|
-
function loadDashboardData(data = null, end_timestamp = null) {
|
|
1323
|
-
function updateTodayOverview(allLinesData) {
|
|
1324
|
-
// Get today's date string (YYYY-MM-DD)
|
|
1325
|
-
// Get today's date string (YYYY-MM-DD), timezone aware (local time)
|
|
1326
|
-
const today = new Date();
|
|
1327
|
-
const pad = n => n.toString().padStart(2, '0');
|
|
1328
|
-
const todayStr = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
|
|
1329
|
-
document.getElementById('todayDate').textContent = todayStr;
|
|
1330
|
-
|
|
1331
|
-
// Filter lines for today
|
|
1332
|
-
const todayLines = (allLinesData || []).filter(line => {
|
|
1333
|
-
if (!line.timestamp) return false;
|
|
1334
|
-
const ts = parseFloat(line.timestamp);
|
|
1335
|
-
if (isNaN(ts)) return false;
|
|
1336
|
-
const dateObj = new Date(ts * 1000);
|
|
1337
|
-
const lineDate = `${dateObj.getFullYear()}-${pad(dateObj.getMonth() + 1)}-${pad(dateObj.getDate())}`;
|
|
1338
|
-
return lineDate === todayStr;
|
|
1339
|
-
});
|
|
1340
|
-
|
|
1341
|
-
// Calculate total characters read today (only valid numbers)
|
|
1342
|
-
const totalChars = todayLines.reduce((sum, line) => {
|
|
1343
|
-
const chars = Number(line.characters);
|
|
1344
|
-
return sum + (isNaN(chars) ? 0 : chars);
|
|
1345
|
-
}, 0);
|
|
1346
|
-
|
|
1347
|
-
// Calculate sessions (count gaps > session threshold as new sessions)
|
|
1348
|
-
let sessions = 0;
|
|
1349
|
-
let sessionGap = window.statsConfig ? window.statsConfig.sessionGapSeconds : 3600;
|
|
1350
|
-
// Try to get session gap from settings modal if available and valid
|
|
1351
|
-
const sessionGapInput = document.getElementById('sessionGap');
|
|
1352
|
-
if (sessionGapInput && sessionGapInput.value) {
|
|
1353
|
-
const parsed = parseInt(sessionGapInput.value, 10);
|
|
1354
|
-
if (!isNaN(parsed) && parsed > 0) sessionGap = parsed;
|
|
1355
|
-
}
|
|
1356
|
-
if (todayLines.length > 0 && todayLines[0].session_id !== undefined) {
|
|
1357
|
-
const sessionSet = new Set(todayLines.map(l => l.session_id));
|
|
1358
|
-
sessions = sessionSet.size;
|
|
1359
|
-
} else {
|
|
1360
|
-
// Use timestamp gap logic
|
|
1361
|
-
const timestamps = todayLines
|
|
1362
|
-
.map(l => parseFloat(l.timestamp))
|
|
1363
|
-
.filter(ts => !isNaN(ts))
|
|
1364
|
-
.sort((a, b) => a - b);
|
|
1365
|
-
if (timestamps.length > 0) {
|
|
1366
|
-
sessions = 1;
|
|
1367
|
-
for (let i = 1; i < timestamps.length; i++) {
|
|
1368
|
-
if (timestamps[i] - timestamps[i - 1] > sessionGap) {
|
|
1369
|
-
sessions += 1;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
} else {
|
|
1373
|
-
sessions = 0;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// Calculate total reading time (reuse AFK logic from calculateHeatmapStreaks)
|
|
1378
|
-
let totalSeconds = 0;
|
|
1379
|
-
const timestamps = todayLines
|
|
1380
|
-
.map(l => parseFloat(l.timestamp))
|
|
1381
|
-
.filter(ts => !isNaN(ts))
|
|
1382
|
-
.sort((a, b) => a - b);
|
|
1383
|
-
// Get AFK timer from settings modal if available
|
|
1384
|
-
let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
1385
|
-
const afkTimerInput = document.getElementById('afkTimer');
|
|
1386
|
-
if (afkTimerInput && afkTimerInput.value) {
|
|
1387
|
-
const parsed = parseInt(afkTimerInput.value, 10);
|
|
1388
|
-
if (!isNaN(parsed) && parsed > 0) afkTimerSeconds = parsed;
|
|
1389
|
-
}
|
|
1390
|
-
if (timestamps.length >= 2) {
|
|
1391
|
-
for (let i = 1; i < timestamps.length; i++) {
|
|
1392
|
-
const gap = timestamps[i] - timestamps[i-1];
|
|
1393
|
-
totalSeconds += Math.min(gap, afkTimerSeconds);
|
|
1394
|
-
}
|
|
1395
|
-
} else if (timestamps.length === 1) {
|
|
1396
|
-
totalSeconds = 1;
|
|
1397
|
-
}
|
|
1398
|
-
let totalHours = totalSeconds / 3600;
|
|
1399
|
-
|
|
1400
|
-
// Calculate chars/hour
|
|
1401
|
-
let charsPerHour = '-';
|
|
1402
|
-
if (totalChars > 0) {
|
|
1403
|
-
// Avoid division by zero, set minimum time to 1 minute if activity exists
|
|
1404
|
-
if (totalHours <= 0) totalHours = 1/60;
|
|
1405
|
-
charsPerHour = Math.round(totalChars / totalHours).toLocaleString();
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
// Format hours for display
|
|
1409
|
-
let hoursDisplay = '-';
|
|
1410
|
-
if (totalHours > 0) {
|
|
1411
|
-
const h = Math.floor(totalHours);
|
|
1412
|
-
const m = Math.round((totalHours - h) * 60);
|
|
1413
|
-
hoursDisplay = h > 0 ? `${h}h${m > 0 ? ' ' + m + 'm' : ''}` : `${m}m`;
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
document.getElementById('todayTotalHours').textContent = hoursDisplay;
|
|
1417
|
-
document.getElementById('todayTotalChars').textContent = totalChars.toLocaleString();
|
|
1418
|
-
document.getElementById('todaySessions').textContent = sessions;
|
|
1419
|
-
document.getElementById('todayCharsPerHour').textContent = charsPerHour;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
function updateOverviewForEndDay(allLinesData, endTimestamp) {
|
|
1423
|
-
if (!endTimestamp) return;
|
|
1424
|
-
|
|
1425
|
-
const pad = n => n.toString().padStart(2, '0');
|
|
1426
|
-
|
|
1427
|
-
// Determine target date string (YYYY-MM-DD) from the end timestamp
|
|
1428
|
-
const endDateObj = new Date(endTimestamp * 1000);
|
|
1429
|
-
const targetDateStr = `${endDateObj.getFullYear()}-${pad(endDateObj.getMonth() + 1)}-${pad(endDateObj.getDate())}`;
|
|
1430
|
-
document.getElementById('todayDate').textContent = targetDateStr;
|
|
1431
|
-
|
|
1432
|
-
// Filter lines that fall on the target date
|
|
1433
|
-
const targetLines = (allLinesData || []).filter(line => {
|
|
1434
|
-
if (!line.timestamp) return false;
|
|
1435
|
-
const ts = parseFloat(line.timestamp);
|
|
1436
|
-
if (isNaN(ts)) return false;
|
|
1437
|
-
const dateObj = new Date(ts * 1000);
|
|
1438
|
-
const lineDate = `${dateObj.getFullYear()}-${pad(dateObj.getMonth() + 1)}-${pad(dateObj.getDate())}`;
|
|
1439
|
-
return lineDate === targetDateStr;
|
|
1440
|
-
});
|
|
1441
|
-
|
|
1442
|
-
// Calculate total characters
|
|
1443
|
-
const totalChars = targetLines.reduce((sum, line) => {
|
|
1444
|
-
const chars = Number(line.characters);
|
|
1445
|
-
return sum + (isNaN(chars) ? 0 : chars);
|
|
1446
|
-
}, 0);
|
|
1447
|
-
|
|
1448
|
-
// Determine session gap (from settings or default)
|
|
1449
|
-
let sessionGap = window.statsConfig?.sessionGapSeconds || 3600;
|
|
1450
|
-
const sessionGapInput = document.getElementById('sessionGap');
|
|
1451
|
-
if (sessionGapInput?.value) {
|
|
1452
|
-
const parsed = parseInt(sessionGapInput.value, 10);
|
|
1453
|
-
if (!isNaN(parsed) && parsed > 0) sessionGap = parsed;
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
// Calculate sessions
|
|
1457
|
-
let sessions = 0;
|
|
1458
|
-
if (targetLines.length > 0 && targetLines[0].session_id !== undefined) {
|
|
1459
|
-
const sessionSet = new Set(targetLines.map(l => l.session_id));
|
|
1460
|
-
sessions = sessionSet.size;
|
|
1461
|
-
} else {
|
|
1462
|
-
const timestamps = targetLines
|
|
1463
|
-
.map(l => parseFloat(l.timestamp))
|
|
1464
|
-
.filter(ts => !isNaN(ts))
|
|
1465
|
-
.sort((a, b) => a - b);
|
|
1466
|
-
if (timestamps.length > 0) {
|
|
1467
|
-
sessions = 1;
|
|
1468
|
-
for (let i = 1; i < timestamps.length; i++) {
|
|
1469
|
-
if (timestamps[i] - timestamps[i - 1] > sessionGap) {
|
|
1470
|
-
sessions += 1;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
750
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
const timestamps = targetLines
|
|
1479
|
-
.map(l => parseFloat(l.timestamp))
|
|
1480
|
-
.filter(ts => !isNaN(ts))
|
|
1481
|
-
.sort((a, b) => a - b);
|
|
1482
|
-
|
|
1483
|
-
let afkTimerSeconds = window.statsConfig?.afkTimerSeconds || 120;
|
|
1484
|
-
const afkTimerInput = document.getElementById('afkTimer');
|
|
1485
|
-
if (afkTimerInput?.value) {
|
|
1486
|
-
const parsed = parseInt(afkTimerInput.value, 10);
|
|
1487
|
-
if (!isNaN(parsed) && parsed > 0) afkTimerSeconds = parsed;
|
|
1488
|
-
}
|
|
751
|
+
const hoursDateInput = document.getElementById('readingHoursTargetDate');
|
|
752
|
+
if (hoursDateInput) hoursDateInput.value = window.statsConfig.readingHoursTargetDate || '';
|
|
1489
753
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
const gap = timestamps[i] - timestamps[i - 1];
|
|
1493
|
-
totalSeconds += Math.min(gap, afkTimerSeconds);
|
|
1494
|
-
}
|
|
1495
|
-
} else if (timestamps.length === 1) {
|
|
1496
|
-
totalSeconds = 1;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
let totalHours = totalSeconds / 3600;
|
|
1500
|
-
|
|
1501
|
-
// Calculate chars/hour
|
|
1502
|
-
let charsPerHour = '-';
|
|
1503
|
-
if (totalChars > 0) {
|
|
1504
|
-
if (totalHours <= 0) totalHours = 1/60; // Minimum 1 minute
|
|
1505
|
-
charsPerHour = Math.round(totalChars / totalHours).toLocaleString();
|
|
1506
|
-
}
|
|
754
|
+
const charsDateInput = document.getElementById('characterCountTargetDate');
|
|
755
|
+
if (charsDateInput) charsDateInput.value = window.statsConfig.characterCountTargetDate || '';
|
|
1507
756
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
if (totalHours > 0) {
|
|
1511
|
-
const h = Math.floor(totalHours);
|
|
1512
|
-
const m = Math.round((totalHours - h) * 60);
|
|
1513
|
-
hoursDisplay = h > 0 ? `${h}h${m > 0 ? ' ' + m + 'm' : ''}` : `${m}m`;
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
// Update DOM
|
|
1517
|
-
document.getElementById('todayTotalHours').textContent = hoursDisplay;
|
|
1518
|
-
document.getElementById('todayTotalChars').textContent = totalChars.toLocaleString();
|
|
1519
|
-
document.getElementById('todaySessions').textContent = sessions;
|
|
1520
|
-
document.getElementById('todayCharsPerHour').textContent = charsPerHour;
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
if (data && data.currentGameStats && data.allGamesStats) {
|
|
1524
|
-
// Use existing data if available
|
|
1525
|
-
updateCurrentGameDashboard(data.currentGameStats);
|
|
1526
|
-
updateAllGamesDashboard(data.allGamesStats);
|
|
1527
|
-
|
|
1528
|
-
if (data.allLinesData) {
|
|
1529
|
-
end_timestamp == null ? updateTodayOverview(data.allLinesData) : updateOverviewForEndDay(data.allLinesData, end_timestamp)
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
hideDashboardLoading();
|
|
1533
|
-
} else {
|
|
1534
|
-
// Fetch fresh data
|
|
1535
|
-
showDashboardLoading();
|
|
1536
|
-
fetch('/api/stats')
|
|
1537
|
-
.then(response => response.json())
|
|
1538
|
-
.then(data => {
|
|
1539
|
-
if (data.currentGameStats && data.allGamesStats) {
|
|
1540
|
-
updateCurrentGameDashboard(data.currentGameStats);
|
|
1541
|
-
updateAllGamesDashboard(data.allGamesStats);
|
|
1542
|
-
if (data.allLinesData) {
|
|
1543
|
-
end_timestamp == null ? updateTodayOverview(data.allLinesData) : updateOverviewForEndDay(data.allLinesData, end_timestamp)
|
|
1544
|
-
}
|
|
1545
|
-
} else {
|
|
1546
|
-
showDashboardError();
|
|
1547
|
-
}
|
|
1548
|
-
hideDashboardLoading();
|
|
1549
|
-
})
|
|
1550
|
-
.catch(error => {
|
|
1551
|
-
console.error('Error fetching dashboard data:', error);
|
|
1552
|
-
showDashboardError();
|
|
1553
|
-
hideDashboardLoading();
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
function updateCurrentGameDashboard(stats) {
|
|
1559
|
-
if (!stats) {
|
|
1560
|
-
showNoDashboardData('currentGameCard', 'No current game data available');
|
|
1561
|
-
return;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
// Update game name and subtitle
|
|
1565
|
-
document.getElementById('currentGameName').textContent = stats.game_name;
|
|
1566
|
-
|
|
1567
|
-
// Update main statistics
|
|
1568
|
-
document.getElementById('currentTotalChars').textContent = stats.total_characters_formatted;
|
|
1569
|
-
document.getElementById('currentTotalTime').textContent = stats.total_time_formatted;
|
|
1570
|
-
document.getElementById('currentReadingSpeed').textContent = stats.reading_speed_formatted;
|
|
1571
|
-
document.getElementById('currentSessions').textContent = stats.sessions.toLocaleString();
|
|
1572
|
-
|
|
1573
|
-
// Update progress section
|
|
1574
|
-
document.getElementById('currentMonthlyChars').textContent = stats.monthly_characters_formatted;
|
|
1575
|
-
document.getElementById('currentFirstDate').textContent = stats.first_date;
|
|
1576
|
-
document.getElementById('currentLastDate').textContent = stats.last_date;
|
|
1577
|
-
|
|
1578
|
-
// Update streak indicator
|
|
1579
|
-
const streakElement = document.getElementById('currentGameStreak');
|
|
1580
|
-
const streakValue = document.getElementById('currentStreakValue');
|
|
1581
|
-
if (stats.current_streak > 0) {
|
|
1582
|
-
streakValue.textContent = stats.current_streak;
|
|
1583
|
-
streakElement.style.display = 'inline-flex';
|
|
1584
|
-
} else {
|
|
1585
|
-
streakElement.style.display = 'none';
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
// Show the card
|
|
1589
|
-
document.getElementById('currentGameCard').style.display = 'block';
|
|
757
|
+
const gamesDateInput = document.getElementById('gamesTargetDate');
|
|
758
|
+
if (gamesDateInput) gamesDateInput.value = window.statsConfig.gamesTargetDate || '';
|
|
1590
759
|
}
|
|
1591
760
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
showNoDashboardData('allGamesCard', 'No games data available');
|
|
1595
|
-
return;
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
// Update subtitle
|
|
1599
|
-
const gamesText = stats.unique_games === 1 ? '1 game played' : `${stats.unique_games} games played`;
|
|
1600
|
-
document.getElementById('totalGamesCount').textContent = gamesText;
|
|
1601
|
-
|
|
1602
|
-
// Update main statistics
|
|
1603
|
-
document.getElementById('allTotalChars').textContent = stats.total_characters_formatted;
|
|
1604
|
-
document.getElementById('allTotalTime').textContent = stats.total_time_formatted;
|
|
1605
|
-
document.getElementById('allReadingSpeed').textContent = stats.reading_speed_formatted;
|
|
1606
|
-
document.getElementById('allSessions').textContent = stats.sessions.toLocaleString();
|
|
1607
|
-
|
|
1608
|
-
// Update progress section
|
|
1609
|
-
document.getElementById('allMonthlyChars').textContent = stats.monthly_characters_formatted;
|
|
1610
|
-
document.getElementById('allUniqueGames').textContent = stats.unique_games.toLocaleString();
|
|
1611
|
-
document.getElementById('allTotalSentences').textContent = stats.total_sentences.toLocaleString();
|
|
1612
|
-
|
|
1613
|
-
// Update streak indicator
|
|
1614
|
-
const streakElement = document.getElementById('allGamesStreak');
|
|
1615
|
-
const streakValue = document.getElementById('allStreakValue');
|
|
1616
|
-
if (stats.current_streak > 0) {
|
|
1617
|
-
streakValue.textContent = stats.current_streak;
|
|
1618
|
-
streakElement.style.display = 'inline-flex';
|
|
1619
|
-
} else {
|
|
1620
|
-
streakElement.style.display = 'none';
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
// Show the card
|
|
1625
|
-
document.getElementById('allGamesCard').style.display = 'block';
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
function showDashboardLoading() {
|
|
1629
|
-
document.getElementById('dashboardLoading').style.display = 'flex';
|
|
1630
|
-
document.getElementById('dashboardError').style.display = 'none';
|
|
1631
|
-
document.getElementById('currentGameCard').style.display = 'none';
|
|
1632
|
-
document.getElementById('allGamesCard').style.display = 'none';
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
function hideDashboardLoading() {
|
|
1636
|
-
document.getElementById('dashboardLoading').style.display = 'none';
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
function showDashboardError() {
|
|
1640
|
-
document.getElementById('dashboardError').style.display = 'block';
|
|
1641
|
-
document.getElementById('dashboardLoading').style.display = 'none';
|
|
1642
|
-
document.getElementById('currentGameCard').style.display = 'none';
|
|
1643
|
-
document.getElementById('allGamesCard').style.display = 'none';
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
function showNoDashboardData(cardId, message) {
|
|
1647
|
-
const card = document.getElementById(cardId);
|
|
1648
|
-
const statsGrid = card.querySelector('.dashboard-stats-grid');
|
|
1649
|
-
const progressSection = card.querySelector('.dashboard-progress-section');
|
|
1650
|
-
|
|
1651
|
-
// Hide stats and progress sections
|
|
1652
|
-
statsGrid.style.display = 'none';
|
|
1653
|
-
progressSection.style.display = 'none';
|
|
1654
|
-
|
|
1655
|
-
// Add no data message
|
|
1656
|
-
let noDataMsg = card.querySelector('.no-data-message');
|
|
1657
|
-
if (!noDataMsg) {
|
|
1658
|
-
noDataMsg = document.createElement('div');
|
|
1659
|
-
noDataMsg.className = 'no-data-message';
|
|
1660
|
-
noDataMsg.style.cssText = 'text-align: center; padding: 40px 20px; color: var(--text-tertiary); font-style: italic;';
|
|
1661
|
-
card.appendChild(noDataMsg);
|
|
1662
|
-
}
|
|
1663
|
-
noDataMsg.textContent = message;
|
|
1664
|
-
|
|
1665
|
-
card.style.display = 'block';
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
// Add click animations for dashboard stat items
|
|
1669
|
-
const statItems = document.querySelectorAll('.dashboard-stat-item');
|
|
1670
|
-
statItems.forEach(item => {
|
|
1671
|
-
item.addEventListener('click', function() {
|
|
1672
|
-
// Add click animation
|
|
1673
|
-
this.style.transform = 'scale(0.95)';
|
|
1674
|
-
setTimeout(() => {
|
|
1675
|
-
this.style.transform = '';
|
|
1676
|
-
}, 150);
|
|
1677
|
-
});
|
|
1678
|
-
});
|
|
1679
|
-
|
|
1680
|
-
// Add accessibility improvements
|
|
1681
|
-
statItems.forEach(item => {
|
|
1682
|
-
item.setAttribute('tabindex', '0');
|
|
1683
|
-
item.setAttribute('role', 'button');
|
|
1684
|
-
|
|
1685
|
-
item.addEventListener('keydown', function(e) {
|
|
1686
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
1687
|
-
e.preventDefault();
|
|
1688
|
-
this.click();
|
|
1689
|
-
}
|
|
1690
|
-
});
|
|
1691
|
-
});
|
|
1692
|
-
|
|
1693
|
-
// Global function to retry dashboard loading
|
|
1694
|
-
window.loadDashboardData = loadDashboardData;
|
|
761
|
+
// Make functions globally available
|
|
762
|
+
window.loadStatsData = loadStatsData;
|
|
1695
763
|
|
|
1696
|
-
// Delete Game Entry Functionality
|
|
1697
|
-
class GameDeletionManager {
|
|
1698
|
-
constructor() {
|
|
1699
|
-
this.games = [];
|
|
1700
|
-
this.selectedGames = new Set();
|
|
1701
|
-
this.isLoading = false;
|
|
1702
|
-
|
|
1703
|
-
this.initializeElements();
|
|
1704
|
-
this.attachEventListeners();
|
|
1705
|
-
this.loadGames();
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
initializeElements() {
|
|
1709
|
-
// Control elements
|
|
1710
|
-
this.selectAllBtn = document.getElementById('selectAllBtn');
|
|
1711
|
-
this.selectNoneBtn = document.getElementById('selectNoneBtn');
|
|
1712
|
-
this.deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
|
|
1713
|
-
this.headerCheckbox = document.getElementById('headerCheckbox');
|
|
1714
|
-
|
|
1715
|
-
// Table elements
|
|
1716
|
-
this.gamesTableBody = document.getElementById('gamesTableBody');
|
|
1717
|
-
this.loadingIndicator = document.getElementById('loadingIndicator');
|
|
1718
|
-
this.noGamesMessage = document.getElementById('noGamesMessage');
|
|
1719
|
-
this.errorMessage = document.getElementById('errorMessage');
|
|
1720
|
-
this.retryBtn = document.getElementById('retryBtn');
|
|
1721
|
-
|
|
1722
|
-
// Modal elements
|
|
1723
|
-
this.confirmationModal = document.getElementById('confirmationModal');
|
|
1724
|
-
this.progressModal = document.getElementById('progressModal');
|
|
1725
|
-
this.resultModal = document.getElementById('resultModal');
|
|
1726
|
-
|
|
1727
|
-
// Modal content elements
|
|
1728
|
-
this.selectedGamesList = document.getElementById('selectedGamesList');
|
|
1729
|
-
this.totalGamesCount = document.getElementById('totalGamesCount');
|
|
1730
|
-
this.totalSentencesCount = document.getElementById('totalSentencesCount');
|
|
1731
|
-
this.totalCharactersCount = document.getElementById('totalCharactersCount');
|
|
1732
|
-
this.progressText = document.getElementById('progressText');
|
|
1733
|
-
this.resultContent = document.getElementById('resultContent');
|
|
1734
|
-
this.resultTitle = document.getElementById('resultTitle');
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
attachEventListeners() {
|
|
1738
|
-
// Control buttons
|
|
1739
|
-
if (this.selectAllBtn) this.selectAllBtn.addEventListener('click', () => this.selectAll());
|
|
1740
|
-
if (this.selectNoneBtn) this.selectNoneBtn.addEventListener('click', () => this.selectNone());
|
|
1741
|
-
if (this.deleteSelectedBtn) this.deleteSelectedBtn.addEventListener('click', () => this.showConfirmation());
|
|
1742
|
-
if (this.headerCheckbox) this.headerCheckbox.addEventListener('change', (e) => this.toggleAll(e.target.checked));
|
|
1743
|
-
|
|
1744
|
-
// Modal controls
|
|
1745
|
-
const closeModalBtn = document.getElementById('closeModal');
|
|
1746
|
-
if (closeModalBtn) closeModalBtn.addEventListener('click', () => this.hideModal('confirmationModal'));
|
|
1747
|
-
const cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
|
|
1748
|
-
if (cancelDeleteBtn) cancelDeleteBtn.addEventListener('click', () => this.hideModal('confirmationModal'));
|
|
1749
|
-
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
|
1750
|
-
if (confirmDeleteBtn) confirmDeleteBtn.addEventListener('click', () => this.performDeletion());
|
|
1751
|
-
const closeResultModalBtn = document.getElementById('closeResultModal');
|
|
1752
|
-
if (closeResultModalBtn) closeResultModalBtn.addEventListener('click', () => this.hideModal('resultModal'));
|
|
1753
|
-
const okBtn = document.getElementById('okBtn');
|
|
1754
|
-
if (okBtn) okBtn.addEventListener('click', () => this.hideModal('resultModal'));
|
|
1755
|
-
|
|
1756
|
-
// Retry button
|
|
1757
|
-
if (this.retryBtn) this.retryBtn.addEventListener('click', () => this.loadGames());
|
|
1758
|
-
|
|
1759
|
-
// Close modals when clicking outside
|
|
1760
|
-
[this.confirmationModal, this.progressModal, this.resultModal].forEach(modal => {
|
|
1761
|
-
modal.addEventListener('click', (e) => {
|
|
1762
|
-
if (e.target === modal) {
|
|
1763
|
-
this.hideModal(modal.id);
|
|
1764
|
-
}
|
|
1765
|
-
});
|
|
1766
|
-
});
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
async loadGames() {
|
|
1770
|
-
this.showLoading(true);
|
|
1771
|
-
hideElement(this.errorMessage);
|
|
1772
|
-
hideElement(this.noGamesMessage);
|
|
1773
|
-
|
|
1774
|
-
try {
|
|
1775
|
-
const response = await fetch('/api/games-list');
|
|
1776
|
-
const data = await response.json();
|
|
1777
|
-
|
|
1778
|
-
if (!response.ok) {
|
|
1779
|
-
throw new Error(data.error || 'Failed to fetch games');
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
this.games = data.games;
|
|
1783
|
-
this.selectedGames.clear();
|
|
1784
|
-
this.renderGamesTable();
|
|
1785
|
-
this.updateDeleteButton();
|
|
1786
|
-
|
|
1787
|
-
if (this.games.length === 0) {
|
|
1788
|
-
showElement(this.noGamesMessage);
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
} catch (error) {
|
|
1792
|
-
console.error('Error loading games:', error);
|
|
1793
|
-
this.showError(error.message);
|
|
1794
|
-
} finally {
|
|
1795
|
-
this.showLoading(false);
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
renderGamesTable() {
|
|
1800
|
-
this.gamesTableBody.innerHTML = '';
|
|
1801
|
-
|
|
1802
|
-
this.games.forEach(game => {
|
|
1803
|
-
const row = document.createElement('tr');
|
|
1804
|
-
row.dataset.gameName = game.name;
|
|
1805
|
-
|
|
1806
|
-
row.innerHTML = `
|
|
1807
|
-
<td class="checkbox-cell">
|
|
1808
|
-
<input type="checkbox" class="game-checkbox" data-game="${game.name}">
|
|
1809
|
-
</td>
|
|
1810
|
-
<td><strong>${escapeHtml(game.name)}</strong></td>
|
|
1811
|
-
<td>${game.sentence_count.toLocaleString()}</td>
|
|
1812
|
-
<td>${game.total_characters.toLocaleString()}</td>
|
|
1813
|
-
<td>${game.date_range}</td>
|
|
1814
|
-
<td>${game.first_entry_date}</td>
|
|
1815
|
-
<td>${game.last_entry_date}</td>
|
|
1816
|
-
`;
|
|
1817
|
-
|
|
1818
|
-
// Add checkbox event listener
|
|
1819
|
-
const checkbox = row.querySelector('.game-checkbox');
|
|
1820
|
-
checkbox.addEventListener('change', (e) => {
|
|
1821
|
-
this.toggleGameSelection(game.name, e.target.checked);
|
|
1822
|
-
});
|
|
1823
|
-
|
|
1824
|
-
this.gamesTableBody.appendChild(row);
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
toggleGameSelection(gameName, isSelected) {
|
|
1829
|
-
if (isSelected) {
|
|
1830
|
-
this.selectedGames.add(gameName);
|
|
1831
|
-
} else {
|
|
1832
|
-
this.selectedGames.delete(gameName);
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
this.updateRowSelection(gameName, isSelected);
|
|
1836
|
-
this.updateHeaderCheckbox();
|
|
1837
|
-
this.updateDeleteButton();
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
updateRowSelection(gameName, isSelected) {
|
|
1841
|
-
const row = document.querySelector(`tr[data-game-name="${gameName}"]`);
|
|
1842
|
-
if (row) {
|
|
1843
|
-
if (isSelected) {
|
|
1844
|
-
row.classList.add('selected');
|
|
1845
|
-
} else {
|
|
1846
|
-
row.classList.remove('selected');
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
selectAll() {
|
|
1852
|
-
this.games.forEach(game => {
|
|
1853
|
-
this.selectedGames.add(game.name);
|
|
1854
|
-
const checkbox = document.querySelector(`input[data-game="${game.name}"]`);
|
|
1855
|
-
if (checkbox) {
|
|
1856
|
-
checkbox.checked = true;
|
|
1857
|
-
this.updateRowSelection(game.name, true);
|
|
1858
|
-
}
|
|
1859
|
-
});
|
|
1860
|
-
this.updateHeaderCheckbox();
|
|
1861
|
-
this.updateDeleteButton();
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
selectNone() {
|
|
1865
|
-
this.selectedGames.clear();
|
|
1866
|
-
document.querySelectorAll('.game-checkbox').forEach(checkbox => {
|
|
1867
|
-
checkbox.checked = false;
|
|
1868
|
-
});
|
|
1869
|
-
document.querySelectorAll('tr.selected').forEach(row => {
|
|
1870
|
-
row.classList.remove('selected');
|
|
1871
|
-
});
|
|
1872
|
-
this.updateHeaderCheckbox();
|
|
1873
|
-
this.updateDeleteButton();
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
toggleAll(checked) {
|
|
1877
|
-
if (checked) {
|
|
1878
|
-
this.selectAll();
|
|
1879
|
-
} else {
|
|
1880
|
-
this.selectNone();
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
updateHeaderCheckbox() {
|
|
1885
|
-
const totalGames = this.games.length;
|
|
1886
|
-
const selectedCount = this.selectedGames.size;
|
|
1887
|
-
|
|
1888
|
-
if (selectedCount === 0) {
|
|
1889
|
-
this.headerCheckbox.checked = false;
|
|
1890
|
-
this.headerCheckbox.indeterminate = false;
|
|
1891
|
-
} else if (selectedCount === totalGames) {
|
|
1892
|
-
this.headerCheckbox.checked = true;
|
|
1893
|
-
this.headerCheckbox.indeterminate = false;
|
|
1894
|
-
} else {
|
|
1895
|
-
this.headerCheckbox.checked = false;
|
|
1896
|
-
this.headerCheckbox.indeterminate = true;
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
updateDeleteButton() {
|
|
1901
|
-
this.deleteSelectedBtn.disabled = this.selectedGames.size === 0;
|
|
1902
|
-
this.deleteSelectedBtn.textContent = this.selectedGames.size > 0
|
|
1903
|
-
? `Delete Selected Games (${this.selectedGames.size})`
|
|
1904
|
-
: 'Delete Selected Games';
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
showConfirmation() {
|
|
1908
|
-
if (this.selectedGames.size === 0) return;
|
|
1909
|
-
|
|
1910
|
-
// Populate confirmation modal
|
|
1911
|
-
this.populateConfirmationModal();
|
|
1912
|
-
this.showModal('confirmationModal');
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
populateConfirmationModal() {
|
|
1916
|
-
const selectedGameData = this.games.filter(game => this.selectedGames.has(game.name));
|
|
1917
|
-
|
|
1918
|
-
// Populate games list
|
|
1919
|
-
this.selectedGamesList.innerHTML = '';
|
|
1920
|
-
selectedGameData.forEach(game => {
|
|
1921
|
-
const gameItem = document.createElement('div');
|
|
1922
|
-
gameItem.className = 'game-item';
|
|
1923
|
-
gameItem.innerHTML = `
|
|
1924
|
-
<div>
|
|
1925
|
-
<div class="game-name">${escapeHtml(game.name)}</div>
|
|
1926
|
-
<div class="game-stats">${game.date_range}</div>
|
|
1927
|
-
</div>
|
|
1928
|
-
<div class="game-stats">
|
|
1929
|
-
${game.sentence_count} sentences, ${game.total_characters.toLocaleString()} chars
|
|
1930
|
-
</div>
|
|
1931
|
-
`;
|
|
1932
|
-
this.selectedGamesList.appendChild(gameItem);
|
|
1933
|
-
});
|
|
1934
|
-
|
|
1935
|
-
// Calculate totals
|
|
1936
|
-
const totalGames = selectedGameData.length;
|
|
1937
|
-
const totalSentences = selectedGameData.reduce((sum, game) => sum + game.sentence_count, 0);
|
|
1938
|
-
const totalCharacters = selectedGameData.reduce((sum, game) => sum + game.total_characters, 0);
|
|
1939
|
-
|
|
1940
|
-
this.totalGamesCount.textContent = totalGames;
|
|
1941
|
-
this.totalSentencesCount.textContent = totalSentences.toLocaleString();
|
|
1942
|
-
this.totalCharactersCount.textContent = totalCharacters.toLocaleString();
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
async performDeletion() {
|
|
1946
|
-
this.hideModal('confirmationModal');
|
|
1947
|
-
this.showModal('progressModal');
|
|
1948
|
-
|
|
1949
|
-
// Show native confirmation as second stage
|
|
1950
|
-
const gameNames = Array.from(this.selectedGames);
|
|
1951
|
-
const confirmText = `Are you absolutely sure you want to delete ${gameNames.length} game(s)? This action cannot be undone.`;
|
|
1952
|
-
|
|
1953
|
-
if (!confirm(confirmText)) {
|
|
1954
|
-
this.hideModal('progressModal');
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
try {
|
|
1959
|
-
this.progressText.textContent = `Deleting ${gameNames.length} games...`;
|
|
1960
|
-
|
|
1961
|
-
const response = await fetch('/api/delete-games', {
|
|
1962
|
-
method: 'POST',
|
|
1963
|
-
headers: {
|
|
1964
|
-
'Content-Type': 'application/json',
|
|
1965
|
-
},
|
|
1966
|
-
body: JSON.stringify({ game_names: gameNames })
|
|
1967
|
-
});
|
|
1968
|
-
|
|
1969
|
-
const result = await response.json();
|
|
1970
|
-
|
|
1971
|
-
this.hideModal('progressModal');
|
|
1972
|
-
this.showResult(result, response.status);
|
|
1973
|
-
|
|
1974
|
-
} catch (error) {
|
|
1975
|
-
console.error('Error deleting games:', error);
|
|
1976
|
-
this.hideModal('progressModal');
|
|
1977
|
-
this.showResult({ error: error.message }, 500);
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
showResult(result, status) {
|
|
1982
|
-
let title, content, isSuccess = false;
|
|
1983
|
-
|
|
1984
|
-
if (status === 200) {
|
|
1985
|
-
// Complete success
|
|
1986
|
-
title = 'Deletion Successful';
|
|
1987
|
-
isSuccess = true;
|
|
1988
|
-
content = `
|
|
1989
|
-
<div class="success-message">
|
|
1990
|
-
<p>✅ Successfully deleted ${result.successful_games.length} games!</p>
|
|
1991
|
-
<p><strong>Total sentences deleted:</strong> ${result.total_sentences_deleted.toLocaleString()}</p>
|
|
1992
|
-
</div>
|
|
1993
|
-
`;
|
|
1994
|
-
} else if (status === 207) {
|
|
1995
|
-
// Partial success
|
|
1996
|
-
title = 'Deletion Partially Successful';
|
|
1997
|
-
content = `
|
|
1998
|
-
<div class="warning-result">
|
|
1999
|
-
<p>⚠️ ${result.successful_games.length} games deleted successfully</p>
|
|
2000
|
-
<p>${result.failed_games.length} games failed to delete</p>
|
|
2001
|
-
<p><strong>Total sentences deleted:</strong> ${result.total_sentences_deleted.toLocaleString()}</p>
|
|
2002
|
-
</div>
|
|
2003
|
-
<div style="margin-top: 15px;">
|
|
2004
|
-
<strong>Failed games:</strong>
|
|
2005
|
-
<ul style="margin: 10px 0; padding-left: 20px;">
|
|
2006
|
-
${result.failed_games.map(game => `<li>${escapeHtml(game)}</li>`).join('')}
|
|
2007
|
-
</ul>
|
|
2008
|
-
</div>
|
|
2009
|
-
`;
|
|
2010
|
-
isSuccess = true; // Still refresh since some succeeded
|
|
2011
|
-
} else {
|
|
2012
|
-
// Complete failure
|
|
2013
|
-
title = 'Deletion Failed';
|
|
2014
|
-
content = `
|
|
2015
|
-
<div class="error-result">
|
|
2016
|
-
<p>❌ Failed to delete games</p>
|
|
2017
|
-
<p><strong>Error:</strong> ${escapeHtml(result.error || 'Unknown error occurred')}</p>
|
|
2018
|
-
</div>
|
|
2019
|
-
`;
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
this.resultTitle.textContent = title;
|
|
2023
|
-
this.resultContent.innerHTML = content;
|
|
2024
|
-
this.showModal('resultModal');
|
|
2025
|
-
|
|
2026
|
-
// Auto-refresh if any deletions were successful
|
|
2027
|
-
if (isSuccess) {
|
|
2028
|
-
setTimeout(() => {
|
|
2029
|
-
this.hideModal('resultModal');
|
|
2030
|
-
window.location.reload();
|
|
2031
|
-
}, 3000);
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
showModal(modalId) {
|
|
2036
|
-
const modal = document.getElementById(modalId);
|
|
2037
|
-
modal.classList.add('show');
|
|
2038
|
-
modal.style.display = 'flex';
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
hideModal(modalId) {
|
|
2042
|
-
const modal = document.getElementById(modalId);
|
|
2043
|
-
modal.classList.remove('show');
|
|
2044
|
-
modal.style.display = 'none';
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
showLoading(show) {
|
|
2048
|
-
this.isLoading = show;
|
|
2049
|
-
if (show) {
|
|
2050
|
-
showElement(this.loadingIndicator);
|
|
2051
|
-
hideElement(this.gamesTableBody.parentElement);
|
|
2052
|
-
} else {
|
|
2053
|
-
hideElement(this.loadingIndicator);
|
|
2054
|
-
showElement(this.gamesTableBody.parentElement);
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
showError(message) {
|
|
2059
|
-
document.getElementById('errorText').textContent = message;
|
|
2060
|
-
showElement(this.errorMessage);
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
// Initialize the deletion manager
|
|
2065
|
-
if (document.getElementById('gamesTableBody')) {
|
|
2066
|
-
new GameDeletionManager();
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
764
|
// ExStatic Import Functionality
|
|
2070
765
|
const exstaticFileInput = document.getElementById('exstaticFile');
|
|
2071
766
|
const importExstaticBtn = document.getElementById('importExstaticBtn');
|