diff --git a/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.html b/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.html index 74e3ed1..a779971 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.html +++ b/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.html @@ -1,56 +1,161 @@ -@if (recording) { - - @if (recordingUrl) { - @if (!videoError) { -
- -
- } @else { -
- error - Error loading video. Please try again later. -
- } - } @else { - @if (['STARTING', 'ACTIVE', 'ENDING'].includes(recording.status)) { -
- hourglass_empty - Recording is still in progress... -
- } @else { -
- error_outline - Recording has failed -
- } - } + +@if (isLoading) { +
+
+ +

Loading recording...

+
+
+} - -

{{ recording.roomId }}

-
- {{ recording.startDate | date: 'M/d/yy, H:mm' }} + +@else if (hasError) { +
+
+
+ error_outline +

Recording Not Found

+

The recording you're looking for doesn't exist or is no longer available.

+
+ + +
+
+
+
+} + + +@else if (recording) { +
+
+ +
+
+
+ +
+
+
+

{{ recording.roomId }}

+ +
+ {{ getStatusIcon() }} + {{ getStatusMessage() }} +
+
+ + @if (recordingUrl && !videoError) { +
+
+ + + +
+
+ + + + } +
+
+
+ + +
@if (recordingUrl && !videoError) { -
- - +
+ + + @if (!isVideoLoaded) { +
+ + Loading video... +
+ } +
+ } @else if (videoError) { +
+
+ error +

Video Playback Error

+

There was an error loading the video. Please check your connection and try again.

+ +
+
+ } @else { +
+
+ {{ getStatusIcon() }} +

{{ getStatusMessage() }}

+ @if (['STARTING', 'ACTIVE', 'ENDING'].includes(recording.status)) { +

The recording is still being processed. Please check back in a few minutes.

+ + } @else { +

This recording is not available for playback.

+ } +
}
- - -} @else { -
- +
} diff --git a/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.scss b/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.scss index 4b8cd22..17bc9a2 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.scss +++ b/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.scss @@ -1,82 +1,337 @@ -.loader-container { - display: flex; - justify-content: center; - align-items: center; - height: 60vh; +@import '../../../../../../src/assets/styles/design-tokens'; + +// === MAIN PAGE LAYOUT === + +.recording-page-content { + @include ov-container; + @include ov-page-content; + max-width: 1000px; + padding-top: var(--ov-meet-spacing-xl); } -.recording-container { - max-width: 900px; - margin: 20px auto; - padding: 0; -} +// === LOADING STATES === -.video-container { - position: relative; - width: 100%; - padding-top: 56.25%; /* 16:9 aspect ratio */ -} - -.video-player { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -.status-message { - display: flex; - align-items: center; - gap: 8px; - color: #555; - font-size: 1.1rem; - padding: 20px; -} - -.status-message.error { - color: #d32f2f; -} - -.video-error { - display: flex; - align-items: center; - gap: 8px; - padding: 16px; - background-color: #fbe9e7; - border: 1px solid #ef9a9a; - color: #c62828; - border-radius: 4px; - margin-bottom: 10px; -} - -.title { - font-size: 1.8rem; - font-weight: 600; - margin: 16px 0 8px; -} - -.info-actions { - display: flex; +.loading-container { + @include ov-flex-center; flex-direction: column; - gap: 8px; -} + gap: var(--ov-meet-spacing-md); + height: 60vh; + color: var(--ov-meet-text-secondary); -@media (min-width: 600px) { - .info-actions { - flex-direction: row; - justify-content: space-between; - align-items: center; + .loading-text { + margin: 0; + font-size: var(--ov-meet-font-size-md); + font-weight: var(--ov-meet-font-weight-medium); } } -.date { - color: #666; - font-size: 0.95rem; +// === ERROR STATES === + +.error-container { + @include ov-flex-center; + min-height: 60vh; + padding: var(--ov-meet-spacing-xl); + + .error-content { + text-align: center; + max-width: 500px; + + .error-icon { + @include ov-icon(xl); + color: var(--ov-meet-color-error); + margin-bottom: var(--ov-meet-spacing-md); + } + + .error-title { + margin: 0 0 var(--ov-meet-spacing-sm) 0; + font-size: var(--ov-meet-font-size-xxl); + font-weight: var(--ov-meet-font-weight-semibold); + color: var(--ov-meet-text-primary); + } + + .error-message { + margin: 0 0 var(--ov-meet-spacing-lg) 0; + font-size: var(--ov-meet-font-size-md); + color: var(--ov-meet-text-secondary); + line-height: var(--ov-meet-line-height-relaxed); + } + + .error-actions { + display: flex; + gap: var(--ov-meet-spacing-md); + justify-content: center; + flex-wrap: wrap; + + button { + @include ov-button-base; + display: flex; + align-items: center; + gap: var(--ov-meet-spacing-sm); + } + } + } } -.actions { - display: flex; - gap: 8px; - align-items: center; +// === HEADER SECTION === + +.recording-header { + margin-bottom: var(--ov-meet-spacing-xl); + + .header-content { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: var(--ov-meet-spacing-lg); + + @include ov-tablet-down { + flex-direction: column; + align-items: stretch; + } + } + + .header-info { + display: flex; + flex: 1; + + .recording-info { + width: 50%; + } + + .recording-title { + margin: 0 0 var(--ov-meet-spacing-sm) 0; + font-size: var(--ov-meet-font-size-hero); + font-weight: var(--ov-meet-font-weight-light); + color: var(--ov-meet-text-primary); + line-height: var(--ov-meet-line-height-tight); + + @include ov-mobile-down { + font-size: var(--ov-meet-font-size-xxl); + } + } + + .recording-metadata { + display: flex; + flex-wrap: wrap; + gap: var(--ov-meet-spacing-lg); + margin-bottom: var(--ov-meet-spacing-sm); + + @include ov-mobile-down { + flex-direction: column; + gap: var(--ov-meet-spacing-xs); + } + + .recording-date, + .recording-duration { + @include ov-flex-center; + gap: var(--ov-meet-spacing-xs); + font-size: var(--ov-meet-font-size-sm); + color: var(--ov-meet-text-secondary); + + mat-icon { + @include ov-icon(sm); + } + } + } + + .recording-status { + @include ov-flex-center; + gap: var(--ov-meet-spacing-xs); + justify-content: flex-start; + + .status-icon { + @include ov-icon(sm); + + &.COMPLETE { + color: var(--ov-meet-color-success); + } + + &.STARTING, + &.ACTIVE, + &.ENDING { + color: var(--ov-meet-color-warning); + } + + &.FAILED { + color: var(--ov-meet-color-error); + } + } + + .status-text { + font-size: var(--ov-meet-font-size-sm); + color: var(--ov-meet-text-secondary); + font-weight: var(--ov-meet-font-weight-medium); + } + } + } + + .header-actions { + display: flex; + gap: var(--ov-meet-spacing-sm); + + @include ov-tablet-down { + justify-content: flex-end; + } + + .back-btn { + @include ov-button-base; + @include ov-theme-transition; + color: var(--ov-meet-text-secondary); + padding: 0; + + &:hover { + color: var(--ov-meet-text-primary); + background-color: var(--ov-meet-surface-hover); + } + } + } +} + +// === VIDEO SECTION === + +.video-section { + margin-bottom: var(--ov-meet-spacing-xl); + + .video-container { + position: relative; + width: 100%; + padding-top: 56.25%; // 16:9 aspect ratio + background: var(--ov-meet-surface-color); + border-radius: var(--ov-meet-radius-lg); + overflow: hidden; + box-shadow: var(--ov-meet-shadow-md); + + .video-player { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--ov-meet-background-color); + } + + .video-loading-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + @include ov-flex-center; + flex-direction: column; + gap: var(--ov-meet-spacing-md); + background: var(--ov-meet-surface-color); + color: var(--ov-meet-text-secondary); + font-size: var(--ov-meet-font-size-sm); + } + } + + .video-error-container, + .video-unavailable-container { + @include ov-flex-center; + min-height: 300px; + background: var(--ov-meet-surface-color); + border-radius: var(--ov-meet-radius-lg); + border: 1px solid var(--ov-meet-border-color-light); + + .video-error-content, + .video-unavailable-content { + text-align: center; + max-width: 400px; + padding: var(--ov-meet-spacing-xl); + + .error-icon, + .status-icon { + @include ov-icon(xl); + margin-bottom: var(--ov-meet-spacing-md); + } + + .error-icon { + color: var(--ov-meet-color-error); + } + + .status-icon { + color: var(--ov-meet-text-secondary); + } + + h3 { + margin: 0 0 var(--ov-meet-spacing-sm) 0; + font-size: var(--ov-meet-font-size-xl); + font-weight: var(--ov-meet-font-weight-semibold); + color: var(--ov-meet-text-primary); + } + + p { + margin: 0 0 var(--ov-meet-spacing-lg) 0; + font-size: var(--ov-meet-font-size-md); + color: var(--ov-meet-text-secondary); + line-height: var(--ov-meet-line-height-relaxed); + } + + button { + @include ov-button-base; + display: inline-flex; + align-items: center; + gap: var(--ov-meet-spacing-sm); + } + } + } +} + +// === ACTIONS SECTION === + +.actions-section { + flex: 2; + align-self: self-end; + + .primary-actions { + @include ov-flex-center; + gap: var(--ov-meet-spacing-md); + flex-wrap: wrap; + justify-content: end; + + .action-btn { + @include ov-button-base; + display: flex; + align-items: center; + gap: var(--ov-meet-spacing-sm); + min-width: 120px; + height: 48px; + + mat-icon { + @include ov-icon(sm); + } + } + } +} + +// === RESPONSIVE ADJUSTMENTS === + +@include ov-mobile-down { + .recording-page-content { + padding: var(--ov-meet-spacing-lg); + padding-top: var(--ov-meet-spacing-xl); + } + + .actions-section .primary-actions { + flex-direction: column; + align-items: stretch; + + .action-btn { + width: 100%; + justify-content: center; + } + } +} + +@include ov-tablet-down { + .video-section { + .video-error-container, + .video-unavailable-container { + min-height: 250px; + + .video-error-content, + .video-unavailable-content { + padding: var(--ov-meet-spacing-lg); + } + } + } } diff --git a/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.ts index 3873173..f5ad215 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.ts @@ -1,59 +1,164 @@ import { DatePipe } from '@angular/common'; -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { ActivatedRoute } from '@angular/router'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { ActivatedRoute, Router } from '@angular/router'; import { RecordingManagerService } from '@lib/services'; import { MeetRecordingInfo, MeetRecordingStatus } from '@lib/typings/ce'; import { ActionService } from 'openvidu-components-angular'; +import { ShareMeetingLinkComponent } from '@lib/components/share-meeting-link/share-meeting-link.component'; @Component({ - selector: 'app-view-recording', + selector: 'ov-view-recording', templateUrl: './view-recording.component.html', styleUrls: ['./view-recording.component.scss'], standalone: true, - imports: [MatCardModule, MatButtonModule, MatIconModule, DatePipe, MatProgressSpinnerModule] + imports: [ + MatCardModule, + MatButtonModule, + MatIconModule, + DatePipe, + MatProgressSpinnerModule, + MatTooltipModule, + MatSnackBarModule, + ShareMeetingLinkComponent + ] }) -export class ViewRecordingComponent implements OnInit { +export class ViewRecordingComponent implements OnInit, OnDestroy { recording?: MeetRecordingInfo; recordingUrl?: string; - videoError = false; + isLoading = true; + hasError = false; + isVideoLoaded = false; constructor( protected recordingService: RecordingManagerService, protected actionService: ActionService, - protected route: ActivatedRoute + protected route: ActivatedRoute, + protected router: Router, + private snackBar: MatSnackBar ) {} async ngOnInit() { + await this.loadRecording(); + } + + ngOnDestroy() {} + + private async loadRecording() { const recordingId = this.route.snapshot.paramMap.get('recording-id'); const secret = this.route.snapshot.queryParams['secret']; + if (!recordingId) { + this.hasError = true; + this.isLoading = false; + return; + } + try { - this.recording = await this.recordingService.getRecording(recordingId!, secret); + this.recording = await this.recordingService.getRecording(recordingId, secret); if (this.recording.status === MeetRecordingStatus.COMPLETE) { - this.recordingUrl = this.recordingService.getRecordingMediaUrl(recordingId!, secret); + this.recordingUrl = this.recordingService.getRecordingMediaUrl(recordingId, secret); } + console.warn('Recording loaded:', this.recordingUrl); } catch (error) { console.error('Error fetching recording:', error); + this.hasError = true; + } finally { + this.isLoading = false; } } + onVideoLoaded = () => { + this.isVideoLoaded = true; + this.videoError = false; + }; + + onVideoError = () => { + this.videoError = true; + this.isVideoLoaded = false; + }; + downloadRecording() { if (!this.recording || !this.recordingUrl) { - console.error('Recording is not available for download'); + this.snackBar.open('Recording is not available for download', 'Close', { duration: 3000 }); return; } this.recordingService.downloadRecording(this.recording); + this.snackBar.open('Download started', 'Close', { duration: 2000 }); } openShareDialog() { const url = window.location.href; this.recordingService.openShareRecordingDialog(this.recording!.recordingId, url); } + + // copyRecordingLink() { + // const url = window.location.href; + // navigator.clipboard.writeText(url).then(() => { + // this.snackBar.open('Link copied to clipboard', 'Close', { duration: 2000 }); + // }).catch(() => { + // this.snackBar.open('Failed to copy link', 'Close', { duration: 3000 }); + // }); + // } + + goBack() { + if (window.history.length > 1) { + window.history.back(); + } else { + this.router.navigate(['/']); + } + } + + retryLoad() { + this.isLoading = true; + this.hasError = false; + this.videoError = false; + this.loadRecording(); + } + + getStatusIcon(): string { + if (!this.recording) return 'error_outline'; + + switch (this.recording.status) { + case MeetRecordingStatus.STARTING: + case MeetRecordingStatus.ACTIVE: + case MeetRecordingStatus.ENDING: + return 'hourglass_empty'; + case MeetRecordingStatus.COMPLETE: + return 'check_circle'; + default: + return 'error_outline'; + } + } + + getStatusMessage(): string { + if (!this.recording) return 'Recording not found'; + + switch (this.recording.status) { + case MeetRecordingStatus.STARTING: + return 'Recording is starting...'; + case MeetRecordingStatus.ACTIVE: + return 'Recording is in progress...'; + case MeetRecordingStatus.ENDING: + return 'Recording is finalizing...'; + case MeetRecordingStatus.COMPLETE: + return 'Recording is ready to watch'; + default: + return 'Recording has failed'; + } + } + + formatDuration(durationSeconds: number): string { + const minutes = Math.floor(durationSeconds / 60); + const seconds = Math.floor(durationSeconds % 60); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + } }