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
|
@@ -1,6 +1,186 @@
|
|
|
1
1
|
// Overview Page JavaScript
|
|
2
2
|
// Dependencies: shared.js (provides utility functions like showElement, hideElement, escapeHtml)
|
|
3
3
|
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// PERFORMANCE OPTIMIZATION: Cache frequently accessed DOM elements
|
|
6
|
+
// ============================================================================
|
|
7
|
+
const DOM_CACHE = {
|
|
8
|
+
// Dashboard cards
|
|
9
|
+
currentGameCard: null,
|
|
10
|
+
allGamesCard: null,
|
|
11
|
+
todayOverviewCard: null,
|
|
12
|
+
|
|
13
|
+
// Current game elements
|
|
14
|
+
currentGameName: null,
|
|
15
|
+
currentTotalChars: null,
|
|
16
|
+
currentTotalTime: null,
|
|
17
|
+
currentReadingSpeed: null,
|
|
18
|
+
currentEstimatedTimeLeft: null,
|
|
19
|
+
currentGameStreak: null,
|
|
20
|
+
currentStreakValue: null,
|
|
21
|
+
gameCompletionBtn: null,
|
|
22
|
+
|
|
23
|
+
// Session elements
|
|
24
|
+
currentSessionTotalHours: null,
|
|
25
|
+
currentSessionTotalChars: null,
|
|
26
|
+
currentSessionStartTime: null,
|
|
27
|
+
currentSessionEndTime: null,
|
|
28
|
+
currentSessionCharsPerHour: null,
|
|
29
|
+
|
|
30
|
+
// Game metadata elements
|
|
31
|
+
gameContentGrid: null,
|
|
32
|
+
gamePhotoSection: null,
|
|
33
|
+
gamePhoto: null,
|
|
34
|
+
gameTitleOriginal: null,
|
|
35
|
+
gameTitleRomaji: null,
|
|
36
|
+
gameTitleEnglish: null,
|
|
37
|
+
gameTypeBadge: null,
|
|
38
|
+
gameDescription: null,
|
|
39
|
+
descriptionExpandBtn: null,
|
|
40
|
+
gameLinksContainer: null,
|
|
41
|
+
gameLinksPills: null,
|
|
42
|
+
gameProgressContainer: null,
|
|
43
|
+
gameProgressPercentage: null,
|
|
44
|
+
gameProgressFill: null,
|
|
45
|
+
gameStartDate: null,
|
|
46
|
+
gameEstimatedEndDate: null,
|
|
47
|
+
|
|
48
|
+
// Today's overview elements
|
|
49
|
+
todayDate: null,
|
|
50
|
+
todayTotalHours: null,
|
|
51
|
+
todayTotalChars: null,
|
|
52
|
+
todaySessions: null,
|
|
53
|
+
todayCharsPerHour: null,
|
|
54
|
+
|
|
55
|
+
// All games elements
|
|
56
|
+
totalGamesCount: null,
|
|
57
|
+
allTotalChars: null,
|
|
58
|
+
allTotalTime: null,
|
|
59
|
+
allReadingSpeed: null,
|
|
60
|
+
allSessions: null,
|
|
61
|
+
allUniqueGames: null,
|
|
62
|
+
allTotalSentences: null,
|
|
63
|
+
allGamesStreak: null,
|
|
64
|
+
allStreakValue: null,
|
|
65
|
+
|
|
66
|
+
// Loading/error states
|
|
67
|
+
dashboardLoading: null,
|
|
68
|
+
dashboardError: null,
|
|
69
|
+
|
|
70
|
+
// Heatmap
|
|
71
|
+
heatmapContainer: null,
|
|
72
|
+
|
|
73
|
+
// Session navigation
|
|
74
|
+
prevSessionBtn: null,
|
|
75
|
+
nextSessionBtn: null,
|
|
76
|
+
deleteSessionBtn: null,
|
|
77
|
+
|
|
78
|
+
// Initialize all cached references
|
|
79
|
+
init() {
|
|
80
|
+
// Dashboard cards
|
|
81
|
+
this.currentGameCard = document.getElementById('currentGameCard');
|
|
82
|
+
this.allGamesCard = document.getElementById('allGamesCard');
|
|
83
|
+
this.todayOverviewCard = document.getElementById('todayOverviewCard');
|
|
84
|
+
|
|
85
|
+
// Current game elements
|
|
86
|
+
this.currentGameName = document.getElementById('currentGameName');
|
|
87
|
+
this.currentTotalChars = document.getElementById('currentTotalChars');
|
|
88
|
+
this.currentTotalTime = document.getElementById('currentTotalTime');
|
|
89
|
+
this.currentReadingSpeed = document.getElementById('currentReadingSpeed');
|
|
90
|
+
this.currentEstimatedTimeLeft = document.getElementById('currentEstimatedTimeLeft');
|
|
91
|
+
this.currentGameStreak = document.getElementById('currentGameStreak');
|
|
92
|
+
this.currentStreakValue = document.getElementById('currentStreakValue');
|
|
93
|
+
this.gameCompletionBtn = document.getElementById('gameCompletionBtn');
|
|
94
|
+
|
|
95
|
+
// Session elements
|
|
96
|
+
this.currentSessionTotalHours = document.getElementById('currentSessionTotalHours');
|
|
97
|
+
this.currentSessionTotalChars = document.getElementById('currentSessionTotalChars');
|
|
98
|
+
this.currentSessionStartTime = document.getElementById('currentSessionStartTime');
|
|
99
|
+
this.currentSessionEndTime = document.getElementById('currentSessionEndTime');
|
|
100
|
+
this.currentSessionCharsPerHour = document.getElementById('currentSessionCharsPerHour');
|
|
101
|
+
|
|
102
|
+
// Game metadata elements
|
|
103
|
+
this.gameContentGrid = document.getElementById('gameContentGrid');
|
|
104
|
+
this.gamePhotoSection = document.getElementById('gamePhotoSection');
|
|
105
|
+
this.gamePhoto = document.getElementById('gamePhoto');
|
|
106
|
+
this.gameTitleOriginal = document.getElementById('gameTitleOriginal');
|
|
107
|
+
this.gameTitleRomaji = document.getElementById('gameTitleRomaji');
|
|
108
|
+
this.gameTitleEnglish = document.getElementById('gameTitleEnglish');
|
|
109
|
+
this.gameTypeBadge = document.getElementById('gameTypeBadge');
|
|
110
|
+
this.gameDescription = document.getElementById('gameDescription');
|
|
111
|
+
this.descriptionExpandBtn = document.getElementById('descriptionExpandBtn');
|
|
112
|
+
this.gameLinksContainer = document.getElementById('gameLinksContainer');
|
|
113
|
+
this.gameLinksPills = document.getElementById('gameLinksPills');
|
|
114
|
+
this.gameProgressContainer = document.getElementById('gameProgressContainer');
|
|
115
|
+
this.gameProgressPercentage = document.getElementById('gameProgressPercentage');
|
|
116
|
+
this.gameProgressFill = document.getElementById('gameProgressFill');
|
|
117
|
+
this.gameStartDate = document.getElementById('gameStartDate');
|
|
118
|
+
this.gameEstimatedEndDate = document.getElementById('gameEstimatedEndDate');
|
|
119
|
+
|
|
120
|
+
// Today's overview elements
|
|
121
|
+
this.todayDate = document.getElementById('todayDate');
|
|
122
|
+
this.todayTotalHours = document.getElementById('todayTotalHours');
|
|
123
|
+
this.todayTotalChars = document.getElementById('todayTotalChars');
|
|
124
|
+
this.todaySessions = document.getElementById('todaySessions');
|
|
125
|
+
this.todayCharsPerHour = document.getElementById('todayCharsPerHour');
|
|
126
|
+
|
|
127
|
+
// All games elements
|
|
128
|
+
this.totalGamesCount = document.getElementById('totalGamesCount');
|
|
129
|
+
this.allTotalChars = document.getElementById('allTotalChars');
|
|
130
|
+
this.allTotalTime = document.getElementById('allTotalTime');
|
|
131
|
+
this.allReadingSpeed = document.getElementById('allReadingSpeed');
|
|
132
|
+
this.allSessions = document.getElementById('allSessions');
|
|
133
|
+
this.allUniqueGames = document.getElementById('allUniqueGames');
|
|
134
|
+
this.allTotalSentences = document.getElementById('allTotalSentences');
|
|
135
|
+
this.allGamesStreak = document.getElementById('allGamesStreak');
|
|
136
|
+
this.allStreakValue = document.getElementById('allStreakValue');
|
|
137
|
+
|
|
138
|
+
// Loading/error states
|
|
139
|
+
this.dashboardLoading = document.getElementById('dashboardLoading');
|
|
140
|
+
this.dashboardError = document.getElementById('dashboardError');
|
|
141
|
+
|
|
142
|
+
// Heatmap
|
|
143
|
+
this.heatmapContainer = document.getElementById('heatmapContainer');
|
|
144
|
+
|
|
145
|
+
// Session navigation
|
|
146
|
+
this.prevSessionBtn = document.querySelector('.prev-session-btn');
|
|
147
|
+
this.nextSessionBtn = document.querySelector('.next-session-btn');
|
|
148
|
+
this.deleteSessionBtn = document.querySelector('.delete-session-btn');
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// PERFORMANCE OPTIMIZATION: Cache API responses to avoid redundant fetches
|
|
154
|
+
// ============================================================================
|
|
155
|
+
const API_CACHE = {
|
|
156
|
+
statsData: null,
|
|
157
|
+
statsDataTimestamp: null,
|
|
158
|
+
CACHE_DURATION: 5000, // 5 seconds cache
|
|
159
|
+
|
|
160
|
+
setStatsData(data) {
|
|
161
|
+
this.statsData = data;
|
|
162
|
+
this.statsDataTimestamp = Date.now();
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
getStatsData() {
|
|
166
|
+
if (!this.statsData || !this.statsDataTimestamp) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
// Check if cache is still valid
|
|
170
|
+
if (Date.now() - this.statsDataTimestamp > this.CACHE_DURATION) {
|
|
171
|
+
this.statsData = null;
|
|
172
|
+
this.statsDataTimestamp = null;
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return this.statsData;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
clearStatsData() {
|
|
179
|
+
this.statsData = null;
|
|
180
|
+
this.statsDataTimestamp = null;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
4
184
|
// Helper function to detect the current theme based on the app's theme system
|
|
5
185
|
function getCurrentTheme() {
|
|
6
186
|
const dataTheme = document.documentElement.getAttribute('data-theme');
|
|
@@ -21,6 +201,8 @@ function getThemeTextColor() {
|
|
|
21
201
|
}
|
|
22
202
|
|
|
23
203
|
document.addEventListener('DOMContentLoaded', function () {
|
|
204
|
+
// Initialize DOM cache
|
|
205
|
+
DOM_CACHE.init();
|
|
24
206
|
|
|
25
207
|
// Custom streak calculation function for activity heatmap (includes average daily time)
|
|
26
208
|
function calculateActivityStreaks(grid, yearData, allLinesForYear = []) {
|
|
@@ -84,54 +266,82 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
84
266
|
// Calculate average daily time for this year
|
|
85
267
|
let avgDailyTime = "-";
|
|
86
268
|
if (allLinesForYear && allLinesForYear.length > 0) {
|
|
87
|
-
//
|
|
88
|
-
const
|
|
89
|
-
for (const line of allLinesForYear) {
|
|
90
|
-
const ts = parseFloat(line.timestamp);
|
|
91
|
-
if (isNaN(ts)) continue;
|
|
92
|
-
const dateObj = new Date(ts * 1000);
|
|
93
|
-
const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
|
|
94
|
-
if (!dailyTimestamps[dateStr]) {
|
|
95
|
-
dailyTimestamps[dateStr] = [];
|
|
96
|
-
}
|
|
97
|
-
dailyTimestamps[dateStr].push(parseFloat(line.timestamp));
|
|
98
|
-
}
|
|
269
|
+
// Check if we have pre-calculated reading time from rollup data
|
|
270
|
+
const hasReadingTimeData = allLinesForYear.some(line => line.reading_time_seconds !== undefined);
|
|
99
271
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
for (let i = 1; i < timestamps.length; i++) {
|
|
111
|
-
const gap = timestamps[i] - timestamps[i-1];
|
|
112
|
-
dayReadingTime += Math.min(gap, afkTimerSeconds);
|
|
272
|
+
if (hasReadingTimeData) {
|
|
273
|
+
// Use pre-calculated reading time from rollup data (FAST!)
|
|
274
|
+
let totalHours = 0;
|
|
275
|
+
let activeDays = 0;
|
|
276
|
+
|
|
277
|
+
for (const line of allLinesForYear) {
|
|
278
|
+
if (line.reading_time_seconds !== undefined && line.reading_time_seconds > 0) {
|
|
279
|
+
totalHours += line.reading_time_seconds / 3600;
|
|
280
|
+
activeDays++;
|
|
113
281
|
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (activeDays > 0) {
|
|
285
|
+
const avgHours = totalHours / activeDays;
|
|
286
|
+
if (avgHours < 1) {
|
|
287
|
+
const minutes = Math.round(avgHours * 60);
|
|
288
|
+
avgDailyTime = `${minutes}m`;
|
|
289
|
+
} else {
|
|
290
|
+
const hours = Math.floor(avgHours);
|
|
291
|
+
const minutes = Math.round((avgHours - hours) * 60);
|
|
292
|
+
avgDailyTime = minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// Fallback: Calculate from individual timestamps (for today's data)
|
|
297
|
+
const dailyTimestamps = {};
|
|
298
|
+
for (const line of allLinesForYear) {
|
|
299
|
+
const ts = parseFloat(line.timestamp);
|
|
300
|
+
if (isNaN(ts)) continue;
|
|
301
|
+
const dateObj = new Date(ts * 1000);
|
|
302
|
+
const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
|
|
303
|
+
if (!dailyTimestamps[dateStr]) {
|
|
304
|
+
dailyTimestamps[dateStr] = [];
|
|
305
|
+
}
|
|
306
|
+
dailyTimestamps[dateStr].push(parseFloat(line.timestamp));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Calculate reading time for each day with activity
|
|
310
|
+
let totalHours = 0;
|
|
311
|
+
let activeDays = 0;
|
|
312
|
+
let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
313
|
+
|
|
314
|
+
for (const [dateStr, timestamps] of Object.entries(dailyTimestamps)) {
|
|
315
|
+
if (timestamps.length >= 2) {
|
|
316
|
+
timestamps.sort((a, b) => a - b);
|
|
317
|
+
let dayReadingTime = 0;
|
|
318
|
+
|
|
319
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
320
|
+
const gap = timestamps[i] - timestamps[i-1];
|
|
321
|
+
dayReadingTime += Math.min(gap, afkTimerSeconds);
|
|
322
|
+
}
|
|
114
323
|
|
|
115
|
-
|
|
116
|
-
|
|
324
|
+
if (dayReadingTime > 0) {
|
|
325
|
+
totalHours += dayReadingTime / 3600;
|
|
326
|
+
activeDays++;
|
|
327
|
+
}
|
|
328
|
+
} else if (timestamps.length === 1) {
|
|
329
|
+
// Single timestamp - count as minimal activity (1 second)
|
|
330
|
+
totalHours += 1 / 3600;
|
|
117
331
|
activeDays++;
|
|
118
332
|
}
|
|
119
|
-
} else if (timestamps.length === 1) {
|
|
120
|
-
// Single timestamp - count as minimal activity (1 second)
|
|
121
|
-
totalHours += 1 / 3600;
|
|
122
|
-
activeDays++;
|
|
123
333
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
334
|
+
|
|
335
|
+
if (activeDays > 0) {
|
|
336
|
+
const avgHours = totalHours / activeDays;
|
|
337
|
+
if (avgHours < 1) {
|
|
338
|
+
const minutes = Math.round(avgHours * 60);
|
|
339
|
+
avgDailyTime = `${minutes}m`;
|
|
340
|
+
} else {
|
|
341
|
+
const hours = Math.floor(avgHours);
|
|
342
|
+
const minutes = Math.round((avgHours - hours) * 60);
|
|
343
|
+
avgDailyTime = minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
344
|
+
}
|
|
135
345
|
}
|
|
136
346
|
}
|
|
137
347
|
}
|
|
@@ -161,20 +371,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
161
371
|
});
|
|
162
372
|
|
|
163
373
|
// Function to load stats data with optional year filter
|
|
164
|
-
function loadStatsData(
|
|
374
|
+
function loadStatsData() {
|
|
165
375
|
let url = '/api/stats';
|
|
166
|
-
const params = new URLSearchParams();
|
|
167
|
-
|
|
168
|
-
if (start_timestamp && end_timestamp) {
|
|
169
|
-
// Only filter by timestamps
|
|
170
|
-
params.append('start', start_timestamp);
|
|
171
|
-
params.append('end', end_timestamp);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const queryString = params.toString();
|
|
175
|
-
if (queryString) {
|
|
176
|
-
url += `?${queryString}`;
|
|
177
|
-
}
|
|
178
376
|
|
|
179
377
|
return fetch(url)
|
|
180
378
|
.then(response => response.json())
|
|
@@ -201,7 +399,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
201
399
|
}
|
|
202
400
|
|
|
203
401
|
// Load dashboard data
|
|
204
|
-
loadDashboardData(data
|
|
402
|
+
loadDashboardData(data);
|
|
205
403
|
|
|
206
404
|
// Load goal progress chart (always refresh)
|
|
207
405
|
if (typeof loadGoalProgress === 'function') {
|
|
@@ -404,7 +602,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
404
602
|
// Calculate current progress
|
|
405
603
|
const currentHours = allGamesStats.total_time_hours || 0;
|
|
406
604
|
const currentCharacters = allGamesStats.total_characters || 0;
|
|
407
|
-
const currentGames = allGamesStats.
|
|
605
|
+
const currentGames = allGamesStats.completed_games || 0;
|
|
408
606
|
|
|
409
607
|
// Calculate 90-day averages for projections
|
|
410
608
|
const dailyHoursAvg = calculate90DayAverage(allLinesData, 'hours');
|
|
@@ -486,95 +684,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
486
684
|
}
|
|
487
685
|
}
|
|
488
686
|
|
|
489
|
-
// ================================
|
|
490
|
-
// Utility to convert date strings to Unix timestamps
|
|
491
|
-
// Returns start of day for startDate and end of day for endDate
|
|
492
|
-
// ================================
|
|
493
|
-
function getUnixTimestamps(startDate, endDate) {
|
|
494
|
-
const start = new Date(startDate + 'T00:00:00');
|
|
495
|
-
const startTimestamp = Math.floor(start.getTime() / 1000); // convert ms to s
|
|
496
|
-
|
|
497
|
-
const end = new Date(endDate + 'T23:59:59.999');
|
|
498
|
-
const endTimestamp = Math.floor(end.getTime() / 1000); // convert ms to s
|
|
499
|
-
|
|
500
|
-
return { startTimestamp, endTimestamp };
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// ================================
|
|
504
|
-
// Initialize date inputs with sessionStorage or fetch initial values
|
|
505
|
-
// Dispatches "datesSet" event once dates are set
|
|
506
|
-
// ================================
|
|
507
|
-
function initializeDates() {
|
|
508
|
-
const fromDateInput = document.getElementById('fromDate');
|
|
509
|
-
const toDateInput = document.getElementById('toDate');
|
|
510
|
-
|
|
511
|
-
const fromDate = sessionStorage.getItem("fromDate");
|
|
512
|
-
const toDate = sessionStorage.getItem("toDate");
|
|
513
|
-
|
|
514
|
-
if (!(fromDate && toDate)) {
|
|
515
|
-
fetch('/api/stats')
|
|
516
|
-
.then(response => response.json())
|
|
517
|
-
.then(response_json => {
|
|
518
|
-
// Get first date from API
|
|
519
|
-
const firstDate = response_json.allGamesStats.first_date;
|
|
520
|
-
fromDateInput.value = firstDate;
|
|
521
|
-
|
|
522
|
-
// Get today's date
|
|
523
|
-
const today = new Date();
|
|
524
|
-
const toDate = today.toLocaleDateString('en-CA');
|
|
525
|
-
toDateInput.value = toDate;
|
|
526
|
-
|
|
527
|
-
// Save in sessionStorage
|
|
528
|
-
sessionStorage.setItem("fromDate", firstDate);
|
|
529
|
-
sessionStorage.setItem("toDate", toDate);
|
|
530
|
-
|
|
531
|
-
document.dispatchEvent(new Event("datesSet"));
|
|
532
|
-
});
|
|
533
|
-
} else {
|
|
534
|
-
// If values already in sessionStorage, set inputs from there
|
|
535
|
-
fromDateInput.value = fromDate;
|
|
536
|
-
toDateInput.value = toDate;
|
|
537
|
-
|
|
538
|
-
document.dispatchEvent(new Event("datesSet"));
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const fromDateInput = document.getElementById('fromDate');
|
|
543
|
-
const toDateInput = document.getElementById('toDate');
|
|
544
|
-
const popup = document.getElementById('dateErrorPopup');
|
|
545
|
-
const closePopupBtn = document.getElementById('closePopupBtn');
|
|
546
|
-
|
|
547
|
-
document.addEventListener("datesSet", () => {
|
|
548
|
-
const fromDate = sessionStorage.getItem("fromDate");
|
|
549
|
-
const toDate = sessionStorage.getItem("toDate");
|
|
550
|
-
const { startTimestamp, endTimestamp } = getUnixTimestamps(fromDate, toDate);
|
|
551
|
-
|
|
552
|
-
loadStatsData(startTimestamp, endTimestamp);
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
function handleDateChange() {
|
|
557
|
-
const fromDateStr = fromDateInput.value;
|
|
558
|
-
const toDateStr = toDateInput.value;
|
|
559
|
-
|
|
560
|
-
sessionStorage.setItem("fromDate", fromDateStr);
|
|
561
|
-
sessionStorage.setItem("toDate", toDateStr);
|
|
562
|
-
|
|
563
|
-
// Validate date order
|
|
564
|
-
if (fromDateStr && toDateStr && new Date(fromDateStr) > new Date(toDateStr)) {
|
|
565
|
-
popup.classList.remove("hidden");
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const { startTimestamp, endTimestamp } = getUnixTimestamps(fromDateStr, toDateStr);
|
|
570
|
-
|
|
571
|
-
loadStatsData(startTimestamp, endTimestamp);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Attach listeners to both date inputs
|
|
575
|
-
fromDateInput.addEventListener("change", handleDateChange);
|
|
576
|
-
toDateInput.addEventListener("change", handleDateChange);
|
|
577
|
-
|
|
578
687
|
// Session navigation button handlers
|
|
579
688
|
const prevSessionBtn = document.querySelector('.prev-session-btn');
|
|
580
689
|
const nextSessionBtn = document.querySelector('.next-session-btn');
|
|
@@ -582,12 +691,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
582
691
|
|
|
583
692
|
function updateSessionNavigationButtons() {
|
|
584
693
|
if (!window.todaySessionDetails || window.todaySessionDetails.length === 0) {
|
|
694
|
+
// Keep buttons visible but disabled when no sessions
|
|
585
695
|
prevSessionBtn.disabled = true;
|
|
586
696
|
nextSessionBtn.disabled = true;
|
|
697
|
+
deleteSessionBtn.disabled = true;
|
|
587
698
|
return;
|
|
588
699
|
}
|
|
700
|
+
// Enable/disable based on navigation state
|
|
589
701
|
prevSessionBtn.disabled = window.currentSessionIndex <= 0;
|
|
590
702
|
nextSessionBtn.disabled = window.currentSessionIndex >= window.todaySessionDetails.length - 1;
|
|
703
|
+
deleteSessionBtn.disabled = false;
|
|
591
704
|
}
|
|
592
705
|
|
|
593
706
|
function showSessionAtIndex(index) {
|
|
@@ -646,29 +759,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
646
759
|
const sessionToDelete = window.todaySessionDetails[idx];
|
|
647
760
|
if (!sessionToDelete) return;
|
|
648
761
|
|
|
649
|
-
// Confirm deletion
|
|
650
|
-
const
|
|
651
|
-
if (!
|
|
652
|
-
const confirm2 = confirm("Are you REALLY sure? This cannot be undone.");
|
|
653
|
-
if (!confirm2) return;
|
|
654
|
-
const confirm3 = confirm("Final warning: Delete this session permanently?");
|
|
655
|
-
if (!confirm3) return;
|
|
762
|
+
// Confirm deletion with clear warning
|
|
763
|
+
const confirmMsg = `All session data will be deleted.\n\nSession: ${new Date(sessionToDelete.startTime * 1000).toLocaleString()}\nLines: ${sessionToDelete.lines.length}\n\nThis action cannot be undone. Continue?`;
|
|
764
|
+
if (!confirm(confirmMsg)) return;
|
|
656
765
|
|
|
657
766
|
// Call the delete function
|
|
658
767
|
deleteSession(sessionToDelete);
|
|
659
768
|
});
|
|
660
769
|
|
|
661
|
-
|
|
662
|
-
document.addEventListener('datesSet', () => {
|
|
663
|
-
setTimeout(updateSessionNavigationButtons, 1200);
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
initializeDates();
|
|
667
|
-
|
|
668
|
-
// Popup close button
|
|
669
|
-
closePopupBtn.addEventListener("click", () => {
|
|
670
|
-
popup.classList.add("hidden");
|
|
671
|
-
});
|
|
770
|
+
loadStatsData();
|
|
672
771
|
|
|
673
772
|
// Function to update goal progress using existing stats data
|
|
674
773
|
async function updateGoalProgressWithData(statsData) {
|
|
@@ -706,10 +805,119 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
706
805
|
loadGoalProgress();
|
|
707
806
|
}, 1000);
|
|
708
807
|
|
|
808
|
+
// Function to update progress timeline with start and estimated end dates
|
|
809
|
+
function updateProgressTimeline(stats) {
|
|
810
|
+
const startDateEl = document.getElementById('gameStartDate');
|
|
811
|
+
const endDateEl = document.getElementById('gameEstimatedEndDate');
|
|
812
|
+
|
|
813
|
+
// Set start date
|
|
814
|
+
if (stats.first_date) {
|
|
815
|
+
startDateEl.textContent = stats.first_date;
|
|
816
|
+
} else {
|
|
817
|
+
startDateEl.textContent = '-';
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Calculate and set estimated end date
|
|
821
|
+
if (!stats.game_character_count || stats.game_character_count <= 0 ||
|
|
822
|
+
!stats.total_characters || stats.total_characters <= 0 ||
|
|
823
|
+
!stats.reading_speed || stats.reading_speed <= 0) {
|
|
824
|
+
endDateEl.textContent = '-';
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const charsRead = stats.total_characters;
|
|
829
|
+
const totalChars = stats.game_character_count;
|
|
830
|
+
const charsRemaining = Math.max(0, totalChars - charsRead);
|
|
831
|
+
|
|
832
|
+
if (charsRemaining === 0) {
|
|
833
|
+
endDateEl.textContent = 'Completed! 🎉';
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Calculate daily character progress
|
|
838
|
+
let dailyCharProgress = 0;
|
|
839
|
+
if (stats.daily_activity && Object.keys(stats.daily_activity).length > 0) {
|
|
840
|
+
const activityDays = Object.values(stats.daily_activity).filter(chars => chars > 0);
|
|
841
|
+
if (activityDays.length > 0) {
|
|
842
|
+
dailyCharProgress = activityDays.reduce((sum, chars) => sum + chars, 0) / activityDays.length;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (dailyCharProgress === 0) {
|
|
847
|
+
dailyCharProgress = stats.reading_speed; // Fallback: assume 1 hour per day
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const daysUntilCompletion = Math.ceil(charsRemaining / dailyCharProgress);
|
|
851
|
+
const today = new Date();
|
|
852
|
+
const completionDate = new Date(today);
|
|
853
|
+
completionDate.setDate(completionDate.getDate() + daysUntilCompletion);
|
|
854
|
+
|
|
855
|
+
// Format as YYYY-MM-DD (estimated)
|
|
856
|
+
const year = completionDate.getFullYear();
|
|
857
|
+
const month = String(completionDate.getMonth() + 1).padStart(2, '0');
|
|
858
|
+
const day = String(completionDate.getDate()).padStart(2, '0');
|
|
859
|
+
endDateEl.textContent = `${year}-${month}-${day} (estimated)`;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Function to update estimated time left stat
|
|
863
|
+
function updateEstimatedTimeLeft(stats) {
|
|
864
|
+
const estimatedTimeLeftEl = document.getElementById('currentEstimatedTimeLeft');
|
|
865
|
+
const estimatedTimeLeftBox = estimatedTimeLeftEl.closest('.dashboard-stat-item');
|
|
866
|
+
|
|
867
|
+
if (!stats.game_character_count || stats.game_character_count <= 0 ||
|
|
868
|
+
!stats.total_characters || stats.total_characters <= 0 ||
|
|
869
|
+
!stats.reading_speed || stats.reading_speed <= 0) {
|
|
870
|
+
// Hide the entire stat box when we can't calculate estimated time
|
|
871
|
+
if (estimatedTimeLeftBox) {
|
|
872
|
+
estimatedTimeLeftBox.style.display = 'none';
|
|
873
|
+
}
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Show the stat box if it was hidden
|
|
878
|
+
if (estimatedTimeLeftBox) {
|
|
879
|
+
estimatedTimeLeftBox.style.display = '';
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const charsRead = stats.total_characters;
|
|
883
|
+
const totalChars = stats.game_character_count;
|
|
884
|
+
const charsRemaining = Math.max(0, totalChars - charsRead);
|
|
885
|
+
|
|
886
|
+
if (charsRemaining === 0) {
|
|
887
|
+
estimatedTimeLeftEl.textContent = '0h';
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const readingSpeed = stats.reading_speed;
|
|
892
|
+
const hoursRemaining = charsRemaining / readingSpeed;
|
|
893
|
+
|
|
894
|
+
// Format hours remaining
|
|
895
|
+
let hoursText;
|
|
896
|
+
if (hoursRemaining < 1) {
|
|
897
|
+
const minutes = Math.round(hoursRemaining * 60);
|
|
898
|
+
hoursText = `${minutes}m`;
|
|
899
|
+
} else if (hoursRemaining < 24) {
|
|
900
|
+
const hours = Math.floor(hoursRemaining);
|
|
901
|
+
const minutes = Math.round((hoursRemaining - hours) * 60);
|
|
902
|
+
hoursText = minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
903
|
+
} else if (hoursRemaining < 168) {
|
|
904
|
+
const days = Math.floor(hoursRemaining / 24);
|
|
905
|
+
const hours = Math.round(hoursRemaining % 24);
|
|
906
|
+
hoursText = hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
907
|
+
} else {
|
|
908
|
+
const days = Math.floor(hoursRemaining / 24);
|
|
909
|
+
hoursText = `${days}d`;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
estimatedTimeLeftEl.textContent = hoursText;
|
|
913
|
+
}
|
|
914
|
+
|
|
709
915
|
// Make functions globally available
|
|
710
916
|
window.createHeatmap = createHeatmap;
|
|
711
917
|
window.loadStatsData = loadStatsData;
|
|
712
918
|
window.loadGoalProgress = loadGoalProgress;
|
|
919
|
+
window.updateProgressTimeline = updateProgressTimeline;
|
|
920
|
+
window.updateEstimatedTimeLeft = updateEstimatedTimeLeft;
|
|
713
921
|
|
|
714
922
|
function updateCurrentSessionOverview(sessionDetails, index = sessionDetails.length - 1) {
|
|
715
923
|
window.currentSessionIndex = index; // Store globally for potential future use
|
|
@@ -718,20 +926,21 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
718
926
|
const lastSession = sessionDetails && sessionDetails.length > 0 ? sessionDetails[index] : null;
|
|
719
927
|
|
|
720
928
|
if (!lastSession) {
|
|
721
|
-
// No current session
|
|
722
|
-
document.getElementById('
|
|
723
|
-
document.getElementById('
|
|
724
|
-
document.getElementById('
|
|
725
|
-
document.getElementById('
|
|
726
|
-
document.getElementById('
|
|
727
|
-
|
|
929
|
+
// No current session - clear session stats
|
|
930
|
+
const sessionHoursEl = document.getElementById('currentSessionTotalHours');
|
|
931
|
+
const sessionCharsEl = document.getElementById('currentSessionTotalChars');
|
|
932
|
+
const sessionStartEl = document.getElementById('currentSessionStartTime');
|
|
933
|
+
const sessionEndEl = document.getElementById('currentSessionEndTime');
|
|
934
|
+
const sessionSpeedEl = document.getElementById('currentSessionCharsPerHour');
|
|
935
|
+
|
|
936
|
+
if (sessionHoursEl) sessionHoursEl.textContent = '-';
|
|
937
|
+
if (sessionCharsEl) sessionCharsEl.textContent = '-';
|
|
938
|
+
if (sessionStartEl) sessionStartEl.textContent = '-';
|
|
939
|
+
if (sessionEndEl) sessionEndEl.textContent = '-';
|
|
940
|
+
if (sessionSpeedEl) sessionSpeedEl.textContent = '-';
|
|
728
941
|
return;
|
|
729
942
|
}
|
|
730
943
|
|
|
731
|
-
// Update session status (show game name if available)
|
|
732
|
-
const statusText = lastSession.gameName ? `Playing: ${lastSession.gameName}` : 'Active session';
|
|
733
|
-
document.getElementById('currentSessionStatus').textContent = statusText;
|
|
734
|
-
|
|
735
944
|
// Format session duration
|
|
736
945
|
let hoursDisplay = '-';
|
|
737
946
|
const sessionHours = lastSession.totalSeconds / 3600;
|
|
@@ -756,170 +965,245 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
756
965
|
|
|
757
966
|
// Update the DOM elements
|
|
758
967
|
document.getElementById('currentSessionTotalHours').textContent = hoursDisplay;
|
|
759
|
-
|
|
968
|
+
|
|
969
|
+
// Update Session Chars with native tooltip
|
|
970
|
+
const sessionCharsEl = document.getElementById('currentSessionTotalChars');
|
|
971
|
+
const sessionCharsBox = sessionCharsEl.closest('.dashboard-stat-item');
|
|
972
|
+
sessionCharsEl.textContent = Math.round(lastSession.totalChars).toLocaleString();
|
|
973
|
+
if (sessionCharsBox) {
|
|
974
|
+
sessionCharsBox.setAttribute('title', `${lastSession.totalChars.toLocaleString(undefined, {maximumFractionDigits: 2})} characters`);
|
|
975
|
+
}
|
|
976
|
+
|
|
760
977
|
document.getElementById('currentSessionStartTime').textContent = startTimeDisplay;
|
|
761
978
|
document.getElementById('currentSessionEndTime').textContent = endTimeDisplay;
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const pad = n => n.toString().padStart(2, '0');
|
|
771
|
-
const todayStr = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
|
|
772
|
-
const afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
773
|
-
document.getElementById('todayDate').textContent = todayStr;
|
|
774
|
-
|
|
775
|
-
// Filter lines for today
|
|
776
|
-
const todayLines = (allLinesData || []).filter(line => {
|
|
777
|
-
if (!line.timestamp) return false;
|
|
778
|
-
const ts = parseFloat(line.timestamp);
|
|
779
|
-
if (isNaN(ts)) return false;
|
|
780
|
-
const dateObj = new Date(ts * 1000);
|
|
781
|
-
const lineDate = `${dateObj.getFullYear()}-${pad(dateObj.getMonth() + 1)}-${pad(dateObj.getDate())}`;
|
|
782
|
-
return lineDate === todayStr;
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
// Calculate total characters read today (only valid numbers)
|
|
786
|
-
const totalChars = todayLines.reduce((sum, line) => {
|
|
787
|
-
const chars = Number(line.characters);
|
|
788
|
-
return sum + (isNaN(chars) ? 0 : chars);
|
|
789
|
-
}, 0);
|
|
790
|
-
|
|
791
|
-
// Calculate sessions (count gaps > session threshold as new sessions)
|
|
792
|
-
let sessions = 0;
|
|
793
|
-
let sessionGap = window.statsConfig ? window.statsConfig.sessionGapSeconds : 3600;
|
|
794
|
-
let minimumSessionLength = 300; // 5 minutes minimum session length
|
|
795
|
-
let sessionDetails = [];
|
|
796
|
-
if (todayLines.length > 0) {
|
|
797
|
-
// Sort lines by timestamp
|
|
798
|
-
const sortedLines = todayLines.slice().sort((a, b) => parseFloat(a.timestamp) - parseFloat(b.timestamp));
|
|
799
|
-
let currentSession = null;
|
|
800
|
-
let lastTimestamp = null;
|
|
801
|
-
let lastGameName = null;
|
|
802
|
-
|
|
803
|
-
for (let i = 0; i < sortedLines.length; i++) {
|
|
804
|
-
const line = sortedLines[i];
|
|
805
|
-
const ts = parseFloat(line.timestamp);
|
|
806
|
-
const gameName = line.game_name || '';
|
|
807
|
-
const chars = Number(line.characters) || 0;
|
|
808
|
-
|
|
809
|
-
// Determine if new session: gap or new game
|
|
810
|
-
const isNewSession =
|
|
811
|
-
(lastTimestamp !== null && ts - lastTimestamp > sessionGap) ||
|
|
812
|
-
(lastGameName !== null && gameName !== lastGameName);
|
|
979
|
+
|
|
980
|
+
// Update Session Chars/Hour with native tooltip
|
|
981
|
+
const sessionSpeedEl = document.getElementById('currentSessionCharsPerHour');
|
|
982
|
+
const sessionSpeedBox = sessionSpeedEl.closest('.dashboard-stat-item');
|
|
983
|
+
sessionSpeedEl.textContent = lastSession.charsPerHour > 0 ? Math.round(lastSession.charsPerHour).toLocaleString() : '-';
|
|
984
|
+
if (sessionSpeedBox && lastSession.charsPerHour > 0) {
|
|
985
|
+
sessionSpeedBox.setAttribute('title', `${lastSession.charsPerHour.toLocaleString(undefined, {maximumFractionDigits: 2})} chars/hour`);
|
|
986
|
+
}
|
|
813
987
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
// Calculate read speed for session
|
|
818
|
-
if (currentSession.totalSeconds > 0) {
|
|
819
|
-
currentSession.readSpeed = Math.round(currentSession.totalChars / (currentSession.totalSeconds / 3600));
|
|
820
|
-
} else {
|
|
821
|
-
currentSession.readSpeed = '-';
|
|
822
|
-
}
|
|
823
|
-
// Only add session if it meets minimum length requirement
|
|
824
|
-
if (currentSession.totalSeconds >= minimumSessionLength) {
|
|
825
|
-
sessionDetails.push(currentSession);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
// Start new session
|
|
829
|
-
currentSession = {
|
|
830
|
-
startTime: ts,
|
|
831
|
-
endTime: ts,
|
|
832
|
-
gameName: gameName,
|
|
833
|
-
totalChars: chars,
|
|
834
|
-
totalSeconds: 0,
|
|
835
|
-
lines: [line]
|
|
836
|
-
};
|
|
837
|
-
} else {
|
|
838
|
-
// Continue current session
|
|
839
|
-
currentSession.endTime = ts + afkTimerSeconds;
|
|
840
|
-
currentSession.totalChars += chars;
|
|
841
|
-
currentSession.lines.push(line);
|
|
842
|
-
if (lastTimestamp !== null) {
|
|
843
|
-
currentSession.totalSeconds += Math.min(ts - lastTimestamp, afkTimerSeconds);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
988
|
+
// Render game metadata if available
|
|
989
|
+
renderSessionGameMetadata(lastSession);
|
|
990
|
+
}
|
|
846
991
|
|
|
847
|
-
|
|
848
|
-
|
|
992
|
+
function renderSessionGameMetadata(session) {
|
|
993
|
+
const gameContentGrid = document.getElementById('gameContentGrid');
|
|
994
|
+
const noGameDataMessage = document.getElementById('noGameDataMessage');
|
|
995
|
+
const noGameDataTitle = document.getElementById('noGameDataTitle');
|
|
996
|
+
const gameMetadata = session.gameMetadata;
|
|
997
|
+
|
|
998
|
+
// Check if we have meaningful game data (image or description)
|
|
999
|
+
const hasImage = gameMetadata && gameMetadata.image && gameMetadata.image.trim();
|
|
1000
|
+
const hasDescription = gameMetadata && gameMetadata.description && gameMetadata.description.trim();
|
|
1001
|
+
const hasManualOverrides = !!(gameMetadata && gameMetadata.manual_overrides && gameMetadata.manual_overrides.length > 0);
|
|
1002
|
+
|
|
1003
|
+
// Show message if: no metadata OR (no image AND no description AND no manual overrides)
|
|
1004
|
+
if (!gameMetadata || (!hasImage && !hasDescription && !hasManualOverrides)) {
|
|
1005
|
+
if (gameContentGrid) {
|
|
1006
|
+
gameContentGrid.style.display = 'none';
|
|
1007
|
+
}
|
|
1008
|
+
if (noGameDataMessage) {
|
|
1009
|
+
// Set the game title in the message
|
|
1010
|
+
if (noGameDataTitle && session.gameName) {
|
|
1011
|
+
noGameDataTitle.textContent = session.gameName;
|
|
849
1012
|
}
|
|
1013
|
+
noGameDataMessage.style.display = 'block';
|
|
1014
|
+
}
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
850
1017
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
sessionDetails.push(currentSession);
|
|
859
|
-
}
|
|
1018
|
+
// Hide the message and show the game content grid
|
|
1019
|
+
if (noGameDataMessage) {
|
|
1020
|
+
noGameDataMessage.style.display = 'none';
|
|
1021
|
+
}
|
|
1022
|
+
if (gameContentGrid) {
|
|
1023
|
+
gameContentGrid.style.display = 'flex';
|
|
1024
|
+
}
|
|
860
1025
|
|
|
861
|
-
|
|
1026
|
+
// Clear existing content
|
|
1027
|
+
const gamePhotoSection = document.getElementById('gamePhotoSection');
|
|
1028
|
+
const gamePhoto = document.getElementById('gamePhoto');
|
|
1029
|
+
const gameTitleOriginal = document.getElementById('gameTitleOriginal');
|
|
1030
|
+
const gameTitleRomaji = document.getElementById('gameTitleRomaji');
|
|
1031
|
+
const gameTitleEnglish = document.getElementById('gameTitleEnglish');
|
|
1032
|
+
const gameTypeBadge = document.getElementById('gameTypeBadge');
|
|
1033
|
+
const gameDescription = document.getElementById('gameDescription');
|
|
1034
|
+
const descriptionExpandBtn = document.getElementById('descriptionExpandBtn');
|
|
1035
|
+
const gameLinksContainer = document.getElementById('gameLinksContainer');
|
|
1036
|
+
const gameLinksPills = document.getElementById('gameLinksPills');
|
|
1037
|
+
|
|
1038
|
+
// Update photo - all images are now stored as PNG base64
|
|
1039
|
+
if (gameMetadata.image && gameMetadata.image.trim()) {
|
|
1040
|
+
let imageSrc = gameMetadata.image.trim();
|
|
1041
|
+
|
|
1042
|
+
// Handle different image formats
|
|
1043
|
+
if (imageSrc.startsWith('data:image')) {
|
|
1044
|
+
// Already has data URI prefix
|
|
1045
|
+
gamePhoto.src = imageSrc;
|
|
1046
|
+
} else if (imageSrc.startsWith('http')) {
|
|
1047
|
+
// External URL
|
|
1048
|
+
gamePhoto.src = imageSrc;
|
|
862
1049
|
} else {
|
|
863
|
-
|
|
864
|
-
|
|
1050
|
+
// Raw base64 data - add PNG data URI prefix (all uploads are converted to PNG)
|
|
1051
|
+
gamePhoto.src = `data:image/png;base64,${imageSrc}`;
|
|
865
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
gamePhotoSection.style.display = 'block';
|
|
1055
|
+
gamePhoto.style.display = 'block';
|
|
1056
|
+
} else {
|
|
1057
|
+
gamePhotoSection.style.display = 'none';
|
|
1058
|
+
}
|
|
866
1059
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1060
|
+
// Update titles
|
|
1061
|
+
if (gameMetadata.title_original) {
|
|
1062
|
+
gameTitleOriginal.textContent = gameMetadata.title_original;
|
|
1063
|
+
gameTitleOriginal.style.display = 'block';
|
|
1064
|
+
} else {
|
|
1065
|
+
gameTitleOriginal.style.display = 'none';
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (gameMetadata.title_romaji) {
|
|
1069
|
+
gameTitleRomaji.textContent = gameMetadata.title_romaji;
|
|
1070
|
+
gameTitleRomaji.style.display = 'block';
|
|
1071
|
+
} else {
|
|
1072
|
+
gameTitleRomaji.style.display = 'none';
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (gameMetadata.title_english) {
|
|
1076
|
+
gameTitleEnglish.textContent = gameMetadata.title_english;
|
|
1077
|
+
gameTitleEnglish.style.display = 'block';
|
|
1078
|
+
} else {
|
|
1079
|
+
gameTitleEnglish.style.display = 'none';
|
|
1080
|
+
}
|
|
887
1081
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1082
|
+
// Update type badge
|
|
1083
|
+
if (gameMetadata.type) {
|
|
1084
|
+
gameTypeBadge.textContent = gameMetadata.type;
|
|
1085
|
+
gameTypeBadge.style.display = 'inline-block';
|
|
1086
|
+
} else {
|
|
1087
|
+
gameTypeBadge.style.display = 'none';
|
|
1088
|
+
}
|
|
895
1089
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1090
|
+
// Update description
|
|
1091
|
+
if (gameMetadata.description) {
|
|
1092
|
+
gameDescription.textContent = gameMetadata.description;
|
|
1093
|
+
gameDescription.classList.remove('expanded');
|
|
1094
|
+
|
|
1095
|
+
// Show/hide expand button based on description length
|
|
1096
|
+
if (gameMetadata.description.length > 150) {
|
|
1097
|
+
descriptionExpandBtn.style.display = 'block';
|
|
1098
|
+
const expandText = descriptionExpandBtn.querySelector('.expand-text');
|
|
1099
|
+
const collapseText = descriptionExpandBtn.querySelector('.collapse-text');
|
|
1100
|
+
if (expandText) expandText.style.display = 'inline';
|
|
1101
|
+
if (collapseText) collapseText.style.display = 'none';
|
|
1102
|
+
} else {
|
|
1103
|
+
descriptionExpandBtn.style.display = 'none';
|
|
902
1104
|
}
|
|
1105
|
+
} else {
|
|
1106
|
+
gameDescription.textContent = '';
|
|
1107
|
+
descriptionExpandBtn.style.display = 'none';
|
|
1108
|
+
}
|
|
903
1109
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1110
|
+
// Update links
|
|
1111
|
+
if (gameMetadata.links && gameMetadata.links.length > 0) {
|
|
1112
|
+
gameLinksPills.innerHTML = '';
|
|
1113
|
+
|
|
1114
|
+
gameMetadata.links.forEach(link => {
|
|
1115
|
+
if (link.url) {
|
|
1116
|
+
const pill = document.createElement('a');
|
|
1117
|
+
pill.href = link.url;
|
|
1118
|
+
pill.target = '_blank';
|
|
1119
|
+
pill.rel = 'noopener noreferrer';
|
|
1120
|
+
pill.className = 'game-link-pill';
|
|
1121
|
+
pill.textContent = extractDomainName(link.url);
|
|
1122
|
+
gameLinksPills.appendChild(pill);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
gameLinksContainer.style.display = 'flex';
|
|
1127
|
+
} else {
|
|
1128
|
+
gameLinksContainer.style.display = 'none';
|
|
911
1129
|
}
|
|
1130
|
+
}
|
|
912
1131
|
|
|
913
|
-
|
|
914
|
-
|
|
1132
|
+
// Function to load today's stats from new API endpoint
|
|
1133
|
+
function loadTodayStats() {
|
|
1134
|
+
fetch('/api/today-stats')
|
|
1135
|
+
.then(response => response.json())
|
|
1136
|
+
.then(data => {
|
|
1137
|
+
// Update today's total hours
|
|
1138
|
+
const totalHours = data.todayTotalHours || 0;
|
|
1139
|
+
let hoursDisplay = '-';
|
|
1140
|
+
if (totalHours > 0) {
|
|
1141
|
+
const h = Math.floor(totalHours);
|
|
1142
|
+
const m = Math.round((totalHours - h) * 60);
|
|
1143
|
+
hoursDisplay = h > 0 ? `${h}h${m > 0 ? ' ' + m + 'm' : ''}` : `${m}m`;
|
|
1144
|
+
}
|
|
1145
|
+
document.getElementById('todayTotalHours').textContent = hoursDisplay;
|
|
1146
|
+
|
|
1147
|
+
// Update today's total characters with native tooltip
|
|
1148
|
+
const todayCharsEl = document.getElementById('todayTotalChars');
|
|
1149
|
+
const todayCharsBox = todayCharsEl.closest('.dashboard-stat-item');
|
|
1150
|
+
todayCharsEl.textContent = data.todayTotalChars.toLocaleString();
|
|
1151
|
+
if (todayCharsBox) {
|
|
1152
|
+
todayCharsBox.setAttribute('title', `${data.todayTotalChars.toLocaleString(undefined, {maximumFractionDigits: 2})} characters`);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Update today's sessions count
|
|
1156
|
+
document.getElementById('todaySessions').textContent = data.todaySessions || 0;
|
|
1157
|
+
|
|
1158
|
+
// Update today's chars/hour with native tooltip
|
|
1159
|
+
const todaySpeedEl = document.getElementById('todayCharsPerHour');
|
|
1160
|
+
const todaySpeedBox = todaySpeedEl.closest('.dashboard-stat-item');
|
|
1161
|
+
todaySpeedEl.textContent = data.todayCharsPerHour > 0 ? data.todayCharsPerHour.toLocaleString() : '-';
|
|
1162
|
+
if (todaySpeedBox && data.todayCharsPerHour > 0) {
|
|
1163
|
+
todaySpeedBox.setAttribute('title', `${data.todayCharsPerHour.toLocaleString(undefined, {maximumFractionDigits: 2})} chars/hour`);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Store sessions globally for navigation
|
|
1167
|
+
window.todaySessionDetails = data.sessions || [];
|
|
1168
|
+
|
|
1169
|
+
// Show the latest session (most recent)
|
|
1170
|
+
if (window.todaySessionDetails.length > 0) {
|
|
1171
|
+
showSessionAtIndex(window.todaySessionDetails.length - 1);
|
|
1172
|
+
} else {
|
|
1173
|
+
// No sessions - clear session displays
|
|
1174
|
+
document.getElementById('currentSessionTotalChars').textContent = '0';
|
|
1175
|
+
document.getElementById('currentSessionCharsPerHour').textContent = '-';
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Update session navigation buttons
|
|
1179
|
+
updateSessionNavigationButtons();
|
|
1180
|
+
})
|
|
1181
|
+
.catch(error => {
|
|
1182
|
+
console.error('Error fetching today\'s stats:', error);
|
|
1183
|
+
// Set default values on error
|
|
1184
|
+
document.getElementById('todayTotalHours').textContent = '-';
|
|
1185
|
+
document.getElementById('todayTotalChars').textContent = '0';
|
|
1186
|
+
document.getElementById('todaySessions').textContent = '0';
|
|
1187
|
+
document.getElementById('todayCharsPerHour').textContent = '-';
|
|
1188
|
+
document.getElementById('currentSessionTotalChars').textContent = '0';
|
|
1189
|
+
document.getElementById('currentSessionCharsPerHour').textContent = '-';
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
915
1192
|
|
|
1193
|
+
// Dashboard functionality
|
|
1194
|
+
function loadDashboardData(data = null) {
|
|
1195
|
+
function updateOverviewForEndDay(allLinesData) {
|
|
916
1196
|
const pad = n => n.toString().padStart(2, '0');
|
|
917
1197
|
|
|
918
1198
|
// Determine target date string (YYYY-MM-DD) from the end timestamp
|
|
919
|
-
const endDateObj = new Date(
|
|
1199
|
+
const endDateObj = new Date();
|
|
920
1200
|
const targetDateStr = `${endDateObj.getFullYear()}-${pad(endDateObj.getMonth() + 1)}-${pad(endDateObj.getDate())}`;
|
|
921
1201
|
const afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
922
1202
|
document.getElementById('todayDate').textContent = targetDateStr;
|
|
1203
|
+
|
|
1204
|
+
// Load today's stats from new API
|
|
1205
|
+
loadTodayStats();
|
|
1206
|
+
return; // Skip old calculation logic below
|
|
923
1207
|
|
|
924
1208
|
// Filter lines that fall on the target date
|
|
925
1209
|
const targetLines = (allLinesData || []).filter(line => {
|
|
@@ -1068,8 +1352,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1068
1352
|
updateAllGamesDashboard(data.allGamesStats);
|
|
1069
1353
|
|
|
1070
1354
|
if (data.allLinesData) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1355
|
+
updateOverviewForEndDay(data.allLinesData);
|
|
1356
|
+
}
|
|
1073
1357
|
|
|
1074
1358
|
hideDashboardLoading();
|
|
1075
1359
|
} else {
|
|
@@ -1081,9 +1365,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1081
1365
|
if (data.currentGameStats && data.allGamesStats) {
|
|
1082
1366
|
updateCurrentGameDashboard(data.currentGameStats);
|
|
1083
1367
|
updateAllGamesDashboard(data.allGamesStats);
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1368
|
+
|
|
1369
|
+
// Always fetch today's data live (don't use rollup data for today)
|
|
1370
|
+
|
|
1371
|
+
if (data.allLinesData) {
|
|
1372
|
+
updateOverviewForEndDay(data.allLinesData);
|
|
1373
|
+
}
|
|
1087
1374
|
} else {
|
|
1088
1375
|
showDashboardError();
|
|
1089
1376
|
}
|
|
@@ -1097,25 +1384,317 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1097
1384
|
}
|
|
1098
1385
|
}
|
|
1099
1386
|
|
|
1387
|
+
// Helper function to extract and format domain names from URLs
|
|
1388
|
+
function extractDomainName(url) {
|
|
1389
|
+
if (!url) return 'Link';
|
|
1390
|
+
|
|
1391
|
+
try {
|
|
1392
|
+
// Parse the URL
|
|
1393
|
+
const urlObj = new URL(url);
|
|
1394
|
+
let domain = urlObj.hostname;
|
|
1395
|
+
|
|
1396
|
+
// Remove 'www.' prefix if present
|
|
1397
|
+
domain = domain.replace(/^www\./, '');
|
|
1398
|
+
|
|
1399
|
+
// Map common domains to friendly names
|
|
1400
|
+
const domainMap = {
|
|
1401
|
+
'vndb.org': 'VNDB',
|
|
1402
|
+
'myanimelist.net': 'MAL',
|
|
1403
|
+
'anilist.co': 'AniList',
|
|
1404
|
+
'anime-planet.com': 'Anime-Planet',
|
|
1405
|
+
'kitsu.io': 'Kitsu',
|
|
1406
|
+
'anidb.net': 'AniDB',
|
|
1407
|
+
'mangaupdates.com': 'MangaUpdates',
|
|
1408
|
+
'novelupdates.com': 'NovelUpdates',
|
|
1409
|
+
'wikipedia.org': 'Wikipedia',
|
|
1410
|
+
'fandom.com': 'Fandom',
|
|
1411
|
+
'steam.com': 'Steam',
|
|
1412
|
+
'steampowered.com': 'Steam',
|
|
1413
|
+
'store.steampowered.com': 'Steam',
|
|
1414
|
+
"Itch.io": "Itch.io",
|
|
1415
|
+
'gog.com': 'GOG',
|
|
1416
|
+
'epicgames.com': 'Epic Games',
|
|
1417
|
+
'nintendo.com': 'Nintendo',
|
|
1418
|
+
'playstation.com': 'PlayStation',
|
|
1419
|
+
'xbox.com': 'Xbox',
|
|
1420
|
+
'crunchyroll.com': 'Crunchyroll',
|
|
1421
|
+
'hidive.com': 'HIDIVE',
|
|
1422
|
+
'funimation.com': 'Funimation',
|
|
1423
|
+
'animenewsnetwork.com': 'ANN',
|
|
1424
|
+
'tvdb.com': 'TheTVDB',
|
|
1425
|
+
'themoviedb.org': 'TMDB',
|
|
1426
|
+
'imdb.com': 'IMDb',
|
|
1427
|
+
'letterboxd.com': 'Letterboxd',
|
|
1428
|
+
'goodreads.com': 'Goodreads',
|
|
1429
|
+
'bookwalker.jp': 'BookWalker',
|
|
1430
|
+
'dlsite.com': 'DLsite',
|
|
1431
|
+
'jlist.com': 'J-List',
|
|
1432
|
+
'getchu.com': 'Getchu',
|
|
1433
|
+
'erogamescape.dyndns.org': 'ErogameScape',
|
|
1434
|
+
'itch.io': 'Itch.io',
|
|
1435
|
+
'gamejolt.com': 'Game Jolt',
|
|
1436
|
+
'mobygames.com': 'MobyGames',
|
|
1437
|
+
'giantbomb.com': 'GiantBomb',
|
|
1438
|
+
'howlongtobeat.com': 'HowLongToBeat',
|
|
1439
|
+
'backloggd.com': 'Backloggd',
|
|
1440
|
+
'vndb.org': 'VNDB',
|
|
1441
|
+
'mangadex.org': 'MangaDex',
|
|
1442
|
+
'animeuknews.net': 'Anime UK News',
|
|
1443
|
+
'mydramalist.com': 'MyDramaList',
|
|
1444
|
+
'metacritic.com': 'Metacritic',
|
|
1445
|
+
'opencritic.com': 'OpenCritic',
|
|
1446
|
+
'itch.io': 'Itch.io',
|
|
1447
|
+
'indiedb.com': 'IndieDB',
|
|
1448
|
+
'moddb.com': 'ModDB',
|
|
1449
|
+
'romhacking.net': 'Romhacking',
|
|
1450
|
+
'nexusmods.com': 'Nexus Mods',
|
|
1451
|
+
'archiveofourown.org': 'AO3',
|
|
1452
|
+
'fanfiction.net': 'FanFiction.net',
|
|
1453
|
+
'tumblr.com': 'Tumblr',
|
|
1454
|
+
'pixiv.net': 'Pixiv',
|
|
1455
|
+
'deviantart.com': 'DeviantArt',
|
|
1456
|
+
'booth.pm': 'BOOTH',
|
|
1457
|
+
'patreon.com': 'Patreon',
|
|
1458
|
+
'kickstarter.com': 'Kickstarter'
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
// Check if we have a friendly name for this domain
|
|
1462
|
+
if (domainMap[domain]) {
|
|
1463
|
+
return domainMap[domain];
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Otherwise, capitalize the main domain name
|
|
1467
|
+
const parts = domain.split('.');
|
|
1468
|
+
if (parts.length >= 2) {
|
|
1469
|
+
// Get the second-to-last part (e.g., 'example' from 'example.com')
|
|
1470
|
+
const mainPart = parts[parts.length - 2];
|
|
1471
|
+
return mainPart.charAt(0).toUpperCase() + mainPart.slice(1);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
return domain;
|
|
1475
|
+
} catch (e) {
|
|
1476
|
+
// If URL parsing fails, return a generic label
|
|
1477
|
+
return 'Link';
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1100
1481
|
function updateCurrentGameDashboard(stats) {
|
|
1101
1482
|
if (!stats) {
|
|
1102
1483
|
showNoDashboardData('currentGameCard', 'No current game data available');
|
|
1103
1484
|
return;
|
|
1104
1485
|
}
|
|
1105
1486
|
|
|
1106
|
-
// Update game name
|
|
1107
|
-
|
|
1487
|
+
// Update subtitle with game name only if title_original is not set
|
|
1488
|
+
// (If title_original exists, it will be shown in the game content grid instead)
|
|
1489
|
+
const currentGameNameEl = document.getElementById('currentGameName');
|
|
1490
|
+
if (stats.title_original && stats.title_original.trim()) {
|
|
1491
|
+
// Hide subtitle when we have a proper title in the game content grid
|
|
1492
|
+
currentGameNameEl.style.display = 'none';
|
|
1493
|
+
} else {
|
|
1494
|
+
// Show game name in subtitle when no title_original is available
|
|
1495
|
+
const gameName = stats.game_name || 'Unknown Game';
|
|
1496
|
+
currentGameNameEl.textContent = gameName;
|
|
1497
|
+
currentGameNameEl.style.display = 'block';
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// Handle completion button visibility and state
|
|
1501
|
+
const completionBtn = document.getElementById('gameCompletionBtn');
|
|
1502
|
+
const currentGameCard = document.getElementById('currentGameCard');
|
|
1503
|
+
|
|
1504
|
+
if (completionBtn) {
|
|
1505
|
+
const completion = stats.progress_percentage || 0;
|
|
1506
|
+
const isCompleted = stats.completed || false;
|
|
1507
|
+
const hasCharacterCount = stats.game_character_count && stats.game_character_count > 0;
|
|
1508
|
+
|
|
1509
|
+
if (isCompleted) {
|
|
1510
|
+
// Game is already completed - show completed state
|
|
1511
|
+
completionBtn.textContent = 'Completed ✓';
|
|
1512
|
+
completionBtn.disabled = true;
|
|
1513
|
+
completionBtn.classList.add('completed');
|
|
1514
|
+
completionBtn.style.display = 'inline-block';
|
|
1515
|
+
currentGameCard.classList.add('completed');
|
|
1516
|
+
} else if (!hasCharacterCount || completion >= 90) {
|
|
1517
|
+
// Show button if: no character count set OR game is ≥90% complete
|
|
1518
|
+
completionBtn.textContent = 'Mark as completed?';
|
|
1519
|
+
completionBtn.disabled = false;
|
|
1520
|
+
completionBtn.classList.remove('completed');
|
|
1521
|
+
completionBtn.style.display = 'inline-block';
|
|
1522
|
+
currentGameCard.classList.remove('completed');
|
|
1523
|
+
} else {
|
|
1524
|
+
// Game has character count and is <90% complete - hide button
|
|
1525
|
+
completionBtn.style.display = 'none';
|
|
1526
|
+
currentGameCard.classList.remove('completed');
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1108
1529
|
|
|
1109
|
-
//
|
|
1110
|
-
document.getElementById('
|
|
1530
|
+
// Check if we have meaningful game data
|
|
1531
|
+
const gameContentGrid = document.getElementById('gameContentGrid');
|
|
1532
|
+
const noGameDataMessage = document.getElementById('noGameDataMessage');
|
|
1533
|
+
const gamePhotoSection = document.getElementById('gamePhotoSection');
|
|
1534
|
+
const gamePhoto = document.getElementById('gamePhoto');
|
|
1535
|
+
|
|
1536
|
+
// Check if we have meaningful game data (image or description)
|
|
1537
|
+
const hasImage = stats.image && stats.image.trim();
|
|
1538
|
+
const hasDescription = stats.description && stats.description.trim();
|
|
1539
|
+
const hasManualOverrides = !!(stats.manual_overrides && stats.manual_overrides.length > 0);
|
|
1540
|
+
|
|
1541
|
+
// Show message if: no image AND no description AND no manual overrides
|
|
1542
|
+
// (If user has manually edited ANY field, don't show the message)
|
|
1543
|
+
if (!hasImage && !hasDescription && !hasManualOverrides) {
|
|
1544
|
+
if (gameContentGrid) {
|
|
1545
|
+
gameContentGrid.style.display = 'none';
|
|
1546
|
+
}
|
|
1547
|
+
if (noGameDataMessage) {
|
|
1548
|
+
// Set the game title in the message
|
|
1549
|
+
const noGameDataTitle = document.getElementById('noGameDataTitle');
|
|
1550
|
+
if (noGameDataTitle) {
|
|
1551
|
+
const gameTitle = stats.title_original || stats.game_name || 'Game';
|
|
1552
|
+
noGameDataTitle.textContent = gameTitle;
|
|
1553
|
+
}
|
|
1554
|
+
noGameDataMessage.style.display = 'block';
|
|
1555
|
+
}
|
|
1556
|
+
} else {
|
|
1557
|
+
// Hide the message and display the content grid
|
|
1558
|
+
if (noGameDataMessage) {
|
|
1559
|
+
noGameDataMessage.style.display = 'none';
|
|
1560
|
+
}
|
|
1561
|
+
gameContentGrid.style.display = 'flex';
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// Update game photo - all images are now stored as PNG base64
|
|
1565
|
+
if (stats.image && stats.image.trim()) {
|
|
1566
|
+
let imageSrc = stats.image.trim();
|
|
1567
|
+
|
|
1568
|
+
// Handle different image formats
|
|
1569
|
+
if (imageSrc.startsWith('data:image')) {
|
|
1570
|
+
// Already has data URI prefix
|
|
1571
|
+
gamePhoto.src = imageSrc;
|
|
1572
|
+
} else if (imageSrc.startsWith('http')) {
|
|
1573
|
+
// External URL
|
|
1574
|
+
gamePhoto.src = imageSrc;
|
|
1575
|
+
} else {
|
|
1576
|
+
// Raw base64 data - add PNG data URI prefix (all uploads are converted to PNG)
|
|
1577
|
+
gamePhoto.src = `data:image/png;base64,${imageSrc}`;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
gamePhotoSection.style.display = 'block';
|
|
1581
|
+
gamePhoto.style.display = 'block';
|
|
1582
|
+
} else {
|
|
1583
|
+
gamePhotoSection.style.display = 'none';
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Update game titles
|
|
1587
|
+
const titleOriginal = document.getElementById('gameTitleOriginal');
|
|
1588
|
+
const titleRomaji = document.getElementById('gameTitleRomaji');
|
|
1589
|
+
const titleEnglish = document.getElementById('gameTitleEnglish');
|
|
1590
|
+
|
|
1591
|
+
if (stats.title_original) {
|
|
1592
|
+
titleOriginal.textContent = stats.title_original;
|
|
1593
|
+
titleOriginal.style.display = 'block';
|
|
1594
|
+
} else {
|
|
1595
|
+
titleOriginal.style.display = 'none';
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (stats.title_romaji) {
|
|
1599
|
+
titleRomaji.textContent = stats.title_romaji;
|
|
1600
|
+
titleRomaji.style.display = 'block';
|
|
1601
|
+
} else {
|
|
1602
|
+
titleRomaji.style.display = 'none';
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if (stats.title_english) {
|
|
1606
|
+
titleEnglish.textContent = stats.title_english;
|
|
1607
|
+
titleEnglish.style.display = 'block';
|
|
1608
|
+
} else {
|
|
1609
|
+
titleEnglish.style.display = 'none';
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// Update game type badge
|
|
1613
|
+
const typeBadge = document.getElementById('gameTypeBadge');
|
|
1614
|
+
if (stats.type) {
|
|
1615
|
+
typeBadge.textContent = stats.type;
|
|
1616
|
+
typeBadge.style.display = 'inline-block';
|
|
1617
|
+
} else {
|
|
1618
|
+
typeBadge.style.display = 'none';
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Update game description
|
|
1622
|
+
const description = document.getElementById('gameDescription');
|
|
1623
|
+
const expandBtn = document.getElementById('descriptionExpandBtn');
|
|
1624
|
+
if (stats.description) {
|
|
1625
|
+
description.textContent = stats.description;
|
|
1626
|
+
// Show expand button if description is long (more than ~150 characters)
|
|
1627
|
+
if (stats.description.length > 150) {
|
|
1628
|
+
expandBtn.style.display = 'block';
|
|
1629
|
+
} else {
|
|
1630
|
+
expandBtn.style.display = 'none';
|
|
1631
|
+
}
|
|
1632
|
+
} else {
|
|
1633
|
+
description.textContent = '';
|
|
1634
|
+
expandBtn.style.display = 'none';
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// Update game links
|
|
1638
|
+
const linksContainer = document.getElementById('gameLinksContainer');
|
|
1639
|
+
const linksPills = document.getElementById('gameLinksPills');
|
|
1640
|
+
if (stats.links && stats.links.length > 0) {
|
|
1641
|
+
// Clear existing pills
|
|
1642
|
+
linksPills.innerHTML = '';
|
|
1643
|
+
|
|
1644
|
+
// Create a pill for each link
|
|
1645
|
+
stats.links.forEach(link => {
|
|
1646
|
+
if (link.url) {
|
|
1647
|
+
const pill = document.createElement('a');
|
|
1648
|
+
pill.href = link.url;
|
|
1649
|
+
pill.target = '_blank';
|
|
1650
|
+
pill.rel = 'noopener noreferrer';
|
|
1651
|
+
pill.className = 'game-link-pill';
|
|
1652
|
+
pill.textContent = extractDomainName(link.url);
|
|
1653
|
+
linksPills.appendChild(pill);
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
// Show the links container
|
|
1658
|
+
linksContainer.style.display = 'flex';
|
|
1659
|
+
} else {
|
|
1660
|
+
// Hide the links container if no links
|
|
1661
|
+
linksContainer.style.display = 'none';
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Update progress bar and timeline
|
|
1665
|
+
const progressContainer = document.getElementById('gameProgressContainer');
|
|
1666
|
+
if (stats.game_character_count > 0) {
|
|
1667
|
+
const percentage = stats.progress_percentage || 0;
|
|
1668
|
+
document.getElementById('gameProgressPercentage').textContent = Math.floor(percentage) + '%';
|
|
1669
|
+
document.getElementById('gameProgressFill').style.width = percentage + '%';
|
|
1670
|
+
|
|
1671
|
+
// Update timeline dates
|
|
1672
|
+
updateProgressTimeline(stats);
|
|
1673
|
+
|
|
1674
|
+
progressContainer.style.display = 'block';
|
|
1675
|
+
} else {
|
|
1676
|
+
progressContainer.style.display = 'none';
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// Update estimated time left stat
|
|
1680
|
+
updateEstimatedTimeLeft(stats);
|
|
1681
|
+
|
|
1682
|
+
// Update main statistics with native tooltips
|
|
1683
|
+
const currentTotalCharsEl = document.getElementById('currentTotalChars');
|
|
1684
|
+
const currentTotalCharsBox = currentTotalCharsEl.closest('.dashboard-stat-item');
|
|
1685
|
+
currentTotalCharsEl.textContent = stats.total_characters_formatted;
|
|
1686
|
+
if (currentTotalCharsBox && stats.total_characters) {
|
|
1687
|
+
currentTotalCharsBox.setAttribute('title', `${stats.total_characters.toLocaleString(undefined, {maximumFractionDigits: 2})} characters`);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1111
1690
|
document.getElementById('currentTotalTime').textContent = stats.total_time_formatted;
|
|
1112
|
-
|
|
1113
|
-
document.getElementById('
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1691
|
+
|
|
1692
|
+
const currentReadingSpeedEl = document.getElementById('currentReadingSpeed');
|
|
1693
|
+
const currentReadingSpeedBox = currentReadingSpeedEl.closest('.dashboard-stat-item');
|
|
1694
|
+
currentReadingSpeedEl.textContent = stats.reading_speed_formatted;
|
|
1695
|
+
if (currentReadingSpeedBox && stats.reading_speed) {
|
|
1696
|
+
currentReadingSpeedBox.setAttribute('title', `${stats.reading_speed.toLocaleString(undefined, {maximumFractionDigits: 2})} chars/hour`);
|
|
1697
|
+
}
|
|
1119
1698
|
|
|
1120
1699
|
// Update streak indicator
|
|
1121
1700
|
const streakElement = document.getElementById('currentGameStreak');
|
|
@@ -1138,18 +1717,30 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1138
1717
|
}
|
|
1139
1718
|
|
|
1140
1719
|
// Update subtitle
|
|
1141
|
-
const gamesText = stats.
|
|
1720
|
+
const gamesText = stats.completed_games === 1 ? '1 game completed' : `${stats.completed_games} games completed`;
|
|
1142
1721
|
document.getElementById('totalGamesCount').textContent = gamesText;
|
|
1143
1722
|
|
|
1144
|
-
// Update main statistics
|
|
1145
|
-
document.getElementById('allTotalChars')
|
|
1723
|
+
// Update main statistics with native tooltips
|
|
1724
|
+
const allTotalCharsEl = document.getElementById('allTotalChars');
|
|
1725
|
+
const allTotalCharsBox = allTotalCharsEl.closest('.dashboard-stat-item');
|
|
1726
|
+
allTotalCharsEl.textContent = stats.total_characters_formatted;
|
|
1727
|
+
if (allTotalCharsBox && stats.total_characters) {
|
|
1728
|
+
allTotalCharsBox.setAttribute('title', `${stats.total_characters.toLocaleString(undefined, {maximumFractionDigits: 2})} characters`);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1146
1731
|
document.getElementById('allTotalTime').textContent = stats.total_time_formatted;
|
|
1147
|
-
|
|
1732
|
+
|
|
1733
|
+
const allReadingSpeedEl = document.getElementById('allReadingSpeed');
|
|
1734
|
+
const allReadingSpeedBox = allReadingSpeedEl.closest('.dashboard-stat-item');
|
|
1735
|
+
allReadingSpeedEl.textContent = stats.reading_speed_formatted;
|
|
1736
|
+
if (allReadingSpeedBox && stats.reading_speed) {
|
|
1737
|
+
allReadingSpeedBox.setAttribute('title', `${stats.reading_speed.toLocaleString(undefined, {maximumFractionDigits: 2})} chars/hour`);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1148
1740
|
document.getElementById('allSessions').textContent = stats.sessions.toLocaleString();
|
|
1149
1741
|
|
|
1150
|
-
// Update progress section
|
|
1151
|
-
document.getElementById('
|
|
1152
|
-
document.getElementById('allUniqueGames').textContent = stats.unique_games.toLocaleString();
|
|
1742
|
+
// Update progress section (removed monthly characters)
|
|
1743
|
+
document.getElementById('allUniqueGames').textContent = stats.completed_games.toLocaleString();
|
|
1153
1744
|
document.getElementById('allTotalSentences').textContent = stats.total_sentences.toLocaleString();
|
|
1154
1745
|
|
|
1155
1746
|
// Update streak indicator
|
|
@@ -1234,4 +1825,279 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1234
1825
|
|
|
1235
1826
|
// Global function to retry dashboard loading
|
|
1236
1827
|
window.loadDashboardData = loadDashboardData;
|
|
1828
|
+
|
|
1829
|
+
// Description expand/collapse functionality
|
|
1830
|
+
const descriptionExpandBtn = document.getElementById('descriptionExpandBtn');
|
|
1831
|
+
if (descriptionExpandBtn) {
|
|
1832
|
+
descriptionExpandBtn.addEventListener('click', function() {
|
|
1833
|
+
const description = document.getElementById('gameDescription');
|
|
1834
|
+
const expandText = this.querySelector('.expand-text');
|
|
1835
|
+
const collapseText = this.querySelector('.collapse-text');
|
|
1836
|
+
|
|
1837
|
+
if (description.classList.contains('expanded')) {
|
|
1838
|
+
// Collapse
|
|
1839
|
+
description.classList.remove('expanded');
|
|
1840
|
+
expandText.style.display = 'inline';
|
|
1841
|
+
collapseText.style.display = 'none';
|
|
1842
|
+
} else {
|
|
1843
|
+
// Expand
|
|
1844
|
+
description.classList.add('expanded');
|
|
1845
|
+
expandText.style.display = 'none';
|
|
1846
|
+
collapseText.style.display = 'inline';
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// Game completion button handler
|
|
1852
|
+
const gameCompletionBtn = document.getElementById('gameCompletionBtn');
|
|
1853
|
+
if (gameCompletionBtn) {
|
|
1854
|
+
gameCompletionBtn.addEventListener('click', async function() {
|
|
1855
|
+
// Don't do anything if already completed
|
|
1856
|
+
if (this.disabled) return;
|
|
1857
|
+
|
|
1858
|
+
// Get the current game ID from the stats
|
|
1859
|
+
// We need to fetch current stats to get the game_id
|
|
1860
|
+
try {
|
|
1861
|
+
const response = await fetch('/api/stats');
|
|
1862
|
+
if (!response.ok) throw new Error('Failed to fetch stats');
|
|
1863
|
+
|
|
1864
|
+
const data = await response.json();
|
|
1865
|
+
const currentGameStats = data.currentGameStats;
|
|
1866
|
+
|
|
1867
|
+
if (!currentGameStats || !currentGameStats.game_name) {
|
|
1868
|
+
console.error('No current game found');
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// Find the game_id by looking up the game
|
|
1873
|
+
// We need to get the game_id from the games management API
|
|
1874
|
+
const gamesResponse = await fetch('/api/games-management');
|
|
1875
|
+
if (!gamesResponse.ok) throw new Error('Failed to fetch games');
|
|
1876
|
+
|
|
1877
|
+
const gamesData = await gamesResponse.json();
|
|
1878
|
+
const currentGame = gamesData.games.find(g =>
|
|
1879
|
+
g.title_original === currentGameStats.game_name ||
|
|
1880
|
+
g.title_original === currentGameStats.title_original
|
|
1881
|
+
);
|
|
1882
|
+
|
|
1883
|
+
if (!currentGame) {
|
|
1884
|
+
console.error('Could not find game ID for current game');
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Confirm with user
|
|
1889
|
+
const confirmMsg = `Mark "${currentGame.title_original}" as completed?`;
|
|
1890
|
+
if (!confirm(confirmMsg)) return;
|
|
1891
|
+
|
|
1892
|
+
// Call the API to mark as complete
|
|
1893
|
+
const markCompleteResponse = await fetch(`/api/games/${currentGame.id}/mark-complete`, {
|
|
1894
|
+
method: 'POST',
|
|
1895
|
+
headers: {
|
|
1896
|
+
'Content-Type': 'application/json'
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
if (!markCompleteResponse.ok) {
|
|
1901
|
+
const errorData = await markCompleteResponse.json();
|
|
1902
|
+
throw new Error(errorData.error || 'Failed to mark game as complete');
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
const result = await markCompleteResponse.json();
|
|
1906
|
+
console.log('Game marked as complete:', result);
|
|
1907
|
+
|
|
1908
|
+
// Trigger confetti celebration!
|
|
1909
|
+
if (typeof confetti !== 'undefined') {
|
|
1910
|
+
// Fire confetti from multiple angles for a nice effect
|
|
1911
|
+
const duration = 3000; // 3 seconds
|
|
1912
|
+
const animationEnd = Date.now() + duration;
|
|
1913
|
+
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 9999 };
|
|
1914
|
+
|
|
1915
|
+
function randomInRange(min, max) {
|
|
1916
|
+
return Math.random() * (max - min) + min;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
const interval = setInterval(function() {
|
|
1920
|
+
const timeLeft = animationEnd - Date.now();
|
|
1921
|
+
|
|
1922
|
+
if (timeLeft <= 0) {
|
|
1923
|
+
return clearInterval(interval);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const particleCount = 50 * (timeLeft / duration);
|
|
1927
|
+
|
|
1928
|
+
// Fire confetti from left side
|
|
1929
|
+
confetti({
|
|
1930
|
+
...defaults,
|
|
1931
|
+
particleCount,
|
|
1932
|
+
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
// Fire confetti from right side
|
|
1936
|
+
confetti({
|
|
1937
|
+
...defaults,
|
|
1938
|
+
particleCount,
|
|
1939
|
+
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }
|
|
1940
|
+
});
|
|
1941
|
+
}, 250);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// Update button to completed state
|
|
1945
|
+
this.textContent = 'Completed ✓';
|
|
1946
|
+
this.disabled = true;
|
|
1947
|
+
this.classList.add('completed');
|
|
1948
|
+
|
|
1949
|
+
// Add completed class to card
|
|
1950
|
+
const currentGameCard = document.getElementById('currentGameCard');
|
|
1951
|
+
if (currentGameCard) {
|
|
1952
|
+
currentGameCard.classList.add('completed');
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// Optionally refresh the dashboard to reflect changes
|
|
1956
|
+
setTimeout(() => {
|
|
1957
|
+
loadDashboardData();
|
|
1958
|
+
}, 500);
|
|
1959
|
+
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
console.error('Error marking game as complete:', error);
|
|
1962
|
+
alert(`Failed to mark game as complete: ${error.message}`);
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// ExStatic Import Functionality
|
|
1968
|
+
const exstaticFileInput = document.getElementById('exstaticFile');
|
|
1969
|
+
const importExstaticBtn = document.getElementById('importExstaticBtn');
|
|
1970
|
+
const importProgress = document.getElementById('importProgress');
|
|
1971
|
+
const importProgressBar = document.getElementById('importProgressBar');
|
|
1972
|
+
const importProgressText = document.getElementById('importProgressText');
|
|
1973
|
+
const importStatus = document.getElementById('importStatus');
|
|
1974
|
+
|
|
1975
|
+
if (exstaticFileInput && importExstaticBtn) {
|
|
1976
|
+
// Enable/disable import button based on file selection
|
|
1977
|
+
exstaticFileInput.addEventListener('change', function(e) {
|
|
1978
|
+
const file = e.target.files[0];
|
|
1979
|
+
// Enable button whenever any file is selected
|
|
1980
|
+
if (file) {
|
|
1981
|
+
importExstaticBtn.disabled = false;
|
|
1982
|
+
importExstaticBtn.style.background = '#2980b9';
|
|
1983
|
+
importExstaticBtn.style.cursor = 'pointer';
|
|
1984
|
+
showImportStatus('', 'info', false);
|
|
1985
|
+
} else {
|
|
1986
|
+
importExstaticBtn.disabled = true;
|
|
1987
|
+
importExstaticBtn.style.background = '#666';
|
|
1988
|
+
importExstaticBtn.style.cursor = 'not-allowed';
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
// Handle import button click
|
|
1993
|
+
importExstaticBtn.addEventListener('click', function() {
|
|
1994
|
+
const file = exstaticFileInput.files[0];
|
|
1995
|
+
if (!file) {
|
|
1996
|
+
showImportStatus('Please select a CSV file first.', 'error', true);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
importExstaticData(file);
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function showImportStatus(message, type, show) {
|
|
2005
|
+
if (!importStatus) return;
|
|
2006
|
+
|
|
2007
|
+
if (show && message) {
|
|
2008
|
+
importStatus.textContent = message;
|
|
2009
|
+
importStatus.style.display = 'block';
|
|
2010
|
+
|
|
2011
|
+
// Set appropriate styling based on type
|
|
2012
|
+
if (type === 'error') {
|
|
2013
|
+
importStatus.style.background = 'var(--danger-color)';
|
|
2014
|
+
importStatus.style.color = 'white';
|
|
2015
|
+
} else if (type === 'success') {
|
|
2016
|
+
importStatus.style.background = 'var(--success-color)';
|
|
2017
|
+
importStatus.style.color = 'white';
|
|
2018
|
+
} else if (type === 'info') {
|
|
2019
|
+
importStatus.style.background = 'var(--primary-color)';
|
|
2020
|
+
importStatus.style.color = 'white';
|
|
2021
|
+
} else {
|
|
2022
|
+
importStatus.style.background = 'var(--bg-tertiary)';
|
|
2023
|
+
importStatus.style.color = 'var(--text-primary)';
|
|
2024
|
+
}
|
|
2025
|
+
} else {
|
|
2026
|
+
importStatus.style.display = 'none';
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
function showImportProgress(show, percentage = 0) {
|
|
2031
|
+
if (!importProgress || !importProgressBar || !importProgressText) return;
|
|
2032
|
+
|
|
2033
|
+
if (show) {
|
|
2034
|
+
importProgress.style.display = 'block';
|
|
2035
|
+
importProgressBar.style.width = percentage + '%';
|
|
2036
|
+
importProgressText.textContent = Math.round(percentage) + '%';
|
|
2037
|
+
} else {
|
|
2038
|
+
importProgress.style.display = 'none';
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
async function importExstaticData(file) {
|
|
2043
|
+
try {
|
|
2044
|
+
// Disable import button and show progress
|
|
2045
|
+
importExstaticBtn.disabled = true;
|
|
2046
|
+
showImportProgress(true, 0);
|
|
2047
|
+
showImportStatus('Preparing import...', 'info', true);
|
|
2048
|
+
|
|
2049
|
+
// Create FormData and append the file
|
|
2050
|
+
const formData = new FormData();
|
|
2051
|
+
formData.append('file', file);
|
|
2052
|
+
|
|
2053
|
+
// Show upload progress
|
|
2054
|
+
showImportProgress(true, 25);
|
|
2055
|
+
showImportStatus('Uploading file...', 'info', true);
|
|
2056
|
+
|
|
2057
|
+
// Send file to backend
|
|
2058
|
+
const response = await fetch('/api/import-exstatic', {
|
|
2059
|
+
method: 'POST',
|
|
2060
|
+
body: formData
|
|
2061
|
+
});
|
|
2062
|
+
|
|
2063
|
+
showImportProgress(true, 75);
|
|
2064
|
+
showImportStatus('Processing data...', 'info', true);
|
|
2065
|
+
|
|
2066
|
+
const result = await response.json();
|
|
2067
|
+
|
|
2068
|
+
showImportProgress(true, 100);
|
|
2069
|
+
|
|
2070
|
+
if (response.ok) {
|
|
2071
|
+
// Success
|
|
2072
|
+
const message = `Successfully imported ${result.imported_count || 0} lines from ${result.games_count || 0} games.`;
|
|
2073
|
+
showImportStatus(message, 'success', true);
|
|
2074
|
+
|
|
2075
|
+
// Reset file input and button
|
|
2076
|
+
exstaticFileInput.value = '';
|
|
2077
|
+
importExstaticBtn.disabled = true;
|
|
2078
|
+
|
|
2079
|
+
// Hide progress after a delay
|
|
2080
|
+
setTimeout(() => {
|
|
2081
|
+
showImportProgress(false);
|
|
2082
|
+
// Optionally refresh the page to show new data
|
|
2083
|
+
if (result.imported_count > 0) {
|
|
2084
|
+
setTimeout(() => {
|
|
2085
|
+
window.location.reload();
|
|
2086
|
+
}, 2000);
|
|
2087
|
+
}
|
|
2088
|
+
}, 1500);
|
|
2089
|
+
} else {
|
|
2090
|
+
// Error
|
|
2091
|
+
showImportStatus(result.error || 'Import failed. Please try again.', 'error', true);
|
|
2092
|
+
showImportProgress(false);
|
|
2093
|
+
}
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
console.error('Import error:', error);
|
|
2096
|
+
showImportStatus('Import failed due to network error. Please try again.', 'error', true);
|
|
2097
|
+
showImportProgress(false);
|
|
2098
|
+
} finally {
|
|
2099
|
+
// Re-enable import button only if a file is still selected
|
|
2100
|
+
importExstaticBtn.disabled = !(exstaticFileInput && exstaticFileInput.files && exstaticFileInput.files.length > 0);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
1237
2103
|
});
|