diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.html index 5f871081..9bf2779d 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.html @@ -160,108 +160,106 @@ @if (recordingList.length > 0) { -
- - @for (recording of recordingList; track trackByRecordingId($index, recording)) { - - - video_file - - -
- {{ recording.filename }} -
- - -
+
+ @for (recording of recordingList; track trackByRecordingId($index, recording)) { +
+ +
+
@if (recording.status === recStatusEnum.STARTED) { - - {{ 'PANEL.RECORDING.IN_PROGRESS' | translate }} - +
} @else { - {{ recording.duration | duration }} - | {{ recording.size / 1024 / 1024 | number: '1.1-2' }} MBs +
}
- - @if (recording.status !== recStatusEnum.STARTED) { -
- {{ recording.startedAt | date: 'HH:mm - dd/MM/yyyy' }} -
- } +
+
{{ recording.filename || 'Recording' }}
- - @if (!isReadOnlyMode) { - @if (recording.status !== recStatusEnum.STARTED) { -
- @if (showControls.play) { - - } - - @if (showControls.externalView) { - - } - - @if (showControls.download) { - - } - - @if (showControls.delete) { - - } + @if (recording.status === recStatusEnum.STARTED) { +
+ {{ 'PANEL.RECORDING.IN_PROGRESS' | translate }} +
+ } @else { + } - } @else { - @if (recording.status !== recStatusEnum.STARTED) { -
+
+
+ + + @if (recording.status !== recStatusEnum.STARTED) { +
+ @if (!isReadOnlyMode) { + @if (showControls.play) { + + } + + @if (showControls.externalView) { -
+ } + + @if (showControls.download) { + + } + + @if (showControls.delete) { + + } + } @else { + } - } - - } - +
+ } +
+ }
}
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.scss index eee53e93..235f94d4 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.scss @@ -99,8 +99,236 @@ margin-top: 10px; } + // Modern recording list styles .recording-list-container { - margin-top: 10px; + display: flex; + flex-direction: column; + gap: 16px; + padding-top: 16px; + max-height: 500px; + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--ov-accent-action-color); + border-radius: 2px; + opacity: 0.3; + } + + &::-webkit-scrollbar-thumb:hover { + opacity: 0.6; + } + } + + .recording-card { + background: var(--ov-surface-background-color); + border: 1px solid rgba(0, 102, 204, 0.1); + border-radius: 12px; + padding: 8px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + width: 100%; + display: flex; + flex-direction: column; + flex-shrink: 0; + box-sizing: border-box; + + &.recording-active { + border-color: var(--ov-primary-action-color); + background: linear-gradient(135deg, + rgba(255, 87, 34, 0.05) 0%, + transparent 50%); + + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background: var(--ov-primary-action-color); + animation: pulse-border 2s infinite; + } + } + } + + .recording-header { + display: flex; + align-items: flex-start; + gap: 5px; + width: 100%; + height: 60px; + flex-shrink: 0; + } + + .recording-status-indicator { + flex-shrink: 0; + padding-top: 2px; + width: 16px; + height: 16px; + display: flex; + justify-content: center; + align-items: flex-start; + } + + .status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + + &.recording-live { + background: var(--ov-primary-action-color); + box-shadow: 0 0 0 4px rgba(255, 87, 34, 0.2); + animation: pulse-dot 2s infinite; + } + + &.recording-ready { + background: var(--ov-accent-action-color); + } + } + + .recording-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + overflow: hidden; + } + + .recording-name { + font-size: 14px; + font-weight: 500; + color: var(--ov-text-surface-color); + margin-bottom: 4px; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + height: 17px; + } + + .recording-status-text { + font-size: 12px; + font-weight: 500; + + &.recording-live-text { + color: var(--ov-primary-action-color); + text-transform: uppercase; + letter-spacing: 0.5px; + } + } + + .recording-metadata { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 4px; + height: auto; + overflow: visible; + } + + .metadata-item { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: var(--ov-text-surface-color); + opacity: 0.7; + white-space: nowrap; + flex-shrink: 0; + + .metadata-icon { + font-size: 14px; + width: 14px; + height: 14px; + flex-shrink: 0; + } + } + + .recording-actions-menu { + display: flex; + gap: 8px; + flex-shrink: 0; + opacity: 1; + align-items: center; + width: 100%; + justify-content: center; + height: 32px; + margin-top: auto; + } + + .action-btn { + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } + + &.action-play { + color: var(--ov-accent-action-color); + + &:hover { + background: rgba(0, 102, 204, 0.1); + color: var(--ov-accent-action-color); + } + } + + &.action-view { + color: var(--ov-accent-action-color); + + &:hover { + background: rgba(0, 102, 204, 0.1); + color: var(--ov-accent-action-color); + } + } + + &.action-download { + color: #4CAF50; + + &:hover { + background: rgba(76, 175, 80, 0.1); + color: #4CAF50; + } + } + + &.action-delete { + color: var(--ov-error-color); + + &:hover { + background: rgba(244, 67, 54, 0.1); + color: var(--ov-error-color); + } + } + } + + // Animations + @keyframes pulse-dot { + 0%, 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.2); + opacity: 0.8; + } + } + + @keyframes pulse-border { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } } .recording-actions { @@ -116,6 +344,62 @@ } } + // Mobile responsive design for new recording cards + @media (max-width: 768px) { + .recording-list-container { + padding-top: 12px; + gap: 12px; + } + + .recording-card { + padding: 8px; + height: 100px; + gap: 8px; + } + + .recording-header { + gap: 8px; + height: 50px; + } + + .recording-info { + min-width: 0; + } + + .recording-metadata { + gap: 8px; + margin-top: 2px; + } + + .metadata-item { + font-size: 11px; + gap: 2px; + + .metadata-icon { + font-size: 12px; + width: 12px; + height: 12px; + } + } + + .recording-actions-menu { + opacity: 1; // Always visible on mobile + gap: 6px; + height: 28px; + } + + .action-btn { + width: 28px; + height: 28px; + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } + } + } + .recording-message { color: var(--ov-text-surface-color); } @@ -124,19 +408,44 @@ color: var(--ov-error-color); font-weight: 600; } + .disable-recording-btn { background-color: var(--ov-secondary-action-color) !important; color: var(--ov-text-surface-color) !important; cursor: not-allowed !important; } - .recording-name { - font-size: 14px; - font-weight: bold; + + // Enhanced empty state + .empty-state { + text-align: center; + padding: 32px 16px; + color: var(--ov-text-surface-color); } - .recording-date { - font-size: 12px !important; - font-style: italic; + .empty-state-icon { + margin-bottom: 16px; + + mat-icon { + font-size: 48px; + width: 48px; + height: 48px; + color: var(--ov-accent-action-color); + opacity: 0.6; + } + } + + .empty-state-title { + font-size: 18px; + font-weight: 500; + margin: 0 0 8px 0; + color: var(--ov-text-surface-color); + } + + .empty-state-subtitle { + font-size: 14px; + margin: 0; + opacity: 0.7; + line-height: 1.4; } .not-allowed-message { diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.ts index ccb4be4a..7b2d9ad7 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/activities-panel/recording-activity/recording-activity.component.ts @@ -304,6 +304,40 @@ export class RecordingActivityComponent implements OnInit, OnDestroy { this.onViewRecordingsClicked.emit(); } + /** + * @internal + * Format duration in seconds to a readable format (e.g., "2m 30s") + */ + formatDuration(seconds: number): string { + if (!seconds || seconds < 0) return '0s'; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = Math.floor(seconds % 60); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } else if (minutes > 0) { + return `${minutes}m ${remainingSeconds}s`; + } else { + return `${remainingSeconds}s`; + } + } + + /** + * @internal + * Format file size in bytes to a readable format (e.g., "2.5 MB") + */ + formatFileSize(bytes: number): string { + if (!bytes || bytes < 0) return '0 B'; + + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const size = bytes / Math.pow(1024, i); + + return `${size.toFixed(1)} ${sizes[i]}`; + } + private subscribeToConfigChanges() { this.libService.recordingActivityReadOnly$.pipe(takeUntil(this.destroy$)).subscribe((readOnly: boolean) => { this.isReadOnlyMode = readOnly;