From de83220e094463fc76a6636554968ed81868f2d1 Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 23 May 2025 12:59:38 +0200 Subject: [PATCH] frontend: enhance RoomRecordingsComponent with improved UI and functionality for managing recordings --- .../room-recordings.component.html | 131 ++++++++++++- .../room-recordings.component.scss | 178 ++++++++++++++++++ .../room-recordings.component.ts | 110 ++++++++--- 3 files changed, 387 insertions(+), 32 deletions(-) diff --git a/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.html b/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.html index f041c27..cb48a04 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.html +++ b/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.html @@ -1,8 +1,123 @@ -@if (!loading) { - -} +
+ + + Recordings for room: {{ roomId }} + + + + + +
+
+ @if (recordings.length === 0) { +
+ There are no recordings for this room +
+ } + + @for (recording of recordings; track recording.recordingId) { +
+ + +
+ +
+ @if (recording.status === 'COMPLETE') { + + + } + @if ( + !['STARTING', 'ACTIVE', 'ENDING'].includes(recording.status) && + canDeleteRecordings + ) { + + } +
+
+
+
+
+ Name + {{ recording.filename }} +
+
+ Room + {{ recording.roomId }} +
+
+ Start date + + {{ + recording.startDate + ? (recording.startDate | date: 'M/d/yy, H:mm:ss') + : '-' + }} + +
+
+ End date + + {{ + recording.endDate ? (recording.endDate | date: 'M/d/yy, H:mm:ss') : '-' + }} + +
+
+ Duration + {{ + recording.duration ? (recording.duration | duration) : '-' + }} +
+
+ Size + + {{ + recording.size + ? (recording.size / 1024 / 1024 | number: '1.1-2') + 'MB' + : '-' + }} + +
+
+ Status + + {{ recording.status }} + +
+
+
+
+
+
+ } +
+ + @if (moreRecordings) { +
+ +
+ } +
+
diff --git a/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.scss b/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.scss index e69de29..4a687d0 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.scss +++ b/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.scss @@ -0,0 +1,178 @@ +.dashboard-container { + height: 100%; +} + +.header { + height: 50px; + background-color: var(--ov-primary-action-color); + color: var(--ov-text-primary-color); + justify-content: space-between; +} + +#header-title { + font-weight: bold; +} + +#room-id { + font-weight: normal; + font-style: italic; +} + +#refresh-btn { + color: var(--ov-text-primary-color); +} + +.dashboard-body { + background-color: var(--ov-secondary-action-color); + height: 100%; +} + +.recordings-container { + background-color: var(--ov-secondary-action-color); + height: 100%; + overflow-y: auto; + overflow-x: hidden; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-auto-rows: max-content; + gap: 15px; + justify-content: center; + align-items: center; + padding: 20px; + padding-bottom: 80px; + box-sizing: border-box; + + .recording-card { + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--ov-surface-color); + } +} + +.video-div-container { + position: relative; + text-align: center; + width: 100%; + height: 42%; + overflow: hidden; + background-color: var(--ov-light-color); + display: flex; + justify-content: center; + align-items: center; + + img { + border-radius: var(--ov-video-radius); + display: inline; + position: relative; + max-width: 100%; + max-height: 100%; + } +} + +.video-btns { + position: absolute; + transform: translate(-50%, -50%); + margin-right: -50%; + top: 50%; + left: 50%; + background-color: var(--ov-secondary-action-color); + border-radius: var(--ov-surface-radius); +} + +.video-btns button #play { + color: var(--ov-text-primary-color); +} +.video-btns button #download { + color: var(--ov-accent-action-color); +} +.video-btns button #delete { + color: var(--ov-error-color); +} + +.video-info-container > div { + width: 100%; + height: 100%; + display: table; + table-layout: fixed; + box-sizing: border-box; + margin-top: 20px; +} + +.video-div-tag:first-child { + margin-top: 20px; +} + +.video-div-tag { + display: table-row; +} + +.video-card-tag { + font-size: 13px; + color: var(--ov-text-surface-color); + font-weight: bold; +} + +.video-card-value { + float: right; + font-size: 13.5px; + padding: 0px 5px; + border-radius: 5px; + + &.COMPLETE { + background-color: #8bffc9; + } + + &.ACTIVE { + background-color: #ffcc00; + } + + &.STARTING, + &.ENDING { + background-color: #f5d34c; + } + + &.FAILED, + &.ABORTED, + &.LIMIT_REACHED { + background-color: #ff6666; + } +} + +.status-value { + font-weight: bold; +} + +.load-more-container { + position: absolute; + left: 50%; + bottom: 35px; + transform: translateX(-50%); + + .load-more-btn { + background-color: var(--ov-surface-color); + color: var(--ov-text-surface-color); + border-radius: var(--ov-surface-radius); + } +} + +.no-recordings-warn { + height: calc(100% - 52px); + width: 100%; + display: table; + text-align: center; + color: var(--ov-text-surface-color); +} + +@media (max-width: 600px) { + .grid-container { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + } +} + +@media (min-width: 601px) and (max-width: 900px) { + .grid-container { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + } +} diff --git a/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.ts index 8a4ac8f..8450e01 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.ts @@ -1,35 +1,54 @@ -import { HttpErrorResponse } from '@angular/common/http'; +import { CommonModule, DatePipe, DecimalPipe } from '@angular/common'; import { Component, OnInit } from '@angular/core'; -import { HttpService } from '@lib/services'; -import { - OpenViduComponentsModule, - ApiDirectiveModule, - RecordingDeleteRequestedEvent -} from 'openvidu-components-angular'; -import { MeetRecordingInfo } from 'shared-meet-components'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { ActivatedRoute } from '@angular/router'; +import { ContextService, HttpService } from '@lib/services'; +import { ActionService, MeetRecordingInfo, OpenViduComponentsUiModule } from 'shared-meet-components'; @Component({ selector: 'app-room-recordings', templateUrl: './room-recordings.component.html', styleUrls: ['./room-recordings.component.scss'], standalone: true, - imports: [OpenViduComponentsModule, ApiDirectiveModule] + imports: [ + CommonModule, + MatToolbarModule, + MatCardModule, + MatButtonModule, + MatIconModule, + DatePipe, + DecimalPipe, + OpenViduComponentsUiModule + ] }) export class RoomRecordingsComponent implements OnInit { + roomId = ''; + canDeleteRecordings = false; recordings: MeetRecordingInfo[] = []; - loading = true; - error = ''; - private continuationToken: string | undefined; + moreRecordings = false; + private nextPageToken: string | undefined; - constructor(private httpService: HttpService) {} + constructor( + protected contextService: ContextService, + protected httpService: HttpService, + protected actionService: ActionService, + protected route: ActivatedRoute + ) {} async ngOnInit() { + this.route.params.subscribe((params) => { + this.roomId = params['room-id']; + }); + this.canDeleteRecordings = this.contextService.canDeleteRecordings(); + await this.loadRecordings(); - console.log('Recordings loaded:', this.recordings); } - async onLoadMoreRecordingsRequested() { - if (!this.continuationToken) { + async loadMoreRecordings() { + if (!this.moreRecordings || !this.nextPageToken) { console.warn('No more recordings to load'); return; } @@ -37,27 +56,70 @@ export class RoomRecordingsComponent implements OnInit { await this.loadRecordings(); } - async onRefreshRecordingsClicked() { + async refreshRecordings() { + this.resetRecordings(); await this.loadRecordings(); } - async onDeleteRecordingClicked(deleteRequest: RecordingDeleteRequestedEvent) { + playRecording(recording: MeetRecordingInfo) { + const queryParamForAvoidCache = `?t=${new Date().getTime()}`; + let recordingUrl = this.httpService.getRecordingMediaUrl(recording.recordingId); + recordingUrl += queryParamForAvoidCache; + this.actionService.openRecordingPlayerDialog(recordingUrl); + } + + downloadRecording(recording: MeetRecordingInfo) { + const queryParamForAvoidCache = `?t=${new Date().getTime()}`; + let recordingUrl = this.httpService.getRecordingMediaUrl(recording.recordingId); + recordingUrl += queryParamForAvoidCache; + + const link = document.createElement('a'); + link.href = recordingUrl; + link.download = recording.filename || 'openvidu-recording.mp4'; + link.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }) + ); + + // For Firefox it is necessary to delay revoking the ObjectURL + setTimeout(() => link.remove(), 100); + } + + async deleteRecording(recording: MeetRecordingInfo) { try { - await this.httpService.deleteRecording(deleteRequest.recordingId!); + await this.httpService.deleteRecording(recording.recordingId!); + this.recordings = this.recordings.filter((r) => r.recordingId !== recording.recordingId); } catch (error) { console.log(error); } - - await this.loadRecordings(); } private async loadRecordings() { try { - const { recordings, continuationToken } = await this.httpService.getRecordings(this.continuationToken); - this.recordings = recordings; - this.continuationToken = continuationToken; + const response = await this.httpService.getRecordings(this.nextPageToken); + this.recordings.push(...response.recordings); + this.recordings = this.sortRecordingsByDate(this.recordings); + this.nextPageToken = response.pagination.nextPageToken; + this.moreRecordings = response.pagination.isTruncated; } catch (error) { console.log(error); } } + + private sortRecordingsByDate(recordings: MeetRecordingInfo[]) { + return recordings.sort((a, b) => { + const dateA = new Date(a.startDate || -1); + const dateB = new Date(b.startDate || -1); + return dateA.getTime() - dateB.getTime(); + }); + } + + private resetRecordings() { + this.recordings = []; + this.nextPageToken = undefined; + this.moreRecordings = false; + } }