Files
meetletacbfree/classes/external/get_tab.php
T

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];
}
}