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
Carlos Santos 2025-07-21 14:12:28 +02:00
parent e486665efd
commit 181c5f0789
3 changed files with 434 additions and 93 deletions

View File

@ -160,50 +160,57 @@
<!-- Recording list -->
@if (recordingList.length > 0) {
<div class="item recording-list-container">
<mat-list>
<div class="recording-list-container">
@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-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 -->
<div class="recording-info">
<div class="recording-name">{{ recording.filename || 'Recording' }}</div>
@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>
}
</div>
</div>
<!-- Actions menu row -->
@if (recording.status !== recStatusEnum.STARTED) {
<div matListItemLine class="recording-date">
{{ recording.startedAt | date: 'HH:mm - dd/MM/yyyy' }}
</div>
}
<!-- Recording action buttons -->
<div class="recording-actions-menu">
@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"
class="action-btn action-play"
>
<mat-icon>play_arrow</mat-icon>
<mat-icon>play_circle</mat-icon>
</button>
}
@ -211,9 +218,8 @@
<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>
@ -223,9 +229,8 @@
<button
mat-icon-button
(click)="download(recording)"
id="download-recording-btn"
matTooltip="{{ 'PANEL.RECORDING.DOWNLOAD' | translate }}"
class="action-button download-button"
class="action-btn action-download"
>
<mat-icon>download</mat-icon>
</button>
@ -235,33 +240,26 @@
<button
mat-icon-button
(click)="deleteRecording(recording)"
id="delete-recording-btn"
matTooltip="{{ 'PANEL.RECORDING.DELETE' | translate }}"
class="action-button delete-button"
class="action-btn action-delete"
>
<mat-icon>delete</mat-icon>
<mat-icon>delete_outline</mat-icon>
</button>
}
</div>
}
} @else {
@if (recording.status !== recStatusEnum.STARTED) {
<div id="recording-action-buttons" class="recording-actions">
<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>
<mat-icon>visibility</mat-icon>
</button>
}
</div>
}
</div>
}
</mat-list-item>
}
</mat-list>
</div>
}
</div>

View File

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

View File

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