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.
@@ -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
+