@hed-hog/lms 0.0.364 → 0.0.365

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.
Files changed (218) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  6. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  7. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  8. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  9. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  10. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  11. package/dist/course/course-export-scorm12.service.js +628 -0
  12. package/dist/course/course-export-scorm12.service.js.map +1 -0
  13. package/dist/course/course-export.service.d.ts +84 -0
  14. package/dist/course/course-export.service.d.ts.map +1 -0
  15. package/dist/course/course-export.service.js +237 -0
  16. package/dist/course/course-export.service.js.map +1 -0
  17. package/dist/course/course-structure.controller.d.ts +17 -9
  18. package/dist/course/course-structure.controller.d.ts.map +1 -1
  19. package/dist/course/course-structure.controller.js +17 -4
  20. package/dist/course/course-structure.controller.js.map +1 -1
  21. package/dist/course/course-structure.service.d.ts +12 -4
  22. package/dist/course/course-structure.service.d.ts.map +1 -1
  23. package/dist/course/course-structure.service.js +98 -23
  24. package/dist/course/course-structure.service.js.map +1 -1
  25. package/dist/course/course-video-hls.service.d.ts +57 -0
  26. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  27. package/dist/course/course-video-hls.service.js +767 -0
  28. package/dist/course/course-video-hls.service.js.map +1 -0
  29. package/dist/course/course.controller.d.ts +45 -13
  30. package/dist/course/course.controller.d.ts.map +1 -1
  31. package/dist/course/course.controller.js +40 -26
  32. package/dist/course/course.controller.js.map +1 -1
  33. package/dist/course/course.mcp-tools.js +1 -1
  34. package/dist/course/course.mcp-tools.js.map +1 -1
  35. package/dist/course/course.module.d.ts.map +1 -1
  36. package/dist/course/course.module.js +11 -0
  37. package/dist/course/course.module.js.map +1 -1
  38. package/dist/course/course.service.d.ts +6 -9
  39. package/dist/course/course.service.d.ts.map +1 -1
  40. package/dist/course/course.service.js +57 -48
  41. package/dist/course/course.service.js.map +1 -1
  42. package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
  43. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
  44. package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
  45. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
  46. package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
  47. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
  48. package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
  49. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
  50. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  51. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  52. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  53. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  54. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  55. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  56. package/dist/course/dto/create-course-export.dto.js +71 -0
  57. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  58. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  59. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  60. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  61. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  62. package/dist/course/lms-bulk-upload-automation.service.d.ts +16 -1
  63. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  64. package/dist/course/lms-bulk-upload-automation.service.js +102 -8
  65. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  66. package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
  67. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
  68. package/dist/course/lms-bulk-upload-infra.service.js +32 -8
  69. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
  70. package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
  71. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  72. package/dist/course/lms-bulk-upload.controller.js +43 -2
  73. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  74. package/dist/course/lms-bulk-upload.service.d.ts +11 -0
  75. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  76. package/dist/course/lms-bulk-upload.service.js +59 -6
  77. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  78. package/dist/course/lms-setting.controller.d.ts +2 -1
  79. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  80. package/dist/course/lms-setting.controller.js +4 -2
  81. package/dist/course/lms-setting.controller.js.map +1 -1
  82. package/dist/course/scorm12-schemas.d.ts +4 -0
  83. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  84. package/dist/course/scorm12-schemas.js +9 -0
  85. package/dist/course/scorm12-schemas.js.map +1 -0
  86. package/dist/enterprise/training/training-student.service.d.ts +51 -0
  87. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  88. package/dist/enterprise/training/training-student.service.js +217 -4
  89. package/dist/enterprise/training/training-student.service.js.map +1 -1
  90. package/dist/evaluation/evaluation.service.d.ts +18 -0
  91. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  92. package/dist/evaluation/evaluation.service.js +125 -0
  93. package/dist/evaluation/evaluation.service.js.map +1 -1
  94. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  95. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  96. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  97. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  98. package/dist/exam/exam.module.d.ts.map +1 -1
  99. package/dist/exam/exam.module.js +2 -1
  100. package/dist/exam/exam.module.js.map +1 -1
  101. package/dist/exam/exam.service.d.ts +21 -0
  102. package/dist/exam/exam.service.d.ts.map +1 -1
  103. package/dist/exam/exam.service.js +80 -0
  104. package/dist/exam/exam.service.js.map +1 -1
  105. package/dist/exam/question.controller.d.ts +27 -0
  106. package/dist/exam/question.controller.d.ts.map +1 -0
  107. package/dist/exam/question.controller.js +53 -0
  108. package/dist/exam/question.controller.js.map +1 -0
  109. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
  110. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  111. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
  112. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  113. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  114. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  115. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  116. package/dist/lms-commerce-access.subscriber.js +74 -0
  117. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  118. package/dist/lms.module.d.ts.map +1 -1
  119. package/dist/lms.module.js +6 -5
  120. package/dist/lms.module.js.map +1 -1
  121. package/dist/platforma/platforma-video.service.d.ts +39 -0
  122. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  123. package/dist/platforma/platforma-video.service.js +301 -0
  124. package/dist/platforma/platforma-video.service.js.map +1 -0
  125. package/dist/platforma/platforma.controller.d.ts +95 -1
  126. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  127. package/dist/platforma/platforma.controller.js +160 -2
  128. package/dist/platforma/platforma.controller.js.map +1 -1
  129. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  130. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  131. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  132. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  133. package/dist/student-xp/student-xp.controller.d.ts +15 -0
  134. package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
  135. package/dist/student-xp/student-xp.controller.js +24 -0
  136. package/dist/student-xp/student-xp.controller.js.map +1 -1
  137. package/dist/student-xp/student-xp.service.d.ts +16 -0
  138. package/dist/student-xp/student-xp.service.d.ts.map +1 -1
  139. package/dist/student-xp/student-xp.service.js +51 -1
  140. package/dist/student-xp/student-xp.service.js.map +1 -1
  141. package/hedhog/data/evaluation_topic.yaml +17 -0
  142. package/hedhog/data/menu.yaml +0 -17
  143. package/hedhog/data/queue_definition.yaml +48 -0
  144. package/hedhog/data/route.yaml +94 -124
  145. package/hedhog/data/setting_group.yaml +19 -19
  146. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
  147. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
  148. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  149. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  150. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +17 -15
  151. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
  152. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  153. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  154. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  155. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
  156. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
  157. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  158. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
  159. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  160. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  161. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  162. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  163. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +0 -1
  164. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  165. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
  166. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  167. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -0
  168. package/hedhog/frontend/messages/en.json +26 -28
  169. package/hedhog/frontend/messages/pt.json +26 -28
  170. package/hedhog/table/course_export.yaml +62 -0
  171. package/package.json +13 -9
  172. package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
  173. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  174. package/src/course/course-export-scorm12.service.ts +668 -0
  175. package/src/course/course-export.service.ts +280 -0
  176. package/src/course/course-structure.controller.ts +14 -2
  177. package/src/course/course-structure.service.ts +100 -7
  178. package/src/course/course-video-hls.service.ts +946 -0
  179. package/src/course/course.controller.ts +33 -19
  180. package/src/course/course.mcp-tools.ts +1 -1
  181. package/src/course/course.module.ts +11 -0
  182. package/src/course/course.service.ts +73 -60
  183. package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
  184. package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
  185. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  186. package/src/course/dto/create-course-export.dto.ts +56 -0
  187. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  188. package/src/course/lms-bulk-upload-automation.service.ts +153 -6
  189. package/src/course/lms-bulk-upload-infra.service.ts +39 -6
  190. package/src/course/lms-bulk-upload.controller.ts +32 -2
  191. package/src/course/lms-bulk-upload.service.ts +70 -7
  192. package/src/course/lms-setting.controller.ts +4 -2
  193. package/src/course/scorm12-schemas.ts +9 -0
  194. package/src/enterprise/training/training-student.service.ts +221 -2
  195. package/src/evaluation/evaluation.service.ts +123 -0
  196. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  197. package/src/exam/exam.module.ts +2 -1
  198. package/src/exam/exam.service.ts +86 -0
  199. package/src/exam/question.controller.ts +28 -0
  200. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
  201. package/src/lms-commerce-access.subscriber.ts +88 -0
  202. package/src/lms.module.ts +6 -5
  203. package/src/platforma/platforma-video.service.ts +346 -0
  204. package/src/platforma/platforma.controller.ts +95 -1
  205. package/src/platforma/platforma.service.ts +268 -268
  206. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  207. package/src/student-xp/student-xp.controller.ts +18 -2
  208. package/src/student-xp/student-xp.service.ts +84 -2
  209. package/hedhog/data/video_resolution_profile.yaml +0 -7
  210. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  211. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  212. package/hedhog/table/video_resolution_profile.yaml +0 -18
  213. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  214. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  215. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  216. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  217. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  218. package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
@@ -0,0 +1,668 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import { FileService } from '@hed-hog/core';
3
+ import { Inject, Injectable, forwardRef } from '@nestjs/common';
4
+ import * as archiver from 'archiver';
5
+ import { createWriteStream } from 'fs';
6
+ import { promises as fs } from 'fs';
7
+ import { join } from 'path';
8
+ import { finished } from 'stream/promises';
9
+ import { CourseExportService, VideoLessonWithFile } from './course-export.service';
10
+ import {
11
+ ADLCP_ROOTV1P2_XSD,
12
+ IMSCP_ROOTV1P1P2_XSD,
13
+ IMS_XML_XSD,
14
+ } from './scorm12-schemas';
15
+
16
+ export type ScormVisualOptions = {
17
+ primaryColor?: string;
18
+ fontFamily?: string;
19
+ fontSize?: string;
20
+ sidebarWidth?: number;
21
+ sidebarPosition?: string;
22
+ progressStyle?: string;
23
+ sidebarTheme?: string;
24
+ };
25
+
26
+ export type Scorm12BuildParams = {
27
+ courseId: number;
28
+ settings: { visual?: ScormVisualOptions };
29
+ workDir: string;
30
+ onProgress: (pct: number, msg: string) => Promise<void>;
31
+ };
32
+
33
+ type LessonData = {
34
+ id: number;
35
+ title: string;
36
+ duration: number | null;
37
+ src: string;
38
+ };
39
+
40
+ type ModuleData = {
41
+ id: number;
42
+ title: string;
43
+ order: number;
44
+ lessons: LessonData[];
45
+ };
46
+
47
+ type CourseData = {
48
+ title: string;
49
+ totalLessons: number;
50
+ modules: ModuleData[];
51
+ };
52
+
53
+ type ResolvedVisual = Required<ScormVisualOptions>;
54
+
55
+ const FONT_STACKS: Record<string, string> = {
56
+ system: `-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif`,
57
+ humanist: `Optima,Candara,'Noto Sans',source-sans-pro,sans-serif`,
58
+ serif: `Georgia,Cambria,'Times New Roman',serif`,
59
+ mono: `'Courier New',Courier,monospace`,
60
+ };
61
+
62
+ const FONT_SIZES: Record<string, { base: string; sm: string; xs: string }> = {
63
+ sm: { base: '.75rem', sm: '.68rem', xs: '.6rem' },
64
+ md: { base: '.82rem', sm: '.75rem', xs: '.65rem' },
65
+ lg: { base: '.92rem', sm: '.84rem', xs: '.72rem' },
66
+ };
67
+
68
+ @Injectable()
69
+ export class CourseExportScorm12Service {
70
+ constructor(
71
+ @Inject(forwardRef(() => PrismaService))
72
+ private readonly prisma: PrismaService,
73
+ @Inject(forwardRef(() => FileService))
74
+ private readonly fileService: FileService,
75
+ @Inject(forwardRef(() => CourseExportService))
76
+ private readonly courseExportService: CourseExportService,
77
+ ) {}
78
+
79
+ async buildPackage(params: Scorm12BuildParams): Promise<{
80
+ zipPath: string;
81
+ filename: string;
82
+ }> {
83
+ const { courseId, settings, workDir, onProgress } = params;
84
+ const visual = this.resolveVisual(settings.visual);
85
+
86
+ await onProgress(10, 'Carregando estrutura do curso...');
87
+
88
+ const course = await this.prisma.course.findUniqueOrThrow({
89
+ where: { id: courseId },
90
+ select: { id: true, title: true, name: true, description: true },
91
+ });
92
+ const courseTitle = course.title || course.name || `Curso ${courseId}`;
93
+ const lessons = await this.courseExportService.getCourseVideoLessons(courseId);
94
+
95
+ if (lessons.length === 0) {
96
+ throw new Error('O curso não possui aulas de vídeo para exportar.');
97
+ }
98
+
99
+ await onProgress(15, `${lessons.length} aulas de vídeo encontradas.`);
100
+
101
+ const filename = `${courseTitle.replace(/[^a-zA-Z0-9]/g, '_')}_SCORM12.zip`;
102
+ const zipPath = join(workDir, filename);
103
+
104
+ const output = createWriteStream(zipPath);
105
+ // level >= 1 obrigatório: com level 0 o archiver grava entradas STORED com
106
+ // data descriptors (streaming), que leitores Java (Rustici/SCORM Cloud) não processam.
107
+ const archive = archiver.create('zip', { zlib: { level: 1 } });
108
+ archive.pipe(output);
109
+
110
+ const videoSrcMap: Record<number, string> = {};
111
+ const embeddedLessonIds: number[] = [];
112
+ const total = lessons.length;
113
+
114
+ for (let i = 0; i < total; i++) {
115
+ const lesson = lessons[i];
116
+ const pct = Math.round(15 + ((i + 1) / total) * 55);
117
+ await onProgress(pct, `Processando aula ${i + 1}/${total}: ${lesson.title}`);
118
+
119
+ const bestFile = this.courseExportService.getBestVideoFile(lesson);
120
+ if (bestFile?.file_id) {
121
+ const videoTmpPath = join(workDir, `video_${lesson.id}.mp4`);
122
+ await this.fileService.downloadToPath(bestFile.file_id, videoTmpPath);
123
+ archive.file(videoTmpPath, { name: `assets/lesson_${lesson.id}.mp4` });
124
+ videoSrcMap[lesson.id] = `assets/lesson_${lesson.id}.mp4`;
125
+ embeddedLessonIds.push(lesson.id);
126
+ } else {
127
+ videoSrcMap[lesson.id] = this.getExternalVideoSrc(lesson);
128
+ }
129
+ }
130
+
131
+ const courseData = this.buildCourseData(courseTitle, lessons, videoSrcMap);
132
+
133
+ archive.append(Buffer.from(this.generateScormApiJs()), { name: 'SCORM12API.js' });
134
+ archive.append(Buffer.from(IMS_XML_XSD), { name: 'ims_xml.xsd' });
135
+ archive.append(Buffer.from(IMSCP_ROOTV1P1P2_XSD), { name: 'imscp_rootv1p1p2.xsd' });
136
+ archive.append(Buffer.from(ADLCP_ROOTV1P2_XSD), { name: 'adlcp_rootv1p2.xsd' });
137
+ archive.append(
138
+ Buffer.from(this.generateMetadataXml(courseTitle, course.description)),
139
+ { name: 'metadata.xml' },
140
+ );
141
+ archive.append(Buffer.from(this.generateShellHtml(courseData, visual)), { name: 'index.html' });
142
+
143
+ const manifestXml = this.generateManifest(courseTitle, embeddedLessonIds);
144
+ archive.append(Buffer.from(manifestXml), { name: 'imsmanifest.xml' });
145
+
146
+ await onProgress(80, 'Finalizando pacote ZIP...');
147
+ await archive.finalize();
148
+ await finished(output);
149
+
150
+ return { zipPath, filename };
151
+ }
152
+
153
+ private resolveVisual(v?: ScormVisualOptions): ResolvedVisual {
154
+ return {
155
+ primaryColor: v?.primaryColor || '#2563eb',
156
+ fontFamily: v?.fontFamily || 'system',
157
+ fontSize: v?.fontSize || 'md',
158
+ sidebarWidth: v?.sidebarWidth || 280,
159
+ sidebarPosition: v?.sidebarPosition || 'left',
160
+ progressStyle: v?.progressStyle || 'bar',
161
+ sidebarTheme: v?.sidebarTheme || 'light',
162
+ };
163
+ }
164
+
165
+ private buildCourseData(
166
+ courseTitle: string,
167
+ lessons: VideoLessonWithFile[],
168
+ videoSrcMap: Record<number, string>,
169
+ ): CourseData {
170
+ const sessionMap = new Map<number, ModuleData>();
171
+
172
+ for (const lesson of lessons) {
173
+ const sid = lesson.session.id;
174
+ if (!sessionMap.has(sid)) {
175
+ sessionMap.set(sid, {
176
+ id: sid,
177
+ title: lesson.session.title,
178
+ order: lesson.session.order,
179
+ lessons: [],
180
+ });
181
+ }
182
+ sessionMap.get(sid)!.lessons.push({
183
+ id: lesson.id,
184
+ title: lesson.title,
185
+ duration: lesson.duration_seconds,
186
+ src: videoSrcMap[lesson.id] || '',
187
+ });
188
+ }
189
+
190
+ const modules = Array.from(sessionMap.values()).sort((a, b) => a.order - b.order);
191
+ return { title: courseTitle, totalLessons: lessons.length, modules };
192
+ }
193
+
194
+ private getExternalVideoSrc(lesson: VideoLessonWithFile): string {
195
+ try {
196
+ const content = lesson.content ? JSON.parse(lesson.content) : {};
197
+ if (content.videoUrl) return content.videoUrl;
198
+ } catch {
199
+ // ignore parse error
200
+ }
201
+ return '';
202
+ }
203
+
204
+ private generateManifest(
205
+ courseTitle: string,
206
+ embeddedLessonIds: number[],
207
+ ): string {
208
+ const manifestId = `manifest-${Date.now()}`;
209
+ const assetFiles = embeddedLessonIds.length
210
+ ? embeddedLessonIds.map((id) => ` <file href="assets/lesson_${id}.mp4"/>`).join('\n') + '\n'
211
+ : '';
212
+
213
+ return `<?xml version="1.0" encoding="UTF-8"?>
214
+ <manifest identifier="${manifestId}" version="1"
215
+ xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
216
+ xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_rootv1p2"
217
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
218
+ xsi:schemaLocation="http://www.imsproject.org/xsd/imscp_rootv1p1p2 imscp_rootv1p1p2.xsd
219
+ http://www.adlnet.org/xsd/adlcp_rootv1p2 adlcp_rootv1p2.xsd">
220
+ <metadata>
221
+ <schema>ADL SCORM</schema>
222
+ <schemaversion>1.2</schemaversion>
223
+ <adlcp:location>metadata.xml</adlcp:location>
224
+ </metadata>
225
+ <organizations default="org_main">
226
+ <organization identifier="org_main">
227
+ <title>${this.escapeXml(courseTitle)}</title>
228
+ <item identifier="item_course" identifierref="resource_course">
229
+ <title>${this.escapeXml(courseTitle)}</title>
230
+ </item>
231
+ </organization>
232
+ </organizations>
233
+ <resources>
234
+ <resource identifier="resource_course" type="webcontent" adlcp:scormtype="sco" href="index.html">
235
+ <file href="index.html"/>
236
+ <file href="SCORM12API.js"/>
237
+ ${assetFiles} </resource>
238
+ </resources>
239
+ </manifest>`;
240
+ }
241
+
242
+ private generateMetadataXml(title: string, description: string | null): string {
243
+ return `<?xml version="1.0" encoding="UTF-8"?>
244
+ <lom xmlns="http://ltsc.ieee.org/xsd/LOM">
245
+ <general>
246
+ <title><string language="pt-BR">${this.escapeXml(title)}</string></title>
247
+ <description><string language="pt-BR">${this.escapeXml(description ?? '')}</string></description>
248
+ </general>
249
+ </lom>`;
250
+ }
251
+
252
+ private generateScormApiJs(): string {
253
+ return `(function(w){'use strict';
254
+ function find(win){
255
+ var d=0;
256
+ try{
257
+ while(!win.API&&win.parent&&win.parent!==win&&d<7){d++;win=win.parent;}
258
+ }catch(e){}
259
+ try{return win.API||null;}catch(e){return null;}
260
+ }
261
+ var real=null;
262
+ try{real=find(w)||(w.top&&w.top.opener?find(w.top.opener):null);}catch(e){}
263
+ if(real)return;
264
+ var d={'cmi.core.lesson_status':'not attempted','cmi.core.lesson_location':'',
265
+ 'cmi.core.score.raw':'','cmi.core.score.min':'0','cmi.core.score.max':'100',
266
+ 'cmi.core.session_time':'00:00:00','cmi.suspend_data':'',
267
+ 'cmi.core.student_id':'anonymous','cmi.core.student_name':'Aluno'};
268
+ w.API={
269
+ LMSInitialize:function(s){return'true';},
270
+ LMSFinish:function(s){return'true';},
271
+ LMSGetValue:function(e){return Object.prototype.hasOwnProperty.call(d,e)?String(d[e]):'';},
272
+ LMSSetValue:function(e,v){d[e]=v;return'true';},
273
+ LMSCommit:function(s){return'true';},
274
+ LMSGetLastError:function(){return'0';},
275
+ LMSGetErrorString:function(c){return'';},
276
+ LMSGetDiagnostic:function(c){return'';}
277
+ };
278
+ })(window);
279
+ `;
280
+ }
281
+
282
+ private generateShellHtml(course: CourseData, visual: ResolvedVisual): string {
283
+ const courseJson = JSON.stringify(course)
284
+ .replace(/</g, '\\u003c')
285
+ .replace(/>/g, '\\u003e')
286
+ .replace(/&/g, '\\u0026');
287
+
288
+ const isPie = visual.progressStyle === 'pie';
289
+ const isRtl = visual.sidebarPosition === 'right';
290
+ const isDark = visual.sidebarTheme === 'dark';
291
+
292
+ const pfil = '#16a34a';
293
+ const pbg = isDark ? '#334155' : '#e5e7eb';
294
+
295
+ const progressHtml = isPie
296
+ ? `<div class="donut-wrap">
297
+ <svg viewBox="0 0 40 40" width="52" height="52" style="flex-shrink:0">
298
+ <circle cx="20" cy="20" r="15.9" fill="none" stroke="${pbg}" stroke-width="3.8"/>
299
+ <circle id="donutFill" cx="20" cy="20" r="15.9" fill="none"
300
+ stroke="${pfil}" stroke-width="3.8"
301
+ stroke-dasharray="99.9" stroke-dashoffset="99.9"
302
+ transform="rotate(-90 20 20)"
303
+ style="transition:stroke-dashoffset .4s ease"/>
304
+ </svg>
305
+ <div class="donut-lbl">
306
+ <span id="pctLabel" class="donut-pct">0%</span>
307
+ <span class="donut-sub">COMPLETO</span>
308
+ </div>
309
+ </div>`
310
+ : `<div class="progress-info">
311
+ <span class="progress-label"><span class="progress-pct" id="pctLabel">0%</span>&nbsp;COMPLETO</span>
312
+ </div>
313
+ <div class="progress-track"><div class="progress-fill-el" id="progressBar"></div></div>`;
314
+
315
+ const menuBtnPos = isRtl ? 'right:10px;left:auto' : 'left:10px';
316
+
317
+ return `<!DOCTYPE html>
318
+ <html lang="pt-BR">
319
+ <head>
320
+ <meta charset="UTF-8">
321
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
322
+ <title>${this.escapeHtml(course.title)}</title>
323
+ <style>
324
+ ${this.generateCss(visual)}
325
+ </style>
326
+ </head>
327
+ <body>
328
+ <button class="menu-toggle" id="menuToggle" style="${menuBtnPos}">&#9776;</button>
329
+ <div class="overlay" id="overlay"></div>
330
+
331
+ <aside class="sidebar" id="sidebar">
332
+ <div class="sidebar-header">
333
+ <h1 class="course-title">${this.escapeHtml(course.title)}</h1>
334
+ ${progressHtml}
335
+ </div>
336
+ <nav class="lesson-nav" id="lessonNav"></nav>
337
+ </aside>
338
+
339
+ <div class="player-area">
340
+ <div class="video-wrap">
341
+ <video id="video" controls preload="metadata"></video>
342
+ <div class="empty-state" id="emptyState">Selecione uma aula para come&#231;ar</div>
343
+ </div>
344
+ <div class="player-footer">
345
+ <div class="footer-info">
346
+ <div class="footer-title" id="footerTitle">&nbsp;</div>
347
+ <div class="footer-meta" id="footerMeta"></div>
348
+ </div>
349
+ <div class="footer-nav">
350
+ <button class="nav-btn" id="prevBtn" disabled>&#8592; Anterior</button>
351
+ <button class="nav-btn primary" id="nextBtn" disabled>Pr&#243;xima &#8594;</button>
352
+ </div>
353
+ </div>
354
+ </div>
355
+
356
+ <script src="SCORM12API.js"></script>
357
+ <script>
358
+ ${this.generateJs(courseJson, visual.progressStyle)}
359
+ </script>
360
+ </body>
361
+ </html>`;
362
+ }
363
+
364
+ private generateCss(visual: ResolvedVisual): string {
365
+ const fontStack = FONT_STACKS[visual.fontFamily] || FONT_STACKS.system;
366
+ const fs = FONT_SIZES[visual.fontSize] || FONT_SIZES.md;
367
+ const isDark = visual.sidebarTheme === 'dark';
368
+ const isRtl = visual.sidebarPosition === 'right';
369
+ const pri = visual.primaryColor;
370
+ const pfil = '#16a34a';
371
+
372
+ const sbg = isDark ? '#1e293b' : '#ffffff';
373
+ const sTxt = isDark ? '#f1f5f9' : '#111827';
374
+ const sMuted = isDark ? '#94a3b8' : '#6b7280';
375
+ const bdr = isDark ? '#334155' : '#e5e7eb';
376
+ const modbg = isDark ? '#162032' : '#f9fafb';
377
+ const hoverBg = isDark ? '#243447' : '#f9fafb';
378
+ const activeBg= isDark ? '#1e3a5f' : '#eff6ff';
379
+ const pbg = isDark ? '#334155' : '#e5e7eb';
380
+ const idleBg = isDark ? '#2d3f52' : '#e5e7eb';
381
+ const idleClr = isDark ? '#94a3b8' : '#6b7280';
382
+ const scrollClr = isDark ? '#475569' : '#d1d5db';
383
+ const sBorder = isRtl
384
+ ? `border-left:1px solid ${bdr}`
385
+ : `border-right:1px solid ${bdr}`;
386
+
387
+ return ` *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
388
+ :root{--sw:${visual.sidebarWidth}px;--pri:${pri};--pfil:${pfil};--pbg:${pbg};--ftbg:#111827}
389
+ html,body{height:100vh;overflow:hidden}
390
+ body{font-family:${fontStack};display:flex;flex-direction:${isRtl ? 'row-reverse' : 'row'};background:#000;color:#111827;font-size:${fs.base}}
391
+ .sidebar{width:var(--sw);min-width:var(--sw);background:${sbg};${sBorder};display:flex;flex-direction:column;overflow:hidden;z-index:10;transition:transform .25s ease}
392
+ .sidebar-header{padding:18px 16px 14px;border-bottom:1px solid ${bdr};flex-shrink:0}
393
+ .course-title{font-size:${fs.base};font-weight:700;line-height:1.4;color:${sTxt};margin-bottom:12px}
394
+ .progress-info{margin-bottom:6px}
395
+ .progress-label{font-size:${fs.xs};font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:${sMuted}}
396
+ .progress-pct{color:${pfil}}
397
+ .progress-track{height:5px;background:${pbg};border-radius:3px;overflow:hidden}
398
+ .progress-fill-el{height:100%;background:${pfil};border-radius:3px;transition:width .4s ease;width:0%}
399
+ .donut-wrap{display:flex;align-items:center;gap:10px;margin-bottom:4px}
400
+ .donut-lbl{display:flex;flex-direction:column;gap:1px}
401
+ .donut-pct{font-size:${fs.sm};font-weight:700;color:${pfil}}
402
+ .donut-sub{font-size:${fs.xs};font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:${sMuted}}
403
+ .lesson-nav{flex:1;overflow-y:auto;scrollbar-width:thin;scrollbar-color:${scrollClr} transparent}
404
+ .lesson-nav::-webkit-scrollbar{width:4px}
405
+ .lesson-nav::-webkit-scrollbar-thumb{background:${scrollClr};border-radius:2px}
406
+ .module-group{border-bottom:1px solid ${bdr}}
407
+ .module-header{display:flex;align-items:center;gap:10px;padding:10px 16px;background:${modbg};cursor:pointer;user-select:none;transition:background .15s}
408
+ .module-header:hover{background:${isDark ? '#1e2d3e' : '#f3f4f6'}}
409
+ .module-info{flex:1;min-width:0}
410
+ .module-seq{font-size:${fs.xs};font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:${sMuted};margin-bottom:1px}
411
+ .module-name{font-size:${fs.sm};font-weight:600;color:${sTxt};white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
412
+ .module-chevron{font-size:.55rem;color:${sMuted};transition:transform .2s;flex-shrink:0}
413
+ .module-group.collapsed .module-lessons{display:none}
414
+ .module-group.collapsed .module-chevron{transform:rotate(-90deg)}
415
+ .lesson-item{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;border-left:3px solid transparent;transition:background .12s}
416
+ .lesson-item:hover{background:${hoverBg}}
417
+ .lesson-item.active{background:${activeBg};border-left-color:${pri}}
418
+ .lesson-icon{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;background:${idleBg};color:${idleClr};font-size:${fs.xs};font-weight:700}
419
+ .lesson-item.active .lesson-icon,.lesson-item.completed .lesson-icon{background:${pri};color:#fff}
420
+ .lesson-text{flex:1;min-width:0}
421
+ .lesson-title-el{font-size:${fs.sm};font-weight:500;color:${sTxt};line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
422
+ .lesson-dur{font-size:${fs.xs};color:${sMuted};margin-top:1px}
423
+ .player-area{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
424
+ .video-wrap{flex:1;display:flex;align-items:center;justify-content:center;background:#000;overflow:hidden;position:relative}
425
+ #video{width:100%;height:100%;object-fit:contain;display:none}
426
+ .empty-state{color:#4b5563;font-size:${fs.sm};text-align:center;position:absolute;pointer-events:none}
427
+ .player-footer{background:var(--ftbg);padding:10px 16px;display:flex;align-items:center;gap:12px;flex-shrink:0;border-top:1px solid #1f2937}
428
+ .footer-info{flex:1;min-width:0}
429
+ .footer-title{font-size:${fs.base};font-weight:600;color:#f9fafb;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
430
+ .footer-meta{font-size:${fs.xs};color:#9ca3af;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
431
+ .footer-nav{display:flex;gap:8px;flex-shrink:0}
432
+ .nav-btn{padding:6px 14px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#f3f4f6;font-size:${fs.sm};cursor:pointer;white-space:nowrap;transition:background .12s;font-family:inherit}
433
+ .nav-btn:hover:not(:disabled){background:#374151}
434
+ .nav-btn:disabled{opacity:.35;cursor:not-allowed}
435
+ .nav-btn.primary{background:${pri};border-color:${pri}}
436
+ .nav-btn.primary:hover:not(:disabled){filter:brightness(.85)}
437
+ .menu-toggle{display:none;position:fixed;top:10px;z-index:30;width:34px;height:34px;border-radius:8px;background:rgba(0,0,0,.75);color:#fff;border:none;cursor:pointer;font-size:1rem;align-items:center;justify-content:center}
438
+ .overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9}
439
+ @media(max-width:768px){
440
+ .menu-toggle{display:flex}
441
+ .sidebar{position:fixed;top:0;bottom:0;${isRtl ? 'right:0;left:auto' : 'left:0;right:auto'};transform:${isRtl ? 'translateX(100%)' : 'translateX(-100%)'}}
442
+ .sidebar.open{transform:translateX(0);box-shadow:${isRtl ? '-4px 0 24px rgba(0,0,0,.35)' : '4px 0 24px rgba(0,0,0,.35)'}}
443
+ .overlay.show{display:block}
444
+ }`;
445
+ }
446
+
447
+ private generateJs(courseJson: string, progressStyle: string): string {
448
+ const isPie = progressStyle === 'pie';
449
+
450
+ return ` 'use strict';
451
+ var COURSE=${courseJson};
452
+ var completed={};
453
+ var currentIdx=-1;
454
+ var allLessons=[];
455
+ var _api=null;
456
+
457
+ COURSE.modules.forEach(function(m){
458
+ m.lessons.forEach(function(l){l._mod=m.title;allLessons.push(l);});
459
+ });
460
+
461
+ function findApi(win){
462
+ var d=0;
463
+ try{
464
+ while(!win.API&&win.parent&&win.parent!==win&&d<7){
465
+ d++;win=win.parent;
466
+ }
467
+ }catch(e){}
468
+ try{return win.API||null;}catch(e){return null;}
469
+ }
470
+
471
+ function api(){
472
+ if(_api)return _api;
473
+ var found=findApi(window);
474
+ if(!found){
475
+ try{if(window.top&&window.top.opener)found=findApi(window.top.opener);}catch(e){}
476
+ }
477
+ return(_api=found||window.API||null);
478
+ }
479
+
480
+ function scorm(fn){
481
+ var a=api();if(!a)return null;
482
+ var args=Array.prototype.slice.call(arguments,1);
483
+ try{return a[fn].apply(a,args);}catch(e){return null;}
484
+ }
485
+
486
+ function loadProgress(){
487
+ var raw=scorm('LMSGetValue','cmi.suspend_data')||'';
488
+ try{var d=JSON.parse(raw||'{}');completed=d.completed||{};}
489
+ catch(e){completed={};}
490
+ }
491
+
492
+ function saveProgress(){
493
+ var count=Object.keys(completed).length;
494
+ var pct=COURSE.totalLessons>0?Math.round(count/COURSE.totalLessons*100):0;
495
+ scorm('LMSSetValue','cmi.suspend_data',JSON.stringify({completed:completed}));
496
+ scorm('LMSSetValue','cmi.core.score.raw',String(pct));
497
+ if(pct>=100){scorm('LMSSetValue','cmi.core.lesson_status','completed');}
498
+ else if(count>0){scorm('LMSSetValue','cmi.core.lesson_status','incomplete');}
499
+ scorm('LMSCommit','');
500
+ }
501
+
502
+ function fmtDur(s){
503
+ if(!s)return'';
504
+ var h=Math.floor(s/3600),m=Math.floor((s%3600)/60),sec=s%60;
505
+ if(h>0)return h+'h '+m+'min';
506
+ if(m>0&&sec>0)return m+'min '+sec+'s';
507
+ if(m>0)return m+' min';
508
+ return sec+'s';
509
+ }
510
+
511
+ function esc(s){
512
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
513
+ }
514
+
515
+ function updateProgress(){
516
+ var count=Object.keys(completed).length;
517
+ var pct=COURSE.totalLessons>0?Math.round(count/COURSE.totalLessons*100):0;
518
+ ${isPie
519
+ ? `var el=document.getElementById('donutFill');
520
+ if(el){var c=2*Math.PI*15.9;el.setAttribute('stroke-dashoffset',(c*(1-pct/100)).toFixed(2));}
521
+ document.getElementById('pctLabel').textContent=pct+'%';`
522
+ : `document.getElementById('pctLabel').textContent=pct+'%';
523
+ document.getElementById('progressBar').style.width=pct+'%';`
524
+ }
525
+ }
526
+
527
+ function buildSidebar(){
528
+ var nav=document.getElementById('lessonNav');
529
+ COURSE.modules.forEach(function(mod,mi){
530
+ var group=document.createElement('div');
531
+ group.className='module-group';
532
+ var hdr=document.createElement('div');
533
+ hdr.className='module-header';
534
+ hdr.innerHTML='<div class="module-info"><div class="module-seq">Módulo '+(mi+1)+'</div><div class="module-name">'+esc(mod.title)+'</div></div><span class="module-chevron">▼</span>';
535
+ hdr.addEventListener('click',function(){group.classList.toggle('collapsed');});
536
+ group.appendChild(hdr);
537
+ var wrap=document.createElement('div');
538
+ wrap.className='module-lessons';
539
+ mod.lessons.forEach(function(lesson){
540
+ var done=!!completed[lesson.id];
541
+ var item=document.createElement('div');
542
+ item.className='lesson-item'+(done?' completed':'');
543
+ item.setAttribute('data-lesson-id',lesson.id);
544
+ var dur=lesson.duration?fmtDur(lesson.duration):'';
545
+ item.innerHTML='<div class="lesson-icon">'+(done?'✓':'►')+'</div><div class="lesson-text"><div class="lesson-title-el">'+esc(lesson.title)+'</div>'+(dur?'<div class="lesson-dur">'+dur+'</div>':'')+'</div>';
546
+ item.addEventListener('click',function(){
547
+ var idx=allLessons.findIndex(function(l){return l.id===lesson.id;});
548
+ navigateTo(idx);
549
+ });
550
+ wrap.appendChild(item);
551
+ });
552
+ group.appendChild(wrap);
553
+ nav.appendChild(group);
554
+ });
555
+ }
556
+
557
+ function navigateTo(idx){
558
+ if(idx<0||idx>=allLessons.length)return;
559
+ if(currentIdx>=0&&idx!==currentIdx)markCompleted(allLessons[currentIdx].id);
560
+ currentIdx=idx;
561
+ var lesson=allLessons[idx];
562
+ var video=document.getElementById('video');
563
+ var empty=document.getElementById('emptyState');
564
+ if(lesson.src){
565
+ video.style.display='block';
566
+ empty.style.display='none';
567
+ if(video.getAttribute('src')!==lesson.src){video.setAttribute('src',lesson.src);video.load();}
568
+ }else{
569
+ video.pause();
570
+ video.removeAttribute('src');
571
+ video.style.display='none';
572
+ empty.textContent='Esta aula n\\u00e3o possui v\\u00eddeo dispon\\u00edvel';
573
+ empty.style.display='block';
574
+ }
575
+ document.getElementById('footerTitle').textContent=lesson.title;
576
+ var meta=[];
577
+ if(lesson._mod)meta.push(lesson._mod);
578
+ if(lesson.duration)meta.push(fmtDur(lesson.duration));
579
+ document.getElementById('footerMeta').textContent=meta.join(' · ');
580
+ document.getElementById('prevBtn').disabled=idx===0;
581
+ document.getElementById('nextBtn').disabled=idx===allLessons.length-1;
582
+ document.querySelectorAll('.lesson-item').forEach(function(el){el.classList.remove('active');});
583
+ var el=document.querySelector('[data-lesson-id="'+lesson.id+'"]');
584
+ if(el){
585
+ el.classList.add('active');
586
+ if(!completed[lesson.id])el.querySelector('.lesson-icon').textContent='►';
587
+ el.scrollIntoView({behavior:'smooth',block:'nearest'});
588
+ }
589
+ scorm('LMSSetValue','cmi.core.lesson_location',String(lesson.id));
590
+ scorm('LMSCommit','');
591
+ }
592
+
593
+ function markCompleted(lessonId){
594
+ if(completed[lessonId])return;
595
+ completed[lessonId]=true;
596
+ var el=document.querySelector('[data-lesson-id="'+lessonId+'"]');
597
+ if(el){el.classList.add('completed');el.querySelector('.lesson-icon').textContent='✓';}
598
+ updateProgress();
599
+ saveProgress();
600
+ }
601
+
602
+ var videoEl=document.getElementById('video');
603
+ videoEl.addEventListener('ended',function(){
604
+ if(currentIdx>=0){
605
+ markCompleted(allLessons[currentIdx].id);
606
+ if(currentIdx<allLessons.length-1)setTimeout(function(){navigateTo(currentIdx+1);},1500);
607
+ }
608
+ });
609
+
610
+ document.getElementById('prevBtn').addEventListener('click',function(){navigateTo(currentIdx-1);});
611
+ document.getElementById('nextBtn').addEventListener('click',function(){navigateTo(currentIdx+1);});
612
+
613
+ (function(){
614
+ var sidebar=document.getElementById('sidebar');
615
+ var overlay=document.getElementById('overlay');
616
+ document.getElementById('menuToggle').addEventListener('click',function(){
617
+ var open=sidebar.classList.toggle('open');
618
+ overlay.classList.toggle('show',open);
619
+ });
620
+ overlay.addEventListener('click',function(){
621
+ sidebar.classList.remove('open');
622
+ overlay.classList.remove('show');
623
+ });
624
+ })();
625
+
626
+ function init(){
627
+ scorm('LMSInitialize','');
628
+ loadProgress();
629
+ buildSidebar();
630
+ updateProgress();
631
+ var bookmark=scorm('LMSGetValue','cmi.core.lesson_location')||'';
632
+ var startIdx=0;
633
+ if(bookmark){
634
+ var bId=parseInt(bookmark,10);
635
+ var found=allLessons.findIndex(function(l){return l.id===bId;});
636
+ if(found>=0)startIdx=found;
637
+ }
638
+ if(allLessons.length>0)navigateTo(startIdx);
639
+ }
640
+
641
+ if(document.readyState==='loading'){
642
+ document.addEventListener('DOMContentLoaded',init);
643
+ }else{
644
+ init();
645
+ }
646
+
647
+ window.addEventListener('beforeunload',function(){
648
+ saveProgress();
649
+ scorm('LMSFinish','');
650
+ });`;
651
+ }
652
+
653
+ private escapeXml(str: string): string {
654
+ return str
655
+ .replace(/&/g, '&amp;')
656
+ .replace(/</g, '&lt;')
657
+ .replace(/>/g, '&gt;')
658
+ .replace(/"/g, '&quot;')
659
+ .replace(/'/g, '&apos;');
660
+ }
661
+
662
+ private escapeHtml(str: string): string {
663
+ return str
664
+ .replace(/&/g, '&amp;')
665
+ .replace(/</g, '&lt;')
666
+ .replace(/>/g, '&gt;');
667
+ }
668
+ }