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
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
// Goals Page JavaScript
|
|
2
|
+
// Dependencies: shared.js (provides utility functions like showElement, hideElement, escapeHtml)
|
|
3
|
+
|
|
4
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
5
|
+
|
|
6
|
+
// Helper function to format large numbers
|
|
7
|
+
function formatGoalNumber(num) {
|
|
8
|
+
if (num >= 1000000) {
|
|
9
|
+
return (num / 1000000).toFixed(1) + 'M';
|
|
10
|
+
} else if (num >= 1000) {
|
|
11
|
+
return (num / 1000).toFixed(1) + 'K';
|
|
12
|
+
}
|
|
13
|
+
return num.toLocaleString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Helper function to format hours
|
|
17
|
+
function formatHours(hours) {
|
|
18
|
+
if (hours < 1) {
|
|
19
|
+
const minutes = Math.round(hours * 60);
|
|
20
|
+
return `${minutes}m`;
|
|
21
|
+
} else {
|
|
22
|
+
const h = Math.floor(hours);
|
|
23
|
+
const m = Math.round((hours - h) * 60);
|
|
24
|
+
return h > 0 ? `${h}h${m > 0 ? ' ' + m + 'm' : ''}` : `${m}m`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Function to update progress bar color based on percentage
|
|
29
|
+
function updateProgressBarColor(progressElement, percentage) {
|
|
30
|
+
progressElement.classList.remove('completion-0', 'completion-25', 'completion-50', 'completion-75', 'completion-100');
|
|
31
|
+
|
|
32
|
+
if (percentage >= 100) {
|
|
33
|
+
progressElement.classList.add('completion-100');
|
|
34
|
+
} else if (percentage >= 75) {
|
|
35
|
+
progressElement.classList.add('completion-75');
|
|
36
|
+
} else if (percentage >= 50) {
|
|
37
|
+
progressElement.classList.add('completion-50');
|
|
38
|
+
} else if (percentage >= 25) {
|
|
39
|
+
progressElement.classList.add('completion-25');
|
|
40
|
+
} else {
|
|
41
|
+
progressElement.classList.add('completion-0');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Function to load goal progress chart
|
|
46
|
+
async function loadGoalProgress() {
|
|
47
|
+
const goalProgressChart = document.getElementById('goalProgressChart');
|
|
48
|
+
const goalProgressLoading = document.getElementById('goalProgressLoading');
|
|
49
|
+
const goalProgressError = document.getElementById('goalProgressError');
|
|
50
|
+
|
|
51
|
+
if (!goalProgressChart) return;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
goalProgressLoading.style.display = 'flex';
|
|
55
|
+
goalProgressError.style.display = 'none';
|
|
56
|
+
|
|
57
|
+
const response = await fetch('/api/stats');
|
|
58
|
+
if (!response.ok) throw new Error('Failed to fetch stats data');
|
|
59
|
+
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
const allGamesStats = data.allGamesStats;
|
|
62
|
+
const allLinesData = data.allLinesData || [];
|
|
63
|
+
|
|
64
|
+
if (!allGamesStats) {
|
|
65
|
+
throw new Error('No stats data available');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Get goal settings
|
|
69
|
+
const goalSettings = window.statsConfig || {};
|
|
70
|
+
const hoursTarget = goalSettings.readingHoursTarget || 1500;
|
|
71
|
+
const charsTarget = goalSettings.characterCountTarget || 25000000;
|
|
72
|
+
const gamesTarget = goalSettings.gamesTarget || 100;
|
|
73
|
+
|
|
74
|
+
// Calculate current progress
|
|
75
|
+
const currentHours = allGamesStats.total_time_hours || 0;
|
|
76
|
+
const currentCharacters = allGamesStats.total_characters || 0;
|
|
77
|
+
const currentGames = allGamesStats.unique_games || 0;
|
|
78
|
+
|
|
79
|
+
// Calculate daily averages for projections using 90-day lookback period (reusing logic from stats.js)
|
|
80
|
+
const dailyHoursAvg = calculateDailyAverage(allLinesData, 'hours');
|
|
81
|
+
const dailyCharsAvg = calculateDailyAverage(allLinesData, 'characters');
|
|
82
|
+
const dailyGamesAvg = calculateDailyAverage(allLinesData, 'games');
|
|
83
|
+
|
|
84
|
+
// Update Hours Goal
|
|
85
|
+
const hoursPercentage = Math.min(100, (currentHours / hoursTarget) * 100);
|
|
86
|
+
document.getElementById('goalHoursCurrent').textContent = Math.floor(currentHours).toLocaleString();
|
|
87
|
+
document.getElementById('goalHoursTarget').textContent = hoursTarget.toLocaleString();
|
|
88
|
+
document.getElementById('goalHoursPercentage').textContent = Math.floor(hoursPercentage) + '%';
|
|
89
|
+
document.getElementById('goalHoursProjection').textContent =
|
|
90
|
+
formatProjection(currentHours, hoursTarget, dailyHoursAvg);
|
|
91
|
+
|
|
92
|
+
const hoursProgressBar = document.getElementById('goalHoursProgress');
|
|
93
|
+
hoursProgressBar.style.width = hoursPercentage + '%';
|
|
94
|
+
hoursProgressBar.setAttribute('data-percentage', Math.floor(hoursPercentage / 25) * 25);
|
|
95
|
+
updateProgressBarColor(hoursProgressBar, hoursPercentage);
|
|
96
|
+
|
|
97
|
+
// Update Characters Goal
|
|
98
|
+
const charsPercentage = Math.min(100, (currentCharacters / charsTarget) * 100);
|
|
99
|
+
document.getElementById('goalCharsCurrent').textContent = formatGoalNumber(currentCharacters);
|
|
100
|
+
document.getElementById('goalCharsTarget').textContent = formatGoalNumber(charsTarget);
|
|
101
|
+
document.getElementById('goalCharsPercentage').textContent = Math.floor(charsPercentage) + '%';
|
|
102
|
+
document.getElementById('goalCharsProjection').textContent =
|
|
103
|
+
formatProjection(currentCharacters, charsTarget, dailyCharsAvg);
|
|
104
|
+
|
|
105
|
+
const charsProgressBar = document.getElementById('goalCharsProgress');
|
|
106
|
+
charsProgressBar.style.width = charsPercentage + '%';
|
|
107
|
+
charsProgressBar.setAttribute('data-percentage', Math.floor(charsPercentage / 25) * 25);
|
|
108
|
+
updateProgressBarColor(charsProgressBar, charsPercentage);
|
|
109
|
+
|
|
110
|
+
// Update Games Goal
|
|
111
|
+
const gamesPercentage = Math.min(100, (currentGames / gamesTarget) * 100);
|
|
112
|
+
document.getElementById('goalGamesCurrent').textContent = currentGames.toLocaleString();
|
|
113
|
+
document.getElementById('goalGamesTarget').textContent = gamesTarget.toLocaleString();
|
|
114
|
+
document.getElementById('goalGamesPercentage').textContent = Math.floor(gamesPercentage) + '%';
|
|
115
|
+
document.getElementById('goalGamesProjection').textContent =
|
|
116
|
+
formatProjection(currentGames, gamesTarget, dailyGamesAvg);
|
|
117
|
+
|
|
118
|
+
const gamesProgressBar = document.getElementById('goalGamesProgress');
|
|
119
|
+
gamesProgressBar.style.width = gamesPercentage + '%';
|
|
120
|
+
gamesProgressBar.setAttribute('data-percentage', Math.floor(gamesPercentage / 25) * 25);
|
|
121
|
+
updateProgressBarColor(gamesProgressBar, gamesPercentage);
|
|
122
|
+
|
|
123
|
+
goalProgressLoading.style.display = 'none';
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('Error loading goal progress:', error);
|
|
127
|
+
goalProgressLoading.style.display = 'none';
|
|
128
|
+
goalProgressError.style.display = 'block';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Function to calculate daily average using a 90-day lookback period (copied from stats.js)
|
|
133
|
+
function calculateDailyAverage(allLinesData, metricType) {
|
|
134
|
+
if (!allLinesData || allLinesData.length === 0) {
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const today = new Date();
|
|
139
|
+
const ninetyDaysAgo = new Date(today.getTime() - (90 * 24 * 60 * 60 * 1000));
|
|
140
|
+
|
|
141
|
+
const recentData = allLinesData.filter(line => {
|
|
142
|
+
const lineDate = new Date(line.timestamp * 1000);
|
|
143
|
+
return lineDate >= ninetyDaysAgo && lineDate <= today;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (recentData.length === 0) {
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let dailyTotals = {};
|
|
151
|
+
|
|
152
|
+
if (metricType === 'hours') {
|
|
153
|
+
const dailyTimestamps = {};
|
|
154
|
+
for (const line of recentData) {
|
|
155
|
+
const ts = parseFloat(line.timestamp);
|
|
156
|
+
if (isNaN(ts)) continue;
|
|
157
|
+
const dateObj = new Date(ts * 1000);
|
|
158
|
+
const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
|
|
159
|
+
if (!dailyTimestamps[dateStr]) {
|
|
160
|
+
dailyTimestamps[dateStr] = [];
|
|
161
|
+
}
|
|
162
|
+
dailyTimestamps[dateStr].push(ts);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const [dateStr, timestamps] of Object.entries(dailyTimestamps)) {
|
|
166
|
+
if (timestamps.length >= 2) {
|
|
167
|
+
timestamps.sort((a, b) => a - b);
|
|
168
|
+
let dayHours = 0;
|
|
169
|
+
const afkTimerSeconds = 120; // Default AFK timer
|
|
170
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
171
|
+
const gap = timestamps[i] - timestamps[i-1];
|
|
172
|
+
dayHours += Math.min(gap, afkTimerSeconds) / 3600;
|
|
173
|
+
}
|
|
174
|
+
dailyTotals[dateStr] = dayHours;
|
|
175
|
+
} else if (timestamps.length === 1) {
|
|
176
|
+
dailyTotals[dateStr] = 1 / 3600;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} else if (metricType === 'characters') {
|
|
180
|
+
for (const line of recentData) {
|
|
181
|
+
const ts = parseFloat(line.timestamp);
|
|
182
|
+
if (isNaN(ts)) continue;
|
|
183
|
+
const dateObj = new Date(ts * 1000);
|
|
184
|
+
const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
|
|
185
|
+
dailyTotals[dateStr] = (dailyTotals[dateStr] || 0) + (line.characters || 0);
|
|
186
|
+
}
|
|
187
|
+
} else if (metricType === 'games') {
|
|
188
|
+
const dailyGames = {};
|
|
189
|
+
for (const line of recentData) {
|
|
190
|
+
const ts = parseFloat(line.timestamp);
|
|
191
|
+
if (isNaN(ts)) continue;
|
|
192
|
+
const dateObj = new Date(ts * 1000);
|
|
193
|
+
const dateStr = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
|
|
194
|
+
if (!dailyGames[dateStr]) {
|
|
195
|
+
dailyGames[dateStr] = new Set();
|
|
196
|
+
}
|
|
197
|
+
dailyGames[dateStr].add(line.game_name);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const [dateStr, gamesSet] of Object.entries(dailyGames)) {
|
|
201
|
+
dailyTotals[dateStr] = gamesSet.size;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const totalDays = Object.keys(dailyTotals).length;
|
|
206
|
+
if (totalDays === 0) {
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const totalValue = Object.values(dailyTotals).reduce((sum, value) => sum + value, 0);
|
|
211
|
+
return totalValue / totalDays;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Function to format projection text
|
|
215
|
+
function formatProjection(currentValue, targetValue, dailyAverage) {
|
|
216
|
+
if (currentValue >= targetValue) {
|
|
217
|
+
return 'Goal achieved! 🎉';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (dailyAverage <= 0) {
|
|
221
|
+
return 'No recent activity';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const remaining = targetValue - currentValue;
|
|
225
|
+
const daysToComplete = Math.ceil(remaining / dailyAverage);
|
|
226
|
+
|
|
227
|
+
if (daysToComplete <= 0) {
|
|
228
|
+
return 'Goal achieved! 🎉';
|
|
229
|
+
} else if (daysToComplete === 1) {
|
|
230
|
+
return '~1 day remaining';
|
|
231
|
+
} else if (daysToComplete <= 7) {
|
|
232
|
+
return `~${daysToComplete} days remaining`;
|
|
233
|
+
} else if (daysToComplete <= 30) {
|
|
234
|
+
const weeks = Math.ceil(daysToComplete / 7);
|
|
235
|
+
return `~${weeks} week${weeks > 1 ? 's' : ''} remaining`;
|
|
236
|
+
} else if (daysToComplete <= 365) {
|
|
237
|
+
const months = Math.ceil(daysToComplete / 30);
|
|
238
|
+
return `~${months} month${months > 1 ? 's' : ''} remaining`;
|
|
239
|
+
} else {
|
|
240
|
+
const years = Math.ceil(daysToComplete / 365);
|
|
241
|
+
return `~${years} year${years > 1 ? 's' : ''} remaining`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Function to load today's goals
|
|
246
|
+
async function loadTodayGoals() {
|
|
247
|
+
try {
|
|
248
|
+
const response = await fetch('/api/goals-today');
|
|
249
|
+
if (!response.ok) throw new Error('Failed to fetch today goals');
|
|
250
|
+
|
|
251
|
+
const data = await response.json();
|
|
252
|
+
const today = new Date();
|
|
253
|
+
const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
254
|
+
document.getElementById('todayGoalsDate').textContent = dateStr;
|
|
255
|
+
|
|
256
|
+
let hasAnyTarget = false;
|
|
257
|
+
|
|
258
|
+
// Update hours goal
|
|
259
|
+
const hoursGoalItem = document.getElementById('hoursGoalItem');
|
|
260
|
+
if (data.hours && data.hours.has_target && !data.hours.expired) {
|
|
261
|
+
hasAnyTarget = true;
|
|
262
|
+
hoursGoalItem.style.display = 'block';
|
|
263
|
+
document.getElementById('hoursDaysRemaining').style.display = 'block';
|
|
264
|
+
|
|
265
|
+
document.getElementById('todayHoursProgress').textContent = formatHours(data.hours.progress);
|
|
266
|
+
document.getElementById('todayHoursRequired').textContent = formatHours(data.hours.required);
|
|
267
|
+
document.getElementById('hoursRemainingValue').textContent = data.hours.days_remaining;
|
|
268
|
+
|
|
269
|
+
// Add green highlight if goal is met
|
|
270
|
+
if (data.hours.progress >= data.hours.required) {
|
|
271
|
+
hoursGoalItem.classList.add('goal-met');
|
|
272
|
+
} else {
|
|
273
|
+
hoursGoalItem.classList.remove('goal-met');
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
hoursGoalItem.style.display = 'none';
|
|
277
|
+
document.getElementById('hoursDaysRemaining').style.display = 'none';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Update characters goal
|
|
281
|
+
const charsGoalItem = document.getElementById('charsGoalItem');
|
|
282
|
+
if (data.characters && data.characters.has_target && !data.characters.expired) {
|
|
283
|
+
hasAnyTarget = true;
|
|
284
|
+
charsGoalItem.style.display = 'block';
|
|
285
|
+
document.getElementById('charsDaysRemaining').style.display = 'block';
|
|
286
|
+
|
|
287
|
+
document.getElementById('todayCharsProgress').textContent = formatGoalNumber(data.characters.progress);
|
|
288
|
+
document.getElementById('todayCharsRequired').textContent = formatGoalNumber(data.characters.required);
|
|
289
|
+
document.getElementById('charsRemainingValue').textContent = data.characters.days_remaining;
|
|
290
|
+
|
|
291
|
+
// Add green highlight if goal is met
|
|
292
|
+
if (data.characters.progress >= data.characters.required) {
|
|
293
|
+
charsGoalItem.classList.add('goal-met');
|
|
294
|
+
} else {
|
|
295
|
+
charsGoalItem.classList.remove('goal-met');
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
charsGoalItem.style.display = 'none';
|
|
299
|
+
document.getElementById('charsDaysRemaining').style.display = 'none';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Show/hide sections based on whether any targets are set
|
|
303
|
+
if (hasAnyTarget) {
|
|
304
|
+
document.getElementById('noTargetsMessage').style.display = 'none';
|
|
305
|
+
document.getElementById('todayGoalsStats').style.display = 'grid';
|
|
306
|
+
document.getElementById('todayGoalsProgress').style.display = 'block';
|
|
307
|
+
} else {
|
|
308
|
+
document.getElementById('noTargetsMessage').style.display = 'block';
|
|
309
|
+
document.getElementById('todayGoalsStats').style.display = 'none';
|
|
310
|
+
document.getElementById('todayGoalsProgress').style.display = 'none';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error('Error loading today goals:', error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Function to load goal projections
|
|
319
|
+
async function loadGoalProjections() {
|
|
320
|
+
try {
|
|
321
|
+
const response = await fetch('/api/goals-projection');
|
|
322
|
+
if (!response.ok) throw new Error('Failed to fetch goal projections');
|
|
323
|
+
|
|
324
|
+
const data = await response.json();
|
|
325
|
+
let hasAnyProjection = false;
|
|
326
|
+
|
|
327
|
+
// Update hours projection
|
|
328
|
+
if (data.hours && data.hours.target_date) {
|
|
329
|
+
hasAnyProjection = true;
|
|
330
|
+
document.getElementById('hoursProjectionItem').style.display = 'block';
|
|
331
|
+
document.getElementById('hoursProjectionSummary').style.display = 'block';
|
|
332
|
+
|
|
333
|
+
document.getElementById('projectionHoursValue').textContent =
|
|
334
|
+
Math.floor(data.hours.projection).toLocaleString() + 'h';
|
|
335
|
+
|
|
336
|
+
// Calculate percentage difference
|
|
337
|
+
const hoursPercentDiff = ((data.hours.projection - data.hours.target) / data.hours.target) * 100;
|
|
338
|
+
|
|
339
|
+
// Status message with pace badge
|
|
340
|
+
const hoursStatus = document.getElementById('hoursProjectionStatus');
|
|
341
|
+
if (hoursPercentDiff >= 5) {
|
|
342
|
+
// Over-achieving by 5% or more
|
|
343
|
+
const badge = `<span class="pace-badge pace-ahead">+${Math.floor(hoursPercentDiff)}%</span>`;
|
|
344
|
+
hoursStatus.innerHTML = `On Track! 🎉 ${badge}`;
|
|
345
|
+
hoursStatus.className = 'dashboard-progress-value positive';
|
|
346
|
+
} else if (hoursPercentDiff >= -5) {
|
|
347
|
+
// Within ±5% - perfect pace
|
|
348
|
+
const badge = `<span class="pace-badge pace-perfect">±${Math.abs(Math.floor(hoursPercentDiff))}%</span>`;
|
|
349
|
+
hoursStatus.innerHTML = `Perfect Pace! ✅ ${badge}`;
|
|
350
|
+
hoursStatus.className = 'dashboard-progress-value positive';
|
|
351
|
+
} else if (hoursPercentDiff >= -15) {
|
|
352
|
+
// Slightly behind (-5% to -15%)
|
|
353
|
+
const shortfall = data.hours.target - data.hours.projection;
|
|
354
|
+
const badge = `<span class="pace-badge pace-behind-mild">${Math.floor(hoursPercentDiff)}%</span>`;
|
|
355
|
+
hoursStatus.innerHTML = `${Math.floor(shortfall)}h short ${badge}`;
|
|
356
|
+
hoursStatus.className = 'dashboard-progress-value';
|
|
357
|
+
hoursStatus.style.color = 'var(--warning-color)';
|
|
358
|
+
} else {
|
|
359
|
+
// Significantly behind (< -15%)
|
|
360
|
+
const shortfall = data.hours.target - data.hours.projection;
|
|
361
|
+
const badge = `<span class="pace-badge pace-behind">${Math.floor(hoursPercentDiff)}%</span>`;
|
|
362
|
+
hoursStatus.innerHTML = `${Math.floor(shortfall)}h short ${badge}`;
|
|
363
|
+
hoursStatus.className = 'dashboard-progress-value';
|
|
364
|
+
hoursStatus.style.color = 'var(--danger-color)';
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
document.getElementById('hoursProjectionItem').style.display = 'none';
|
|
368
|
+
document.getElementById('hoursProjectionSummary').style.display = 'none';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Update characters projection
|
|
372
|
+
if (data.characters && data.characters.target_date) {
|
|
373
|
+
hasAnyProjection = true;
|
|
374
|
+
document.getElementById('charsProjectionItem').style.display = 'block';
|
|
375
|
+
document.getElementById('charsProjectionSummary').style.display = 'block';
|
|
376
|
+
|
|
377
|
+
document.getElementById('projectionCharsValue').textContent = formatGoalNumber(data.characters.projection);
|
|
378
|
+
|
|
379
|
+
// Calculate percentage difference
|
|
380
|
+
const charsPercentDiff = ((data.characters.projection - data.characters.target) / data.characters.target) * 100;
|
|
381
|
+
|
|
382
|
+
// Status message with pace badge
|
|
383
|
+
const charsStatus = document.getElementById('charsProjectionStatus');
|
|
384
|
+
if (charsPercentDiff >= 5) {
|
|
385
|
+
// Over-achieving by 5% or more
|
|
386
|
+
const badge = `<span class="pace-badge pace-ahead">+${Math.floor(charsPercentDiff)}%</span>`;
|
|
387
|
+
charsStatus.innerHTML = `On Track! 🎉 ${badge}`;
|
|
388
|
+
charsStatus.className = 'dashboard-progress-value positive';
|
|
389
|
+
} else if (charsPercentDiff >= -5) {
|
|
390
|
+
// Within ±5% - perfect pace
|
|
391
|
+
const badge = `<span class="pace-badge pace-perfect">±${Math.abs(Math.floor(charsPercentDiff))}%</span>`;
|
|
392
|
+
charsStatus.innerHTML = `Perfect Pace! ✅ ${badge}`;
|
|
393
|
+
charsStatus.className = 'dashboard-progress-value positive';
|
|
394
|
+
} else if (charsPercentDiff >= -15) {
|
|
395
|
+
// Slightly behind (-5% to -15%)
|
|
396
|
+
const shortfall = data.characters.target - data.characters.projection;
|
|
397
|
+
const badge = `<span class="pace-badge pace-behind-mild">${Math.floor(charsPercentDiff)}%</span>`;
|
|
398
|
+
charsStatus.innerHTML = `${formatGoalNumber(shortfall)} short ${badge}`;
|
|
399
|
+
charsStatus.className = 'dashboard-progress-value';
|
|
400
|
+
charsStatus.style.color = 'var(--warning-color)';
|
|
401
|
+
} else {
|
|
402
|
+
// Significantly behind (< -15%)
|
|
403
|
+
const shortfall = data.characters.target - data.characters.projection;
|
|
404
|
+
const badge = `<span class="pace-badge pace-behind">${Math.floor(charsPercentDiff)}%</span>`;
|
|
405
|
+
charsStatus.innerHTML = `${formatGoalNumber(shortfall)} short ${badge}`;
|
|
406
|
+
charsStatus.className = 'dashboard-progress-value';
|
|
407
|
+
charsStatus.style.color = 'var(--danger-color)';
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
document.getElementById('charsProjectionItem').style.display = 'none';
|
|
411
|
+
document.getElementById('charsProjectionSummary').style.display = 'none';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Update games projection
|
|
415
|
+
if (data.games && data.games.target_date) {
|
|
416
|
+
hasAnyProjection = true;
|
|
417
|
+
document.getElementById('gamesProjectionItem').style.display = 'block';
|
|
418
|
+
document.getElementById('gamesProjectionSummary').style.display = 'block';
|
|
419
|
+
|
|
420
|
+
document.getElementById('projectionGamesValue').textContent = data.games.projection.toLocaleString();
|
|
421
|
+
|
|
422
|
+
// Calculate percentage difference
|
|
423
|
+
const gamesPercentDiff = ((data.games.projection - data.games.target) / data.games.target) * 100;
|
|
424
|
+
|
|
425
|
+
// Status message with pace badge
|
|
426
|
+
const gamesStatus = document.getElementById('gamesProjectionStatus');
|
|
427
|
+
if (gamesPercentDiff >= 5) {
|
|
428
|
+
// Over-achieving by 5% or more
|
|
429
|
+
const badge = `<span class="pace-badge pace-ahead">+${Math.floor(gamesPercentDiff)}%</span>`;
|
|
430
|
+
gamesStatus.innerHTML = `On Track! 🎉 ${badge}`;
|
|
431
|
+
gamesStatus.className = 'dashboard-progress-value positive';
|
|
432
|
+
} else if (gamesPercentDiff >= -5) {
|
|
433
|
+
// Within ±5% - perfect pace
|
|
434
|
+
const badge = `<span class="pace-badge pace-perfect">±${Math.abs(Math.floor(gamesPercentDiff))}%</span>`;
|
|
435
|
+
gamesStatus.innerHTML = `Perfect Pace! ✅ ${badge}`;
|
|
436
|
+
gamesStatus.className = 'dashboard-progress-value positive';
|
|
437
|
+
} else if (gamesPercentDiff >= -15) {
|
|
438
|
+
// Slightly behind (-5% to -15%)
|
|
439
|
+
const shortfall = data.games.target - data.games.projection;
|
|
440
|
+
const badge = `<span class="pace-badge pace-behind-mild">${Math.floor(gamesPercentDiff)}%</span>`;
|
|
441
|
+
gamesStatus.innerHTML = `${shortfall} short ${badge}`;
|
|
442
|
+
gamesStatus.className = 'dashboard-progress-value';
|
|
443
|
+
gamesStatus.style.color = 'var(--warning-color)';
|
|
444
|
+
} else {
|
|
445
|
+
// Significantly behind (< -15%)
|
|
446
|
+
const shortfall = data.games.target - data.games.projection;
|
|
447
|
+
const badge = `<span class="pace-badge pace-behind">${Math.floor(gamesPercentDiff)}%</span>`;
|
|
448
|
+
gamesStatus.innerHTML = `${shortfall} short ${badge}`;
|
|
449
|
+
gamesStatus.className = 'dashboard-progress-value';
|
|
450
|
+
gamesStatus.style.color = 'var(--danger-color)';
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
document.getElementById('gamesProjectionItem').style.display = 'none';
|
|
454
|
+
document.getElementById('gamesProjectionSummary').style.display = 'none';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Show/hide sections
|
|
458
|
+
if (hasAnyProjection) {
|
|
459
|
+
document.getElementById('noProjectionsMessage').style.display = 'none';
|
|
460
|
+
document.getElementById('projectionStats').style.display = 'grid';
|
|
461
|
+
document.getElementById('projectionProgress').style.display = 'block';
|
|
462
|
+
} else {
|
|
463
|
+
document.getElementById('noProjectionsMessage').style.display = 'block';
|
|
464
|
+
document.getElementById('projectionStats').style.display = 'none';
|
|
465
|
+
document.getElementById('projectionProgress').style.display = 'none';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error('Error loading goal projections:', error);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Load initial data
|
|
474
|
+
loadGoalProgress();
|
|
475
|
+
loadTodayGoals();
|
|
476
|
+
loadGoalProjections();
|
|
477
|
+
|
|
478
|
+
// Settings modal functionality
|
|
479
|
+
const settingsModal = document.getElementById('settingsModal');
|
|
480
|
+
const settingsToggle = document.getElementById('settingsToggle');
|
|
481
|
+
const closeSettingsModal = document.getElementById('closeSettingsModal');
|
|
482
|
+
const cancelSettingsBtn = document.getElementById('cancelSettingsBtn');
|
|
483
|
+
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
|
484
|
+
const settingsForm = document.getElementById('settingsForm');
|
|
485
|
+
|
|
486
|
+
// Populate settings modal with current values
|
|
487
|
+
if (window.statsConfig) {
|
|
488
|
+
const hoursTargetInput = document.getElementById('readingHoursTarget');
|
|
489
|
+
if (hoursTargetInput) hoursTargetInput.value = window.statsConfig.readingHoursTarget || 1500;
|
|
490
|
+
|
|
491
|
+
const charsTargetInput = document.getElementById('characterCountTarget');
|
|
492
|
+
if (charsTargetInput) charsTargetInput.value = window.statsConfig.characterCountTarget || 25000000;
|
|
493
|
+
|
|
494
|
+
const gamesTargetInput = document.getElementById('gamesTarget');
|
|
495
|
+
if (gamesTargetInput) gamesTargetInput.value = window.statsConfig.gamesTarget || 100;
|
|
496
|
+
|
|
497
|
+
const hoursDateInput = document.getElementById('readingHoursTargetDate');
|
|
498
|
+
if (hoursDateInput) hoursDateInput.value = window.statsConfig.readingHoursTargetDate || '';
|
|
499
|
+
|
|
500
|
+
const charsDateInput = document.getElementById('characterCountTargetDate');
|
|
501
|
+
if (charsDateInput) charsDateInput.value = window.statsConfig.characterCountTargetDate || '';
|
|
502
|
+
|
|
503
|
+
const gamesDateInput = document.getElementById('gamesTargetDate');
|
|
504
|
+
if (gamesDateInput) gamesDateInput.value = window.statsConfig.gamesTargetDate || '';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Open settings modal
|
|
508
|
+
if (settingsToggle) {
|
|
509
|
+
settingsToggle.addEventListener('click', function() {
|
|
510
|
+
settingsModal.style.display = 'flex';
|
|
511
|
+
settingsModal.classList.add('show');
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Close settings modal
|
|
516
|
+
function closeModal() {
|
|
517
|
+
settingsModal.style.display = 'none';
|
|
518
|
+
settingsModal.classList.remove('show');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (closeSettingsModal) {
|
|
522
|
+
closeSettingsModal.addEventListener('click', closeModal);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (cancelSettingsBtn) {
|
|
526
|
+
cancelSettingsBtn.addEventListener('click', closeModal);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Close modal when clicking outside
|
|
530
|
+
settingsModal.addEventListener('click', function(e) {
|
|
531
|
+
if (e.target === settingsModal) {
|
|
532
|
+
closeModal();
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Save settings
|
|
537
|
+
if (saveSettingsBtn) {
|
|
538
|
+
saveSettingsBtn.addEventListener('click', async function() {
|
|
539
|
+
try {
|
|
540
|
+
const formData = {
|
|
541
|
+
reading_hours_target: parseInt(document.getElementById('readingHoursTarget').value),
|
|
542
|
+
character_count_target: parseInt(document.getElementById('characterCountTarget').value),
|
|
543
|
+
games_target: parseInt(document.getElementById('gamesTarget').value),
|
|
544
|
+
reading_hours_target_date: document.getElementById('readingHoursTargetDate').value || '',
|
|
545
|
+
character_count_target_date: document.getElementById('characterCountTargetDate').value || '',
|
|
546
|
+
games_target_date: document.getElementById('gamesTargetDate').value || ''
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const response = await fetch('/api/settings', {
|
|
550
|
+
method: 'POST',
|
|
551
|
+
headers: {
|
|
552
|
+
'Content-Type': 'application/json',
|
|
553
|
+
},
|
|
554
|
+
body: JSON.stringify(formData)
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const result = await response.json();
|
|
558
|
+
|
|
559
|
+
if (response.ok) {
|
|
560
|
+
// Update global config
|
|
561
|
+
window.statsConfig = {
|
|
562
|
+
...window.statsConfig,
|
|
563
|
+
readingHoursTarget: formData.reading_hours_target,
|
|
564
|
+
characterCountTarget: formData.character_count_target,
|
|
565
|
+
gamesTarget: formData.games_target,
|
|
566
|
+
readingHoursTargetDate: formData.reading_hours_target_date,
|
|
567
|
+
characterCountTargetDate: formData.character_count_target_date,
|
|
568
|
+
gamesTargetDate: formData.games_target_date
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
// Show success message
|
|
572
|
+
const successDiv = document.getElementById('settingsSuccess');
|
|
573
|
+
successDiv.textContent = 'Settings saved successfully!';
|
|
574
|
+
successDiv.style.display = 'block';
|
|
575
|
+
setTimeout(() => {
|
|
576
|
+
successDiv.style.display = 'none';
|
|
577
|
+
}, 3000);
|
|
578
|
+
|
|
579
|
+
// Reload all data
|
|
580
|
+
setTimeout(() => {
|
|
581
|
+
loadGoalProgress();
|
|
582
|
+
loadTodayGoals();
|
|
583
|
+
loadGoalProjections();
|
|
584
|
+
}, 500);
|
|
585
|
+
|
|
586
|
+
// Close modal after short delay
|
|
587
|
+
setTimeout(closeModal, 1500);
|
|
588
|
+
} else {
|
|
589
|
+
// Show error message
|
|
590
|
+
const errorDiv = document.getElementById('settingsError');
|
|
591
|
+
errorDiv.textContent = result.error || 'Failed to save settings';
|
|
592
|
+
errorDiv.style.display = 'block';
|
|
593
|
+
setTimeout(() => {
|
|
594
|
+
errorDiv.style.display = 'none';
|
|
595
|
+
}, 5000);
|
|
596
|
+
}
|
|
597
|
+
} catch (error) {
|
|
598
|
+
console.error('Error saving settings:', error);
|
|
599
|
+
const errorDiv = document.getElementById('settingsError');
|
|
600
|
+
errorDiv.textContent = 'Network error while saving settings';
|
|
601
|
+
errorDiv.style.display = 'block';
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Make functions globally available
|
|
607
|
+
window.loadGoalProgress = loadGoalProgress;
|
|
608
|
+
window.loadTodayGoals = loadTodayGoals;
|
|
609
|
+
window.loadGoalProjections = loadGoalProjections;
|
|
610
|
+
});
|