feat: manage recordings

- list recordings
- delete recording
- store url to player as url file

fix #19
pull/36/head
sualko 2020-05-17 11:09:16 +02:00
parent 6005928e78
commit b437d7c33d
10 changed files with 478 additions and 97 deletions

View File

@ -2,9 +2,11 @@
return [ return [
'resources' => [ 'resources' => [
'room' => ['url' => '/rooms'], 'room' => ['url' => '/rooms'],
'room_api' => ['url' => '/api/0.1/rooms'] 'room_api' => ['url' => '/api/0.1/rooms'],
], ],
'routes' => [ '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' => 'join#index', 'url' => '/b/{token}', 'verb' => 'GET'],
['name' => 'room_api#preflighted_cors', 'url' => '/api/0.1/{path}', ['name' => 'room_api#preflighted_cors', 'url' => '/api/0.1/{path}',
'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']] 'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']]

120
composer.lock generated
View File

@ -12,12 +12,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sualko/bigbluebutton-api-php.git", "url": "https://github.com/sualko/bigbluebutton-api-php.git",
"reference": "75230993ab7714ef27b7177486dd4c99146b3bf7" "reference": "3b8da2e1b9469deebe2713bb679e9c88e258ec9b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sualko/bigbluebutton-api-php/zipball/75230993ab7714ef27b7177486dd4c99146b3bf7", "url": "https://api.github.com/repos/sualko/bigbluebutton-api-php/zipball/3b8da2e1b9469deebe2713bb679e9c88e258ec9b",
"reference": "75230993ab7714ef27b7177486dd4c99146b3bf7", "reference": "3b8da2e1b9469deebe2713bb679e9c88e258ec9b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -62,7 +62,7 @@
"support": { "support": {
"source": "https://github.com/sualko/bigbluebutton-api-php/tree/fork" "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": [ "packages-dev": [
@ -1964,7 +1964,7 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v5.0.7", "version": "v5.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
@ -2054,7 +2054,7 @@
}, },
{ {
"name": "symfony/event-dispatcher", "name": "symfony/event-dispatcher",
"version": "v5.0.7", "version": "v5.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/event-dispatcher.git", "url": "https://github.com/symfony/event-dispatcher.git",
@ -2196,16 +2196,16 @@
}, },
{ {
"name": "symfony/filesystem", "name": "symfony/filesystem",
"version": "v5.0.7", "version": "v5.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/filesystem.git", "url": "https://github.com/symfony/filesystem.git",
"reference": "ca3b87dd09fff9b771731637f5379965fbfab420" "reference": "7cd0dafc4353a0f62e307df90b48466379c8cc91"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/ca3b87dd09fff9b771731637f5379965fbfab420", "url": "https://api.github.com/repos/symfony/filesystem/zipball/7cd0dafc4353a0f62e307df90b48466379c8cc91",
"reference": "ca3b87dd09fff9b771731637f5379965fbfab420", "reference": "7cd0dafc4353a0f62e307df90b48466379c8cc91",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2256,11 +2256,11 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-03-27T16:56:45+00:00" "time": "2020-04-12T14:40:17+00:00"
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v5.0.7", "version": "v5.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
@ -2323,16 +2323,16 @@
}, },
{ {
"name": "symfony/options-resolver", "name": "symfony/options-resolver",
"version": "v5.0.7", "version": "v5.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/options-resolver.git", "url": "https://github.com/symfony/options-resolver.git",
"reference": "09dccfffd24b311df7f184aa80ee7b61ad61ed8d" "reference": "3707e3caeff2b797c0bfaadd5eba723dd44e6bf1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/09dccfffd24b311df7f184aa80ee7b61ad61ed8d", "url": "https://api.github.com/repos/symfony/options-resolver/zipball/3707e3caeff2b797c0bfaadd5eba723dd44e6bf1",
"reference": "09dccfffd24b311df7f184aa80ee7b61ad61ed8d", "reference": "3707e3caeff2b797c0bfaadd5eba723dd44e6bf1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2387,20 +2387,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-03-27T16:56:45+00:00" "time": "2020-04-06T10:40:56+00:00"
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.15.0", "version": "v1.17.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
"reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2412,7 +2412,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.15-dev" "dev-master": "1.17-dev"
} }
}, },
"autoload": { "autoload": {
@ -2459,20 +2459,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-02-27T09:26:54+00:00" "time": "2020-05-12T16:14:59+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.15.0", "version": "v1.17.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac" "reference": "fa79b11539418b02fc5e1897267673ba2c19419c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c",
"reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac", "reference": "fa79b11539418b02fc5e1897267673ba2c19419c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2484,7 +2484,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.15-dev" "dev-master": "1.17-dev"
} }
}, },
"autoload": { "autoload": {
@ -2532,20 +2532,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-03-09T19:04:49+00:00" "time": "2020-05-12T16:47:27+00:00"
}, },
{ {
"name": "symfony/polyfill-php70", "name": "symfony/polyfill-php70",
"version": "v1.15.0", "version": "v1.17.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php70.git", "url": "https://github.com/symfony/polyfill-php70.git",
"reference": "2a18e37a489803559284416df58c71ccebe50bf0" "reference": "82225c2d7d23d7e70515496d249c0152679b468e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/2a18e37a489803559284416df58c71ccebe50bf0", "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/82225c2d7d23d7e70515496d249c0152679b468e",
"reference": "2a18e37a489803559284416df58c71ccebe50bf0", "reference": "82225c2d7d23d7e70515496d249c0152679b468e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2555,7 +2555,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.15-dev" "dev-master": "1.17-dev"
} }
}, },
"autoload": { "autoload": {
@ -2591,20 +2591,34 @@
"portable", "portable",
"shim" "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", "name": "symfony/polyfill-php72",
"version": "v1.15.0", "version": "v1.17.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php72.git", "url": "https://github.com/symfony/polyfill-php72.git",
"reference": "37b0976c78b94856543260ce09b460a7bc852747" "reference": "f048e612a3905f34931127360bdd2def19a5e582"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747", "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/f048e612a3905f34931127360bdd2def19a5e582",
"reference": "37b0976c78b94856543260ce09b460a7bc852747", "reference": "f048e612a3905f34931127360bdd2def19a5e582",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2613,7 +2627,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.15-dev" "dev-master": "1.17-dev"
} }
}, },
"autoload": { "autoload": {
@ -2660,20 +2674,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-02-27T09:26:54+00:00" "time": "2020-05-12T16:47:27+00:00"
}, },
{ {
"name": "symfony/polyfill-php73", "name": "symfony/polyfill-php73",
"version": "v1.15.0", "version": "v1.17.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php73.git", "url": "https://github.com/symfony/polyfill-php73.git",
"reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7" "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7", "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a760d8964ff79ab9bf057613a5808284ec852ccc",
"reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7", "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2682,7 +2696,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.15-dev" "dev-master": "1.17-dev"
} }
}, },
"autoload": { "autoload": {
@ -2732,20 +2746,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-02-27T09:26:54+00:00" "time": "2020-05-12T16:47:27+00:00"
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v5.0.7", "version": "v5.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e" "reference": "3179f68dff5bad14d38c4114a1dab98030801fd7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e", "url": "https://api.github.com/repos/symfony/process/zipball/3179f68dff5bad14d38c4114a1dab98030801fd7",
"reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e", "reference": "3179f68dff5bad14d38c4114a1dab98030801fd7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2795,7 +2809,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-03-27T16:56:45+00:00" "time": "2020-04-15T15:59:10+00:00"
}, },
{ {
"name": "symfony/service-contracts", "name": "symfony/service-contracts",
@ -2857,7 +2871,7 @@
}, },
{ {
"name": "symfony/stopwatch", "name": "symfony/stopwatch",
"version": "v5.0.7", "version": "v5.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/stopwatch.git", "url": "https://github.com/symfony/stopwatch.git",

View File

@ -5,6 +5,9 @@ namespace OCA\BigBlueButton\BigBlueButton;
use BigBlueButton\BigBlueButton; use BigBlueButton\BigBlueButton;
use BigBlueButton\Parameters\CreateMeetingParameters; use BigBlueButton\Parameters\CreateMeetingParameters;
use BigBlueButton\Parameters\JoinMeetingParameters; use BigBlueButton\Parameters\JoinMeetingParameters;
use BigBlueButton\Parameters\GetRecordingsParameters;
use BigBlueButton\Core\Record;
use BigBlueButton\Parameters\DeleteRecordingsParameters;
use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Db\Room;
use OCP\IConfig; use OCP\IConfig;
use OCP\IURLGenerator; use OCP\IURLGenerator;
@ -112,4 +115,69 @@ class API
return $createMeetingParams; 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(),
];
}
} }

View File

@ -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);
}
}

View File

@ -9,6 +9,19 @@ export interface Room {
record: boolean; 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 { class Api {
public getUrl(endpoint: string): string { public getUrl(endpoint: string): string {
return OC.generateUrl(`apps/bbb/${endpoint}`); return OC.generateUrl(`apps/bbb/${endpoint}`);
@ -42,6 +55,26 @@ class Api {
return response.data; 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(); export const api = new Api();

View File

@ -102,4 +102,25 @@
background-color: rgba(0, 128, 0, 0.445); 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%;
}
}
} }

View File

@ -109,6 +109,9 @@ const App: React.FC<Props> = () => {
<th onClick={() => onOrderBy('record')}> <th onClick={() => onOrderBy('record')}>
{t('bbb', 'Record')} <SortArrow name='record' value={orderBy} direction={sortOrder} /> {t('bbb', 'Record')} <SortArrow name='record' value={orderBy} direction={sortOrder} />
</th> </th>
<th>
{t('bbb', 'Recordings')}
</th>
<th /> <th />
</tr> </tr>
</thead> </thead>

View File

@ -10,6 +10,8 @@ declare namespace OC {
} }
namespace dialogs { 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 info(text: string, title: string, callback: () => void, modal?: boolean): void;
function confirm(text: string, title: string, callback: (result: boolean) => 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_SHARE = 16;
const PERMISSION_ALL = 31; const PERMISSION_ALL = 31;
const currentUser: string;
const config: { const config: {
blacklist_files_regex: string; blacklist_files_regex: string;
enable_avatars: boolean; 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 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' { declare module 'NC' {
export interface OCSResult<T> { export interface OCSResult<T> {
ocs: { ocs: {

View File

@ -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;

View File

@ -1,28 +1,51 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import {CopyToClipboard} from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import { SubmitInput } from './SubmitInput'; import { SubmitInput } from './SubmitInput';
import { Room, api } from './Api'; import { Room, Recording, api } from './Api';
import RecordingRow from './RecordingRow';
type Props = { type Props = {
room: Room; room: Room;
updateRoom: (room: Room) => void; updateRoom: (room: Room) => void;
deleteRoom: (id: number) => void; deleteRoom: (id: number) => void;
} }
type EditableValueProps = { type EditableValueProps = {
setValue: (key: string, value: string|number) => void; setValue: (key: string, value: string | number) => void;
setActive: (key: string) => void; setActive: (key: string) => void;
active: string; active: string;
field: string; field: string;
value: string; value: string;
type: 'text' | 'number'; 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 }) => { const EditableValue: React.FC<EditableValueProps> = ({ setValue, setActive, active, field, value, type }) => {
if (active === field) { if (active === field) {
return <SubmitInput return <SubmitInput
autoFocus={true} autoFocus={true}
onSubmitValue={(value) => setValue(field, type === 'number' ? parseInt(value):value)} onSubmitValue={(value) => setValue(field, type === 'number' ? parseInt(value) : value)}
onClick={event => event.stopPropagation()} onClick={event => event.stopPropagation()}
initialValue={value} initialValue={value}
type={type} type={type}
@ -40,9 +63,26 @@ const EditableValue: React.FC<EditableValueProps> = ({ setValue, setActive, acti
const RoomRow: React.FC<Props> = (props) => { const RoomRow: React.FC<Props> = (props) => {
const [activeEdit, setActiveEdit] = useState(''); const [activeEdit, setActiveEdit] = useState('');
const [recordings, setRecordings] = useState<Recording[] | null>(null);
const [showRecordings, setShowRecordings] = useState<boolean>(false);
const room = props.room; 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.updateRoom({
...props.room, ...props.room,
[key]: value, [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 <EditableValue field={field} value={room[field]} active={activeEdit} setActive={setActiveEdit} setValue={updateRoom} type={type} />;
} }
return ( return (
<tr key={room.id}> <>
<td className="share icon-col"> <tr className={showRecordings ? 'selected-row' : ''}>
<CopyToClipboard text={window.location.origin + api.getUrl(`b/${room.uid}`)}> <td className="share icon-col">
<span className="icon icon-clippy icon-visible copy-to-clipboard" ></span> <CopyToClipboard text={window.location.origin + api.getUrl(`b/${room.uid}`)}>
</CopyToClipboard> <span className="icon icon-clippy icon-visible copy-to-clipboard" ></span>
</td> </CopyToClipboard>
<td className="start icon-col"> </td>
<a href={api.getUrl(`b/${room.uid}`)} className="icon icon-play icon-visible" target="_blank" rel="noopener noreferrer"></a> <td className="start icon-col">
</td> <a href={api.getUrl(`b/${room.uid}`)} className="icon icon-play icon-visible" target="_blank" rel="noopener noreferrer"></a>
<td className="name"> </td>
{edit('name')} <td className="name">
</td> {edit('name')}
<td className="welcome"> </td>
{edit('welcome')} <td className="welcome">
</td> {edit('welcome')}
<td className="max-participants"> </td>
{edit('maxParticipants', 'number')} <td className="max-participants">
</td> {edit('maxParticipants', 'number')}
<td className="record"> </td>
<input id={`bbb-record-${room.id}`} type="checkbox" className="checkbox" checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} /> <td className="record">
<label htmlFor={`bbb-record-${room.id}`}></label> <input id={`bbb-record-${room.id}`} type="checkbox" className="checkbox" checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
</td> <label htmlFor={`bbb-record-${room.id}`}></label>
<td className="remove icon-col"> </td>
<a className="icon icon-delete icon-visible" <td><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
onClick={deleteRow as any} <td className="remove icon-col">
title={t('bbb', 'Delete')} /> <a className="icon icon-delete icon-visible"
</td> onClick={deleteRow as any}
</tr> 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>}
</>
); );
}; };