ov-components: Improve prejoin card styles

master
Carlos Santos 2025-08-21 11:47:44 +02:00
parent 68d855a245
commit f92ee9b886
6 changed files with 115 additions and 46 deletions

View File

@ -1,14 +1,11 @@
.prejoin-mode { .prejoin-mode {
margin-top: 0px; margin: 0 10px 0px 10px;
max-height: 100%; max-height: 100%;
min-height: 100%; min-height: 100%;
.effects-container {
padding: 0px;
}
} }
.background-title { .background-title {
color: var(--ov-text-surface-color); color: var(--ov-text-surface-color);
margin: 10px 0;
} }
.effects-container { .effects-container {
display: block !important; display: block !important;

View File

@ -13,13 +13,13 @@
</div> </div>
</div> </div>
} @else { } @else {
<!-- Main Content with Side Panel Layout --> <!-- Main Content -->
<div class="prejoin-content"> <div class="prejoin-content">
<!-- Main Card --> <!-- Main Card -->
<div class="prejoin-main"> <div class="prejoin-main">
<!-- Video Preview Section --> <!-- Video Preview Section -->
<div class="video-preview-section"> <div class="video-preview-section">
<div class="video-preview-container"> <div class="video-preview-container" [@containerResize]="showBackgroundPanel ? 'compact' : 'normal'">
<div class="video-frame"> <div class="video-frame">
<ov-media-element <ov-media-element
[track]="videoTrack" [track]="videoTrack"
@ -63,8 +63,9 @@
class="background-button" class="background-button"
(click)="toggleBackgroundPanel()" (click)="toggleBackgroundPanel()"
[matTooltip]="'Virtual Backgrounds'" [matTooltip]="'Virtual Backgrounds'"
[disabled]="!isVideoEnabled"
> >
<mat-icon>auto_fix_high</mat-icon> <mat-icon class="material-symbols-outlined">background_replace</mat-icon>
</button> </button>
</div> </div>
</div> </div>
@ -73,7 +74,9 @@
</div> </div>
@if (showBackgroundPanel) { @if (showBackgroundPanel) {
<div class="vb-container" [@slideInOut]>
<ov-background-effects-panel [mode]="'prejoin'" (onClose)="closeBackgroundPanel()"> </ov-background-effects-panel> <ov-background-effects-panel [mode]="'prejoin'" (onClose)="closeBackgroundPanel()"> </ov-background-effects-panel>
</div>
} @else { } @else {
<!-- Configuration Section --> <!-- Configuration Section -->
<div class="configuration-section"> <div class="configuration-section">

View File

@ -85,6 +85,7 @@
.prejoin-main { .prejoin-main {
width: 100%; width: 100%;
max-width: 520px; max-width: 520px;
max-height: 544px;
background: var(--ov-surface-color, #ffffff); background: var(--ov-surface-color, #ffffff);
border-radius: var(--ov-surface-radius); border-radius: var(--ov-surface-radius);
overflow: hidden; overflow: hidden;
@ -93,18 +94,16 @@
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
} }
// Video Preview Section (moved to top with no padding) // Video Preview Section
.video-preview-section { .video-preview-section {
padding: 0; padding: 0;
.video-preview-container { .video-preview-container {
position: relative; position: relative;
width: 100%; width: 100%;
aspect-ratio: 4/3; // Changed from 16/9 to 4/3 for taller video aspect-ratio: 4/3;
border-radius: var(--ov-surface-radius) var(--ov-surface-radius) 0 0; // Only top corners rounded border-radius: var(--ov-surface-radius) var(--ov-surface-radius) 0 0;
overflow: hidden; overflow: hidden;
background: #000; background: #000;
.video-frame { .video-frame {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -156,16 +155,23 @@
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: #333333; color: #333333;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transform: translateZ(0);
&:hover {
color: rgba(255, 255, 255, 1);
}
&:active { &:active {
transform: translateY(-1px); transform: translateY(-1px);
transition: all 0.15s ease; transition: all 0.15s ease;
} }
&.mat-mdc-button-disabled {
background: rgba(255, 255, 255, 0.137);
color: rgba(233, 233, 233, 0.5);
cursor: not-allowed;
&:hover {
transform: none;
}
}
mat-icon { mat-icon {
font-size: 22px; font-size: 22px;
width: 22px; width: 22px;
@ -183,6 +189,11 @@
} }
} }
.vb-container {
height: fit-content;
overflow: hidden;
}
// Configuration Section // Configuration Section
.configuration-section { .configuration-section {
padding: 24px 24px 24px; // Added top padding since video has no padding padding: 24px 24px 24px; // Added top padding since video has no padding
@ -393,8 +404,8 @@
transform: translateY(0); transform: translateY(0);
} }
} }
.prejoin-main { .prejoin-main {
animation: fadeIn 0.3s ease-out; animation: fadeIn 0.3s ease-out;
transform: translateZ(0);
} }
} }

View File

@ -9,6 +9,7 @@ import {
OnInit, OnInit,
Output Output
} from '@angular/core'; } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { filter, Subject, takeUntil, tap } from 'rxjs'; import { filter, Subject, takeUntil, tap } from 'rxjs';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service'; import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
@ -28,7 +29,47 @@ import { LangOption } from '../../models/lang.model';
templateUrl: './pre-join.component.html', templateUrl: './pre-join.component.html',
styleUrls: ['./pre-join.component.scss'], styleUrls: ['./pre-join.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false standalone: false,
animations: [
trigger('containerResize', [
state(
'normal',
style({
height: '*'
})
),
state(
'compact',
style({
height: '28vh'
})
),
transition('normal => compact', [animate('250ms cubic-bezier(0.25, 0.8, 0.25, 1)')]),
transition('compact => normal', [animate('350ms cubic-bezier(0.25, 0.8, 0.25, 1)')])
]),
trigger('slideInOut', [
transition(':enter', [
style({
opacity: 0
}),
animate(
'300ms cubic-bezier(0.34, 1.56, 0.64, 1)',
style({
opacity: 1
})
)
]),
transition(':leave', [
animate(
'200ms cubic-bezier(0.25, 0.46, 0.45, 0.94)',
style({
opacity: 0,
transform: 'translateY(-10px)'
})
)
])
])
]
}) })
export class PreJoinComponent implements OnInit, OnDestroy { export class PreJoinComponent implements OnInit, OnDestroy {
@Input() set error(error: { name: string; message: string } | undefined) { @Input() set error(error: { name: string; message: string } | undefined) {
@ -61,6 +102,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
videoTrack: LocalTrack | undefined; videoTrack: LocalTrack | undefined;
audioTrack: LocalTrack | undefined; audioTrack: LocalTrack | undefined;
isVideoEnabled: boolean = false;
private tracks: LocalTrack[]; private tracks: LocalTrack[];
private log: ILogger; private log: ILogger;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -195,12 +237,16 @@ export class PreJoinComponent implements OnInit, OnDestroy {
} }
async videoEnabledChanged(enabled: boolean) { async videoEnabledChanged(enabled: boolean) {
if (enabled && !this.videoTrack) { this.isVideoEnabled = enabled;
if (!enabled) {
this.closeBackgroundPanel();
} else if (!this.videoTrack) {
const newVideoTrack = await this.openviduService.createLocalTracks(true, false); const newVideoTrack = await this.openviduService.createLocalTracks(true, false);
this.videoTrack = newVideoTrack[0]; this.videoTrack = newVideoTrack[0];
this.tracks.push(this.videoTrack); this.tracks.push(this.videoTrack);
this.openviduService.setLocalTracks(this.tracks); this.openviduService.setLocalTracks(this.tracks);
} }
this.onVideoEnabledChanged.emit(enabled); this.onVideoEnabledChanged.emit(enabled);
} }
@ -215,19 +261,32 @@ export class PreJoinComponent implements OnInit, OnDestroy {
} }
/** /**
* Toggle virtual background panel visibility * Toggle virtual background panel visibility with smooth animation
*/ */
toggleBackgroundPanel() { toggleBackgroundPanel() {
this.showBackgroundPanel = !this.showBackgroundPanel; // Add a small delay to ensure smooth transition
if (!this.showBackgroundPanel) {
// Opening panel
this.showBackgroundPanel = true;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
} else {
// Closing panel - add slight delay for smooth animation
setTimeout(() => {
this.showBackgroundPanel = false;
this.changeDetector.markForCheck();
}, 50);
}
} }
/** /**
* Close virtual background panel * Close virtual background panel with smooth animation
*/ */
closeBackgroundPanel() { closeBackgroundPanel() {
// Add animation delay for smooth closing
setTimeout(() => {
this.showBackgroundPanel = false; this.showBackgroundPanel = false;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
}, 100);
} }
/** /**
@ -249,6 +308,8 @@ export class PreJoinComponent implements OnInit, OnDestroy {
this.openviduService.setLocalTracks(this.tracks); this.openviduService.setLocalTracks(this.tracks);
this.videoTrack = this.tracks.find((track) => track.kind === 'video'); this.videoTrack = this.tracks.find((track) => track.kind === 'video');
this.audioTrack = this.tracks.find((track) => track.kind === 'audio'); this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
this.isVideoEnabled = this.openviduService.isVideoTrackEnabled();
return; // Success, exit retry loop return; // Success, exit retry loop
} catch (error) { } catch (error) {
this.log.w(`Device initialization attempt ${attempt} failed:`, error); this.log.w(`Device initialization attempt ${attempt} failed:`, error);

View File

@ -87,8 +87,7 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
// Constants // Constants
private static readonly PARTICIPANT_NAME_TIMEOUT_MS = 1000; private static readonly PARTICIPANT_NAME_TIMEOUT_MS = 1000;
private static readonly ANIMATION_DURATION_MS = 300; private static readonly ANIMATION_DURATION_MS = 300;
private static readonly MATERIAL_ICONS_URL = private static readonly MATERIAL_ICONS_URL = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined';
'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined';
private static readonly MATERIAL_ICONS_SELECTOR = 'link[href*="Material+Symbols+Outlined"]'; private static readonly MATERIAL_ICONS_SELECTOR = 'link[href*="Material+Symbols+Outlined"]';
private static readonly SPINNER_DIAMETER = 50; private static readonly SPINNER_DIAMETER = 50;
// *** Toolbar *** // *** Toolbar ***
@ -690,6 +689,8 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
) { ) {
this.log = this.loggerSrv.get('VideoconferenceComponent'); this.log = this.loggerSrv.get('VideoconferenceComponent');
this.addMaterialIconsIfNeeded();
// Initialize state // Initialize state
this.updateComponentState({ this.updateComponentState({
state: VideoconferenceState.INITIALIZING, state: VideoconferenceState.INITIALIZING,
@ -713,7 +714,6 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal * @internal
*/ */
ngAfterViewInit() { ngAfterViewInit() {
this.addMaterialIconsIfNeeded();
this.setupTemplates(); this.setupTemplates();
this.deviceSrv.initializeDevices().then(() => { this.deviceSrv.initializeDevices().then(() => {
this.updateComponentState({ this.updateComponentState({

View File

@ -238,23 +238,20 @@ export class OpenViduService {
videoDeviceId: string | boolean | undefined = undefined, videoDeviceId: string | boolean | undefined = undefined,
audioDeviceId: string | boolean | undefined = undefined audioDeviceId: string | boolean | undefined = undefined
): Promise<LocalTrack[]> { ): Promise<LocalTrack[]> {
// If video and audio device IDs are not provided, check if they are enabled and use the default devices // Default values: true if device is enabled, false otherwise
if (videoDeviceId === undefined) videoDeviceId = this.deviceService.isCameraEnabled(); videoDeviceId ??= this.deviceService.isCameraEnabled();
if (audioDeviceId === undefined) audioDeviceId = this.deviceService.isMicrophoneEnabled(); audioDeviceId ??= this.deviceService.isMicrophoneEnabled();
let options: CreateLocalTracksOptions = { const options: CreateLocalTracksOptions = {
audio: { echoCancellation: true, noiseSuppression: true }, audio: { echoCancellation: true, noiseSuppression: true },
video: {} video: {}
}; };
// Video device // Video device
if (videoDeviceId === true) { if (videoDeviceId === true) {
if (this.deviceService.hasVideoDeviceAvailable()) { options.video = this.deviceService.hasVideoDeviceAvailable()
videoDeviceId = this.deviceService.getCameraSelected()?.device || 'default'; ? { deviceId: this.deviceService.getCameraSelected()?.device || 'default' }
(options.video as VideoCaptureOptions).deviceId = videoDeviceId; : false;
} else {
options.video = false;
}
} else if (videoDeviceId === false) { } else if (videoDeviceId === false) {
options.video = false; options.video = false;
} else { } else {
@ -279,13 +276,13 @@ export class OpenViduService {
if (options.audio || options.video) { if (options.audio || options.video) {
this.log.d('Creating local tracks with options', options); this.log.d('Creating local tracks with options', options);
newLocalTracks = await createLocalTracks(options); newLocalTracks = await createLocalTracks(options);
// Mute tracks if devices are disabled
if (!this.deviceService.isCameraEnabled()) { if (!this.deviceService.isCameraEnabled()) {
const videoTrack = newLocalTracks.find((track) => track.kind === Track.Kind.Video); newLocalTracks.find((t) => t.kind === Track.Kind.Video)?.mute();
if (videoTrack) videoTrack.mute();
} }
if (!this.deviceService.isMicrophoneEnabled()) { if (!this.deviceService.isMicrophoneEnabled()) {
const audioTrack = newLocalTracks.find((track) => track.kind === Track.Kind.Audio); newLocalTracks.find((t) => t.kind === Track.Kind.Audio)?.mute();
if (audioTrack) audioTrack.mute();
} }
} }
return newLocalTracks; return newLocalTracks;
@ -331,7 +328,7 @@ export class OpenViduService {
return this.deviceService.isCameraEnabled(); return this.deviceService.isCameraEnabled();
} }
const videoTrack = this.localTracks.find((track) => track.kind === Track.Kind.Video); const videoTrack = this.localTracks.find((track) => track.kind === Track.Kind.Video);
return !!videoTrack && !videoTrack.isMuted; return !!videoTrack && !videoTrack.isMuted && videoTrack?.mediaStreamTrack?.enabled;
} }
/** /**
@ -344,7 +341,7 @@ export class OpenViduService {
return this.deviceService.isMicrophoneEnabled(); return this.deviceService.isMicrophoneEnabled();
} }
const audioTrack = this.localTracks.find((track) => track.kind === Track.Kind.Audio); const audioTrack = this.localTracks.find((track) => track.kind === Track.Kind.Audio);
return !!audioTrack && !audioTrack.isMuted; return !!audioTrack && !audioTrack.isMuted && audioTrack?.mediaStreamTrack?.enabled;
} }
/** /**