91334a46b5
Co-authored-by: Cursor <cursoragent@cursor.com>
347 lines
14 KiB
PHP
347 lines
14 KiB
PHP
<?php
|
|
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
/**
|
|
* Ajax: load tab HTML/data.
|
|
*
|
|
* @package mod_meetletacbfree
|
|
* @copyright 2026 MeetLet
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
namespace mod_meetletacbfree\external;
|
|
|
|
use core_external\external_api;
|
|
use core_external\external_function_parameters;
|
|
use core_external\external_single_structure;
|
|
use core_external\external_value;
|
|
use mod_meetletacbfree\api\service;
|
|
use mod_meetletacbfree\local\config;
|
|
use mod_meetletacbfree\local\content;
|
|
use mod_meetletacbfree\local\featureaccess;
|
|
use mod_meetletacbfree\local\icons;
|
|
use mod_meetletacbfree\local\instance;
|
|
use mod_meetletacbfree\local\recordings;
|
|
use moodle_exception;
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
/**
|
|
* Return tab payload for the activity UI.
|
|
*/
|
|
class get_tab extends external_api {
|
|
public static function execute_parameters(): external_function_parameters {
|
|
return new external_function_parameters([
|
|
'cmid' => new external_value(PARAM_INT, 'Course module id'),
|
|
'tab' => new external_value(PARAM_ALPHA, 'Tab name'),
|
|
'groupid' => new external_value(PARAM_INT, 'Group id', VALUE_DEFAULT, 0),
|
|
]);
|
|
}
|
|
|
|
public static function execute(int $cmid, string $tab, int $groupid = 0): array {
|
|
global $PAGE;
|
|
|
|
$params = self::validate_parameters(self::execute_parameters(), [
|
|
'cmid' => $cmid,
|
|
'tab' => $tab,
|
|
'groupid' => $groupid,
|
|
]);
|
|
|
|
\mod_meetletacbfree\registration\manager::require_registered();
|
|
|
|
$inst = instance::get_from_cmid($params['cmid'], $params['groupid']);
|
|
self::validate_context($inst->context);
|
|
require_capability('mod/meetletacbfree:view', $inst->context);
|
|
$PAGE->set_context($inst->context);
|
|
|
|
if (!config::is_configured()) {
|
|
return [
|
|
'html' => \html_writer::div(get_string('notconfigured', 'meetletacbfree'), 'meetlet-alert'),
|
|
'tab' => $params['tab'],
|
|
];
|
|
}
|
|
|
|
$html = '';
|
|
switch ($params['tab']) {
|
|
case 'details':
|
|
// Served locally in the activity page; keep endpoint defensive.
|
|
$html = \html_writer::div(get_string('emptydetails', 'meetletacbfree'), 'meetlet-empty');
|
|
break;
|
|
case 'info':
|
|
// Legacy tab id — no Adobe call.
|
|
$html = \html_writer::div(get_string('emptydetails', 'meetletacbfree'), 'meetlet-empty');
|
|
break;
|
|
case 'recordings':
|
|
featureaccess::require_feature(featureaccess::RECORDINGS, $inst->context, $inst->record);
|
|
$html = self::render_recordings($inst);
|
|
break;
|
|
case 'content':
|
|
featureaccess::require_feature(featureaccess::CONTENT, $inst->context, $inst->record);
|
|
$html = self::render_content($inst);
|
|
break;
|
|
default:
|
|
throw new moodle_exception('error', 'meetletacbfree');
|
|
}
|
|
|
|
return ['html' => $html, 'tab' => $params['tab']];
|
|
}
|
|
|
|
public static function execute_returns(): external_single_structure {
|
|
return new external_single_structure([
|
|
'html' => new external_value(PARAM_RAW, 'Tab HTML'),
|
|
'tab' => new external_value(PARAM_ALPHA, 'Tab id'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Toolbar with refresh for cacheable Adobe tabs.
|
|
*/
|
|
protected static function wrap_refreshable(string $html, string $tab, bool $pending = false): string {
|
|
$refreshlabel = get_string('refreshtab', 'meetletacbfree');
|
|
$refreshcontent = icons::svg('refresh')
|
|
. \html_writer::span($refreshlabel, 'meetlet-btn-with-icon__label');
|
|
$bits = [
|
|
\html_writer::tag('button', $refreshcontent, [
|
|
'type' => 'button',
|
|
'class' => 'meetlet-btn meetlet-btn--ghost meetlet-btn--sm meetlet-btn-with-icon meetlet-tabtoolbar__refresh',
|
|
'data-action' => 'refresh-tab',
|
|
'data-tab' => $tab,
|
|
'title' => $refreshlabel,
|
|
'aria-label' => $refreshlabel,
|
|
]),
|
|
];
|
|
if ($pending) {
|
|
$bits[] = \html_writer::div(
|
|
\html_writer::span(get_string('jobpollwait', 'meetletacbfree'), 'meetlet-jobpoll__label') .
|
|
\html_writer::div(\html_writer::div('', 'meetlet-jobpoll__bar'), 'meetlet-jobpoll__track'),
|
|
'meetlet-jobpoll',
|
|
['aria-live' => 'polite']
|
|
);
|
|
}
|
|
$bar = \html_writer::div(implode('', $bits), 'meetlet-tabtoolbar');
|
|
$attrs = ['data-meetlet-tab' => $tab];
|
|
if ($pending) {
|
|
$attrs['data-meetlet-jobs-pending'] = '1';
|
|
$attrs['data-meetlet-jobs-poll-ms'] = '60000';
|
|
}
|
|
return \html_writer::div($bar . $html, 'meetlet-tabbody', $attrs);
|
|
}
|
|
|
|
/**
|
|
* Toolbar control with icon and visible label (matches refresh button styling).
|
|
*
|
|
* @param array $attrs Extra attributes (href makes it a link).
|
|
*/
|
|
protected static function labeled_action(string $icon, string $label, array $attrs = []): string {
|
|
$extraclass = $attrs['class'] ?? '';
|
|
unset($attrs['class']);
|
|
$classes = trim(
|
|
'meetlet-btn meetlet-btn--ghost meetlet-btn--sm meetlet-btn-with-icon ' . $extraclass
|
|
);
|
|
$attrs['class'] = $classes;
|
|
$attrs['title'] = $label;
|
|
$attrs['aria-label'] = $label;
|
|
$content = icons::svg($icon)
|
|
. \html_writer::span($label, 'meetlet-btn-with-icon__label');
|
|
if (!empty($attrs['href'])) {
|
|
$url = $attrs['href'];
|
|
unset($attrs['href']);
|
|
$attrs['target'] = $attrs['target'] ?? '_blank';
|
|
if (($attrs['target'] ?? '') === '_self') {
|
|
unset($attrs['rel']);
|
|
} else {
|
|
$attrs['rel'] = $attrs['rel'] ?? 'noopener';
|
|
}
|
|
return \html_writer::link($url, $content, $attrs);
|
|
}
|
|
$attrs['type'] = $attrs['type'] ?? 'button';
|
|
return \html_writer::tag('button', $content, $attrs);
|
|
}
|
|
|
|
/**
|
|
* Icon control with hover tip.
|
|
*
|
|
* @param array $attrs Extra attributes (href makes it a link).
|
|
*/
|
|
protected static function icon_action(string $icon, string $label, array $attrs = []): string {
|
|
$classes = trim('meetlet-iconbtn ' . ($attrs['class'] ?? ''));
|
|
unset($attrs['class']);
|
|
$attrs['class'] = $classes;
|
|
$attrs['data-tip'] = $label;
|
|
$attrs['aria-label'] = $label;
|
|
$content = icons::svg($icon) . \html_writer::span($label, 'meetlet-sr-only');
|
|
if (!empty($attrs['href'])) {
|
|
$url = $attrs['href'];
|
|
unset($attrs['href']);
|
|
$attrs['target'] = $attrs['target'] ?? '_blank';
|
|
if (($attrs['target'] ?? '') === '_self') {
|
|
unset($attrs['rel']);
|
|
} else {
|
|
$attrs['rel'] = $attrs['rel'] ?? 'noopener';
|
|
}
|
|
return \html_writer::link($url, $content, $attrs);
|
|
}
|
|
$attrs['type'] = $attrs['type'] ?? 'button';
|
|
return \html_writer::tag('button', $content, $attrs);
|
|
}
|
|
|
|
protected static function format_jobstatus(string $status, bool $mp4ready = false): string {
|
|
return recordings::jobstatus_label($status, $mp4ready);
|
|
}
|
|
|
|
protected static function render_recordings(instance $inst): string {
|
|
$rows = recordings::for_tab($inst);
|
|
if (empty($rows)) {
|
|
return self::wrap_refreshable(
|
|
\html_writer::div(get_string('norecordings', 'meetletacbfree'), 'meetlet-empty'),
|
|
'recordings'
|
|
);
|
|
}
|
|
$canedit = featureaccess::allowed(featureaccess::EDITRECORDING, $inst->context, $inst->record);
|
|
$canadobeedit = featureaccess::allowed(featureaccess::ADOBEEDITOR, $inst->context, $inst->record);
|
|
$table = new \html_table();
|
|
$table->attributes['class'] = 'generaltable meetlet-table meetlet-table--recordings';
|
|
$table->head = [
|
|
get_string('recordingname', 'meetletacbfree'),
|
|
get_string('recordingdate', 'meetletacbfree'),
|
|
get_string('duration', 'meetletacbfree'),
|
|
get_string('sessiondetails', 'meetletacbfree'),
|
|
];
|
|
foreach ($rows as $r) {
|
|
$actions = [];
|
|
$actions[] = self::icon_action('play', get_string('play', 'meetletacbfree'), [
|
|
'class' => 'meetlet-iconbtn--play',
|
|
'data-action' => 'launch-recording',
|
|
'data-mode' => 'play',
|
|
'data-recording' => $r->recordingscoid,
|
|
]);
|
|
if ($canadobeedit && !empty($r->urlpath)) {
|
|
$actions[] = self::icon_action('adobeedit', get_string('adobeeditor', 'meetletacbfree'), [
|
|
'class' => 'meetlet-iconbtn--adobeedit',
|
|
'data-action' => 'launch-recording',
|
|
'data-mode' => 'edit',
|
|
'data-recording' => $r->recordingscoid,
|
|
]);
|
|
}
|
|
$namehtml = self::render_recording_name((string)$r->name, (int)$r->recordingscoid, $canedit);
|
|
$dur = $r->duration ? gmdate('H:i:s', (int)$r->duration) : '-';
|
|
$recdate = !empty($r->timestart) ? userdate((int)$r->timestart) : '—';
|
|
$table->data[] = [
|
|
$namehtml,
|
|
$recdate,
|
|
$dur,
|
|
\html_writer::div(implode('', $actions), 'meetlet-iconactions'),
|
|
];
|
|
}
|
|
return self::wrap_refreshable(\html_writer::div(\html_writer::table($table), 'meetlet-card'), 'recordings', false);
|
|
}
|
|
|
|
/**
|
|
* Recording title with optional inline rename control.
|
|
*/
|
|
protected static function render_recording_name(string $name, int $recordingscoid, bool $canedit): string {
|
|
$text = \html_writer::span(format_string($name), 'meetlet-recname__text');
|
|
if (!$canedit) {
|
|
return \html_writer::div($text, 'meetlet-recname');
|
|
}
|
|
$pencil = self::icon_action('edit', get_string('editrecording', 'meetletacbfree'), [
|
|
'class' => 'meetlet-iconbtn--edit meetlet-iconbtn--inline',
|
|
'data-action' => 'inline-rename',
|
|
]);
|
|
$input = \html_writer::empty_tag('input', [
|
|
'type' => 'text',
|
|
'class' => 'meetlet-recname__input',
|
|
'value' => $name,
|
|
'maxlength' => 255,
|
|
'hidden' => 'hidden',
|
|
]);
|
|
return \html_writer::div($text . $pencil . $input, 'meetlet-recname', [
|
|
'data-recording' => $recordingscoid,
|
|
'data-name' => $name,
|
|
]);
|
|
}
|
|
|
|
protected static function render_content(instance $inst): string {
|
|
global $USER;
|
|
|
|
$service = new service();
|
|
$service->login_admin();
|
|
$items = $service->list_meeting_content($inst->require_meeting());
|
|
$service->logout();
|
|
if (empty($items)) {
|
|
return self::wrap_refreshable(
|
|
\html_writer::div(get_string('nocontent', 'meetletacbfree'), 'meetlet-empty'),
|
|
'content'
|
|
);
|
|
}
|
|
$table = new \html_table();
|
|
$table->attributes['class'] = 'generaltable meetlet-table meetlet-table--content';
|
|
$table->head = [
|
|
get_string('name'),
|
|
get_string('contenttype', 'meetletacbfree'),
|
|
get_string('contentsize', 'meetletacbfree'),
|
|
get_string('contentdate', 'meetletacbfree'),
|
|
get_string('sessiondetails', 'meetletacbfree'),
|
|
];
|
|
foreach ($items as $item) {
|
|
$scoid = (int)($item->scoid ?? 0);
|
|
$actions = [
|
|
self::icon_action('play', get_string('view'), [
|
|
'class' => 'meetlet-iconbtn--play',
|
|
'data-action' => 'launch-content',
|
|
'data-content' => $scoid,
|
|
]),
|
|
];
|
|
if ($scoid > 0 && !empty($item->name)) {
|
|
$dlurl = content::download_url((int)$USER->id, (int)$inst->cm->id, $scoid)->out(false);
|
|
$actions[] = self::icon_action('download', get_string('downloadcontent', 'meetletacbfree'), [
|
|
'href' => $dlurl,
|
|
'class' => 'meetlet-iconbtn--download',
|
|
'target' => '_self',
|
|
'download' => $item->name,
|
|
'data-action' => 'download-content',
|
|
]);
|
|
}
|
|
$table->data[] = [
|
|
format_string($item->name),
|
|
s($item->typelabel ?? service::content_type_label(
|
|
(string)($item->icon ?? ''),
|
|
(string)($item->type ?? ''),
|
|
(string)($item->name ?? '')
|
|
)),
|
|
self::format_bytes((int)($item->bytesize ?? 0)),
|
|
s($item->datecreated_fmt !== '' ? $item->datecreated_fmt : '—'),
|
|
\html_writer::div(implode('', $actions), 'meetlet-iconactions'),
|
|
];
|
|
}
|
|
return self::wrap_refreshable(\html_writer::div(\html_writer::table($table), 'meetlet-card'), 'content');
|
|
}
|
|
|
|
protected static function format_bytes(int $bytes): string {
|
|
if ($bytes <= 0) {
|
|
return '—';
|
|
}
|
|
$units = ['B', 'KB', 'MB', 'GB'];
|
|
$i = 0;
|
|
$n = (float)$bytes;
|
|
while ($n >= 1024 && $i < count($units) - 1) {
|
|
$n /= 1024;
|
|
$i++;
|
|
}
|
|
return format_float($n, $i === 0 ? 0 : 1) . ' ' . $units[$i];
|
|
}
|
|
}
|