';
- $file = $node['resourceFiles'] ? current($node['resourceFiles']) : null;
- $extension = '';
- if ($file) {
- $extension = pathinfo($file['title'], PATHINFO_EXTENSION);
- }
-
+ $file = !empty($node['resourceFiles']) ? current($node['resourceFiles']) : null;
$folder = $folderIcon;
- if ($node['resourceFiles']) {
+ if (!empty($node['resourceFiles'])) {
$link .= '
';
$link .= $icon;
$link .= '';
@@ -2273,10 +2270,10 @@ class=" '.$disableDrag.' list-group-item nested-'.$child['level'].'"
}
$link .= '
';
+ data_id="'.$node['id'].'"
+ data_type="'.$lpItemType.'"
+ class="moved ui-sortable-handle link_with_id"
+ >';
$link .= $folder.' ';
$link .= '';
$link .= cut(addslashes($node['title']), 30);
@@ -2290,17 +2287,25 @@ class="moved ui-sortable-handle link_with_id"
$em = Database::getManager();
$qb = $em
->createQueryBuilder()
- ->select('node')
+ ->select('node, files')
->from(ResourceNode::class, 'node')
->innerJoin('node.resourceType', 'type')
->innerJoin('node.resourceLinks', 'links')
->innerJoin('node.resourceFiles', 'files')
- ->addSelect('files')
+ ->innerJoin(
+ CDocument::class,
+ 'doc',
+ 'WITH',
+ 'doc.resourceNode = node'
+ )
->where('type = :type')
->andWhere('links.course = :course')
- ->setParameters(['type' => $type, 'course' => $course])
- ->orderBy('node.parent', 'ASC')
- ;
+ ->setParameters([
+ 'type' => $type,
+ 'course' => $course,
+ ])
+ ->orderBy('node.parent', 'ASC');
+
$sessionId = api_get_session_id();
if (empty($sessionId)) {
@@ -2308,18 +2313,46 @@ class="moved ui-sortable-handle link_with_id"
} else {
$qb
->andWhere('links.session = :session')
- ->setParameter('session', $sessionId)
- ;
+ ->setParameter('session', $sessionId);
+ }
+
+ if ($filterByFiletype !== null) {
+ if (is_array($filterByFiletype)) {
+ $qb->andWhere('doc.filetype IN (:filetypes)');
+ $qb->setParameter('filetypes', $filterByFiletype);
+ } else {
+ $qb->andWhere('doc.filetype = :filetype');
+ $qb->setParameter('filetype', $filterByFiletype);
+ }
}
if (!empty($filterByExtension)) {
$orX = $qb->expr()->orX();
foreach ($filterByExtension as $extension) {
- $orX->add($qb->expr()->like('file.originalName', ':'.$extension));
- $qb->setParameter($extension, '%'.$extension);
+ $paramName = 'ext_' . $extension;
+ $orX->add(
+ $qb->expr()->like(
+ 'LOWER(files.originalName)',
+ ':' . $paramName
+ )
+ );
+ $qb->setParameter($paramName, '%.' . strtolower($extension));
}
$qb->andWhere($orX);
}
+
+ if (!empty($excludeByExtension)) {
+ foreach ($excludeByExtension as $extension) {
+ $qb->andWhere(
+ $qb->expr()->notLike(
+ 'LOWER(files.originalName)',
+ ':exclude_' . $extension
+ )
+ );
+ $qb->setParameter('exclude_' . $extension, '%.' . strtolower($extension));
+ }
+ }
+
$query = $qb->getQuery();
return $nodeRepository->buildTree($query->getArrayResult(), $options);
diff --git a/public/main/lp/learnpath.class.php b/public/main/lp/learnpath.class.php
index 5d3e24701eb..61f8b9d8ed3 100644
--- a/public/main/lp/learnpath.class.php
+++ b/public/main/lp/learnpath.class.php
@@ -121,6 +121,7 @@ class learnpath
public $categoryId;
public $scormUrl;
public $entity;
+ public $auto_forward_video = 1;
public function __construct(CLp $entity = null, $course_info, $user_id)
{
@@ -159,6 +160,7 @@ public function __construct(CLp $entity = null, $course_info, $user_id)
$this->created_on = $entity->getCreatedOn()->format('Y-m-d H:i:s');
$this->modified_on = $entity->getModifiedOn()->format('Y-m-d H:i:s');
$this->ref = $entity->getRef();
+ $this->auto_forward_video = $entity->getAutoForwardVideo();
$this->categoryId = 0;
if ($entity->getCategory()) {
$this->categoryId = $entity->getCategory()->getIid();
@@ -5224,6 +5226,7 @@ public function display_item($lpItem, $msg = null, $show_actions = true)
$return .= $this->getSavedFinalItem();
break;
case TOOL_DOCUMENT:
+ case 'video':
case TOOL_READOUT_TEXT:
$repo = Container::getDocumentRepository();
/** @var CDocument $document */
@@ -5268,6 +5271,7 @@ public function display_edit_item($lpItem, $excludeExtraFields = [])
break;
case TOOL_LP_FINAL_ITEM:
case TOOL_DOCUMENT:
+ case 'video':
case TOOL_READOUT_TEXT:
$return .= $this->displayItemMenu($lpItem);
$return .= $this->displayDocumentForm('edit', $lpItem);
@@ -6014,6 +6018,7 @@ public function display_move_item($lpItem)
);
break;
case TOOL_DOCUMENT:
+ case 'video':
$return .= $this->displayItemMenu($lpItem);
$return .= $this->displayDocumentForm('move', $lpItem);
break;
@@ -6331,7 +6336,13 @@ public function get_documents($showInvisibleFiles = false)
null,
null,
$showInvisibleFiles,
- true
+ true,
+ false,
+ true,
+ false,
+ [],
+ [],
+ ['file', 'folder']
);
$form = new FormValidator(
@@ -6407,25 +6418,47 @@ public function get_documents($showInvisibleFiles = false)
;
$new = $this->displayDocumentForm('add', $lpItem);
- /*$lpItem = new CLpItem();
- $lpItem->setItemType(TOOL_READOUT_TEXT);
- $frmReadOutText = $this->displayDocumentForm('add');*/
-
+ $videosTree = $this->get_videos();
$headers = [
get_lang('Files'),
+ get_lang('Videos'),
get_lang('Create a new document'),
- //get_lang('Create read-out text'),
get_lang('Upload'),
];
return Display::tabs(
$headers,
- [$documentTree, $new, $form->returnForm()],
+ [$documentTree, $videosTree, $new, $form->returnForm()],
'subtab',
['class' => 'mt-2']
);
}
+ public function get_videos()
+ {
+ $sessionId = api_get_session_id();
+
+ $documentTree = DocumentManager::get_document_preview(
+ api_get_course_entity(),
+ $this->lp_id,
+ null,
+ $sessionId,
+ true,
+ null,
+ null,
+ false,
+ false,
+ false,
+ true,
+ false,
+ [],
+ [],
+ 'video'
+ );
+
+ return $documentTree ?: get_lang('No videos found');
+ }
+
/**
* Creates a list with all the exercises (quiz) in it.
*
@@ -7977,6 +8010,7 @@ public static function rl_get_resource_link_for_learnpath(
return api_get_path(WEB_CODE_PATH).
'lp/readout_text.php?&id='.$id.'&lp_id='.$learningPathId.'&'.$extraParams;
case TOOL_DOCUMENT:
+ case 'video':
$repo = Container::getDocumentRepository();
$document = $repo->find($rowItem->getPath());
if ($document) {
@@ -8157,6 +8191,7 @@ public static function rl_get_resource_name($course_code, $learningPathId, $id_i
break;
case 'dir':
case TOOL_DOCUMENT:
+ case 'video':
$title = $row_item['title'];
$output = '-';
if (!empty($title)) {
@@ -8883,4 +8918,42 @@ public function recalculateResultsForLp(int $userId): void
}
}
}
+
+ /**
+ * Returns the video player HTML for a video-type document LP item.
+ *
+ * @param int $lpItemId
+ * @param string $autostart
+ *
+ * @return string
+ */
+ public function getVideoPlayer(CDocument $document, string $autostart = 'true'): string
+ {
+ $resourceNode = $document->getResourceNode();
+ $resourceFile = $resourceNode?->getFirstResourceFile();
+
+ if (!$resourceNode || !$resourceFile) {
+ return '';
+ }
+
+ $resourceNodeRepository = Container::getResourceNodeRepository();
+ $videoUrl = $resourceNodeRepository->getResourceFileUrl($resourceNode);
+
+ if (empty($videoUrl)) {
+ return '';
+ }
+
+ $fileName = $resourceFile->getTitle();
+ $ext = pathinfo($fileName, PATHINFO_EXTENSION);
+ $mimeType = $resourceFile->getMimeType() ?: 'video/mp4';
+ $autoplayAttr = ($autostart === 'true') ? 'autoplay muted playsinline' : '';
+
+ $html = '';
+ $html .= '
+
';
+
+ return $html;
+ }
}
diff --git a/public/main/lp/learnpathItem.class.php b/public/main/lp/learnpathItem.class.php
index f6e5bd54d51..b15b301c037 100644
--- a/public/main/lp/learnpathItem.class.php
+++ b/public/main/lp/learnpathItem.class.php
@@ -174,9 +174,9 @@ public function __construct($id, $item_content = null)
}
/*
- // Xapian full text search does not work
+ // Xapian full text search does not work
// and if the option is activated it generates an error
- // So I comment this part of the code to avoid unnecesary errors
+ // So I comment this part of the code to avoid unnecesary errors
// Get search_did.
if ('true' === api_get_setting('search_enabled')) {
$tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
@@ -490,6 +490,7 @@ public function get_file_path($path_to_scorm_dir = '')
case 'dir':
return '';
case TOOL_DOCUMENT:
+ case 'video':
$table_doc = Database::get_course_table(TABLE_DOCUMENT);
$sql = 'SELECT path
FROM '.$table_doc.'
diff --git a/public/main/lp/lp_controller.php b/public/main/lp/lp_controller.php
index 4f9c2757fb3..886b296cbd3 100644
--- a/public/main/lp/lp_controller.php
+++ b/public/main/lp/lp_controller.php
@@ -3,6 +3,7 @@
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Framework\Container;
+use Chamilo\CourseBundle\Entity\CDocument;
use Chamilo\CourseBundle\Entity\CLp;
use Chamilo\CourseBundle\Entity\CLpItem;
use ChamiloSession as Session;
@@ -304,7 +305,7 @@
if (isset($_POST['submit_button']) && !empty($post_title)) {
Session::write('post_time', $_POST['post_time']);
- $directoryParentId = isset($_POST['directory_parent_id']) ? $_POST['directory_parent_id'] : 0;
+ $directoryParentId = $_POST['directory_parent_id'] ?? 0;
if (empty($directoryParentId) || '/' === $directoryParentId) {
$result = $oLP->generate_lp_folder($courseInfo);
@@ -326,8 +327,8 @@
$prerequisites = $_POST['prerequisites'] ?? '';
$maxTimeAllowed = $_POST['maxTimeAllowed'] ?? '';
- if (TOOL_DOCUMENT === $_POST['type']) {
- if (isset($_POST['path']) && isset($_GET['id']) && !empty($_GET['id'])) {
+ if (in_array($_POST['type'], [TOOL_DOCUMENT, 'video'])) {
+ if (isset($_POST['path']) && !empty($_GET['id'])) {
$document_id = $_POST['path'];
} else {
if ($_POST['content_lp']) {
@@ -341,6 +342,12 @@
}
}
+ $documentRepo = Database::getManager()->getRepository(CDocument::class);
+ $document = $documentRepo->find((int)$document_id);
+ if ($document && $document->getFiletype() === 'video') {
+ $type = 'video';
+ }
+
$oLP->add_item(
$parent,
$previous,
diff --git a/public/main/lp/lp_video_view.php b/public/main/lp/lp_video_view.php
new file mode 100644
index 00000000000..0f0c43e320b
--- /dev/null
+++ b/public/main/lp/lp_video_view.php
@@ -0,0 +1,46 @@
+items[$lpItemId] ?? null;
+
+if (!$lpItem) {
+ echo get_lang('Invalid LP Item');
+ exit;
+}
+
+$documentIid = (int) $lpItem->get_path();
+
+if (empty($documentIid)) {
+ echo get_lang('Document not found');
+ exit;
+}
+
+$em = Database::getManager();
+$document = $em->getRepository(CDocument::class)
+ ->find($documentIid);
+
+if (!$document instanceof CDocument) {
+ echo get_lang('Document not found');
+ exit;
+}
+
+$html = $oLP->getVideoPlayer($document, $autostart);
+
+echo $html;
diff --git a/public/main/lp/lp_view.php b/public/main/lp/lp_view.php
index 2ae04bd1fdc..ba1afe3e8bb 100644
--- a/public/main/lp/lp_view.php
+++ b/public/main/lp/lp_view.php
@@ -561,6 +561,13 @@
$template->assign('disable_js_in_lp_view', (int) ('true' === api_get_setting('lp.disable_js_in_lp_view')));
$template->assign('lp_preview_image', '

');
+if ('video' === $itemType) {
+ $src = api_get_path(WEB_CODE_PATH)
+ . "lp/lp_video_view.php?lp_id=$lp_id&lp_item_id=$lpCurrentItemId&" . api_get_cidreq();
+}
+$htmlHeadXtra[] = '';
$frameReady = Display::getFrameReadyBlock(
'#content_id, #content_id_blank',
$itemType,
diff --git a/public/main/lp/scorm_api.php b/public/main/lp/scorm_api.php
index 20810183463..8b9ba3e5512 100644
--- a/public/main/lp/scorm_api.php
+++ b/public/main/lp/scorm_api.php
@@ -204,6 +204,7 @@ function APIobject() {
olms.userlname = '';
olms.execute_stats = false;
olms.lms_lp_item_parents = '';
+olms.lms_auto_forward_video = auto_forward_video; ?>;
var courseUrl = '?cid='+olms.lms_course_id+'&sid='+olms.lms_session_id;
var statsUrl = 'lp_controller.php' + courseUrl + '&action=stats';
@@ -292,7 +293,7 @@ function LMSInitialize() {
'start_time': 0
};
- if (olms.lms_lp_type == 1 || olms.lms_item_type == 'asset' || olms.lms_item_type == 'document') {
+ if (olms.lms_lp_type == 1 || olms.lms_item_type == 'asset' || olms.lms_item_type == 'document' || olms.lms_item_type == 'video') {
params['start_time'] = 1;
//xajax_start_timer();
}
@@ -305,6 +306,16 @@ function LMSInitialize() {
async: false,
success:function(data) {
$('video:not(.skip), audio:not(.skip)').mediaelementplayer();
+ if (olms.lms_item_type === 'video') {
+ var cont_f = document.getElementById("content_id");
+ if (cont_f && (cont_f.contentDocument || cont_f.contentWindow.document).readyState === "complete") {
+ onIframeLoaded(cont_f);
+ } else {
+ $("#content_id").on("load", function() {
+ onIframeLoaded(this);
+ });
+ }
+ }
}
});
@@ -368,6 +379,96 @@ function LMSInitialize() {
}
}
+/**
+* Handles post-processing once the SCORM content iframe has loaded.
+* Specifically, detects video elements and attaches logic to
+* automatically display a MediaElement postroll and switch to
+* the next learning path item 10 seconds after the video ends.
+*
+* @param {HTMLIFrameElement} iframe - The iframe DOM element containing the SCORM content.
+*/
+function onIframeLoaded(iframe) {
+ var contentDocument = iframe.contentDocument || iframe.contentWindow.document;
+ var $video = $("video:not(.skip)", contentDocument);
+ if ($video.length > 0) {
+ $video.each(function() {
+ var videoElement = this;
+
+ // Create overlay DIV in iframe
+ var overlayHtml = `
+
+
+ `;
+
+ if (olms.lms_auto_forward_video == 1) {
+ overlayHtml += `
+
10 ...
+ `;
+ }
+
+ overlayHtml += `
+
+
+ `;
+
+ var $overlay = $(overlayHtml);
+ $(contentDocument.body).append($overlay);
+ videoElement.addEventListener("ended", function () {
+ $overlay.show();
+ if (olms.lms_auto_forward_video == 1) {
+ startPostrollCountdown(contentDocument);
+ } else {
+ console.log("Auto-forward disabled, waiting for user click.");
+ }
+ });
+
+ // Click on next button
+ $(contentDocument).on("click", "#postroll-next-btn", function () {
+ switch_item(olms.lms_item_id, olms.lms_next_item);
+ });
+ });
+ }
+}
+
+/**
+* Starts the countdown timer displayed in the postroll overlay.
+*
+* @param {Document} doc - The iframe's document where the countdown is displayed.
+*/
+function startPostrollCountdown(doc) {
+ var seconds = 10;
+ var interval = setInterval(function() {
+ seconds--;
+ $(doc).find("#postroll-counter").text(seconds);
+ if (seconds <= 0) {
+ clearInterval(interval);
+ switch_item(olms.lms_item_id, olms.lms_next_item);
+ }
+ }, 1000);
+}
+
/**
* Twin sister of LMSInitialize(). Only provided for backwards compatibility.
* this is the initialize function of all APIobjects
@@ -1115,7 +1216,7 @@ function addListeners(){
return;
}
//assign event handlers to objects
- if (olms.lms_lp_type==1 || olms.lms_item_type=='asset' || olms.lms_item_type == 'document') {
+ if (olms.lms_lp_type==1 || olms.lms_item_type=='asset' || olms.lms_item_type == 'document' || olms.lms_item_type == 'video') {
logit_lms('Chamilo LP or asset');
//if this path is a Chamilo learnpath, then start manual save
//when something is loaded in there
@@ -1171,7 +1272,7 @@ function lms_save_asset() {
}
//For scorms do not show stats
- if (olms.lms_lp_type == 2 && olms.lms_lp_item_type != 'document') {
+ if (olms.lms_lp_type == 2 && olms.lms_lp_item_type != 'document' && olms.lms_lp_item_type != 'video') {
olms.execute_stats = false;
}
@@ -1179,7 +1280,7 @@ function lms_save_asset() {
olms.execute_stats = true;
}
- if (olms.lms_lp_type == 1 || olms.lms_item_type == 'asset' || olms.lms_item_type == 'document') {
+ if (olms.lms_lp_type == 1 || olms.lms_item_type == 'asset' || olms.lms_item_type == 'document' || olms.lms_item_type == 'video') {
logit_lms('lms_save_asset');
logit_lms('execute_stats :'+ olms.execute_stats);
xajax_save_item(
@@ -1685,7 +1786,7 @@ function switch_item(current_item, next_item)
} ?>
let startTime = 0;
- if (olms.lms_lp_type==1 || olms.lms_item_type == 'asset' || olms.lms_item_type == 'document') {
+ if (olms.lms_lp_type==1 || olms.lms_item_type == 'asset' || olms.lms_item_type == 'document' || olms.lms_item_type == 'video') {
startTime = 1;
//xajax_start_timer();
}
@@ -1902,7 +2003,7 @@ function xajax_save_item(
params += '&switch_next='+switchNext;
params += '&load_nav='+loadNav;
- if (olms.lms_lp_type == 1 || item_type == 'document' || item_type == 'asset') {
+ if (olms.lms_lp_type == 1 || item_type == 'document' || item_type == 'video' || item_type == 'asset') {
logit_lms('xajax_save_item with params:' + params, 3);
return $.ajax({
type:"POST",
diff --git a/src/CoreBundle/Controller/Api/BaseResourceFileAction.php b/src/CoreBundle/Controller/Api/BaseResourceFileAction.php
index 74b12f7b50d..3979ce13f66 100644
--- a/src/CoreBundle/Controller/Api/BaseResourceFileAction.php
+++ b/src/CoreBundle/Controller/Api/BaseResourceFileAction.php
@@ -383,9 +383,20 @@ public function handleCreateFileRequest(
$resource->setResourceLinkArray($resourceLinkList);
}
+ // Detect if file is a video
+ $filetypeResult = $fileType;
+
+ if (isset($uploadedFile) && $uploadedFile instanceof UploadedFile) {
+ $mimeType = $uploadedFile->getMimeType();
+ if (str_starts_with($mimeType, 'video/')) {
+ $filetypeResult = 'video';
+ $comment = trim($comment . ' [video]');
+ }
+ }
+
return [
'title' => $title,
- 'filetype' => $fileType,
+ 'filetype' => $filetypeResult,
'comment' => $comment,
];
}
@@ -566,6 +577,14 @@ private function saveZipContentsAsDocuments(array $folderStructure, EntityManage
$fileName
);
+ $mimeType = $uploadedFile->getMimeType();
+ if (str_starts_with($mimeType, 'video/')) {
+ $document->setFiletype('video');
+ $document->setComment('[video]');
+ } else {
+ $document->setFiletype('file');
+ }
+
$document->setUploadFile($uploadedFile);
$em->persist($document);
$em->flush();
diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20201210100010.php b/src/CoreBundle/Migrations/Schema/V200/Version20201210100010.php
new file mode 100644
index 00000000000..70e12ce2e56
--- /dev/null
+++ b/src/CoreBundle/Migrations/Schema/V200/Version20201210100010.php
@@ -0,0 +1,28 @@
+addSql('ALTER TABLE c_lp ADD auto_forward_video TINYINT(1) DEFAULT 0 NOT NULL;');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE c_lp DROP COLUMN auto_forward_video;');
+ }
+}
diff --git a/src/CoreBundle/Resources/views/LearnPath/view.html.twig b/src/CoreBundle/Resources/views/LearnPath/view.html.twig
index 7477e2e04bc..8ea76edaf42 100644
--- a/src/CoreBundle/Resources/views/LearnPath/view.html.twig
+++ b/src/CoreBundle/Resources/views/LearnPath/view.html.twig
@@ -4,7 +4,8 @@
{% block content %}
{% autoescape false %}
-
+
{% if show_left_column == 1 %}