mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-02-04 06:22:59 -05:00
all!
This commit is contained in:
@@ -1 +1 @@
|
|||||||
VERSION = "8.07"
|
VERSION = "8.08"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function MediaItem(props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const finalClassname = containerClassname +
|
const finalClassname = containerClassname +
|
||||||
(props.showSelection && !isEmbedMode ? ' with-selection' : '') +
|
(props.showSelection ? ' with-selection' : '') +
|
||||||
(props.isSelected ? ' selected' : '') +
|
(props.isSelected ? ' selected' : '') +
|
||||||
(props.hasAnySelection || isEmbedMode ? ' has-any-selection' : '');
|
(props.hasAnySelection || isEmbedMode ? ' has-any-selection' : '');
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function MediaItemAudio(props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const finalClassname = containerClassname +
|
const finalClassname = containerClassname +
|
||||||
(props.showSelection && !isEmbedMode ? ' with-selection' : '') +
|
(props.showSelection ? ' with-selection' : '') +
|
||||||
(props.isSelected ? ' selected' : '') +
|
(props.isSelected ? ' selected' : '') +
|
||||||
(props.hasAnySelection || isEmbedMode ? ' has-any-selection' : '');
|
(props.hasAnySelection || isEmbedMode ? ' has-any-selection' : '');
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export function MediaItemVideo(props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const finalClassname = containerClassname +
|
const finalClassname = containerClassname +
|
||||||
(props.showSelection && !isEmbedMode ? ' with-selection' : '') +
|
(props.showSelection ? ' with-selection' : '') +
|
||||||
(props.isSelected ? ' selected' : '') +
|
(props.isSelected ? ' selected' : '') +
|
||||||
(props.hasAnySelection || isEmbedMode ? ' has-any-selection' : '');
|
(props.hasAnySelection || isEmbedMode ? ' has-any-selection' : '');
|
||||||
|
|
||||||
|
|||||||
1745
frontend/yarn.lock
1745
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
62
lms-plugins/mediacms-moodle/README.md
Normal file
62
lms-plugins/mediacms-moodle/README.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# MediaCMS for Moodle
|
||||||
|
|
||||||
|
This package provides the integration between MediaCMS and Moodle (versions 4.x and 5.x).
|
||||||
|
It consists of two components that work together to provide a seamless video experience:
|
||||||
|
|
||||||
|
1. **Filter (filter_mediacms):** Handles the display of videos using secure LTI 1.3 launches. It also provides the "Auto-convert" feature to turn URLs into players.
|
||||||
|
2. **Editor Plugin (tiny_mediacms):** Adds a "Insert MediaCMS Video" button to the TinyMCE editor, allowing users to select videos from the MediaCMS library or paste URLs.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This package is distributed as a single repository but contains two standard Moodle plugins that must be installed in their respective directories.
|
||||||
|
|
||||||
|
### 1. Copy Files
|
||||||
|
|
||||||
|
Copy the directories into your Moodle installation as follows:
|
||||||
|
|
||||||
|
* Copy `filter/mediacms` to `YOUR_MOODLE_ROOT/filter/mediacms`.
|
||||||
|
* Copy `tiny/mediacms` to `YOUR_MOODLE_ROOT/lib/editor/tiny/plugins/mediacms`.
|
||||||
|
|
||||||
|
### 2. Set Permissions
|
||||||
|
|
||||||
|
Ensure the web server user (typically `www-data`) has ownership of the new directories:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example for Ubuntu/Debian systems
|
||||||
|
chown -R www-data:www-data YOUR_MOODLE_ROOT/filter/mediacms
|
||||||
|
chown -R www-data:www-data YOUR_MOODLE_ROOT/lib/editor/tiny/plugins/mediacms
|
||||||
|
chmod -R 755 YOUR_MOODLE_ROOT/filter/mediacms
|
||||||
|
chmod -R 755 YOUR_MOODLE_ROOT/lib/editor/tiny/plugins/mediacms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Install Plugins
|
||||||
|
|
||||||
|
1. Log in to Moodle as an Administrator.
|
||||||
|
2. Go to **Site administration > Notifications**.
|
||||||
|
3. Follow the prompts to upgrade the database and install the new plugins.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All configuration is centralized in the Filter settings. You do **not** need to configure the TinyMCE plugin separately.
|
||||||
|
|
||||||
|
1. Go to **Site administration > Plugins > Filters > Manage filters**.
|
||||||
|
2. Enable **MediaCMS** (set it to "On").
|
||||||
|
3. Click **Settings** next to MediaCMS.
|
||||||
|
4. **MediaCMS URL:** Enter the base URL of your MediaCMS instance (e.g., `https://video.example.com`).
|
||||||
|
5. **LTI Tool:** Select the External Tool configuration that corresponds to MediaCMS.
|
||||||
|
* *Note:* You must first create an LTI 1.3 External Tool in *Site administration > Plugins > Activity modules > External tool > Manage tools*.
|
||||||
|
6. **Auto-convert:** Check "Enable auto-convert" if you want plain text URLs (e.g., `https://video.example.com/view?m=xyz`) to automatically become video players.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### For Teachers (Editor)
|
||||||
|
|
||||||
|
1. In any text editor (TinyMCE), click the **MediaCMS** icon (or "Insert MediaCMS Video" from the Insert menu).
|
||||||
|
2. You can:
|
||||||
|
* **Paste a URL:** Paste a View or Embed URL.
|
||||||
|
* **Video Library:** Click the "Video Library" tab to browse and select videos (requires LTI Deep Linking configuration).
|
||||||
|
3. The video will appear as a placeholder or iframe in the editor.
|
||||||
|
|
||||||
|
### For Students (Display)
|
||||||
|
|
||||||
|
When content is viewed, the Filter will ensure the video is loaded securely via LTI 1.3, authenticating the user with MediaCMS automatically.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
namespace filter_mediacms\privacy;
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
class provider implements \core_privacy\local\metadata\null_provider {
|
||||||
|
public static function get_reason(): string {
|
||||||
|
return 'privacy:metadata';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace filter_mediacms;
|
||||||
|
|
||||||
|
use moodle_url;
|
||||||
|
use html_writer;
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MediaCMS text filter.
|
||||||
|
*
|
||||||
|
* @package filter_mediacms
|
||||||
|
* @copyright 2026 MediaCMS
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
class text_filter extends \core_filters\text_filter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter method.
|
||||||
|
*
|
||||||
|
* @param string $text The text to filter.
|
||||||
|
* @param array $options Filter options.
|
||||||
|
* @return string The filtered text.
|
||||||
|
*/
|
||||||
|
public function filter($text, array $options = array()) {
|
||||||
|
if (!is_string($text) or empty($text)) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
|
||||||
|
if (empty($mediacmsurl)) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newtext = $text;
|
||||||
|
|
||||||
|
// 1. Handle [mediacms:TOKEN] tag
|
||||||
|
$pattern_tag = '/\[mediacms:([a-zA-Z0-9]+)\]/';
|
||||||
|
$newtext = preg_replace_callback($pattern_tag, [$this, 'callback_tag'], $newtext);
|
||||||
|
|
||||||
|
// 2. Handle Auto-convert URLs if enabled
|
||||||
|
if (get_config('filter_mediacms', 'enableautoconvert')) {
|
||||||
|
// Regex for MediaCMS view URLs: https://domain/view?m=TOKEN
|
||||||
|
// We need to be careful to match the configured domain
|
||||||
|
$parsed_url = parse_url($mediacmsurl);
|
||||||
|
$host = preg_quote($parsed_url['host'] ?? '', '/');
|
||||||
|
$scheme = preg_quote($parsed_url['scheme'] ?? 'https', '/');
|
||||||
|
|
||||||
|
// Allow http or https, and optional path prefix
|
||||||
|
$path_prefix = preg_quote(rtrim($parsed_url['path'] ?? '', '/'), '/');
|
||||||
|
|
||||||
|
// Pattern: https://HOST/PREFIX/view?m=TOKEN
|
||||||
|
// Also handle /embed?m=TOKEN
|
||||||
|
$pattern_url = '/(' . $scheme . ':\/\/' . $host . $path_prefix . '\/(view|embed)\?m=([a-zA-Z0-9]+)(?:&[^\s<]*)?)/';
|
||||||
|
|
||||||
|
$newtext = preg_replace_callback($pattern_url, [$this, 'callback_url'], $newtext);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $newtext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for [mediacms:TOKEN]
|
||||||
|
*/
|
||||||
|
public function callback_tag($matches) {
|
||||||
|
return $this->generate_iframe($matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for URLs
|
||||||
|
*/
|
||||||
|
public function callback_url($matches) {
|
||||||
|
// matches[1] is full URL, matches[3] is token
|
||||||
|
$token = $matches[3];
|
||||||
|
return $this->generate_iframe($token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the Iframe pointing to launch.php
|
||||||
|
*/
|
||||||
|
private function generate_iframe($token) {
|
||||||
|
global $CFG, $COURSE;
|
||||||
|
|
||||||
|
$width = get_config('filter_mediacms', 'iframewidth') ?: 960;
|
||||||
|
$height = get_config('filter_mediacms', 'iframeheight') ?: 540;
|
||||||
|
$courseid = $COURSE->id ?? 0;
|
||||||
|
|
||||||
|
$launchurl = new moodle_url('/filter/mediacms/launch.php', [
|
||||||
|
'token' => $token,
|
||||||
|
'courseid' => $courseid,
|
||||||
|
'width' => $width,
|
||||||
|
'height' => $height
|
||||||
|
]);
|
||||||
|
|
||||||
|
$iframe = html_writer::tag('iframe', '', [
|
||||||
|
'src' => $launchurl->out(false),
|
||||||
|
'width' => $width,
|
||||||
|
'height' => $height,
|
||||||
|
'frameborder' => 0,
|
||||||
|
'allowfullscreen' => 'allowfullscreen',
|
||||||
|
'class' => 'mediacms-embed',
|
||||||
|
'title' => 'MediaCMS Video'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $iframe;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
lms-plugins/mediacms-moodle/filter/mediacms/db/install.php
Normal file
41
lms-plugins/mediacms-moodle/filter/mediacms/db/install.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post-installation hook.
|
||||||
|
*/
|
||||||
|
function xmldb_filter_mediacms_install() {
|
||||||
|
global $CFG, $DB;
|
||||||
|
require_once($CFG->libdir . '/filterlib.php');
|
||||||
|
|
||||||
|
// 1. Enable the filter globally.
|
||||||
|
filter_set_global_state('filter_mediacms', TEXTFILTER_ON);
|
||||||
|
|
||||||
|
// 2. Move to top priority (lowest sortorder).
|
||||||
|
// Get all global active filters.
|
||||||
|
$filters = $DB->get_records('filter_active', ['contextid' => SYSCONTEXTID], 'sortorder ASC', 'filter, id, sortorder');
|
||||||
|
|
||||||
|
// If we are already the only one or something failed, stop.
|
||||||
|
if (empty($filters)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the new order: mediacms first, then everyone else (excluding mediacms if present).
|
||||||
|
$sortedfilters = ['filter_mediacms'];
|
||||||
|
foreach ($filters as $filtername => $record) {
|
||||||
|
if ($filtername !== 'filter_mediacms') {
|
||||||
|
$sortedfilters[] = $filtername;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back the new sort orders.
|
||||||
|
$sortorder = 1;
|
||||||
|
foreach ($sortedfilters as $filtername) {
|
||||||
|
if ($record = $DB->get_record('filter_active', ['filter' => $filtername, 'contextid' => SYSCONTEXTID])) {
|
||||||
|
$record->sortorder = $sortorder;
|
||||||
|
$DB->update_record('filter_active', $record);
|
||||||
|
$sortorder++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
$string['filtername'] = 'MediaCMS';
|
||||||
|
$string['pluginname'] = 'MediaCMS';
|
||||||
|
$string['mediacmsurl'] = 'MediaCMS URL';
|
||||||
|
$string['mediacmsurl_desc'] = 'The base URL of your MediaCMS instance (e.g., https://video.example.com).';
|
||||||
|
$string['ltitoolid'] = 'LTI Tool';
|
||||||
|
$string['ltitoolid_desc'] = 'Select the External Tool configuration for MediaCMS. If "Auto-detect" is selected, the plugin will try to find a tool matching the MediaCMS URL.';
|
||||||
|
$string['noltitoolsfound'] = 'No LTI tools found';
|
||||||
|
$string['iframewidth'] = 'Default Width';
|
||||||
|
$string['iframewidth_desc'] = 'Default width for embedded videos (pixels).';
|
||||||
|
$string['iframeheight'] = 'Default Height';
|
||||||
|
$string['iframeheight_desc'] = 'Default height for embedded videos (pixels).';
|
||||||
|
$string['enableautoconvert'] = 'Auto-convert URLs';
|
||||||
|
$string['enableautoconvert_desc'] = 'Automatically convert MediaCMS URLs (e.g., /view?m=xyz) in text to embedded players.';
|
||||||
|
$string['privacy:metadata'] = 'The MediaCMS filter does not store any personal data.';
|
||||||
101
lms-plugins/mediacms-moodle/filter/mediacms/launch.php
Normal file
101
lms-plugins/mediacms-moodle/filter/mediacms/launch.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* LTI Launch for MediaCMS Filter
|
||||||
|
*
|
||||||
|
* @package filter_mediacms
|
||||||
|
* @copyright 2026 MediaCMS
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/../../config.php');
|
||||||
|
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
||||||
|
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||||
|
|
||||||
|
global $SITE, $DB, $PAGE, $OUTPUT, $CFG;
|
||||||
|
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
$mediatoken = required_param('token', PARAM_ALPHANUMEXT);
|
||||||
|
$courseid = optional_param('courseid', 0, PARAM_INT);
|
||||||
|
$height = optional_param('height', 0, PARAM_INT);
|
||||||
|
$width = optional_param('width', 0, PARAM_INT);
|
||||||
|
|
||||||
|
// Get configuration
|
||||||
|
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
|
||||||
|
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
|
||||||
|
$defaultwidth = get_config('filter_mediacms', 'iframewidth') ?: 960;
|
||||||
|
$defaultheight = get_config('filter_mediacms', 'iframeheight') ?: 540;
|
||||||
|
|
||||||
|
if (empty($width)) {
|
||||||
|
$width = $defaultwidth;
|
||||||
|
}
|
||||||
|
if (empty($height)) {
|
||||||
|
$height = $defaultheight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($mediacmsurl)) {
|
||||||
|
die('MediaCMS URL not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool Selection Logic
|
||||||
|
$type = false;
|
||||||
|
if (!empty($ltitoolid)) {
|
||||||
|
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$type) {
|
||||||
|
die('LTI tool not found or not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up context
|
||||||
|
if ($courseid && $courseid != SITEID) {
|
||||||
|
$context = context_course::instance($courseid);
|
||||||
|
$course = get_course($courseid);
|
||||||
|
} else {
|
||||||
|
$context = context_system::instance();
|
||||||
|
$course = $SITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up page
|
||||||
|
$PAGE->set_url(new moodle_url('/filter/mediacms/launch.php', [
|
||||||
|
'token' => $mediatoken,
|
||||||
|
'courseid' => $courseid,
|
||||||
|
'width' => $width,
|
||||||
|
'height' => $height
|
||||||
|
]));
|
||||||
|
$PAGE->set_context($context);
|
||||||
|
$PAGE->set_pagelayout('embedded');
|
||||||
|
$PAGE->set_title('MediaCMS');
|
||||||
|
|
||||||
|
// Create a dummy LTI instance object
|
||||||
|
$instance = new stdClass();
|
||||||
|
$instance->id = 0;
|
||||||
|
$instance->course = $course->id;
|
||||||
|
$instance->typeid = $type->id;
|
||||||
|
$instance->name = 'MediaCMS Video';
|
||||||
|
$instance->instructorchoiceacceptgrades = 0;
|
||||||
|
$instance->grade = 0;
|
||||||
|
$instance->instructorchoicesendname = 1;
|
||||||
|
$instance->instructorchoicesendemailaddr = 1;
|
||||||
|
$instance->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
|
||||||
|
|
||||||
|
// Pass the token in custom parameters
|
||||||
|
// MediaCMS expects 'media_friendly_token' to identify the video
|
||||||
|
$instance->instructorcustomparameters = "media_friendly_token=" . $mediatoken;
|
||||||
|
|
||||||
|
// Get type config
|
||||||
|
$typeconfig = lti_get_type_type_config($type->id);
|
||||||
|
|
||||||
|
// Initiate LTI Login
|
||||||
|
$content = lti_initiate_login($course->id, 0, $instance, $typeconfig, null, 'MediaCMS Video');
|
||||||
|
|
||||||
|
// Inject media_token as a hidden field for OIDC flow state if needed
|
||||||
|
// This ensures the token survives the OIDC roundtrip if the provider supports it
|
||||||
|
// Standard LTI 1.3 passes it via Custom Claims (instructorcustomparameters) which is handled above.
|
||||||
|
// However, the original plugin also injected it into the form. We'll keep it for safety.
|
||||||
|
$hidden_field = '<input type="hidden" name="media_token" value="' . htmlspecialchars($mediatoken, ENT_QUOTES) . '" />';
|
||||||
|
$content = str_replace('</form>', $hidden_field . '</form>', $content);
|
||||||
|
|
||||||
|
echo $OUTPUT->header();
|
||||||
|
echo $content;
|
||||||
|
echo $OUTPUT->footer();
|
||||||
57
lms-plugins/mediacms-moodle/filter/mediacms/settings.php
Normal file
57
lms-plugins/mediacms-moodle/filter/mediacms/settings.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
defined('MOODLE_INTERNAL') || die;
|
||||||
|
|
||||||
|
if ($ADMIN->fulltree) {
|
||||||
|
// MediaCMS URL
|
||||||
|
$settings->add(new admin_setting_configtext(
|
||||||
|
'filter_mediacms/mediacmsurl',
|
||||||
|
get_string('mediacmsurl', 'filter_mediacms'),
|
||||||
|
get_string('mediacmsurl_desc', 'filter_mediacms'),
|
||||||
|
'https://lti.mediacms.io',
|
||||||
|
PARAM_URL
|
||||||
|
));
|
||||||
|
|
||||||
|
// LTI Tool Selector
|
||||||
|
$ltioptions = [0 => get_string('autodetect', 'filter_mediacms')];
|
||||||
|
try {
|
||||||
|
$tools = $DB->get_records('lti_types', null, 'name ASC', 'id, name, baseurl');
|
||||||
|
foreach ($tools as $tool) {
|
||||||
|
$ltioptions[$tool->id] = $tool->name . ' (' . $tool->baseurl . ')';
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Database might not be ready during install
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings->add(new admin_setting_configselect(
|
||||||
|
'filter_mediacms/ltitoolid',
|
||||||
|
get_string('ltitoolid', 'filter_mediacms'),
|
||||||
|
get_string('ltitoolid_desc', 'filter_mediacms'),
|
||||||
|
0,
|
||||||
|
$ltioptions
|
||||||
|
));
|
||||||
|
|
||||||
|
// Dimensions
|
||||||
|
$settings->add(new admin_setting_configtext(
|
||||||
|
'filter_mediacms/iframewidth',
|
||||||
|
get_string('iframewidth', 'filter_mediacms'),
|
||||||
|
get_string('iframewidth_desc', 'filter_mediacms'),
|
||||||
|
'960',
|
||||||
|
PARAM_INT
|
||||||
|
));
|
||||||
|
|
||||||
|
$settings->add(new admin_setting_configtext(
|
||||||
|
'filter_mediacms/iframeheight',
|
||||||
|
get_string('iframeheight', 'filter_mediacms'),
|
||||||
|
get_string('iframeheight_desc', 'filter_mediacms'),
|
||||||
|
'540',
|
||||||
|
PARAM_INT
|
||||||
|
));
|
||||||
|
|
||||||
|
// Auto-convert
|
||||||
|
$settings->add(new admin_setting_configcheckbox(
|
||||||
|
'filter_mediacms/enableautoconvert',
|
||||||
|
get_string('enableautoconvert', 'filter_mediacms'),
|
||||||
|
get_string('enableautoconvert_desc', 'filter_mediacms'),
|
||||||
|
1
|
||||||
|
));
|
||||||
|
}
|
||||||
8
lms-plugins/mediacms-moodle/filter/mediacms/version.php
Normal file
8
lms-plugins/mediacms-moodle/filter/mediacms/version.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
$plugin->version = 2026020100;
|
||||||
|
$plugin->requires = 2024100700; // Requires Moodle 4.5+
|
||||||
|
$plugin->component = 'filter_mediacms';
|
||||||
|
$plugin->maturity = MATURITY_STABLE;
|
||||||
|
$plugin->release = 'v1.0.0';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
import { getTinyMCE } from 'editor_tiny/loader';
|
||||||
|
import { component } from './common';
|
||||||
|
import IframeEmbed from './iframeembed';
|
||||||
|
import { getString } from 'core/str';
|
||||||
|
|
||||||
|
export const getSetup = async () => {
|
||||||
|
const [tinyMCE, buttonTitle, menuTitle] = await Promise.all([
|
||||||
|
getTinyMCE(),
|
||||||
|
getString('insertmedia', component),
|
||||||
|
getString('mediacms', component),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (editor) => {
|
||||||
|
const iframeEmbed = new IframeEmbed(editor);
|
||||||
|
|
||||||
|
editor.ui.registry.addButton(component, {
|
||||||
|
icon: 'embed',
|
||||||
|
tooltip: buttonTitle,
|
||||||
|
onAction: () => iframeEmbed.displayDialogue(),
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.ui.registry.addMenuItem(component, {
|
||||||
|
icon: 'embed',
|
||||||
|
text: menuTitle,
|
||||||
|
onAction: () => iframeEmbed.displayDialogue(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
export const component = 'tiny_mediacms';
|
||||||
|
export const pluginName = 'mediacms';
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
export const configure = (instanceConfig) => {
|
||||||
|
return {
|
||||||
|
mediacmsurl: instanceConfig.mediacmsurl,
|
||||||
|
launchUrl: instanceConfig.launchUrl,
|
||||||
|
lti: instanceConfig.lti
|
||||||
|
};
|
||||||
|
};
|
||||||
368
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js
Normal file
368
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
|
||||||
|
import Templates from 'core/templates';
|
||||||
|
import { getString } from 'core/str';
|
||||||
|
import * as ModalEvents from 'core/modal_events';
|
||||||
|
import { component } from './common';
|
||||||
|
import IframeModal from './iframemodal';
|
||||||
|
import Selectors from './selectors';
|
||||||
|
import { getLti, getLaunchUrl, getMediaCMSUrl } from './options';
|
||||||
|
|
||||||
|
export default class IframeEmbed {
|
||||||
|
editor = null;
|
||||||
|
currentModal = null;
|
||||||
|
isUpdating = false;
|
||||||
|
selectedIframe = null;
|
||||||
|
debounceTimer = null;
|
||||||
|
iframeLibraryLoaded = false;
|
||||||
|
selectedLibraryVideo = null;
|
||||||
|
|
||||||
|
constructor(editor) {
|
||||||
|
this.editor = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseInput(input) {
|
||||||
|
if (!input || !input.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
input = input.trim();
|
||||||
|
|
||||||
|
// Check for iframe
|
||||||
|
const iframeMatch = input.match(/<iframe[^>]*src=["']([^"']+)["'][^>]*>/i);
|
||||||
|
if (iframeMatch) {
|
||||||
|
return this.parseEmbedUrl(iframeMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check URL
|
||||||
|
if (input.startsWith('http://') || input.startsWith('https://')) {
|
||||||
|
return this.parseVideoUrl(input);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseVideoUrl(url) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const baseUrl = `${urlObj.protocol}//${urlObj.host}`;
|
||||||
|
|
||||||
|
// Check if it matches configured MediaCMS URL (if strictly required)
|
||||||
|
// For now we accept any valid MediaCMS-like structure
|
||||||
|
|
||||||
|
// /view?m=ID
|
||||||
|
if (urlObj.pathname.includes('/view') && urlObj.searchParams.has('m')) {
|
||||||
|
return {
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
videoId: urlObj.searchParams.get('m'),
|
||||||
|
isEmbed: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// /embed?m=ID
|
||||||
|
if (urlObj.pathname.includes('/embed') && urlObj.searchParams.has('m')) {
|
||||||
|
return {
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
videoId: urlObj.searchParams.get('m'),
|
||||||
|
isEmbed: true,
|
||||||
|
// Parse options
|
||||||
|
showTitle: urlObj.searchParams.get('showTitle') === '1',
|
||||||
|
linkTitle: urlObj.searchParams.get('linkTitle') === '1',
|
||||||
|
showRelated: urlObj.searchParams.get('showRelated') === '1',
|
||||||
|
showUserAvatar: urlObj.searchParams.get('showUserAvatar') === '1',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's already a launch.php URL
|
||||||
|
if (urlObj.pathname.includes('/filter/mediacms/launch.php') && urlObj.searchParams.has('token')) {
|
||||||
|
return {
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
videoId: urlObj.searchParams.get('token'),
|
||||||
|
isEmbed: true,
|
||||||
|
isLaunchUrl: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
rawUrl: url,
|
||||||
|
isGeneric: true
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseEmbedUrl(url) {
|
||||||
|
return this.parseVideoUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildEmbedUrl(parsed, options) {
|
||||||
|
if (parsed.isGeneric) {
|
||||||
|
return parsed.rawUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const launchUrl = getLaunchUrl(this.editor);
|
||||||
|
if (launchUrl && parsed.videoId) {
|
||||||
|
const url = new URL(launchUrl);
|
||||||
|
url.searchParams.set('token', parsed.videoId);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to direct embed if launchUrl missing
|
||||||
|
const url = new URL(`${parsed.baseUrl}/embed`);
|
||||||
|
url.searchParams.set('m', parsed.videoId);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTemplateContext(data = {}) {
|
||||||
|
return {
|
||||||
|
elementid: this.editor.getElement().id,
|
||||||
|
isupdating: this.isUpdating,
|
||||||
|
url: data.url || '',
|
||||||
|
showTitle: data.showTitle !== false,
|
||||||
|
linkTitle: data.linkTitle !== false,
|
||||||
|
showRelated: data.showRelated !== false,
|
||||||
|
showUserAvatar: data.showUserAvatar !== false,
|
||||||
|
responsive: data.responsive !== false,
|
||||||
|
startAtEnabled: data.startAtEnabled || false,
|
||||||
|
startAt: data.startAt || '0:00',
|
||||||
|
width: data.width || 560,
|
||||||
|
height: data.height || 315,
|
||||||
|
is16_9: !data.aspectRatio || data.aspectRatio === '16:9',
|
||||||
|
is4_3: data.aspectRatio === '4:3',
|
||||||
|
is1_1: data.aspectRatio === '1:1',
|
||||||
|
isCustom: data.aspectRatio === 'custom',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async displayDialogue() {
|
||||||
|
this.selectedIframe = this.getSelectedIframe();
|
||||||
|
const data = this.getCurrentIframeData();
|
||||||
|
this.isUpdating = data !== null;
|
||||||
|
this.iframeLibraryLoaded = false;
|
||||||
|
|
||||||
|
this.currentModal = await IframeModal.create({
|
||||||
|
title: await getString('iframemodaltitle', component),
|
||||||
|
templateContext: await this.getTemplateContext(data || {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.registerEventListeners(this.currentModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedIframe() {
|
||||||
|
const node = this.editor.selection.getNode();
|
||||||
|
if (node.nodeName.toLowerCase() === 'iframe') return node;
|
||||||
|
return node.querySelector('iframe') || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentIframeData() {
|
||||||
|
if (!this.selectedIframe) return null;
|
||||||
|
const src = this.selectedIframe.getAttribute('src');
|
||||||
|
const parsed = this.parseInput(src);
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: src,
|
||||||
|
width: this.selectedIframe.getAttribute('width') || 560,
|
||||||
|
height: this.selectedIframe.getAttribute('height') || 315,
|
||||||
|
showTitle: parsed?.showTitle ?? true,
|
||||||
|
// ... other defaults
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormValues(root) {
|
||||||
|
const form = root.querySelector(Selectors.IFRAME.elements.form);
|
||||||
|
// Helper to safely get value or checked state
|
||||||
|
const getVal = (sel) => form.querySelector(sel)?.value;
|
||||||
|
const getCheck = (sel) => form.querySelector(sel)?.checked;
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: getVal(Selectors.IFRAME.elements.url).trim(),
|
||||||
|
showTitle: getCheck(Selectors.IFRAME.elements.showTitle),
|
||||||
|
linkTitle: getCheck(Selectors.IFRAME.elements.linkTitle),
|
||||||
|
showRelated: getCheck(Selectors.IFRAME.elements.showRelated),
|
||||||
|
showUserAvatar: getCheck(Selectors.IFRAME.elements.showUserAvatar),
|
||||||
|
responsive: getCheck(Selectors.IFRAME.elements.responsive),
|
||||||
|
aspectRatio: getVal(Selectors.IFRAME.elements.aspectRatio),
|
||||||
|
width: parseInt(getVal(Selectors.IFRAME.elements.width)) || 560,
|
||||||
|
height: parseInt(getVal(Selectors.IFRAME.elements.height)) || 315,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateIframeHtml(values) {
|
||||||
|
const parsed = this.parseInput(values.url);
|
||||||
|
if (!parsed) return '';
|
||||||
|
|
||||||
|
const embedUrl = this.buildEmbedUrl(parsed, values);
|
||||||
|
|
||||||
|
const aspectRatioCalcs = {
|
||||||
|
'16:9': '16 / 9',
|
||||||
|
'4:3': '4 / 3',
|
||||||
|
'1:1': '1 / 1',
|
||||||
|
'custom': `${values.width} / ${values.height}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
src: embedUrl,
|
||||||
|
width: values.width,
|
||||||
|
height: values.height,
|
||||||
|
responsive: values.responsive,
|
||||||
|
aspectRatioValue: aspectRatioCalcs[values.aspectRatio] || '16 / 9',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { html } = await Templates.renderForPromise(
|
||||||
|
`${component}/iframe_embed_output`,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePreview(root) {
|
||||||
|
const values = this.getFormValues(root);
|
||||||
|
const previewContainer = root.querySelector(Selectors.IFRAME.elements.preview);
|
||||||
|
|
||||||
|
if (!values.url) {
|
||||||
|
previewContainer.innerHTML = '<span>Enter URL</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = this.parseInput(values.url);
|
||||||
|
if (!parsed) {
|
||||||
|
previewContainer.innerHTML = '<span class="text-danger">Invalid URL</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embedUrl = this.buildEmbedUrl(parsed, values);
|
||||||
|
// Simple preview
|
||||||
|
previewContainer.innerHTML = `<iframe src="${embedUrl}" width="100%" height="200" frameborder="0"></iframe>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... Event listeners and Modal handling ...
|
||||||
|
// Simplified for brevity, assuming standard handlers
|
||||||
|
|
||||||
|
async registerEventListeners(modal) {
|
||||||
|
const root = modal.getRoot()[0];
|
||||||
|
const form = root.querySelector(Selectors.IFRAME.elements.form);
|
||||||
|
|
||||||
|
// Input changes update preview
|
||||||
|
form.addEventListener('change', () => this.updatePreview(root));
|
||||||
|
form.querySelector(Selectors.IFRAME.elements.url).addEventListener('input', () => this.updatePreview(root));
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
const tabUrl = form.querySelector(Selectors.IFRAME.elements.tabUrlBtn);
|
||||||
|
const tabLib = form.querySelector(Selectors.IFRAME.elements.tabIframeLibraryBtn);
|
||||||
|
|
||||||
|
if (tabLib) {
|
||||||
|
tabLib.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.switchToTab(root, 'library');
|
||||||
|
this.loadIframeLibrary(root);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabUrl) {
|
||||||
|
tabUrl.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.switchToTab(root, 'url');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.getRoot().on(ModalEvents.save, () => this.handleDialogueSubmission(modal));
|
||||||
|
|
||||||
|
// Listen for messages
|
||||||
|
window.addEventListener('message', (e) => this.handleIframeLibraryMessage(root, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
switchToTab(root, tab) {
|
||||||
|
const form = root.querySelector(Selectors.IFRAME.elements.form);
|
||||||
|
const urlPane = form.querySelector(Selectors.IFRAME.elements.paneUrl);
|
||||||
|
const libPane = form.querySelector(Selectors.IFRAME.elements.paneIframeLibrary);
|
||||||
|
const urlBtn = form.querySelector(Selectors.IFRAME.elements.tabUrlBtn);
|
||||||
|
const libBtn = form.querySelector(Selectors.IFRAME.elements.tabIframeLibraryBtn);
|
||||||
|
|
||||||
|
if (tab === 'url') {
|
||||||
|
urlPane.classList.add('show', 'active');
|
||||||
|
libPane.classList.remove('show', 'active');
|
||||||
|
urlBtn.classList.add('active');
|
||||||
|
libBtn.classList.remove('active');
|
||||||
|
} else {
|
||||||
|
urlPane.classList.remove('show', 'active');
|
||||||
|
libPane.classList.add('show', 'active');
|
||||||
|
urlBtn.classList.remove('active');
|
||||||
|
libBtn.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadIframeLibrary(root) {
|
||||||
|
const ltiConfig = getLti(this.editor);
|
||||||
|
if (ltiConfig && ltiConfig.contentItemUrl) {
|
||||||
|
const iframe = root.querySelector(Selectors.IFRAME.elements.iframeLibraryFrame);
|
||||||
|
const loading = root.querySelector(Selectors.IFRAME.elements.iframeLibraryLoading);
|
||||||
|
|
||||||
|
if (iframe && !iframe.src) {
|
||||||
|
loading.classList.remove('d-none');
|
||||||
|
iframe.classList.add('d-none');
|
||||||
|
|
||||||
|
iframe.onload = () => {
|
||||||
|
loading.classList.add('d-none');
|
||||||
|
iframe.classList.remove('d-none');
|
||||||
|
};
|
||||||
|
|
||||||
|
iframe.src = ltiConfig.contentItemUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleIframeLibraryMessage(root, event) {
|
||||||
|
const data = event.data;
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
let embedUrl = null;
|
||||||
|
let videoId = null;
|
||||||
|
|
||||||
|
// LTI Deep Linking Response
|
||||||
|
if (data.type === 'ltiDeepLinkingResponse' || data.messageType === 'LtiDeepLinkingResponse') {
|
||||||
|
const items = data.content_items || data.contentItems || [];
|
||||||
|
if (items.length) {
|
||||||
|
embedUrl = items[0].url || items[0].embed_url;
|
||||||
|
// Try to extract ID from URL if not provided
|
||||||
|
// But actually we just need the ID to build the launch URL
|
||||||
|
// If the response gives us a full embed URL, we can parse it
|
||||||
|
if (embedUrl) {
|
||||||
|
const parsed = this.parseInput(embedUrl);
|
||||||
|
if (parsed) videoId = parsed.videoId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaCMS custom message
|
||||||
|
if (data.action === 'selectMedia' || data.type === 'videoSelected') {
|
||||||
|
embedUrl = data.embedUrl || data.url;
|
||||||
|
videoId = data.mediaId || data.videoId || data.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoId) {
|
||||||
|
// Populate URL field with the clean embed URL (launch URL) if possible,
|
||||||
|
// or just the direct URL which parseInput will handle
|
||||||
|
|
||||||
|
// Actually, best to set the raw MediaCMS URL and let parseInput -> buildEmbedUrl handle the conversion
|
||||||
|
// But if we have the ID, we can construct a view URL
|
||||||
|
const mediaCMSUrl = getMediaCMSUrl(this.editor);
|
||||||
|
if (mediaCMSUrl) {
|
||||||
|
const viewUrl = `${mediaCMSUrl}/view?m=${videoId}`;
|
||||||
|
const urlInput = root.querySelector(Selectors.IFRAME.elements.url);
|
||||||
|
urlInput.value = viewUrl;
|
||||||
|
this.updatePreview(root);
|
||||||
|
this.switchToTab(root, 'url');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDialogueSubmission(modal) {
|
||||||
|
const root = modal.getRoot()[0];
|
||||||
|
const values = this.getFormValues(root);
|
||||||
|
const html = await this.generateIframeHtml(values);
|
||||||
|
|
||||||
|
if (html) {
|
||||||
|
this.editor.insertContent(html);
|
||||||
|
}
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
import Modal from 'core/modal';
|
||||||
|
import ModalRegistry from 'core/modal_registry';
|
||||||
|
import { component } from './common';
|
||||||
|
|
||||||
|
export default class IframeModal extends Modal {
|
||||||
|
static TYPE = `${component}/iframe_embed_modal`;
|
||||||
|
static TEMPLATE = `${component}/iframe_embed_modal`;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModalRegistry.register(IframeModal.TYPE, IframeModal, IframeModal.TEMPLATE);
|
||||||
25
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/options.js
Normal file
25
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/options.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
import { component } from './common';
|
||||||
|
|
||||||
|
export const register = (editor) => {
|
||||||
|
const registerOption = editor.options.register;
|
||||||
|
|
||||||
|
registerOption(`${component}:mediacmsurl`, {
|
||||||
|
processor: 'string',
|
||||||
|
default: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
registerOption(`${component}:launchUrl`, {
|
||||||
|
processor: 'string',
|
||||||
|
default: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
registerOption(`${component}:lti`, {
|
||||||
|
processor: 'object',
|
||||||
|
default: {}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMediaCMSUrl = (editor) => editor.options.get(`${component}:mediacmsurl`);
|
||||||
|
export const getLaunchUrl = (editor) => editor.options.get(`${component}:launchUrl`);
|
||||||
|
export const getLti = (editor) => editor.options.get(`${component}:lti`);
|
||||||
23
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/plugin.js
Normal file
23
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/plugin.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
import { getTinyMCE } from 'editor_tiny/loader';
|
||||||
|
import { getPluginMetadata } from 'editor_tiny/utils';
|
||||||
|
import { component, pluginName } from './common';
|
||||||
|
import * as Commands from './commands';
|
||||||
|
import * as Options from './options';
|
||||||
|
import * as Configuration from './configuration';
|
||||||
|
|
||||||
|
export default new Promise(async (resolve) => {
|
||||||
|
const [tinyMCE, setupCommands, pluginMetadata] = await Promise.all([
|
||||||
|
getTinyMCE(),
|
||||||
|
Commands.getSetup(),
|
||||||
|
getPluginMetadata(component, pluginName),
|
||||||
|
]);
|
||||||
|
|
||||||
|
tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => {
|
||||||
|
Options.register(editor);
|
||||||
|
setupCommands(editor);
|
||||||
|
return pluginMetadata;
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve([`${component}/plugin`, Configuration]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
export default {
|
||||||
|
IFRAME: {
|
||||||
|
elements: {
|
||||||
|
form: '.tiny-mediacms-iframe-form',
|
||||||
|
url: 'input[name="mediacms_url"]',
|
||||||
|
width: 'input[name="mediacms_width"]',
|
||||||
|
height: 'input[name="mediacms_height"]',
|
||||||
|
preview: '.tiny-mediacms-preview',
|
||||||
|
tabUrlBtn: '#tiny-mediacms-tab-url',
|
||||||
|
tabIframeLibraryBtn: '#tiny-mediacms-tab-library',
|
||||||
|
paneUrl: '#tiny-mediacms-pane-url',
|
||||||
|
paneIframeLibrary: '#tiny-mediacms-pane-library',
|
||||||
|
iframeLibraryFrame: '.tiny-mediacms-library-frame',
|
||||||
|
iframeLibraryLoading: '.tiny-mediacms-library-loading',
|
||||||
|
iframeLibraryPlaceholder: '.tiny-mediacms-library-placeholder'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace tiny_mediacms;
|
||||||
|
|
||||||
|
use context;
|
||||||
|
use moodle_url;
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
class plugininfo extends \editor_tiny\plugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the plugin configuration for the editor.
|
||||||
|
*/
|
||||||
|
protected static function get_plugin_configuration_for_context(context $context, array $options = []): array {
|
||||||
|
global $CFG;
|
||||||
|
|
||||||
|
// Read settings from the FILTER plugin
|
||||||
|
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
|
||||||
|
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
|
||||||
|
|
||||||
|
// Construct launch URL for the filter
|
||||||
|
$launchurl = new moodle_url('/filter/mediacms/launch.php');
|
||||||
|
|
||||||
|
// Content Item URL for LTI Deep Linking (if using the picker)
|
||||||
|
// Usually: /mod/lti/contentitem.php?id=LTI_TYPE_ID
|
||||||
|
$contentitemurl = '';
|
||||||
|
if ($ltitoolid) {
|
||||||
|
$contentitemurl = new moodle_url('/mod/lti/contentitem.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'mediacmsurl' => $mediacmsurl,
|
||||||
|
'launchUrl' => $launchurl->out(false),
|
||||||
|
'lti' => [
|
||||||
|
'toolId' => (int) $ltitoolid,
|
||||||
|
'courseId' => $context->get_course_context(false)->instanceid ?? 0,
|
||||||
|
'contentItemUrl' => $contentitemurl ? $contentitemurl->out(false) : '',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
namespace tiny_mediacms\privacy;
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
class provider implements \core_privacy\local\metadata\null_provider {
|
||||||
|
public static function get_reason(): string {
|
||||||
|
return 'privacy:metadata';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
$string['pluginname'] = 'MediaCMS';
|
||||||
|
$string['mediacms:view'] = 'View MediaCMS';
|
||||||
|
$string['mediacms'] = 'MediaCMS';
|
||||||
|
$string['insertmedia'] = 'Insert MediaCMS Video';
|
||||||
|
$string['iframemodaltitle'] = 'Insert MediaCMS Video';
|
||||||
|
$string['urltab'] = 'URL';
|
||||||
|
$string['iframelibrarytab'] = 'Video Library';
|
||||||
|
$string['enterurl'] = 'Enter Video URL';
|
||||||
|
$string['invalidurl'] = 'Invalid MediaCMS URL';
|
||||||
|
$string['removeiframeconfirm'] = 'Are you sure you want to remove this video?';
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{{!
|
||||||
|
@template tiny_mediacms/iframe_embed_modal
|
||||||
|
}}
|
||||||
|
{{< core/modal }}
|
||||||
|
{{$body}}
|
||||||
|
<form class="tiny_iframecms_form" id="{{elementid}}_tiny_iframecms_form">
|
||||||
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link active" id="{{elementid}}_tab_url" data-bs-toggle="tab" data-bs-target="#{{elementid}}_pane_url" type="button" role="tab">{{#str}} urltab, tiny_mediacms {{/str}}</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link" id="{{elementid}}_tab_iframe_library" data-bs-toggle="tab" data-bs-target="#{{elementid}}_pane_iframe_library" type="button" role="tab">{{#str}} iframelibrarytab, tiny_mediacms {{/str}}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane fade show active" id="{{elementid}}_pane_url" role="tabpanel">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{#str}} enterurl, tiny_mediacms {{/str}}</label>
|
||||||
|
<input type="text" class="form-control tiny_iframecms_url" name="mediacms_url" value="{{url}}">
|
||||||
|
</div>
|
||||||
|
{{> tiny_mediacms/iframe_embed_options }}
|
||||||
|
<div class="tiny_mediacms_preview border p-2 text-center bg-light" style="min-height:200px">
|
||||||
|
Preview
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="{{elementid}}_pane_iframe_library" role="tabpanel">
|
||||||
|
<div class="tiny_mediacms_library_container text-center" style="min-height:400px">
|
||||||
|
<div class="tiny_mediacms_library_loading d-none">Loading...</div>
|
||||||
|
<iframe class="tiny_mediacms_library_frame d-none" style="width:100%;height:400px;border:0;"></iframe>
|
||||||
|
<div class="tiny_mediacms_library_placeholder p-5">Library will load here</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{/body}}
|
||||||
|
{{$footer}}
|
||||||
|
<button type="button" class="btn btn-primary" data-action="save">{{#str}} insertmedia, tiny_mediacms {{/str}}</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel, moodle {{/str}}</button>
|
||||||
|
{{/footer}}
|
||||||
|
{{/ core/modal }}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{{!
|
||||||
|
@template tiny_mediacms/iframe_embed_options
|
||||||
|
}}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label font-weight-bold">{{#str}} embedoptions, tiny_mediacms {{/str}}</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input type="checkbox" class="form-check-input tiny_iframecms_showtitle" id="{{elementid}}_showtitle" {{#showTitle}}checked{{/showTitle}}>
|
||||||
|
<label class="form-check-label" for="{{elementid}}_showtitle">{{#str}} showtitle, tiny_mediacms {{/str}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input type="checkbox" class="form-check-input tiny_iframecms_linktitle" id="{{elementid}}_linktitle" {{#linkTitle}}checked{{/linkTitle}}>
|
||||||
|
<label class="form-check-label" for="{{elementid}}_linktitle">{{#str}} linktitle, tiny_mediacms {{/str}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input type="checkbox" class="form-check-input tiny_iframecms_responsive" id="{{elementid}}_responsive" {{#responsive}}checked{{/responsive}}>
|
||||||
|
<label class="form-check-label" for="{{elementid}}_responsive">{{#str}} responsive, tiny_mediacms {{/str}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<label class="form-check-label">{{#str}} aspectratio, tiny_mediacms {{/str}}</label>
|
||||||
|
<select class="form-control form-control-sm tiny_iframecms_aspectratio" id="{{elementid}}_aspectratio">
|
||||||
|
<option value="16:9" {{#is16_9}}selected{{/is16_9}}>16:9</option>
|
||||||
|
<option value="4:3" {{#is4_3}}selected{{/is4_3}}>4:3</option>
|
||||||
|
<option value="1:1" {{#is1_1}}selected{{/is1_1}}>1:1</option>
|
||||||
|
<option value="custom" {{#isCustom}}selected{{/isCustom}}>Custom</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text">{{#str}} width, tiny_mediacms {{/str}}</span>
|
||||||
|
<input type="number" class="form-control tiny_iframecms_width" id="{{elementid}}_width" value="{{width}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text">{{#str}} height, tiny_mediacms {{/str}}</span>
|
||||||
|
<input type="number" class="form-control tiny_iframecms_height" id="{{elementid}}_height" value="{{height}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{{!
|
||||||
|
@template tiny_mediacms/iframe_embed_output
|
||||||
|
}}
|
||||||
|
{{#responsive}}
|
||||||
|
<iframe src="{{src}}" style="width:100%;aspect-ratio:{{aspectRatioValue}};display:block;border:0;" allowFullScreen></iframe>
|
||||||
|
{{/responsive}}
|
||||||
|
{{^responsive}}
|
||||||
|
<iframe width="{{width}}" height="{{height}}" src="{{src}}" frameBorder="0" allowFullScreen></iframe>
|
||||||
|
{{/responsive}}
|
||||||
9
lms-plugins/mediacms-moodle/tiny/mediacms/version.php
Normal file
9
lms-plugins/mediacms-moodle/tiny/mediacms/version.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
$plugin->version = 2026020100;
|
||||||
|
$plugin->requires = 2024100700;
|
||||||
|
$plugin->component = 'tiny_mediacms';
|
||||||
|
$plugin->maturity = MATURITY_STABLE;
|
||||||
|
$plugin->release = 'v1.0.0';
|
||||||
|
$plugin->dependencies = ['filter_mediacms' => 2026020100]; // Requires the filter
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user