mirror of https://github.com/OpenVidu/openvidu.git
ov-components: Improves recording activity UI
Refactors the recording activity component's template and styles to use cards for displaying recording information. Enhances the display of recording metadata, including duration, size, and date, with appropriate icons. Adds visual cues for active recordings and improves overall responsiveness of the recording list.master
parent
e486665efd
commit
181c5f0789
|
@ -160,108 +160,106 @@
|
|||
|
||||
<!-- Recording list -->
|
||||
@if (recordingList.length > 0) {
|
||||
<div class="item recording-list-container">
|
||||
<mat-list>
|
||||
@for (recording of recordingList; track trackByRecordingId($index, recording)) {
|
||||
<mat-list-item class="recording-item" [class.blink]="recording.status === recStatusEnum.STARTED">
|
||||
<!-- Recording icon -->
|
||||
<mat-icon class="recording-icon" matListItemIcon>video_file</mat-icon>
|
||||
|
||||
<!-- Recording title -->
|
||||
<div matListItemTitle>
|
||||
<span class="recording-name">{{ recording.filename }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Recording status/progress -->
|
||||
<div matListItemLine class="time-container">
|
||||
<div class="recording-list-container">
|
||||
@for (recording of recordingList; track trackByRecordingId($index, recording)) {
|
||||
<div class="recording-card" [class.recording-active]="recording.status === recStatusEnum.STARTED">
|
||||
<!-- Recording header with status indicator and info -->
|
||||
<div class="recording-header">
|
||||
<div class="recording-status-indicator">
|
||||
@if (recording.status === recStatusEnum.STARTED) {
|
||||
<span>
|
||||
{{ 'PANEL.RECORDING.IN_PROGRESS' | translate }}
|
||||
</span>
|
||||
<div class="status-dot recording-live"></div>
|
||||
} @else {
|
||||
<span class="recording-duration">{{ recording.duration | duration }}</span>
|
||||
<span class="recording-size">| {{ recording.size / 1024 / 1024 | number: '1.1-2' }} MBs</span>
|
||||
<div class="status-dot recording-ready"></div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Recording date -->
|
||||
@if (recording.status !== recStatusEnum.STARTED) {
|
||||
<div matListItemLine class="recording-date">
|
||||
{{ recording.startedAt | date: 'HH:mm - dd/MM/yyyy' }}
|
||||
</div>
|
||||
}
|
||||
<div class="recording-info">
|
||||
<div class="recording-name">{{ recording.filename || 'Recording' }}</div>
|
||||
|
||||
<!-- Recording action buttons -->
|
||||
@if (!isReadOnlyMode) {
|
||||
@if (recording.status !== recStatusEnum.STARTED) {
|
||||
<div id="recording-action-buttons" class="recording-actions">
|
||||
@if (showControls.play) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="play(recording)"
|
||||
id="play-recording-btn"
|
||||
matTooltip="{{ 'PANEL.RECORDING.PLAY' | translate }}"
|
||||
class="action-button play-button"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (showControls.externalView) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="onViewRecordingClicked.emit(recording.id)"
|
||||
id="watch-recording-btn"
|
||||
matTooltip="{{ 'PANEL.RECORDING.WATCH' | translate }}"
|
||||
class="action-button watch-button"
|
||||
>
|
||||
<mat-icon>open_in_new</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (showControls.download) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="download(recording)"
|
||||
id="download-recording-btn"
|
||||
matTooltip="{{ 'PANEL.RECORDING.DOWNLOAD' | translate }}"
|
||||
class="action-button download-button"
|
||||
>
|
||||
<mat-icon>download</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (showControls.delete) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="deleteRecording(recording)"
|
||||
id="delete-recording-btn"
|
||||
matTooltip="{{ 'PANEL.RECORDING.DELETE' | translate }}"
|
||||
class="action-button delete-button"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (recording.status === recStatusEnum.STARTED) {
|
||||
<div class="recording-status-text recording-live-text">
|
||||
{{ 'PANEL.RECORDING.IN_PROGRESS' | translate }}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="recording-metadata">
|
||||
<span class="metadata-item">
|
||||
<mat-icon class="metadata-icon">schedule</mat-icon>
|
||||
{{ formatDuration(recording.duration) }}
|
||||
</span>
|
||||
<span class="metadata-item">
|
||||
<mat-icon class="metadata-icon">storage</mat-icon>
|
||||
{{ formatFileSize(recording.size) }}
|
||||
</span>
|
||||
<span class="metadata-item">
|
||||
<mat-icon class="metadata-icon">today</mat-icon>
|
||||
{{ recording.startedAt | date: 'MMM d, y' }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
@if (recording.status !== recStatusEnum.STARTED) {
|
||||
<div id="recording-action-buttons" class="recording-actions">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions menu row -->
|
||||
@if (recording.status !== recStatusEnum.STARTED) {
|
||||
<div class="recording-actions-menu">
|
||||
@if (!isReadOnlyMode) {
|
||||
@if (showControls.play) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="play(recording)"
|
||||
matTooltip="{{ 'PANEL.RECORDING.PLAY' | translate }}"
|
||||
class="action-btn action-play"
|
||||
>
|
||||
<mat-icon>play_circle</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (showControls.externalView) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="onViewRecordingClicked.emit(recording.id)"
|
||||
id="watch-recording-btn"
|
||||
matTooltip="{{ 'PANEL.RECORDING.WATCH' | translate }}"
|
||||
class="action-button watch-button"
|
||||
class="action-btn action-view"
|
||||
>
|
||||
<mat-icon>open_in_new</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showControls.download) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="download(recording)"
|
||||
matTooltip="{{ 'PANEL.RECORDING.DOWNLOAD' | translate }}"
|
||||
class="action-btn action-download"
|
||||
>
|
||||
<mat-icon>download</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (showControls.delete) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="deleteRecording(recording)"
|
||||
matTooltip="{{ 'PANEL.RECORDING.DELETE' | translate }}"
|
||||
class="action-btn action-delete"
|
||||
>
|
||||
<mat-icon>delete_outline</mat-icon>
|
||||
</button>
|
||||
}
|
||||
} @else {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="onViewRecordingClicked.emit(recording.id)"
|
||||
matTooltip="{{ 'PANEL.RECORDING.WATCH' | translate }}"
|
||||
class="action-btn action-view"
|
||||
>
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</mat-list-item>
|
||||
}
|
||||
</mat-list>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue