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,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>

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;