mirror of https://github.com/sualko/cloud_bbb
feat: manage recordings
- list recordings - delete recording - store url to player as url file fix #19pull/36/head
parent
6005928e78
commit
b437d7c33d
|
@ -2,9 +2,11 @@
|
|||
return [
|
||||
'resources' => [
|
||||
'room' => ['url' => '/rooms'],
|
||||
'room_api' => ['url' => '/api/0.1/rooms']
|
||||
'room_api' => ['url' => '/api/0.1/rooms'],
|
||||
],
|
||||
'routes' => [
|
||||
['name' => 'server#records', 'url' => '/server/{roomUid}/records', 'verb' => 'GET'],
|
||||
['name' => 'server#delete_record', 'url' => '/server/record/{recordId}', 'verb' => 'DELETE'],
|
||||
['name' => 'join#index', 'url' => '/b/{token}', 'verb' => 'GET'],
|
||||
['name' => 'room_api#preflighted_cors', 'url' => '/api/0.1/{path}',
|
||||
'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']]
|
||||
|
|
|
@ -12,12 +12,12 @@
|
|||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sualko/bigbluebutton-api-php.git",
|
||||
"reference": "75230993ab7714ef27b7177486dd4c99146b3bf7"
|
||||
"reference": "3b8da2e1b9469deebe2713bb679e9c88e258ec9b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sualko/bigbluebutton-api-php/zipball/75230993ab7714ef27b7177486dd4c99146b3bf7",
|
||||
"reference": "75230993ab7714ef27b7177486dd4c99146b3bf7",
|
||||
"url": "https://api.github.com/repos/sualko/bigbluebutton-api-php/zipball/3b8da2e1b9469deebe2713bb679e9c88e258ec9b",
|
||||
"reference": "3b8da2e1b9469deebe2713bb679e9c88e258ec9b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -62,7 +62,7 @@
|
|||
"support": {
|
||||
"source": "https://github.com/sualko/bigbluebutton-api-php/tree/fork"
|
||||
},
|
||||
"time": "2020-04-27T14:32:50+00:00"
|
||||
"time": "2020-05-17T08:37:02+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
@ -1964,7 +1964,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v5.0.7",
|
||||
"version": "v5.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
|
@ -2054,7 +2054,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v5.0.7",
|
||||
"version": "v5.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
|
@ -2196,16 +2196,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v5.0.7",
|
||||
"version": "v5.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "ca3b87dd09fff9b771731637f5379965fbfab420"
|
||||
"reference": "7cd0dafc4353a0f62e307df90b48466379c8cc91"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/ca3b87dd09fff9b771731637f5379965fbfab420",
|
||||
"reference": "ca3b87dd09fff9b771731637f5379965fbfab420",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/7cd0dafc4353a0f62e307df90b48466379c8cc91",
|
||||
"reference": "7cd0dafc4353a0f62e307df90b48466379c8cc91",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2256,11 +2256,11 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-03-27T16:56:45+00:00"
|
||||
"time": "2020-04-12T14:40:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v5.0.7",
|
||||
"version": "v5.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
|
@ -2323,16 +2323,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v5.0.7",
|
||||
"version": "v5.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/options-resolver.git",
|
||||
"reference": "09dccfffd24b311df7f184aa80ee7b61ad61ed8d"
|
||||
"reference": "3707e3caeff2b797c0bfaadd5eba723dd44e6bf1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/09dccfffd24b311df7f184aa80ee7b61ad61ed8d",
|
||||
"reference": "09dccfffd24b311df7f184aa80ee7b61ad61ed8d",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/3707e3caeff2b797c0bfaadd5eba723dd44e6bf1",
|
||||
"reference": "3707e3caeff2b797c0bfaadd5eba723dd44e6bf1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2387,20 +2387,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-03-27T16:56:45+00:00"
|
||||
"time": "2020-04-06T10:40:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.15.0",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14"
|
||||
"reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14",
|
||||
"reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
|
||||
"reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2412,7 +2412,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.15-dev"
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -2459,20 +2459,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-02-27T09:26:54+00:00"
|
||||
"time": "2020-05-12T16:14:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.15.0",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
|
||||
"reference": "fa79b11539418b02fc5e1897267673ba2c19419c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
|
||||
"reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c",
|
||||
"reference": "fa79b11539418b02fc5e1897267673ba2c19419c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2484,7 +2484,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.15-dev"
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -2532,20 +2532,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-03-09T19:04:49+00:00"
|
||||
"time": "2020-05-12T16:47:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php70",
|
||||
"version": "v1.15.0",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php70.git",
|
||||
"reference": "2a18e37a489803559284416df58c71ccebe50bf0"
|
||||
"reference": "82225c2d7d23d7e70515496d249c0152679b468e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/2a18e37a489803559284416df58c71ccebe50bf0",
|
||||
"reference": "2a18e37a489803559284416df58c71ccebe50bf0",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/82225c2d7d23d7e70515496d249c0152679b468e",
|
||||
"reference": "82225c2d7d23d7e70515496d249c0152679b468e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2555,7 +2555,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.15-dev"
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -2591,20 +2591,34 @@
|
|||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"time": "2020-02-27T09:26:54+00:00"
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-12T16:47:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php72",
|
||||
"version": "v1.15.0",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php72.git",
|
||||
"reference": "37b0976c78b94856543260ce09b460a7bc852747"
|
||||
"reference": "f048e612a3905f34931127360bdd2def19a5e582"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747",
|
||||
"reference": "37b0976c78b94856543260ce09b460a7bc852747",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/f048e612a3905f34931127360bdd2def19a5e582",
|
||||
"reference": "f048e612a3905f34931127360bdd2def19a5e582",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2613,7 +2627,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.15-dev"
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -2660,20 +2674,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-02-27T09:26:54+00:00"
|
||||
"time": "2020-05-12T16:47:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php73",
|
||||
"version": "v1.15.0",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php73.git",
|
||||
"reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7"
|
||||
"reference": "a760d8964ff79ab9bf057613a5808284ec852ccc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
|
||||
"reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a760d8964ff79ab9bf057613a5808284ec852ccc",
|
||||
"reference": "a760d8964ff79ab9bf057613a5808284ec852ccc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2682,7 +2696,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.15-dev"
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -2732,20 +2746,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-02-27T09:26:54+00:00"
|
||||
"time": "2020-05-12T16:47:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v5.0.7",
|
||||
"version": "v5.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e"
|
||||
"reference": "3179f68dff5bad14d38c4114a1dab98030801fd7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e",
|
||||
"reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/3179f68dff5bad14d38c4114a1dab98030801fd7",
|
||||
"reference": "3179f68dff5bad14d38c4114a1dab98030801fd7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2795,7 +2809,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-03-27T16:56:45+00:00"
|
||||
"time": "2020-04-15T15:59:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
|
@ -2857,7 +2871,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
"version": "v5.0.7",
|
||||
"version": "v5.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/stopwatch.git",
|
||||
|
|
|
@ -5,6 +5,9 @@ namespace OCA\BigBlueButton\BigBlueButton;
|
|||
use BigBlueButton\BigBlueButton;
|
||||
use BigBlueButton\Parameters\CreateMeetingParameters;
|
||||
use BigBlueButton\Parameters\JoinMeetingParameters;
|
||||
use BigBlueButton\Parameters\GetRecordingsParameters;
|
||||
use BigBlueButton\Core\Record;
|
||||
use BigBlueButton\Parameters\DeleteRecordingsParameters;
|
||||
use OCA\BigBlueButton\Db\Room;
|
||||
use OCP\IConfig;
|
||||
use OCP\IURLGenerator;
|
||||
|
@ -112,4 +115,69 @@ class API
|
|||
|
||||
return $createMeetingParams;
|
||||
}
|
||||
|
||||
public function getRecording(string $recordId)
|
||||
{
|
||||
$recordingParams = new GetRecordingsParameters();
|
||||
$recordingParams->setRecordId($recordId);
|
||||
$recordingParams->setState('any');
|
||||
|
||||
$response = $this->getServer()->getRecordings($recordingParams);
|
||||
|
||||
if (!$response->success()) {
|
||||
throw new \Exception('Could not process get recording request');
|
||||
}
|
||||
|
||||
$records = $response->getRecords();
|
||||
|
||||
if (count($records) === 0) {
|
||||
throw new \Exception('Found no record with given id');
|
||||
}
|
||||
|
||||
return $this->recordToArray($records[0]);
|
||||
}
|
||||
|
||||
public function getRecordings(Room $room)
|
||||
{
|
||||
$recordingParams = new GetRecordingsParameters();
|
||||
$recordingParams->setMeetingId($room->uid);
|
||||
$recordingParams->setState('processing,processed,published,unpublished');
|
||||
|
||||
$response = $this->getServer()->getRecordings($recordingParams);
|
||||
|
||||
if (!$response->success()) {
|
||||
throw new \Exception('Could not process get recordings request');
|
||||
}
|
||||
|
||||
$records = $response->getRecords();
|
||||
|
||||
return array_map(function ($record) {
|
||||
return $this->recordToArray($record);
|
||||
}, $records);
|
||||
}
|
||||
|
||||
public function deleteRecording(string $recordingId): bool
|
||||
{
|
||||
$deleteParams = new DeleteRecordingsParameters($recordingId);
|
||||
|
||||
$response = $this->getServer()->deleteRecordings($deleteParams);
|
||||
|
||||
return $response->isDeleted();
|
||||
}
|
||||
|
||||
private function recordToArray(Record $record)
|
||||
{
|
||||
return [
|
||||
'id' => $record->getRecordId(),
|
||||
'name' => $record->getName(),
|
||||
'published' => $record->isPublished(),
|
||||
'state' => $record->getState(),
|
||||
'startTime' => $record->getStartTime(),
|
||||
'participants' => $record->getParticipantCount(),
|
||||
'type' => $record->getPlaybackType(),
|
||||
'length' => $record->getPlaybackLength(),
|
||||
'url' => $record->getPlaybackUrl(),
|
||||
'metas' => $record->getMetas(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\BigBlueButton\Controller;
|
||||
|
||||
use OCA\BigBlueButton\BigBlueButton\API;
|
||||
use OCP\IRequest;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Controller;
|
||||
|
||||
use OCA\BigBlueButton\Service\RoomService;
|
||||
|
||||
class ServerController extends Controller
|
||||
{
|
||||
/** @var RoomService */
|
||||
private $service;
|
||||
|
||||
/** @var API */
|
||||
private $server;
|
||||
|
||||
/** @var string */
|
||||
private $userId;
|
||||
|
||||
public function __construct(
|
||||
$appName,
|
||||
IRequest $request,
|
||||
RoomService $service,
|
||||
API $server,
|
||||
$UserId
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->service = $service;
|
||||
$this->server = $server;
|
||||
$this->userId = $UserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function records(string $roomUid): DataResponse
|
||||
{
|
||||
$room = $this->service->findByUid($roomUid);
|
||||
|
||||
if ($room === null) {
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($room->userId !== $this->userId) {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$recordings = $this->server->getRecordings($room);
|
||||
|
||||
return new DataResponse($recordings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function deleteRecord(string $recordId): DataResponse
|
||||
{
|
||||
$record = $this->server->getRecording($recordId);
|
||||
|
||||
$room = $this->service->findByUid($record['metas']['meetingId']);
|
||||
|
||||
if ($room === null) {
|
||||
return new DataResponse(false, Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($room->userId !== $this->userId) {
|
||||
return new DataResponse(false, Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$success = $this->server->deleteRecording($recordId);
|
||||
|
||||
return new DataResponse($success);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,19 @@ export interface Room {
|
|||
record: boolean;
|
||||
}
|
||||
|
||||
export type Recording = {
|
||||
id: string;
|
||||
name: string;
|
||||
published: boolean;
|
||||
state: 'processing' | 'processed' | 'published' | 'unpublished' | 'deleted';
|
||||
startTime: number;
|
||||
participants: number;
|
||||
type: string;
|
||||
length: number;
|
||||
url: string;
|
||||
meta: any;
|
||||
}
|
||||
|
||||
class Api {
|
||||
public getUrl(endpoint: string): string {
|
||||
return OC.generateUrl(`apps/bbb/${endpoint}`);
|
||||
|
@ -42,6 +55,26 @@ class Api {
|
|||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getRecordings(uid: string) {
|
||||
const response = await axios.get(this.getUrl(`server/${uid}/records`));
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteRecording(id: string) {
|
||||
const response = await axios.delete(this.getUrl(`server/record/${id}`));
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async storeRecording(recording: Recording, path: string) {
|
||||
const startDate = new Date(recording.startTime);
|
||||
const url = `/remote.php/dav/files/${OC.currentUser}${path}/${encodeURIComponent(recording.name + ' ' + startDate.toISOString())}.url`;
|
||||
const response = await axios.put(url, `[InternetShortcut]\nURL=${recording.url}`);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new Api();
|
||||
|
|
|
@ -102,4 +102,25 @@
|
|||
background-color: rgba(0, 128, 0, 0.445);
|
||||
}
|
||||
}
|
||||
|
||||
.selected-row,
|
||||
.recordings-row {
|
||||
border-left: 3px solid #888;
|
||||
}
|
||||
|
||||
.selected-row {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.recordings-row {
|
||||
background-color: #f7f7f7;
|
||||
|
||||
&> td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,6 +109,9 @@ const App: React.FC<Props> = () => {
|
|||
<th onClick={() => onOrderBy('record')}>
|
||||
{t('bbb', 'Record')} <SortArrow name='record' value={orderBy} direction={sortOrder} />
|
||||
</th>
|
||||
<th>
|
||||
{t('bbb', 'Recordings')}
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
|
@ -10,6 +10,8 @@ declare namespace OC {
|
|||
}
|
||||
|
||||
namespace dialogs {
|
||||
function alert(text: string, title: string, callback: () => void, modal?: boolean): void;
|
||||
|
||||
function info(text: string, title: string, callback: () => void, modal?: boolean): void;
|
||||
|
||||
function confirm(text: string, title: string, callback: (result: boolean) => void, modal?: boolean): void;
|
||||
|
@ -61,6 +63,8 @@ declare namespace OC {
|
|||
const PERMISSION_SHARE = 16;
|
||||
const PERMISSION_ALL = 31;
|
||||
|
||||
const currentUser: string;
|
||||
|
||||
const config: {
|
||||
blacklist_files_regex: string;
|
||||
enable_avatars: boolean;
|
||||
|
@ -77,6 +81,8 @@ declare namespace OC {
|
|||
|
||||
declare function t(app: string, string: string, vars?: { [key: string]: string }, count?: number, options?: EscapeOptions): string;
|
||||
|
||||
declare function n(app: string, singular: string, plural: string, number: number, vars?: { [key: string]: string }): string;
|
||||
|
||||
declare module 'NC' {
|
||||
export interface OCSResult<T> {
|
||||
ocs: {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { Recording } from './Api';
|
||||
|
||||
type Props = {
|
||||
recording: Recording;
|
||||
deleteRecording: (recording: Recording) => void;
|
||||
storeRecording: (recording: Recording) => void;
|
||||
}
|
||||
|
||||
const RecordingRow: React.FC<Props> = ({recording, deleteRecording, storeRecording}) => {
|
||||
return (
|
||||
<tr key={recording.id}>
|
||||
<td className="share icon-col">
|
||||
<CopyToClipboard text={recording.url}>
|
||||
<span className="icon icon-clippy icon-visible copy-to-clipboard" ></span>
|
||||
</CopyToClipboard>
|
||||
</td>
|
||||
<td className="start icon-col">
|
||||
<a href={recording.url} className="icon icon-external icon-visible" target="_blank" rel="noopener noreferrer"></a>
|
||||
</td>
|
||||
<td className="icon-col">
|
||||
<a onClick={() => storeRecording(recording)} className="icon icon-download icon-visible"></a>
|
||||
</td>
|
||||
<td>
|
||||
{(new Date(recording.startTime)).toLocaleString()}
|
||||
</td>
|
||||
<td>
|
||||
{recording.length === 0 ? '< 1 min' : (recording.length + ' min')}
|
||||
</td>
|
||||
<td>
|
||||
{n('bbb', '%n participant', '%n participants', recording.participants)}
|
||||
</td>
|
||||
<td>
|
||||
{recording.type}
|
||||
</td>
|
||||
<td className="remove icon-col">
|
||||
<a className="icon icon-delete icon-visible"
|
||||
onClick={() => deleteRecording(recording)}
|
||||
title={t('bbb', 'Delete')} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordingRow;
|
|
@ -1,28 +1,51 @@
|
|||
import React, { useState } from 'react';
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { SubmitInput } from './SubmitInput';
|
||||
import { Room, api } from './Api';
|
||||
import { Room, Recording, api } from './Api';
|
||||
import RecordingRow from './RecordingRow';
|
||||
|
||||
type Props = {
|
||||
room: Room;
|
||||
updateRoom: (room: Room) => void;
|
||||
deleteRoom: (id: number) => void;
|
||||
room: Room;
|
||||
updateRoom: (room: Room) => void;
|
||||
deleteRoom: (id: number) => void;
|
||||
}
|
||||
|
||||
type EditableValueProps = {
|
||||
setValue: (key: string, value: string|number) => void;
|
||||
setActive: (key: string) => void;
|
||||
active: string;
|
||||
field: string;
|
||||
value: string;
|
||||
type: 'text' | 'number';
|
||||
setValue: (key: string, value: string | number) => void;
|
||||
setActive: (key: string) => void;
|
||||
active: string;
|
||||
field: string;
|
||||
value: string;
|
||||
type: 'text' | 'number';
|
||||
}
|
||||
|
||||
type RecordingsNumberProps = {
|
||||
recordings: null | Recording[];
|
||||
showRecordings: boolean;
|
||||
setShowRecordings: (showRecordings: boolean) => void;
|
||||
}
|
||||
|
||||
const RecordingsNumber: React.FC<RecordingsNumberProps> = ({ recordings, showRecordings, setShowRecordings }) => {
|
||||
if (recordings === null) {
|
||||
return <span className="icon icon-loading-small icon-visible"></span>;
|
||||
}
|
||||
|
||||
if (recordings.length > 0) {
|
||||
return (
|
||||
<a onClick={() => setShowRecordings(!showRecordings)}>
|
||||
{recordings.length} <span className='sort_arrow'>{showRecordings ? '▼' : '▲'}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>0</span>;
|
||||
};
|
||||
|
||||
const EditableValue: React.FC<EditableValueProps> = ({ setValue, setActive, active, field, value, type }) => {
|
||||
if (active === field) {
|
||||
return <SubmitInput
|
||||
autoFocus={true}
|
||||
onSubmitValue={(value) => setValue(field, type === 'number' ? parseInt(value):value)}
|
||||
onSubmitValue={(value) => setValue(field, type === 'number' ? parseInt(value) : value)}
|
||||
onClick={event => event.stopPropagation()}
|
||||
initialValue={value}
|
||||
type={type}
|
||||
|
@ -40,9 +63,26 @@ const EditableValue: React.FC<EditableValueProps> = ({ setValue, setActive, acti
|
|||
|
||||
const RoomRow: React.FC<Props> = (props) => {
|
||||
const [activeEdit, setActiveEdit] = useState('');
|
||||
const [recordings, setRecordings] = useState<Recording[] | null>(null);
|
||||
const [showRecordings, setShowRecordings] = useState<boolean>(false);
|
||||
const room = props.room;
|
||||
const areRecordingsLoaded = recordings !== null;
|
||||
|
||||
function updateRoom(key: string, value: string|boolean|number) {
|
||||
useEffect(() => {
|
||||
if (areRecordingsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.getRecordings(room.uid).then(recordings => {
|
||||
setRecordings(recordings);
|
||||
}).catch(err => {
|
||||
console.warn('Could not request recordings: ' + room.uid, err);
|
||||
|
||||
setRecordings([]);
|
||||
});
|
||||
}, [areRecordingsLoaded]);
|
||||
|
||||
function updateRoom(key: string, value: string | boolean | number) {
|
||||
props.updateRoom({
|
||||
...props.room,
|
||||
[key]: value,
|
||||
|
@ -66,39 +106,108 @@ const RoomRow: React.FC<Props> = (props) => {
|
|||
);
|
||||
}
|
||||
|
||||
function edit(field: string, type: 'text' | 'number' = 'text'){
|
||||
function storeRecording(recording: Recording) {
|
||||
OC.dialogs.filepicker(t('bbb', 'Select target folder'), (path: string) => {
|
||||
api.storeRecording(recording, path).then(() => {
|
||||
OC.dialogs.info(
|
||||
t('bbb', 'URL to presentation was stored in "{path}"', { path: path + '/' }),
|
||||
t('bbb', 'File stored'),
|
||||
() => undefined,
|
||||
);
|
||||
}).catch(err => {
|
||||
console.warn('Could not store recording', err);
|
||||
|
||||
OC.dialogs.alert(
|
||||
t('bbb', 'URL to presentation could not be stored.'),
|
||||
t('bbb', 'Error'),
|
||||
() => undefined
|
||||
);
|
||||
});
|
||||
}, undefined, 'httpd/unix-directory');
|
||||
}
|
||||
|
||||
function deleteRecording(recording: Recording) {
|
||||
OC.dialogs.confirm(
|
||||
t('bbb', 'Are you sure you want to delete the recording from "{startDate}"? This operation can not be undone', { startDate: (new Date(recording.startTime)).toLocaleString() }),
|
||||
t('bbb', 'Delete?'),
|
||||
confirmed => {
|
||||
if (confirmed) {
|
||||
api.deleteRecording(recording.id).then(success => {
|
||||
if (!success) {
|
||||
OC.dialogs.info(
|
||||
t('bbb', 'Could not delete record'),
|
||||
t('bbb', 'Error'),
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordings === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRecordings(recordings.filter(r => r.id !== recording.id));
|
||||
}).catch(err => {
|
||||
console.warn('Could not delete recording', err);
|
||||
|
||||
OC.dialogs.info(
|
||||
t('bbb', 'Could not delete record'),
|
||||
t('bbb', 'Server error'),
|
||||
() => undefined,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function edit(field: string, type: 'text' | 'number' = 'text') {
|
||||
return <EditableValue field={field} value={room[field]} active={activeEdit} setActive={setActiveEdit} setValue={updateRoom} type={type} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={room.id}>
|
||||
<td className="share icon-col">
|
||||
<CopyToClipboard text={window.location.origin + api.getUrl(`b/${room.uid}`)}>
|
||||
<span className="icon icon-clippy icon-visible copy-to-clipboard" ></span>
|
||||
</CopyToClipboard>
|
||||
</td>
|
||||
<td className="start icon-col">
|
||||
<a href={api.getUrl(`b/${room.uid}`)} className="icon icon-play icon-visible" target="_blank" rel="noopener noreferrer"></a>
|
||||
</td>
|
||||
<td className="name">
|
||||
{edit('name')}
|
||||
</td>
|
||||
<td className="welcome">
|
||||
{edit('welcome')}
|
||||
</td>
|
||||
<td className="max-participants">
|
||||
{edit('maxParticipants', 'number')}
|
||||
</td>
|
||||
<td className="record">
|
||||
<input id={`bbb-record-${room.id}`} type="checkbox" className="checkbox" checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
|
||||
<label htmlFor={`bbb-record-${room.id}`}></label>
|
||||
</td>
|
||||
<td className="remove icon-col">
|
||||
<a className="icon icon-delete icon-visible"
|
||||
onClick={deleteRow as any}
|
||||
title={t('bbb', 'Delete')} />
|
||||
</td>
|
||||
</tr>
|
||||
<>
|
||||
<tr className={showRecordings ? 'selected-row' : ''}>
|
||||
<td className="share icon-col">
|
||||
<CopyToClipboard text={window.location.origin + api.getUrl(`b/${room.uid}`)}>
|
||||
<span className="icon icon-clippy icon-visible copy-to-clipboard" ></span>
|
||||
</CopyToClipboard>
|
||||
</td>
|
||||
<td className="start icon-col">
|
||||
<a href={api.getUrl(`b/${room.uid}`)} className="icon icon-play icon-visible" target="_blank" rel="noopener noreferrer"></a>
|
||||
</td>
|
||||
<td className="name">
|
||||
{edit('name')}
|
||||
</td>
|
||||
<td className="welcome">
|
||||
{edit('welcome')}
|
||||
</td>
|
||||
<td className="max-participants">
|
||||
{edit('maxParticipants', 'number')}
|
||||
</td>
|
||||
<td className="record">
|
||||
<input id={`bbb-record-${room.id}`} type="checkbox" className="checkbox" checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
|
||||
<label htmlFor={`bbb-record-${room.id}`}></label>
|
||||
</td>
|
||||
<td><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
|
||||
<td className="remove icon-col">
|
||||
<a className="icon icon-delete icon-visible"
|
||||
onClick={deleteRow as any}
|
||||
title={t('bbb', 'Delete')} />
|
||||
</td>
|
||||
</tr>
|
||||
{showRecordings && <tr className="recordings-row">
|
||||
<td colSpan={8}>
|
||||
<table>
|
||||
<tbody>
|
||||
{recordings?.map(recording => <RecordingRow key={recording.id} recording={recording} deleteRecording={deleteRecording} storeRecording={storeRecording} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue