react-native-pdf-jsi 2.2.8 → 3.0.0
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.
- package/README.md +308 -173
- package/android/src/main/java/org/wonday/pdf/FileDownloader.java +292 -0
- package/android/src/main/java/org/wonday/pdf/FileManager.java +123 -0
- package/android/src/main/java/org/wonday/pdf/LicenseVerifier.java +311 -0
- package/android/src/main/java/org/wonday/pdf/PDFExporter.java +769 -0
- package/android/src/main/java/org/wonday/pdf/RNPDFPackage.java +7 -0
- package/index.js +58 -0
- package/ios/RNPDFPdf/PDFExporter.h +16 -0
- package/ios/RNPDFPdf/PDFExporter.m +537 -0
- package/package.json +3 -2
- package/src/components/AnalyticsPanel.jsx +243 -0
- package/src/components/BookmarkIndicator.jsx +66 -0
- package/src/components/BookmarkListModal.jsx +378 -0
- package/src/components/BookmarkModal.jsx +253 -0
- package/src/components/BottomSheet.jsx +121 -0
- package/src/components/ExportMenu.jsx +223 -0
- package/src/components/LoadingOverlay.jsx +52 -0
- package/src/components/OperationsMenu.jsx +231 -0
- package/src/components/SidePanel.jsx +95 -0
- package/src/components/Toast.jsx +140 -0
- package/src/components/Toolbar.jsx +135 -0
- package/src/managers/AnalyticsManager.js +695 -0
- package/src/managers/BookmarkManager.js +538 -0
- package/src/managers/ExportManager.js +687 -0
- package/src/managers/FileManager.js +89 -0
- package/src/utils/ErrorHandler.js +179 -0
- package/src/utils/TestData.js +112 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnalyticsManager - Reading Analytics and Insights
|
|
3
|
+
* Calculates reading statistics and generates personalized insights
|
|
4
|
+
*
|
|
5
|
+
* LICENSE: Commercial License (Pro feature)
|
|
6
|
+
*
|
|
7
|
+
* @author Punith M
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import bookmarkManager from '../bookmarks/BookmarkManager';
|
|
12
|
+
import licenseManager from '../license/LicenseManager';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* AnalyticsManager Class
|
|
16
|
+
*/
|
|
17
|
+
export class AnalyticsManager {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.initialized = false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize analytics
|
|
24
|
+
*/
|
|
25
|
+
async initialize() {
|
|
26
|
+
// Check Pro license
|
|
27
|
+
licenseManager.requirePro('Reading Analytics');
|
|
28
|
+
|
|
29
|
+
if (!this.initialized) {
|
|
30
|
+
await bookmarkManager.initialize();
|
|
31
|
+
this.initialized = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================
|
|
36
|
+
// CORE ANALYTICS
|
|
37
|
+
// ============================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get complete analytics for a PDF
|
|
41
|
+
* @param {string} pdfId - PDF identifier
|
|
42
|
+
* @returns {Promise<Object>} Complete analytics
|
|
43
|
+
*/
|
|
44
|
+
async getAnalytics(pdfId) {
|
|
45
|
+
await this.initialize();
|
|
46
|
+
|
|
47
|
+
const progress = await bookmarkManager.getProgress(pdfId);
|
|
48
|
+
const bookmarks = await bookmarkManager.getBookmarks(pdfId);
|
|
49
|
+
const statistics = await bookmarkManager.getStatistics(pdfId);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
// Basic stats
|
|
53
|
+
...statistics,
|
|
54
|
+
|
|
55
|
+
// Reading metrics
|
|
56
|
+
readingMetrics: this.calculateReadingMetrics(progress),
|
|
57
|
+
|
|
58
|
+
// Engagement metrics
|
|
59
|
+
engagementMetrics: this.calculateEngagement(progress, bookmarks),
|
|
60
|
+
|
|
61
|
+
// Page analytics
|
|
62
|
+
pageAnalytics: this.analyzePages(progress, bookmarks),
|
|
63
|
+
|
|
64
|
+
// Time analytics
|
|
65
|
+
timeAnalytics: this.analyzeTime(progress),
|
|
66
|
+
|
|
67
|
+
// Predictions
|
|
68
|
+
predictions: this.generatePredictions(progress),
|
|
69
|
+
|
|
70
|
+
// Insights
|
|
71
|
+
insights: this.generateInsights(progress, bookmarks, statistics),
|
|
72
|
+
|
|
73
|
+
// Generated at
|
|
74
|
+
generatedAt: new Date().toISOString()
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// READING METRICS
|
|
80
|
+
// ============================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Calculate reading metrics
|
|
84
|
+
*/
|
|
85
|
+
calculateReadingMetrics(progress) {
|
|
86
|
+
if (!progress || !progress.pagesRead || progress.pagesRead.length === 0) {
|
|
87
|
+
return this.getEmptyMetrics();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { pagesRead, totalPages, timeSpent } = progress;
|
|
91
|
+
|
|
92
|
+
// Pages per hour
|
|
93
|
+
const hoursSpent = timeSpent / 3600;
|
|
94
|
+
const pagesPerHour = hoursSpent > 0 ? pagesRead.length / hoursSpent : 0;
|
|
95
|
+
|
|
96
|
+
// Minutes per page
|
|
97
|
+
const minutesPerPage = pagesRead.length > 0 ? (timeSpent / 60) / pagesRead.length : 0;
|
|
98
|
+
|
|
99
|
+
// Completion rate
|
|
100
|
+
const completionRate = totalPages > 0 ? (pagesRead.length / totalPages) * 100 : 0;
|
|
101
|
+
|
|
102
|
+
// Reading speed (words per minute estimate)
|
|
103
|
+
// Assuming ~250 words per page average
|
|
104
|
+
const estimatedWords = pagesRead.length * 250;
|
|
105
|
+
const minutesSpent = timeSpent / 60;
|
|
106
|
+
const wordsPerMinute = minutesSpent > 0 ? estimatedWords / minutesSpent : 0;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
pagesPerHour: Math.round(pagesPerHour * 10) / 10,
|
|
110
|
+
minutesPerPage: Math.round(minutesPerPage * 10) / 10,
|
|
111
|
+
completionRate: Math.round(completionRate),
|
|
112
|
+
wordsPerMinute: Math.round(wordsPerMinute),
|
|
113
|
+
totalPagesRead: pagesRead.length,
|
|
114
|
+
totalPages,
|
|
115
|
+
timeSpent: Math.round(timeSpent),
|
|
116
|
+
estimatedTotalTime: this.estimateTotalTime(progress),
|
|
117
|
+
estimatedTimeRemaining: this.estimateTimeRemaining(progress)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Calculate engagement metrics
|
|
123
|
+
*/
|
|
124
|
+
calculateEngagement(progress, bookmarks) {
|
|
125
|
+
const bookmarkRate = progress.totalPages > 0
|
|
126
|
+
? (bookmarks.length / progress.totalPages) * 100
|
|
127
|
+
: 0;
|
|
128
|
+
|
|
129
|
+
const uniqueBookmarkPages = new Set(bookmarks.map(b => b.page)).size;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
totalBookmarks: bookmarks.length,
|
|
133
|
+
uniqueBookmarkedPages: uniqueBookmarkPages,
|
|
134
|
+
bookmarkRate: Math.round(bookmarkRate * 10) / 10,
|
|
135
|
+
averageBookmarksPerPage: bookmarks.length > 0
|
|
136
|
+
? Math.round((bookmarks.length / uniqueBookmarkPages) * 10) / 10
|
|
137
|
+
: 0,
|
|
138
|
+
engagementScore: this.calculateEngagementScore(progress, bookmarks)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Calculate engagement score (0-100)
|
|
144
|
+
*/
|
|
145
|
+
calculateEngagementScore(progress, bookmarks) {
|
|
146
|
+
let score = 0;
|
|
147
|
+
|
|
148
|
+
// Completion contributes 40 points
|
|
149
|
+
score += (progress.percentage || 0) * 0.4;
|
|
150
|
+
|
|
151
|
+
// Bookmarks contribute 30 points
|
|
152
|
+
const bookmarkScore = Math.min((bookmarks.length / progress.totalPages) * 100, 30);
|
|
153
|
+
score += bookmarkScore;
|
|
154
|
+
|
|
155
|
+
// Session frequency contributes 30 points
|
|
156
|
+
const sessionScore = Math.min((progress.sessions || 0) * 3, 30);
|
|
157
|
+
score += sessionScore;
|
|
158
|
+
|
|
159
|
+
return Math.min(Math.round(score), 100);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================
|
|
163
|
+
// PAGE ANALYTICS
|
|
164
|
+
// ============================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Analyze page patterns
|
|
168
|
+
*/
|
|
169
|
+
analyzePages(progress, bookmarks) {
|
|
170
|
+
const { pagesRead, totalPages } = progress;
|
|
171
|
+
|
|
172
|
+
// Create page heatmap
|
|
173
|
+
const heatmap = {};
|
|
174
|
+
bookmarks.forEach(b => {
|
|
175
|
+
heatmap[b.page] = (heatmap[b.page] || 0) + 1;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Find most bookmarked pages
|
|
179
|
+
const sortedPages = Object.entries(heatmap)
|
|
180
|
+
.sort((a, b) => b[1] - a[1])
|
|
181
|
+
.slice(0, 5)
|
|
182
|
+
.map(([page, count]) => ({ page: parseInt(page), bookmarks: count }));
|
|
183
|
+
|
|
184
|
+
// Identify reading gaps
|
|
185
|
+
const gaps = this.findReadingGaps(pagesRead, totalPages);
|
|
186
|
+
|
|
187
|
+
// Reading pattern
|
|
188
|
+
const pattern = this.identifyReadingPattern(pagesRead);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
mostBookmarkedPages: sortedPages,
|
|
192
|
+
readingGaps: gaps,
|
|
193
|
+
readingPattern: pattern,
|
|
194
|
+
heatmap
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Find gaps in reading (unread sections)
|
|
200
|
+
*/
|
|
201
|
+
findReadingGaps(pagesRead, totalPages) {
|
|
202
|
+
if (!pagesRead || pagesRead.length === 0) return [];
|
|
203
|
+
|
|
204
|
+
const gaps = [];
|
|
205
|
+
const sortedPages = [...pagesRead].sort((a, b) => a - b);
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < sortedPages.length - 1; i++) {
|
|
208
|
+
const current = sortedPages[i];
|
|
209
|
+
const next = sortedPages[i + 1];
|
|
210
|
+
|
|
211
|
+
if (next - current > 1) {
|
|
212
|
+
gaps.push({
|
|
213
|
+
start: current + 1,
|
|
214
|
+
end: next - 1,
|
|
215
|
+
size: next - current - 1
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return gaps.slice(0, 5); // Top 5 gaps
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Identify reading pattern (linear, random, skip)
|
|
225
|
+
*/
|
|
226
|
+
identifyReadingPattern(pagesRead) {
|
|
227
|
+
if (!pagesRead || pagesRead.length < 3) {
|
|
228
|
+
return 'insufficient_data';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const sortedPages = [...pagesRead].sort((a, b) => a - b);
|
|
232
|
+
let sequentialCount = 0;
|
|
233
|
+
let skipCount = 0;
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < sortedPages.length - 1; i++) {
|
|
236
|
+
const diff = sortedPages[i + 1] - sortedPages[i];
|
|
237
|
+
if (diff === 1) {
|
|
238
|
+
sequentialCount++;
|
|
239
|
+
} else if (diff > 5) {
|
|
240
|
+
skipCount++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const sequentialRate = sequentialCount / (sortedPages.length - 1);
|
|
245
|
+
const skipRate = skipCount / (sortedPages.length - 1);
|
|
246
|
+
|
|
247
|
+
if (sequentialRate > 0.7) return 'linear'; // Reading sequentially
|
|
248
|
+
if (skipRate > 0.5) return 'selective'; // Jumping around
|
|
249
|
+
return 'mixed'; // Mix of both
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================
|
|
253
|
+
// TIME ANALYTICS
|
|
254
|
+
// ============================================
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Analyze time patterns
|
|
258
|
+
*/
|
|
259
|
+
analyzeTime(progress) {
|
|
260
|
+
const { timeSpent, sessions, pagesRead } = progress;
|
|
261
|
+
|
|
262
|
+
if (!timeSpent || !sessions) {
|
|
263
|
+
return {
|
|
264
|
+
averageSessionTime: 0,
|
|
265
|
+
totalTime: 0,
|
|
266
|
+
efficiency: 0
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const averageSessionTime = timeSpent / sessions;
|
|
271
|
+
const minutesSpent = timeSpent / 60;
|
|
272
|
+
const hoursSpent = timeSpent / 3600;
|
|
273
|
+
|
|
274
|
+
// Efficiency: pages per minute
|
|
275
|
+
const efficiency = pagesRead.length > 0 ? pagesRead.length / minutesSpent : 0;
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
totalTimeSeconds: timeSpent,
|
|
279
|
+
totalTimeMinutes: Math.round(minutesSpent),
|
|
280
|
+
totalTimeHours: Math.round(hoursSpent * 10) / 10,
|
|
281
|
+
totalSessions: sessions,
|
|
282
|
+
averageSessionTime: Math.round(averageSessionTime),
|
|
283
|
+
averageSessionMinutes: Math.round(averageSessionTime / 60),
|
|
284
|
+
efficiency: Math.round(efficiency * 100) / 100,
|
|
285
|
+
formattedTotalTime: this.formatDuration(timeSpent),
|
|
286
|
+
formattedAverageSession: this.formatDuration(averageSessionTime)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Estimate total reading time
|
|
292
|
+
*/
|
|
293
|
+
estimateTotalTime(progress) {
|
|
294
|
+
const { pagesRead, totalPages, timeSpent } = progress;
|
|
295
|
+
|
|
296
|
+
if (!pagesRead || pagesRead.length === 0 || !timeSpent) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const avgTimePerPage = timeSpent / pagesRead.length;
|
|
301
|
+
const estimatedTotal = avgTimePerPage * totalPages;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
seconds: Math.round(estimatedTotal),
|
|
305
|
+
minutes: Math.round(estimatedTotal / 60),
|
|
306
|
+
hours: Math.round((estimatedTotal / 3600) * 10) / 10,
|
|
307
|
+
formatted: this.formatDuration(estimatedTotal)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Estimate time remaining
|
|
313
|
+
*/
|
|
314
|
+
estimateTimeRemaining(progress) {
|
|
315
|
+
const { pagesRead, totalPages, timeSpent } = progress;
|
|
316
|
+
|
|
317
|
+
if (!pagesRead || pagesRead.length === 0 || !timeSpent) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const avgTimePerPage = timeSpent / pagesRead.length;
|
|
322
|
+
const pagesRemaining = totalPages - pagesRead.length;
|
|
323
|
+
const timeRemaining = avgTimePerPage * pagesRemaining;
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
seconds: Math.round(timeRemaining),
|
|
327
|
+
minutes: Math.round(timeRemaining / 60),
|
|
328
|
+
hours: Math.round((timeRemaining / 3600) * 10) / 10,
|
|
329
|
+
formatted: this.formatDuration(timeRemaining)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ============================================
|
|
334
|
+
// PREDICTIONS & INSIGHTS
|
|
335
|
+
// ============================================
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Generate predictions
|
|
339
|
+
*/
|
|
340
|
+
generatePredictions(progress) {
|
|
341
|
+
const { pagesRead, totalPages, timeSpent, sessions } = progress;
|
|
342
|
+
|
|
343
|
+
if (!pagesRead || pagesRead.length < 5) {
|
|
344
|
+
return {
|
|
345
|
+
completionDate: null,
|
|
346
|
+
remainingSessions: null,
|
|
347
|
+
message: 'Read at least 5 pages for predictions'
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Calculate average progress per session
|
|
352
|
+
const pagesPerSession = pagesRead.length / sessions;
|
|
353
|
+
|
|
354
|
+
// Estimate sessions needed
|
|
355
|
+
const pagesRemaining = totalPages - pagesRead.length;
|
|
356
|
+
const sessionsRemaining = Math.ceil(pagesRemaining / pagesPerSession);
|
|
357
|
+
|
|
358
|
+
// Estimate completion date (assuming 1 session per day)
|
|
359
|
+
const completionDate = new Date();
|
|
360
|
+
completionDate.setDate(completionDate.getDate() + sessionsRemaining);
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
sessionsRemaining: Math.round(sessionsRemaining),
|
|
364
|
+
completionDate: completionDate.toISOString(),
|
|
365
|
+
completionDateFormatted: completionDate.toLocaleDateString(),
|
|
366
|
+
pagesPerSession: Math.round(pagesPerSession * 10) / 10,
|
|
367
|
+
estimatedDaysToComplete: sessionsRemaining
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Generate personalized insights
|
|
373
|
+
*/
|
|
374
|
+
generateInsights(progress, bookmarks, statistics) {
|
|
375
|
+
const insights = [];
|
|
376
|
+
|
|
377
|
+
// Reading speed insight
|
|
378
|
+
const minutesPerPage = statistics.avgTimePerPage / 60;
|
|
379
|
+
if (minutesPerPage > 0) {
|
|
380
|
+
if (minutesPerPage < 2) {
|
|
381
|
+
insights.push({
|
|
382
|
+
type: 'speed',
|
|
383
|
+
icon: '⚡',
|
|
384
|
+
title: 'Fast Reader',
|
|
385
|
+
message: `You're reading at ${Math.round(minutesPerPage * 10) / 10} minutes per page - that's fast!`,
|
|
386
|
+
sentiment: 'positive'
|
|
387
|
+
});
|
|
388
|
+
} else if (minutesPerPage > 5) {
|
|
389
|
+
insights.push({
|
|
390
|
+
type: 'speed',
|
|
391
|
+
icon: '🐢',
|
|
392
|
+
title: 'Thorough Reader',
|
|
393
|
+
message: `You take your time (${Math.round(minutesPerPage)} min/page). Quality over speed!`,
|
|
394
|
+
sentiment: 'neutral'
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Progress insight
|
|
400
|
+
if (progress.percentage >= 75) {
|
|
401
|
+
insights.push({
|
|
402
|
+
type: 'progress',
|
|
403
|
+
icon: '🎯',
|
|
404
|
+
title: 'Almost There!',
|
|
405
|
+
message: `You've completed ${progress.percentage}% - keep going!`,
|
|
406
|
+
sentiment: 'positive'
|
|
407
|
+
});
|
|
408
|
+
} else if (progress.percentage < 25 && progress.sessions > 3) {
|
|
409
|
+
insights.push({
|
|
410
|
+
type: 'progress',
|
|
411
|
+
icon: '💪',
|
|
412
|
+
title: 'Keep Reading',
|
|
413
|
+
message: `You've started strong with ${progress.sessions} sessions. Keep the momentum!`,
|
|
414
|
+
sentiment: 'encouraging'
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Bookmark insight
|
|
419
|
+
const bookmarkRate = statistics.pagesRead > 0
|
|
420
|
+
? (bookmarks.length / statistics.pagesRead) * 100
|
|
421
|
+
: 0;
|
|
422
|
+
|
|
423
|
+
if (bookmarkRate > 20) {
|
|
424
|
+
insights.push({
|
|
425
|
+
type: 'engagement',
|
|
426
|
+
icon: '📚',
|
|
427
|
+
title: 'Active Reader',
|
|
428
|
+
message: `You've bookmarked ${bookmarks.length} pages - you're highly engaged!`,
|
|
429
|
+
sentiment: 'positive'
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Session consistency
|
|
434
|
+
if (progress.sessions >= 5) {
|
|
435
|
+
const avgPagesPerSession = progress.pagesRead.length / progress.sessions;
|
|
436
|
+
insights.push({
|
|
437
|
+
type: 'consistency',
|
|
438
|
+
icon: '📊',
|
|
439
|
+
title: 'Consistent Reader',
|
|
440
|
+
message: `You average ${Math.round(avgPagesPerSession)} pages per session across ${progress.sessions} sessions.`,
|
|
441
|
+
sentiment: 'neutral'
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Reading gaps
|
|
446
|
+
const gaps = this.findReadingGaps(progress.pagesRead, progress.totalPages);
|
|
447
|
+
if (gaps.length > 0 && gaps[0].size > 10) {
|
|
448
|
+
insights.push({
|
|
449
|
+
type: 'gaps',
|
|
450
|
+
icon: '📖',
|
|
451
|
+
title: 'Reading Gap Detected',
|
|
452
|
+
message: `You skipped pages ${gaps[0].start}-${gaps[0].end}. Want to go back?`,
|
|
453
|
+
sentiment: 'suggestion',
|
|
454
|
+
action: {
|
|
455
|
+
type: 'navigate',
|
|
456
|
+
page: gaps[0].start
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Time remaining
|
|
462
|
+
const estimate = this.estimateTimeRemaining(progress);
|
|
463
|
+
if (estimate && estimate.hours > 0) {
|
|
464
|
+
insights.push({
|
|
465
|
+
type: 'prediction',
|
|
466
|
+
icon: '⏱️',
|
|
467
|
+
title: 'Time to Finish',
|
|
468
|
+
message: `About ${estimate.formatted} of reading time remaining.`,
|
|
469
|
+
sentiment: 'informative'
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return insights;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ============================================
|
|
477
|
+
// STATISTICS CALCULATIONS
|
|
478
|
+
// ============================================
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Calculate reading streak
|
|
482
|
+
* @param {string} pdfId - PDF identifier
|
|
483
|
+
* @returns {Promise<Object>} Streak information
|
|
484
|
+
*/
|
|
485
|
+
async getReadingStreak(pdfId) {
|
|
486
|
+
// This would require session timestamps
|
|
487
|
+
// For now, return basic info
|
|
488
|
+
const progress = await bookmarkManager.getProgress(pdfId);
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
currentStreak: progress.sessions || 0,
|
|
492
|
+
longestStreak: progress.sessions || 0,
|
|
493
|
+
lastReadDate: progress.lastRead
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Get reading history
|
|
499
|
+
* @param {string} pdfId - PDF identifier
|
|
500
|
+
* @returns {Promise<Array>} Reading history
|
|
501
|
+
*/
|
|
502
|
+
async getReadingHistory(pdfId) {
|
|
503
|
+
const progress = await bookmarkManager.getProgress(pdfId);
|
|
504
|
+
|
|
505
|
+
// Create basic history from available data
|
|
506
|
+
return {
|
|
507
|
+
sessions: progress.sessions || 0,
|
|
508
|
+
firstSession: progress.createdAt,
|
|
509
|
+
lastSession: progress.lastRead,
|
|
510
|
+
totalTime: progress.timeSpent || 0,
|
|
511
|
+
pagesRead: progress.pagesRead || []
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Compare with average reader
|
|
517
|
+
*/
|
|
518
|
+
getComparison(progress) {
|
|
519
|
+
const { pagesRead, timeSpent, sessions } = progress;
|
|
520
|
+
|
|
521
|
+
// Industry averages (approximate)
|
|
522
|
+
const avgPagesPerHour = 30; // Average reader
|
|
523
|
+
const avgMinutesPerPage = 2;
|
|
524
|
+
const avgSessionsPerWeek = 5;
|
|
525
|
+
|
|
526
|
+
// User's stats
|
|
527
|
+
const hoursSpent = timeSpent / 3600;
|
|
528
|
+
const userPagesPerHour = hoursSpent > 0 ? pagesRead.length / hoursSpent : 0;
|
|
529
|
+
const userMinutesPerPage = pagesRead.length > 0 ? (timeSpent / 60) / pagesRead.length : 0;
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
speedComparison: {
|
|
533
|
+
user: Math.round(userPagesPerHour),
|
|
534
|
+
average: avgPagesPerHour,
|
|
535
|
+
percentile: this.calculatePercentile(userPagesPerHour, avgPagesPerHour)
|
|
536
|
+
},
|
|
537
|
+
thoroughness: {
|
|
538
|
+
user: Math.round(userMinutesPerPage * 10) / 10,
|
|
539
|
+
average: avgMinutesPerPage,
|
|
540
|
+
message: userMinutesPerPage > avgMinutesPerPage
|
|
541
|
+
? 'You read more thoroughly than average'
|
|
542
|
+
: 'You read faster than average'
|
|
543
|
+
},
|
|
544
|
+
engagement: {
|
|
545
|
+
user: sessions,
|
|
546
|
+
message: sessions > 5
|
|
547
|
+
? 'You\'re a dedicated reader!'
|
|
548
|
+
: 'Keep building your reading habit'
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Calculate percentile
|
|
555
|
+
*/
|
|
556
|
+
calculatePercentile(userValue, avgValue) {
|
|
557
|
+
const ratio = userValue / avgValue;
|
|
558
|
+
|
|
559
|
+
if (ratio >= 1.5) return 90; // Top 10%
|
|
560
|
+
if (ratio >= 1.2) return 75; // Top 25%
|
|
561
|
+
if (ratio >= 0.8) return 50; // Average
|
|
562
|
+
if (ratio >= 0.5) return 25; // Below average
|
|
563
|
+
return 10; // Bottom 10%
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ============================================
|
|
567
|
+
// RECOMMENDATIONS
|
|
568
|
+
// ============================================
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Generate reading recommendations
|
|
572
|
+
*/
|
|
573
|
+
generateRecommendations(progress, bookmarks) {
|
|
574
|
+
const recommendations = [];
|
|
575
|
+
|
|
576
|
+
// Recommend filling gaps
|
|
577
|
+
const gaps = this.findReadingGaps(progress.pagesRead, progress.totalPages);
|
|
578
|
+
if (gaps.length > 0) {
|
|
579
|
+
recommendations.push({
|
|
580
|
+
type: 'gap',
|
|
581
|
+
priority: 'high',
|
|
582
|
+
title: 'Fill Reading Gaps',
|
|
583
|
+
message: `You have ${gaps.length} gaps in your reading. Review pages ${gaps[0].start}-${gaps[0].end}?`,
|
|
584
|
+
action: {
|
|
585
|
+
type: 'navigate',
|
|
586
|
+
page: gaps[0].start
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Recommend review of bookmarked pages
|
|
592
|
+
if (bookmarks.length > 5 && progress.percentage > 50) {
|
|
593
|
+
recommendations.push({
|
|
594
|
+
type: 'review',
|
|
595
|
+
priority: 'medium',
|
|
596
|
+
title: 'Review Bookmarks',
|
|
597
|
+
message: `You have ${bookmarks.length} bookmarks. Consider reviewing important sections.`,
|
|
598
|
+
action: {
|
|
599
|
+
type: 'show_bookmarks'
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Recommend completion
|
|
605
|
+
if (progress.percentage >= 80 && progress.percentage < 100) {
|
|
606
|
+
const remaining = progress.totalPages - progress.pagesRead.length;
|
|
607
|
+
recommendations.push({
|
|
608
|
+
type: 'completion',
|
|
609
|
+
priority: 'high',
|
|
610
|
+
title: 'Finish Reading',
|
|
611
|
+
message: `Only ${remaining} pages left! You can finish this.`,
|
|
612
|
+
action: {
|
|
613
|
+
type: 'navigate',
|
|
614
|
+
page: progress.currentPage
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return recommendations;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ============================================
|
|
623
|
+
// UTILITY METHODS
|
|
624
|
+
// ============================================
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Format duration in human-readable form
|
|
628
|
+
*/
|
|
629
|
+
formatDuration(seconds) {
|
|
630
|
+
if (!seconds || seconds < 60) {
|
|
631
|
+
return `${Math.round(seconds)}s`;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const minutes = Math.floor(seconds / 60);
|
|
635
|
+
if (minutes < 60) {
|
|
636
|
+
return `${minutes}m`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const hours = Math.floor(minutes / 60);
|
|
640
|
+
const remainingMinutes = minutes % 60;
|
|
641
|
+
|
|
642
|
+
if (hours < 24) {
|
|
643
|
+
return remainingMinutes > 0
|
|
644
|
+
? `${hours}h ${remainingMinutes}m`
|
|
645
|
+
: `${hours}h`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const days = Math.floor(hours / 24);
|
|
649
|
+
const remainingHours = hours % 24;
|
|
650
|
+
|
|
651
|
+
return remainingHours > 0
|
|
652
|
+
? `${days}d ${remainingHours}h`
|
|
653
|
+
: `${days}d`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Get empty metrics (when no data)
|
|
658
|
+
*/
|
|
659
|
+
getEmptyMetrics() {
|
|
660
|
+
return {
|
|
661
|
+
pagesPerHour: 0,
|
|
662
|
+
minutesPerPage: 0,
|
|
663
|
+
completionRate: 0,
|
|
664
|
+
wordsPerMinute: 0,
|
|
665
|
+
totalPagesRead: 0,
|
|
666
|
+
totalPages: 0,
|
|
667
|
+
timeSpent: 0,
|
|
668
|
+
estimatedTotalTime: null,
|
|
669
|
+
estimatedTimeRemaining: null
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Export analytics data
|
|
675
|
+
* @param {string} pdfId - PDF identifier
|
|
676
|
+
* @returns {Promise<Object>} Export data
|
|
677
|
+
*/
|
|
678
|
+
async exportAnalytics(pdfId) {
|
|
679
|
+
const analytics = await this.getAnalytics(pdfId);
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
pdfId,
|
|
683
|
+
analytics,
|
|
684
|
+
exportedAt: new Date().toISOString(),
|
|
685
|
+
format: 'json',
|
|
686
|
+
version: '1.0.0'
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Create singleton instance
|
|
692
|
+
const analyticsManager = new AnalyticsManager();
|
|
693
|
+
|
|
694
|
+
export default analyticsManager;
|
|
695
|
+
|