frontend: enhance mobile experience with responsive UI in view recording page
This commit is contained in:
parent
681cf24e22
commit
06e350d8fb
@ -121,7 +121,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.download-button mat-icon {
|
&.download-button mat-icon {
|
||||||
color: var(--ov-meet-color-primary);
|
color: var(--ov-meet-color-info);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.delete-button mat-icon {
|
&.delete-button mat-icon {
|
||||||
|
|||||||
@ -29,73 +29,78 @@
|
|||||||
|
|
||||||
<!-- Recording Content -->
|
<!-- Recording Content -->
|
||||||
@else if (recording) {
|
@else if (recording) {
|
||||||
<div class="ov-page-container">
|
@if (viewportService.isMobileView()) {
|
||||||
<div class="recording-page-content fade-in">
|
<!-- Mobile Experience -->
|
||||||
<!-- Header Section -->
|
<div class="mobile-container">
|
||||||
<div class="recording-header">
|
<!-- Fixed Header Toolbar -->
|
||||||
<div class="header-content">
|
<div class="mobile-header-toolbar">
|
||||||
<div class="header-info">
|
<div class="toolbar-content">
|
||||||
<div class="recording-info">
|
<!-- Left: Back button -->
|
||||||
<h1 class="recording-title">{{ recording.roomName }}</h1>
|
<button mat-icon-button class="back-button" (click)="goBack()">
|
||||||
<div class="recording-metadata">
|
<mat-icon>arrow_back</mat-icon>
|
||||||
<span class="recording-date">
|
</button>
|
||||||
<mat-icon class="ov-action-icon">schedule</mat-icon>
|
|
||||||
{{ recording.startDate | date: 'MMM d, y - h:mm a' }}
|
|
||||||
</span>
|
|
||||||
@if (recording.duration) {
|
|
||||||
<span class="recording-duration">
|
|
||||||
<mat-icon class="ov-action-icon">timer</mat-icon>
|
|
||||||
{{ formatDuration(recording.duration) }}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="recording-status">
|
|
||||||
<mat-icon [class]="'status-icon ' + recording.status">{{ getStatusIcon() }}</mat-icon>
|
|
||||||
<span class="status-text">{{ getStatusMessage() }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Actions Section -->
|
|
||||||
@if (!videoError) {
|
|
||||||
<div class="actions-section">
|
|
||||||
<div class="primary-actions">
|
|
||||||
@if (recordingUrl) {
|
|
||||||
<button
|
|
||||||
mat-button
|
|
||||||
(click)="downloadRecording()"
|
|
||||||
matTooltip="Download recording"
|
|
||||||
class="action-btn primary-button"
|
|
||||||
>
|
|
||||||
<mat-icon>download</mat-icon>
|
|
||||||
<span>Download</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button
|
<!-- Center: Recording info -->
|
||||||
mat-raised-button
|
<div class="recording-info">
|
||||||
color="accent"
|
<h1 class="recording-title">{{ recording.roomName }}</h1>
|
||||||
(click)="openShareDialog()"
|
<div class="recording-metadata">
|
||||||
matTooltip="Share recording"
|
<span class="recording-date">
|
||||||
class="action-btn"
|
<mat-icon>schedule</mat-icon>
|
||||||
>
|
{{ recording.startDate | date: 'MMM d, y' }}
|
||||||
<mat-icon>share</mat-icon>
|
</span>
|
||||||
<span>Share</span>
|
@if (recording.duration) {
|
||||||
</button>
|
<span class="recording-duration">
|
||||||
</div>
|
<mat-icon>timer</mat-icon>
|
||||||
</div>
|
{{ formatDuration(recording.duration) }}
|
||||||
}
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Actions -->
|
||||||
|
@if (!videoError && recordingUrl) {
|
||||||
|
<div class="header-actions">
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
(click)="downloadRecording()"
|
||||||
|
matTooltip="Download"
|
||||||
|
class="action-button download-button"
|
||||||
|
>
|
||||||
|
<mat-icon>download</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
color="accent"
|
||||||
|
(click)="openShareDialog()"
|
||||||
|
matTooltip="Share"
|
||||||
|
class="action-button share-buttosssn"
|
||||||
|
>
|
||||||
|
<mat-icon>share</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Status indicator for non-complete recordings -->
|
||||||
|
@if (recording.status !== 'complete') {
|
||||||
|
<div class="status-bar">
|
||||||
|
<mat-icon class="status-icon">{{ getStatusIcon() }}</mat-icon>
|
||||||
|
<span class="status-text">{{ getStatusMessage() }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Video Player Section -->
|
<!-- Video Content -->
|
||||||
<div class="video-section">
|
<div class="mobile-video-content">
|
||||||
@if (recordingUrl && !videoError) {
|
@if (recordingUrl && !videoError) {
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<video
|
<video
|
||||||
[src]="recordingUrl"
|
[src]="recordingUrl"
|
||||||
controls
|
controls
|
||||||
controlsList="nodownload"
|
controlsList="nodownload nofullscreen"
|
||||||
playsinline
|
playsinline
|
||||||
|
disablePictureInPicture
|
||||||
class="video-player"
|
class="video-player"
|
||||||
(loadeddata)="onVideoLoaded()"
|
(loadeddata)="onVideoLoaded()"
|
||||||
(error)="onVideoError()"
|
(error)="onVideoError()"
|
||||||
@ -110,7 +115,7 @@
|
|||||||
</div>
|
</div>
|
||||||
} @else if (videoError) {
|
} @else if (videoError) {
|
||||||
<div class="video-error-container">
|
<div class="video-error-container">
|
||||||
<div class="video-error-content">
|
<div class="error-content">
|
||||||
<mat-icon class="error-icon">error</mat-icon>
|
<mat-icon class="error-icon">error</mat-icon>
|
||||||
<h3>Video Playback Error</h3>
|
<h3>Video Playback Error</h3>
|
||||||
<p>There was an error loading the video. Please check your connection and try again.</p>
|
<p>There was an error loading the video. Please check your connection and try again.</p>
|
||||||
@ -122,7 +127,7 @@
|
|||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="video-unavailable-container">
|
<div class="video-unavailable-container">
|
||||||
<div class="video-unavailable-content">
|
<div class="unavailable-content">
|
||||||
<mat-icon class="status-icon">{{ getStatusIcon() }}</mat-icon>
|
<mat-icon class="status-icon">{{ getStatusIcon() }}</mat-icon>
|
||||||
<h3>{{ getStatusMessage() }}</h3>
|
<h3>{{ getStatusMessage() }}</h3>
|
||||||
@if (['starting', 'active', 'ending'].includes(recording.status)) {
|
@if (['starting', 'active', 'ending'].includes(recording.status)) {
|
||||||
@ -132,12 +137,127 @@
|
|||||||
<span>Refresh</span>
|
<span>Refresh</span>
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<p>This recording is not available for playback.</p>
|
<p>This recording is not available for playbook.</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
} @else {
|
||||||
|
<!-- Desktop/Tablet Experience -->
|
||||||
|
<div class="ov-page-container">
|
||||||
|
<div class="recording-page-content fade-in">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="recording-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-info">
|
||||||
|
<div class="recording-info">
|
||||||
|
<h1 class="recording-title">{{ recording.roomName }}</h1>
|
||||||
|
<div class="recording-metadata">
|
||||||
|
<span class="recording-date">
|
||||||
|
<mat-icon class="ov-action-icon">schedule</mat-icon>
|
||||||
|
{{ recording.startDate | date: 'MMM d, y - h:mm a' }}
|
||||||
|
</span>
|
||||||
|
@if (recording.duration) {
|
||||||
|
<span class="recording-duration">
|
||||||
|
<mat-icon class="ov-action-icon">timer</mat-icon>
|
||||||
|
{{ formatDuration(recording.duration) }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="recording-status">
|
||||||
|
<mat-icon [class]="'status-icon ' + recording.status">{{
|
||||||
|
getStatusIcon()
|
||||||
|
}}</mat-icon>
|
||||||
|
<span class="status-text">{{ getStatusMessage() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Actions Section -->
|
||||||
|
@if (!videoError) {
|
||||||
|
<div class="actions-section">
|
||||||
|
<div class="primary-actions">
|
||||||
|
@if (recordingUrl) {
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
(click)="downloadRecording()"
|
||||||
|
matTooltip="Download recording"
|
||||||
|
class="action-btn primary-button"
|
||||||
|
>
|
||||||
|
<mat-icon>download</mat-icon>
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
color="accent"
|
||||||
|
(click)="openShareDialog()"
|
||||||
|
matTooltip="Share recording"
|
||||||
|
class="action-btn"
|
||||||
|
>
|
||||||
|
<mat-icon>share</mat-icon>
|
||||||
|
<span>Share</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Player Section -->
|
||||||
|
<div class="video-section">
|
||||||
|
@if (recordingUrl && !videoError) {
|
||||||
|
<div class="video-container">
|
||||||
|
<video
|
||||||
|
[src]="recordingUrl"
|
||||||
|
controls
|
||||||
|
controlsList="nodownload"
|
||||||
|
playsinline
|
||||||
|
class="video-player"
|
||||||
|
(loadeddata)="onVideoLoaded()"
|
||||||
|
(error)="onVideoError()"
|
||||||
|
></video>
|
||||||
|
|
||||||
|
@if (!isVideoLoaded) {
|
||||||
|
<div class="video-loading-overlay">
|
||||||
|
<mat-spinner diameter="40"></mat-spinner>
|
||||||
|
<span>Loading video...</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else if (videoError) {
|
||||||
|
<div class="video-error-container">
|
||||||
|
<div class="video-error-content">
|
||||||
|
<mat-icon class="error-icon">error</mat-icon>
|
||||||
|
<h3>Video Playback Error</h3>
|
||||||
|
<p>There was an error loading the video. Please check your connection and try again.</p>
|
||||||
|
<button mat-raised-button color="primary" (click)="retryLoad()">
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
<span>Retry</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="video-unavailable-container">
|
||||||
|
<div class="video-unavailable-content">
|
||||||
|
<mat-icon class="status-icon">{{ getStatusIcon() }}</mat-icon>
|
||||||
|
<h3>{{ getStatusMessage() }}</h3>
|
||||||
|
@if (['starting', 'active', 'ending'].includes(recording.status)) {
|
||||||
|
<p>The recording is still being processed. Please check back in a few minutes.</p>
|
||||||
|
<button mat-raised-button color="primary" (click)="retryLoad()">
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
<span>Refresh</span>
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<p>This recording is not available for playback.</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,212 @@
|
|||||||
@import '../../../../../../src/assets/styles/design-tokens';
|
@import '../../../../../../src/assets/styles/design-tokens';
|
||||||
|
|
||||||
|
// === MOBILE RESPONSIVE EXPERIENCE ===
|
||||||
|
|
||||||
|
.mobile-container {
|
||||||
|
touch-action: manipulation;
|
||||||
|
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--ov-meet-background-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@include ov-tablet-up {
|
||||||
|
display: none; // Hide on tablet and desktop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MOBILE HEADER TOOLBAR ===
|
||||||
|
|
||||||
|
.mobile-header-toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1010;
|
||||||
|
background-color: var(--ov-meet-background-color);
|
||||||
|
padding: var(--ov-meet-spacing-sm) var(--ov-meet-spacing-md);
|
||||||
|
padding-top: max(var(--ov-meet-spacing-sm), env(safe-area-inset-top));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ov-meet-spacing-md);
|
||||||
|
min-height: 48px;
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
// @extend .ov-icon-button;
|
||||||
|
color: var(--ov-meet-text-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
@include ov-icon(md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recording-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.recording-title {
|
||||||
|
margin: 0 0 2px 0;
|
||||||
|
font-size: var(--ov-meet-font-size-md);
|
||||||
|
font-weight: var(--ov-meet-font-weight-semibold);
|
||||||
|
color: var(--ov-meet-text-primary);
|
||||||
|
line-height: var(--ov-meet-line-height-tight);
|
||||||
|
|
||||||
|
// Truncate long titles
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recording-metadata {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--ov-meet-spacing-md);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.recording-date,
|
||||||
|
.recording-duration {
|
||||||
|
@include ov-flex-center;
|
||||||
|
gap: var(--ov-meet-spacing-xs);
|
||||||
|
font-size: var(--ov-meet-font-size-xs);
|
||||||
|
color: var(--ov-meet-text-secondary);
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
@include ov-icon(xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--ov-meet-spacing-xs);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
&.download-button,
|
||||||
|
&.share-button {
|
||||||
|
color: var(--ov-meet-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.download-button {
|
||||||
|
color: var(--ov-meet-color-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
@include ov-icon(md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
@include ov-flex-center;
|
||||||
|
gap: var(--ov-meet-spacing-xs);
|
||||||
|
justify-content: center;
|
||||||
|
background: color-mix(in srgb, var(--ov-meet-color-warning) 12%, transparent);
|
||||||
|
border-top: 1px solid var(--ov-meet-color-warning);
|
||||||
|
padding: var(--ov-meet-spacing-xs) var(--ov-meet-spacing-md);
|
||||||
|
font-size: var(--ov-meet-font-size-xs);
|
||||||
|
color: var(--ov-meet-color-warning);
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
@include ov-icon(xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-weight: var(--ov-meet-font-weight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MOBILE VIDEO CONTENT ===
|
||||||
|
|
||||||
|
.mobile-video-content {
|
||||||
|
background: var(--ov-meet-background-color);
|
||||||
|
min-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
.video-player {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
background: var(--ov-meet-background-color);
|
||||||
|
}
|
||||||
|
.video-player::-webkit-media-controls-fullscreen-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-background-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);
|
||||||
|
margin-bottom: var(--ov-meet-spacing-lg);
|
||||||
|
|
||||||
|
.error-content,
|
||||||
|
.unavailable-content {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 300px;
|
||||||
|
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-lg);
|
||||||
|
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-sm);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === MAIN PAGE LAYOUT ===
|
// === MAIN PAGE LAYOUT ===
|
||||||
|
|
||||||
.recording-page-content {
|
.recording-page-content {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@ -7,7 +7,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NotificationService, RecordingService } from '@lib/services';
|
import { NotificationService, RecordingService, ViewportService } from '@lib/services';
|
||||||
import { MeetRecordingInfo, MeetRecordingStatus } from '@lib/typings/ce';
|
import { MeetRecordingInfo, MeetRecordingStatus } from '@lib/typings/ce';
|
||||||
import { formatDurationToTime } from '@lib/utils';
|
import { formatDurationToTime } from '@lib/utils';
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ import { formatDurationToTime } from '@lib/utils';
|
|||||||
MatSnackBarModule
|
MatSnackBarModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ViewRecordingComponent implements OnInit {
|
export class ViewRecordingComponent implements OnInit, OnDestroy {
|
||||||
recording?: MeetRecordingInfo;
|
recording?: MeetRecordingInfo;
|
||||||
recordingUrl?: string;
|
recordingUrl?: string;
|
||||||
videoError = false;
|
videoError = false;
|
||||||
@ -34,11 +34,16 @@ export class ViewRecordingComponent implements OnInit {
|
|||||||
hasError = false;
|
hasError = false;
|
||||||
isVideoLoaded = false;
|
isVideoLoaded = false;
|
||||||
|
|
||||||
|
// Mobile UI state
|
||||||
|
showMobileControls = true;
|
||||||
|
private controlsTimeout?: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected recordingService: RecordingService,
|
protected recordingService: RecordingService,
|
||||||
protected notificationService: NotificationService,
|
protected notificationService: NotificationService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected router: Router
|
protected router: Router,
|
||||||
|
public viewportService: ViewportService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -72,6 +77,11 @@ export class ViewRecordingComponent implements OnInit {
|
|||||||
onVideoLoaded() {
|
onVideoLoaded() {
|
||||||
this.isVideoLoaded = true;
|
this.isVideoLoaded = true;
|
||||||
this.videoError = false;
|
this.videoError = false;
|
||||||
|
|
||||||
|
// Start controls timeout for mobile
|
||||||
|
if (this.viewportService.isMobileView()) {
|
||||||
|
this.resetControlsTimeout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVideoError() {
|
onVideoError() {
|
||||||
@ -136,4 +146,33 @@ export class ViewRecordingComponent implements OnInit {
|
|||||||
formatDuration(duration: number): string {
|
formatDuration(duration: number): string {
|
||||||
return formatDurationToTime(duration);
|
return formatDurationToTime(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goBack(): void {
|
||||||
|
// Try to go back in browser history, otherwise navigate to recordings
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
window.history.back();
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/recordings']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile UI interactions
|
||||||
|
|
||||||
|
private resetControlsTimeout(): void {
|
||||||
|
if (this.controlsTimeout) {
|
||||||
|
clearTimeout(this.controlsTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showMobileControls) {
|
||||||
|
this.controlsTimeout = window.setTimeout(() => {
|
||||||
|
this.showMobileControls = false;
|
||||||
|
}, 3000); // Hide controls after 3 seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.controlsTimeout) {
|
||||||
|
clearTimeout(this.controlsTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
--ov-meet-color-accent-dark: #6a1b9a;
|
--ov-meet-color-accent-dark: #6a1b9a;
|
||||||
--ov-meet-color-warn: #f44336;
|
--ov-meet-color-warn: #f44336;
|
||||||
--ov-meet-color-success: #4caf50;
|
--ov-meet-color-success: #4caf50;
|
||||||
--ov-meet-color-info: #045496;
|
--ov-meet-color-info: #086abb;
|
||||||
--ov-meet-color-warning: #ff9800;
|
--ov-meet-color-warning: #ff9800;
|
||||||
--ov-meet-color-error: #f44336;
|
--ov-meet-color-error: #f44336;
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,9 @@
|
|||||||
// === DARK THEME ===
|
// === DARK THEME ===
|
||||||
// Activated when the [data-theme="dark"] attribute is present on html
|
// Activated when the [data-theme="dark"] attribute is present on html
|
||||||
[data-theme='dark'] {
|
[data-theme='dark'] {
|
||||||
|
|
||||||
|
--ov-meet-color-info: #2a9bf7;
|
||||||
|
|
||||||
// === SURFACE COLORS - DARK THEME ===
|
// === SURFACE COLORS - DARK THEME ===
|
||||||
--ov-meet-background-color: #29292e;
|
--ov-meet-background-color: #29292e;
|
||||||
--ov-meet-background-secondary: #21212b;
|
--ov-meet-background-secondary: #21212b;
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>OpenVidu Meet</title>
|
<title>OpenVidu Meet</title>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1 maximum-scale=1, user-scalable=no" />
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user