mirror of https://github.com/OpenVidu/openvidu.git
ov-components: Enhanced prejoin component
- Introduced background effect feature with options for 'none', 'blur', 'office', and 'nature'. - Enhanced error handling during device initialization with retry logic and user feedback. - Updated participant name handling to trim whitespace and clear errors on input change. style(audio-devices): refactor audio device selection UI - Redesigned audio device selection to use buttons instead of dropdowns for better UX. - Improved styling for audio toggle button and device selection menu. style(video-devices): refactor video device selection UI - Updated video device selection to use buttons for toggling camera and selecting devices. - Enhanced styling for video toggle button and device selection menu. style(lang-selector): improve language selection UI - Redesigned language selector for better usability with compact and full versions. - Enhanced styling for language selection buttons and menu items. style(participant-name-input): refactor participant name input field - Updated participant name input to use a custom styled input field instead of mat-form-field. - Improved styling for input field and error handling. style: general UI improvements across components - Enhanced overall styling for better consistency and user experience across various components.master
parent
622a2f6707
commit
72e7469012
|
@ -1,64 +1,89 @@
|
|||
<div class="container" id="prejoin-container">
|
||||
|
||||
<div *ngIf="isLoading" id="loading-container">
|
||||
<mat-spinner [diameter]="50"></mat-spinner>
|
||||
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||
<div class="prejoin-container" id="prejoin-container">
|
||||
<!-- Loading State -->
|
||||
<div *ngIf="isLoading" class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<mat-spinner [diameter]="40"></mat-spinner>
|
||||
<span class="loading-text">{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isLoading" id="prejoin-card">
|
||||
<ov-lang-selector *ngIf="!isMinimal" [compact]="true" class="lang-btn" (onLangChanged)="onLangChanged.emit($event)">
|
||||
</ov-lang-selector>
|
||||
<!-- Main Content -->
|
||||
<div *ngIf="!isLoading" class="prejoin-main">
|
||||
<!-- Header with Language Selector -->
|
||||
<div class="prejoin-header" *ngIf="!isMinimal">
|
||||
<ov-lang-selector [compact]="true" class="language-selector" (onLangChanged)="onLangChanged.emit($event)"> </ov-lang-selector>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="video-container">
|
||||
<div id="video-poster">
|
||||
<!-- Video Preview Section -->
|
||||
<div class="video-preview-section">
|
||||
<div class="video-preview-container">
|
||||
<div class="video-frame">
|
||||
<ov-media-element
|
||||
[track]="videoTrack"
|
||||
[showAvatar]="!videoTrack || videoTrack.isMuted"
|
||||
[avatarName]="participantName"
|
||||
[avatarColor]="'hsl(48, 100%, 50%)'"
|
||||
[isLocal]="true"
|
||||
></ov-media-element>
|
||||
</div>
|
||||
</div>
|
||||
class="video-element"
|
||||
>
|
||||
</ov-media-element>
|
||||
|
||||
<div class="media-controls-container">
|
||||
<!-- Camera -->
|
||||
<div class="video-controls-container" *ngIf="showCameraButton">
|
||||
<!-- Video Controls Overlay -->
|
||||
<div class="video-overlay">
|
||||
<div class="device-controls">
|
||||
<div class="control-group" *ngIf="showCameraButton">
|
||||
<ov-video-devices-select
|
||||
[compact]="true"
|
||||
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
||||
(onVideoEnabledChanged)="videoEnabledChanged($event)"
|
||||
></ov-video-devices-select>
|
||||
class="device-selector"
|
||||
>
|
||||
</ov-video-devices-select>
|
||||
</div>
|
||||
|
||||
<!-- Microphone -->
|
||||
<div class="audio-controls-container" *ngIf="showMicrophoneButton">
|
||||
<div class="control-group" *ngIf="showMicrophoneButton">
|
||||
<ov-audio-devices-select
|
||||
[compact]="true"
|
||||
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
||||
(onAudioEnabledChanged)="audioEnabledChanged($event)"
|
||||
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
|
||||
></ov-audio-devices-select>
|
||||
class="device-selector"
|
||||
>
|
||||
</ov-audio-devices-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participant-name-container" *ngIf="showParticipantName">
|
||||
<!-- Configuration Section -->
|
||||
<div class="configuration-section">
|
||||
<!-- Participant Name Input -->
|
||||
<div class="input-section" *ngIf="showParticipantName">
|
||||
<ov-participant-name-input
|
||||
[isPrejoinPage]="true"
|
||||
[error]="!!_error"
|
||||
(onNameUpdated)="onParticipantNameChanged($event)"
|
||||
(onEnterPressed)="onEnterPressed()"
|
||||
></ov-participant-name-input>
|
||||
class="name-input"
|
||||
>
|
||||
</ov-participant-name-input>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!!_error" id="token-error">
|
||||
<span class="error">{{ _error }}</span>
|
||||
<!-- Error Message -->
|
||||
<div *ngIf="!!_error" class="error-message">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<span class="error-text">{{ _error }}</span>
|
||||
</div>
|
||||
|
||||
<div class="join-btn-container">
|
||||
<button mat-flat-button (click)="join()" id="join-button">
|
||||
<!-- Join Button -->
|
||||
<div class="join-section">
|
||||
<button mat-flat-button (click)="join()" class="join-button" [disabled]="showParticipantName && !participantName">
|
||||
<mat-icon class="join-icon">videocam</mat-icon>
|
||||
{{ 'PREJOIN.JOIN' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,159 +1,350 @@
|
|||
:host {
|
||||
.container {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--ov-background-color);
|
||||
|
||||
.prejoin-container {
|
||||
min-height: 100vh;
|
||||
background: var(--ov-background-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#loading-container {
|
||||
// Loading State
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
color: var(--ov-text-primary-color);
|
||||
.mat-mdc-progress-spinner {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--ov-background-color, #f5f5f5);
|
||||
z-index: 1000;
|
||||
|
||||
#prejoin-card {
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
gap: 16px;
|
||||
|
||||
.loading-text {
|
||||
color: var(--ov-text-primary-color, #333);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mat-mdc-progress-spinner {
|
||||
--mdc-circular-progress-active-indicator-color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main Content
|
||||
.prejoin-main {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
background: var(--ov-surface-color, #ffffff);
|
||||
border-radius: var(--ov-surface-radius);
|
||||
width: 90%;
|
||||
max-width: 370px;
|
||||
// max-height: 650px;
|
||||
height: min-content;
|
||||
padding: 55px 30px;
|
||||
background-color: var(--ov-surface-color);
|
||||
box-shadow: 6px 4px 20px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
::ng-deep .lang-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
height: 25px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
::ng-deep .lang-btn mat-icon {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
margin: auto;
|
||||
height: 35vh;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.08),
|
||||
0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.prejoin-header {
|
||||
padding: 16px 20px 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
::ng-deep .language-selector {
|
||||
.mat-mdc-button {
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--ov-border-color, #e0e0e0);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ov-hover-color, #f5f5f5);
|
||||
}
|
||||
}
|
||||
|
||||
#video-poster {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
mat-icon {
|
||||
color: var(--ov-text-secondary-color, #666) !important;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Video Preview Section
|
||||
.video-preview-section {
|
||||
padding: 24px 24px 20px;
|
||||
|
||||
.video-preview-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
|
||||
|
||||
.video-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
::ng-deep .video-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-controls-container {
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
// background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, transparent 100%);
|
||||
padding: 16px;
|
||||
z-index: 9999;
|
||||
|
||||
.device-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
height: auto;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.participant-name-container {
|
||||
display: block !important;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.video-controls-container,
|
||||
.audio-controls-container {
|
||||
width: calc(50% - 10px);
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.join-btn-container {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#join-button {
|
||||
background-color: var(--ov-primary-action-color);
|
||||
color: var(--ov-secondary-action-color);
|
||||
font-weight: bold;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
// #join-button:hover {
|
||||
// background-color: lighten(var(--ov-primary-action-color), 10%);
|
||||
// }
|
||||
|
||||
.error {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
color: var(--ov-error-color);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#prejoin-card {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.media-controls-container {
|
||||
// Configuration Section
|
||||
.configuration-section {
|
||||
padding: 0 24px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: auto;
|
||||
}
|
||||
gap: 20px;
|
||||
|
||||
.video-controls-container,
|
||||
.audio-controls-container {
|
||||
.input-section {
|
||||
::ng-deep .name-input {
|
||||
.mat-mdc-form-field {
|
||||
width: 100%;
|
||||
|
||||
.mat-mdc-text-field-wrapper {
|
||||
border-radius: var(--ov-surface-radius);
|
||||
background-color: var(--ov-input-background, #f8f9fa);
|
||||
border: 1px solid var(--ov-border-color, #e0e0e0);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
|
||||
&.mdc-text-field--focused {
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) and (orientation: landscape) {
|
||||
.media-controls-container {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
.mat-mdc-form-field-subscript-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.video-controls-container,
|
||||
.audio-controls-container {
|
||||
width: 48%;
|
||||
input {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--ov-text-primary-color, #333);
|
||||
padding: 16px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 630px) {
|
||||
.video-container {
|
||||
height: 30vh;
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background-color: rgba(244, 67, 54, 0.08);
|
||||
border: 1px solid rgba(244, 67, 54, 0.2);
|
||||
border-radius: var(--ov-surface-radius);
|
||||
color: var(--ov-error-color, #d32f2f);
|
||||
|
||||
.error-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.media-controls-container {
|
||||
height: auto;
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.join-section {
|
||||
.join-button {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
background: var(--ov-primary-action-color, #4285f4);
|
||||
color: white;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.2);
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
background: var(--ov-primary-action-hover, #3367d6);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 16px rgba(66, 133, 244, 0.3);
|
||||
}
|
||||
|
||||
&:active:not([disabled]) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background: var(--ov-disabled-color, #ccc);
|
||||
color: var(--ov-disabled-text-color, #999);
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.join-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 640px) {
|
||||
.prejoin-container {
|
||||
padding: 16px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.prejoin-main {
|
||||
max-width: 100%;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
|
||||
.video-preview-section {
|
||||
padding: 16px 20px 12px;
|
||||
|
||||
.video-preview-container {
|
||||
aspect-ratio: 4/3;
|
||||
}
|
||||
}
|
||||
|
||||
.configuration-section {
|
||||
padding: 0 20px 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.prejoin-header {
|
||||
padding: 12px 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.prejoin-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.video-preview-section {
|
||||
padding: 12px 16px 8px;
|
||||
}
|
||||
|
||||
.configuration-section {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.video-overlay .device-controls {
|
||||
gap: 8px;
|
||||
|
||||
::ng-deep .device-selector .mat-mdc-icon-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 640px) {
|
||||
.prejoin-container {
|
||||
align-items: flex-start;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.video-preview-section .video-preview-container {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark theme support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.prejoin-container {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
}
|
||||
|
||||
.prejoin-main {
|
||||
background: #2d2d2d;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.configuration-section .input-section ::ng-deep .name-input .participant-name-input-container .input-wrapper {
|
||||
background-color: #3a3a3a;
|
||||
border-color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
// Animation keyframes
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.prejoin-main {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import { TranslateService } from '../../services/translate/translate.service';
|
|||
import { LocalTrack } from 'livekit-client';
|
||||
import { CustomDevice } from '../../models/device.model';
|
||||
import { LangOption } from '../../models/lang.model';
|
||||
import { VirtualBackgroundService } from '../../services/virtual-background/virtual-background.service';
|
||||
import { BackgroundEffect } from '../../models/background-effect.model';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
@ -56,6 +58,11 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
showLogo: boolean = true;
|
||||
showParticipantName: boolean = true;
|
||||
|
||||
// Future feature preparation
|
||||
backgroundEffectEnabled: boolean = false;
|
||||
availableBackgroundEffects: BackgroundEffect[] = [];
|
||||
selectedBackgroundEffect: BackgroundEffect | undefined;
|
||||
|
||||
videoTrack: LocalTrack | undefined;
|
||||
audioTrack: LocalTrack | undefined;
|
||||
private tracks: LocalTrack[];
|
||||
|
@ -74,9 +81,11 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
private cdkSrv: CdkOverlayService,
|
||||
private openviduService: OpenViduService,
|
||||
private translateService: TranslateService,
|
||||
private virtualBackgroundService: VirtualBackgroundService,
|
||||
private changeDetector: ChangeDetectorRef
|
||||
) {
|
||||
this.log = this.loggerSrv.get('PreJoinComponent');
|
||||
this.availableBackgroundEffects = this.virtualBackgroundService.getBackgrounds();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -105,14 +114,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private async initializeDevices() {
|
||||
try {
|
||||
this.tracks = await this.openviduService.createLocalTracks();
|
||||
this.openviduService.setLocalTracks(this.tracks);
|
||||
this.videoTrack = this.tracks.find((track) => track.kind === 'video');
|
||||
this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
|
||||
} catch (error) {
|
||||
this.log.e('Error creating local tracks:', error);
|
||||
}
|
||||
await this.initializeDevicesWithRetry();
|
||||
}
|
||||
|
||||
onDeviceSelectorClicked() {
|
||||
|
@ -122,24 +124,27 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
join() {
|
||||
if (this.showParticipantName && !this.participantName) {
|
||||
if (this.showParticipantName && !this.participantName?.trim()) {
|
||||
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any previous errors
|
||||
this._error = undefined;
|
||||
|
||||
// Mark tracks as permanent for avoiding to be removed in ngOnDestroy
|
||||
this.shouldRemoveTracksWhenComponentIsDestroyed = false;
|
||||
|
||||
// Assign participant name to the observable if it is defined
|
||||
if (this.participantName) {
|
||||
this.libService.updateGeneralConfig({ participantName: this.participantName });
|
||||
if (this.participantName?.trim()) {
|
||||
this.libService.updateGeneralConfig({ participantName: this.participantName.trim() });
|
||||
|
||||
// Wait for the next tick to ensure the participant name propagates
|
||||
// through the observable before emitting onReadyToJoin
|
||||
this.libService.participantName$
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
filter((name) => name === this.participantName),
|
||||
filter((name) => name === this.participantName?.trim()),
|
||||
tap(() => this.onReadyToJoin.emit())
|
||||
)
|
||||
.subscribe();
|
||||
|
@ -150,7 +155,11 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
onParticipantNameChanged(name: string) {
|
||||
if (name) this.participantName = name;
|
||||
this.participantName = name?.trim() || '';
|
||||
// Clear error when user starts typing
|
||||
if (this._error && this.participantName) {
|
||||
this._error = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
onEnterPressed() {
|
||||
|
@ -210,4 +219,48 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
this.onAudioEnabledChanged.emit(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Future method for background effects
|
||||
* @param effect - The background effect to apply
|
||||
*/
|
||||
onBackgroundEffectChanged(effect: string) {
|
||||
// TODO: Implement background effect logic
|
||||
// this.selectedBackgroundEffect = effect;
|
||||
// this.log.d('Background effect changed to:', effect);
|
||||
// this.virtualBackgroundService.applyBackground(this.virtualBackgroundService.getBackgrounds()[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced error handling with better UX
|
||||
*/
|
||||
private handleError(error: any) {
|
||||
this.log.e('PreJoin component error:', error);
|
||||
this._error = error.message || 'An unexpected error occurred';
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved device initialization with error handling
|
||||
*/
|
||||
private async initializeDevicesWithRetry(maxRetries: number = 3): Promise<void> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
this.tracks = await this.openviduService.createLocalTracks();
|
||||
this.openviduService.setLocalTracks(this.tracks);
|
||||
this.videoTrack = this.tracks.find((track) => track.kind === 'video');
|
||||
this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
|
||||
return; // Success, exit retry loop
|
||||
} catch (error) {
|
||||
this.log.w(`Device initialization attempt ${attempt} failed:`, error);
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
this.handleError(error);
|
||||
} else {
|
||||
// Wait before retrying
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +1,81 @@
|
|||
<div class="device-container-element" [class.mute-btn]="!isMicrophoneEnabled">
|
||||
<!-- <button mat-stroked-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" id="audio-devices-menu">
|
||||
<mat-icon class="audio-icon">mic</mat-icon>
|
||||
<span class="device-label"> {{ microphoneSelected.label }} </span>
|
||||
<mat-icon iconPositionEnd class="chevron-icon">
|
||||
{{ menuTrigger.menuOpen ? 'expand_less' : 'expand_more' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item *ngFor="let microphone of microphones">{{ microphone.label }}</button>
|
||||
</mat-menu> -->
|
||||
<mat-form-field id="audio-devices-form" *ngIf="microphones.length > 0">
|
||||
<mat-select
|
||||
[disabled]="!hasAudioDevices"
|
||||
[compareWith]="compareObjectDevices"
|
||||
[value]="microphoneSelected"
|
||||
(selectionChange)="onMicrophoneSelected($event)"
|
||||
>
|
||||
<mat-select-trigger>
|
||||
<div class="audio-device-selector" [class.compact]="compact">
|
||||
<!-- Unified Device Button (Compact Mode) -->
|
||||
@if (compact) {
|
||||
<div class="unified-device-button">
|
||||
<!-- Main toggle button -->
|
||||
<button
|
||||
mat-flat-button
|
||||
id="microphone-button"
|
||||
[disableRipple]="true"
|
||||
class="toggle-section"
|
||||
[disabled]="!hasAudioDevices || microphoneStatusChanging"
|
||||
[class.mute-btn]="!isMicrophoneEnabled"
|
||||
[class.device-enabled]="isMicrophoneEnabled"
|
||||
[class.device-disabled]="!isMicrophoneEnabled"
|
||||
(click)="toggleMic($event)"
|
||||
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
|
||||
[matTooltipDisabled]="!hasAudioDevices"
|
||||
>
|
||||
<mat-icon *ngIf="isMicrophoneEnabled" id="mic"> mic </mat-icon>
|
||||
<mat-icon *ngIf="!isMicrophoneEnabled" id="mic_off"> mic_off </mat-icon>
|
||||
<mat-icon>{{ isMicrophoneEnabled ? 'mic' : 'mic_off' }}</mat-icon>
|
||||
</button>
|
||||
<span class="selected-text" *ngIf="!isMicrophoneEnabled">{{ 'PANEL.SETTINGS.DISABLED_AUDIO' | translate }}</span>
|
||||
<span class="selected-text" *ngIf="isMicrophoneEnabled"> {{ microphoneSelected.label }} </span>
|
||||
</mat-select-trigger>
|
||||
<mat-option
|
||||
*ngFor="let microphone of microphones"
|
||||
[disabled]="!isMicrophoneEnabled"
|
||||
[value]="microphone"
|
||||
id="option-{{ microphone.label }}"
|
||||
>
|
||||
{{ microphone.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div id="audio-devices-form" *ngIf="microphones.length === 0">
|
||||
<div id="mat-select-trigger">
|
||||
<button mat-icon-button id="microphone-button" class="mute-btn" [disabled]="true">
|
||||
<mat-icon id="mic_off"> mic_off </mat-icon>
|
||||
<!-- Dropdown section -->
|
||||
@if (microphones.length > 1 && isMicrophoneEnabled) {
|
||||
<button mat-flat-button class="dropdown-section" [matMenuTriggerFor]="microphoneMenu" [disabled]="microphoneStatusChanging">
|
||||
<mat-icon>expand_more</mat-icon>
|
||||
</button>
|
||||
<span id="audio-devices-not-found"> {{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }} </span>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Normal Mode - Input Style Selector -->
|
||||
<div class="normal-device-selector">
|
||||
<!-- Input-style Device Selector -->
|
||||
<div class="device-input-selector" [class.disabled]="!hasAudioDevices || !isMicrophoneEnabled">
|
||||
<!-- When microphone is enabled -->
|
||||
@if (isMicrophoneEnabled) {
|
||||
<div class="device-input-selector">
|
||||
<button
|
||||
mat-flat-button
|
||||
class="selector-button"
|
||||
[disabled]="microphoneStatusChanging || microphones.length <= 1"
|
||||
[matMenuTriggerFor]="microphoneMenu"
|
||||
[attr.aria-expanded]="false"
|
||||
>
|
||||
<mat-icon class="device-icon">mic</mat-icon>
|
||||
<span class="selected-device-name">{{ microphoneSelected?.label || 'No microphone selected' }}</span>
|
||||
<mat-icon class="dropdown-icon" *ngIf="microphones.length > 1">expand_more</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- When microphone is disabled -->
|
||||
<div class="device-input-selector disabled">
|
||||
<div class="selector-button disabled">
|
||||
<mat-icon class="device-icon">mic_off</mat-icon>
|
||||
<span class="selected-device-name">
|
||||
{{ !hasAudioDevices ? ('PREJOIN.NO_AUDIO_DEVICE' | translate) : 'Microphone disabled' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Device Selection Menu (Shared) -->
|
||||
<mat-menu #microphoneMenu="matMenu" class="device-menu">
|
||||
@for (microphone of microphones; track microphone.device) {
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onMicrophoneSelected({ value: microphone })"
|
||||
[class.selected]="microphone.device === microphoneSelected.device"
|
||||
>
|
||||
<mat-icon *ngIf="microphone.device === microphoneSelected.device">check</mat-icon>
|
||||
<span>{{ microphone.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
|
||||
<!-- No Microphone Available -->
|
||||
@if (microphones.length === 0) {
|
||||
<div class="no-device-message">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<span>{{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -1,103 +1,29 @@
|
|||
$ov-selection-color-btn: #afafaf;
|
||||
$ov-selection-color: #cccccc;
|
||||
@use '../device-selector-shared' as shared;
|
||||
|
||||
:host {
|
||||
.device-container-element {
|
||||
border-radius: var(--ov-surface-radius);
|
||||
border: 1px solid $ov-selection-color-btn;
|
||||
}
|
||||
.device-container-element.mute-btn {
|
||||
border: 1px solid var(--ov-error-color);
|
||||
}
|
||||
#audio-devices-form {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
#audio-devices-not-found {
|
||||
font-size: 13px;
|
||||
}
|
||||
.audio-device-selector {
|
||||
@include shared.device-selector-base();
|
||||
|
||||
#microphone-button {
|
||||
color:#000000
|
||||
// Audio-specific overrides for normal mode
|
||||
&:not(.compact) {
|
||||
.normal-device-selector {
|
||||
.device-input-selector {
|
||||
&:not(.disabled) {
|
||||
.selector-button {
|
||||
// Audio-specific hover effect (simpler than video)
|
||||
&:hover:not([disabled]) {
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper,
|
||||
::ng-deep .mat-mdc-form-field-flex,
|
||||
::ng-deep .mat-mdc-select-trigger {
|
||||
height: 50px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
background-color: $ov-selection-color !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
::ng-deep .mdc-button--unelevated {
|
||||
border-top-left-radius: var(--ov-surface-radius);
|
||||
border-bottom-left-radius: var(--ov-surface-radius);
|
||||
border-bottom-right-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
background-color: $ov-selection-color-btn !important;
|
||||
width: 48px !important;
|
||||
min-width: 48px !important;
|
||||
padding: 0;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-unelevated-button > .mat-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
font-size: 24px !important;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
padding: 0px !important;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.selected-text {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
|
||||
border: 0px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-button-touch-target {
|
||||
border-radius: var(--ov-surface-radius) !important;
|
||||
}
|
||||
|
||||
.mute-btn {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--ov-error-color) !important;
|
||||
}
|
||||
}
|
||||
::ng-deep .mat-mdc-select-panel {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-option {
|
||||
padding: 10px 10px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
|
||||
color: var(--ov-primary-action-color) !important;
|
||||
}
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after {
|
||||
border-bottom-color: var(--ov-primary-action-color) !important;
|
||||
}
|
||||
::ng-deep .mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-multiple) {
|
||||
background-color: $ov-selection-color !important;
|
||||
}
|
||||
// Include shared device menu styles
|
||||
@include shared.device-menu-styles();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CustomDevice } from '../../../models/device.model';
|
||||
import { DeviceService } from '../../../services/device/device.service';
|
||||
|
@ -16,6 +16,7 @@ import { ParticipantModel } from '../../../models/participant.model';
|
|||
standalone: false
|
||||
})
|
||||
export class AudioDevicesComponent implements OnInit, OnDestroy {
|
||||
@Input() compact: boolean = false;
|
||||
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
|
||||
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
|
||||
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
// Shared styles for device selectors (video and audio)
|
||||
// This file contains common styling for both video-devices and audio-devices components
|
||||
|
||||
@mixin device-selector-base() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
// Compact Mode - Unified Button
|
||||
&.compact {
|
||||
.unified-device-button {
|
||||
display: flex;
|
||||
background: var(--ov-secondary-action-color);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.toggle-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 50px;
|
||||
width: 50px;
|
||||
height: 48px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.device-enabled {
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
|
||||
mat-icon {
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
}
|
||||
|
||||
&.device-disabled {
|
||||
background: rgba(244, 67, 54, 0.9);
|
||||
color: white;
|
||||
|
||||
mat-icon {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background: rgba(150, 150, 150, 0.5);
|
||||
color: rgba(150, 150, 150, 0.8);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 30px;
|
||||
width: 30px;
|
||||
height: 48px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normal Mode - Input Style Selector
|
||||
&:not(.compact) {
|
||||
.normal-device-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
.device-input-selector {
|
||||
flex: 1;
|
||||
|
||||
&:not(.disabled) {
|
||||
.selector-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
background: var(--ov-surface-color, #ffffff);
|
||||
border: 2px solid var(--ov-border-color, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
color: var(--ov-text-surface-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
justify-content: flex-start;
|
||||
|
||||
&[disabled] {
|
||||
background: var(--ov-disabled-background, #f5f5f5);
|
||||
color: var(--ov-disabled-text-color, #999);
|
||||
cursor: not-allowed;
|
||||
border-color: var(--ov-disabled-border-color, #ddd);
|
||||
}
|
||||
|
||||
.device-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.selected-device-name {
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&[aria-expanded='true'] .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.selector-button.disabled {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
background: var(--ov-disabled-background, #f5f5f5);
|
||||
border: 2px solid var(--ov-disabled-border-color, #ddd);
|
||||
border-radius: 8px;
|
||||
color: var(--ov-disabled-text-color, #999);
|
||||
font-size: 14px;
|
||||
cursor: not-allowed;
|
||||
|
||||
.device-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--ov-error-color, #d32f2f);
|
||||
}
|
||||
|
||||
.selected-device-name {
|
||||
flex: 1;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-device-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
border-radius: 8px;
|
||||
color: var(--ov-warning-color, #ff9800);
|
||||
font-size: 12px;
|
||||
|
||||
.warning-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared device menu styles
|
||||
@mixin device-menu-styles() {
|
||||
::ng-deep .device-menu.mat-mdc-menu-panel {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid var(--ov-border-color, #e0e0e0);
|
||||
overflow: hidden;
|
||||
background-color: var(--ov-surface-color);
|
||||
|
||||
.mat-mdc-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ov-hover-color, #f5f5f5);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(66, 133, 244, 0.08);
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
|
||||
mat-icon {
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,39 @@
|
|||
<button id="lang-btn-compact" *ngIf="compact" mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<div class="language-selector-container">
|
||||
<!-- Compact version (icon only) -->
|
||||
<button
|
||||
*ngIf="compact"
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="menu"
|
||||
class="compact-lang-button"
|
||||
[matTooltip]="'Change language'"
|
||||
>
|
||||
<mat-icon>translate</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="!compact" mat-flat-button [matMenuTriggerFor]="menu" class="lang-button" id="lang-btn">
|
||||
<span id="lang-selected-name">{{ langSelected?.name }}</span>
|
||||
<mat-icon class="expand-more-icon">expand_more</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
</button>
|
||||
|
||||
<!-- Full version (with text) -->
|
||||
<button
|
||||
*ngIf="!compact"
|
||||
mat-flat-button
|
||||
[matMenuTriggerFor]="menu"
|
||||
class="full-lang-button"
|
||||
>
|
||||
<mat-icon class="lang-icon">translate</mat-icon>
|
||||
<span class="lang-name">{{ langSelected?.name }}</span>
|
||||
<mat-icon class="expand-icon">expand_more</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Language Menu -->
|
||||
<mat-menu #menu="matMenu" class="language-menu">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngFor="let lang of languages"
|
||||
(click)="onLangSelected(lang.lang)"
|
||||
[attr.id]="'lang-opt-' + lang.lang"
|
||||
class="lang-menu-opt"
|
||||
[class.selected]="langSelected?.lang === lang.lang"
|
||||
class="language-option"
|
||||
>
|
||||
<span>{{ lang.name }}</span>
|
||||
<mat-icon *ngIf="langSelected?.lang === lang.lang" class="check-icon">check</mat-icon>
|
||||
<span class="lang-option-name">{{ lang.name }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,120 @@
|
|||
$ov-surface-color-lighter: color-mix(in srgb, var(--ov-surface-color), #fff 5%);
|
||||
:host {
|
||||
display: inline-block;
|
||||
|
||||
.lang-button {
|
||||
background-color: var(--ov-primary-action-color) !important;
|
||||
color: var(--ov-secondary-action-color) !important;
|
||||
}
|
||||
.lang-button .mat-icon {
|
||||
color: var(--ov-secondary-action-color);
|
||||
.language-selector-container {
|
||||
.compact-lang-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--ov-border-color, #e0e0e0);
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
|
||||
}
|
||||
::ng-deep .mat-mdc-menu-panel {
|
||||
border-radius: var(--ov-surface-radius) !important;
|
||||
background-color: $ov-surface-color-lighter !important;
|
||||
box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2) !important;
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.full-lang-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: var(--ov-surface-color, #ffffff);
|
||||
border: 2px solid var(--ov-border-color, #e0e0e0);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--ov-text-primary-color, #333);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.1);
|
||||
}
|
||||
|
||||
.lang-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
}
|
||||
|
||||
.lang-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] .expand-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-menu-item,
|
||||
.mat-mdc-menu-item:visited,
|
||||
.mat-mdc-menu-item:link {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
::ng-deep .language-menu {
|
||||
.mat-mdc-menu-panel {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid var(--ov-border-color, #e0e0e0);
|
||||
overflow: hidden;
|
||||
background: var(--ov-surface-color, #ffffff);
|
||||
}
|
||||
|
||||
.language-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 14px;
|
||||
min-height: 48px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ov-hover-color, #f5f5f5);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(66, 133, 244, 0.08);
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
|
||||
.check-icon {
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.lang-option-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
color: var(--ov-text-primary-color, #333);
|
||||
}
|
||||
|
||||
&.selected .lang-option-name {
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
<div id="name-input-container" [ngClass]="{ warn: !name }">
|
||||
<mat-form-field id="name-form" [ngClass]="{ error: error }">
|
||||
<mat-select-trigger>
|
||||
<button mat-flat-button disabled>
|
||||
<mat-icon>person</mat-icon>
|
||||
</button>
|
||||
</mat-select-trigger>
|
||||
<div class="participant-name-input-container" [class.error]="error">
|
||||
<div class="input-wrapper">
|
||||
<mat-icon class="input-icon">person</mat-icon>
|
||||
<input
|
||||
id="name-input"
|
||||
matInput
|
||||
(change)="updateName()"
|
||||
type="text"
|
||||
maxlength="20"
|
||||
[(ngModel)]="name"
|
||||
autocomplete="off"
|
||||
[disabled]="!isPrejoinPage"
|
||||
(change)="updateName()"
|
||||
(keypress)="eventKeyPress($event)"
|
||||
[placeholder]="'PREJOIN.NICKNAME' | translate"
|
||||
class="name-input-field"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,67 +1,71 @@
|
|||
$ov-selection-color-btn: #afafaf;
|
||||
$ov-selection-color: #cccccc;
|
||||
:host {
|
||||
#name-input-container {
|
||||
height: 70px;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
|
||||
#name-input-container mat-form-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: var(--ov-secondary-action-color);
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
display: inline-flex;
|
||||
padding: 0px !important;
|
||||
}
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
.participant-name-input-container {
|
||||
width: 100%;
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--ov-input-background, #f8f9fa);
|
||||
border: 2px solid var(--ov-border-color, #e0e0e0);
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
height: 70px;
|
||||
background-color: $ov-selection-color !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
|
||||
border: 0px !important;
|
||||
}
|
||||
::ng-deep .mdc-button--unelevated {
|
||||
border-top-left-radius: var(--ov-surface-radius) !important;
|
||||
border-bottom-left-radius: var(--ov-surface-radius) !important;
|
||||
border-bottom-right-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
background-color: $ov-selection-color-btn !important;
|
||||
width: 48px !important;
|
||||
min-width: 48px !important;
|
||||
padding: 0;
|
||||
height: 70px;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
|
||||
}
|
||||
|
||||
.error {
|
||||
::ng-deep .mdc-button--unelevated {
|
||||
background-color: var(--ov-error-color) !important;
|
||||
.input-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 56px;
|
||||
background: var(--ov-surface-secondary, #f0f0f0);
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
font-size: 20px;
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.name-input-field {
|
||||
flex: 1;
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--ov-disabled-background, #f5f5f5);
|
||||
color: var(--ov-disabled-text-color, #999);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-unelevated-button > .mat-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
font-size: 24px !important;
|
||||
margin: auto;
|
||||
color: #000000 !important;
|
||||
}
|
||||
&.error .input-wrapper {
|
||||
border-color: var(--ov-error-color, #d32f2f);
|
||||
box-shadow: 0 0 0 3px rgba(211, 47, 47, 0.1);
|
||||
|
||||
input {
|
||||
padding-left: 10px !important;
|
||||
border-top-right-radius: var(--ov-surface-radius) !important;
|
||||
border-bottom-right-radius: var(--ov-surface-radius) !important;
|
||||
border-bottom-left-radius: 0px !important;
|
||||
border-top-left-radius: 0px !important;
|
||||
border: 1px solid $ov-selection-color-btn;
|
||||
color: #000000;
|
||||
caret-color: #000000 !important;
|
||||
.input-icon {
|
||||
background: rgba(211, 47, 47, 0.1);
|
||||
color: var(--ov-error-color, #d32f2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
|
||||
color: var(--ov-primary-action-color) !important;
|
||||
}
|
||||
|
|
|
@ -1,40 +1,72 @@
|
|||
<div class="device-container-element" [class.mute-btn]="!isCameraEnabled">
|
||||
<mat-form-field id="video-devices-form" *ngIf="cameras.length > 0">
|
||||
<mat-select
|
||||
[disabled]="!hasVideoDevices"
|
||||
[compareWith]="compareObjectDevices"
|
||||
[value]="cameraSelected"
|
||||
(selectionChange)="onCameraSelected($event)"
|
||||
>
|
||||
<mat-select-trigger id="mat-select-trigger">
|
||||
<div class="video-device-selector" [class.compact]="compact">
|
||||
<!-- Unified Device Button (Compact Mode) -->
|
||||
@if (compact) {
|
||||
<div class="unified-device-button">
|
||||
<!-- Main toggle button -->
|
||||
<button
|
||||
mat-flat-button
|
||||
id="camera-button"
|
||||
[disableRipple]="true"
|
||||
class="toggle-section"
|
||||
[disabled]="!hasVideoDevices || cameraStatusChanging"
|
||||
[class.mute-btn]="!isCameraEnabled"
|
||||
[class.device-enabled]="isCameraEnabled"
|
||||
[class.device-disabled]="!isCameraEnabled"
|
||||
(click)="toggleCam($event)"
|
||||
[matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)"
|
||||
[matTooltipDisabled]="!hasVideoDevices"
|
||||
>
|
||||
<mat-icon *ngIf="isCameraEnabled" id="videocam"> videocam </mat-icon>
|
||||
<mat-icon *ngIf="!isCameraEnabled" id="videocam_off"> videocam_off </mat-icon>
|
||||
<mat-icon>{{ isCameraEnabled ? 'videocam' : 'videocam_off' }}</mat-icon>
|
||||
</button>
|
||||
<span class="selected-text" *ngIf="!isCameraEnabled"> {{ 'PANEL.SETTINGS.DISABLED_VIDEO' | translate }} </span>
|
||||
<span class="selected-text" *ngIf="isCameraEnabled"> {{ cameraSelected.label }} </span>
|
||||
</mat-select-trigger>
|
||||
<mat-option *ngFor="let camera of cameras" [disabled]="!isCameraEnabled" [value]="camera" id="option-{{ camera.label }}">
|
||||
{{ camera.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div id="video-devices-form" *ngIf="cameras.length === 0">
|
||||
<div id="mat-select-trigger">
|
||||
<button mat-icon-button id="camera-button" class="mute-btn" [disabled]="true">
|
||||
<mat-icon id="videocam_off"> videocam_off </mat-icon>
|
||||
<!-- Dropdown section -->
|
||||
@if (isCameraEnabled && cameras.length > 1) {
|
||||
<button mat-flat-button class="dropdown-section" [matMenuTriggerFor]="cameraMenu" [disabled]="cameraStatusChanging">
|
||||
<mat-icon>expand_more</mat-icon>
|
||||
</button>
|
||||
<span id="video-devices-not-found"> {{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }} </span>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Normal Mode - Input-style Selector -->
|
||||
<div class="normal-device-selector">
|
||||
<!-- Device Selector (Input Style) -->
|
||||
@if (isCameraEnabled) {
|
||||
<div class="device-input-selector">
|
||||
<button
|
||||
mat-flat-button
|
||||
class="selector-button"
|
||||
[matMenuTriggerFor]="cameraMenu"
|
||||
[disabled]="cameraStatusChanging || cameras.length <= 1"
|
||||
>
|
||||
<mat-icon class="device-icon">videocam</mat-icon>
|
||||
<span class="selected-device-name">{{ cameraSelected?.label || 'No camera selected' }}</span>
|
||||
<mat-icon class="dropdown-icon" *ngIf="cameras.length > 1">expand_more</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Disabled state message -->
|
||||
<div class="device-input-selector disabled">
|
||||
<div class="selector-button disabled">
|
||||
<mat-icon class="device-icon">videocam_off</mat-icon>
|
||||
<span class="selected-device-name">{{ 'PANEL.SETTINGS.DISABLED_VIDEO' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Device Selection Menu (Shared) -->
|
||||
<mat-menu #cameraMenu="matMenu" class="device-menu">
|
||||
@for (camera of cameras; track camera.device) {
|
||||
<button mat-menu-item (click)="onCameraSelected({ value: camera })" [class.selected]="camera.device === cameraSelected?.device">
|
||||
<mat-icon *ngIf="camera.device === cameraSelected?.device" class="check-icon">check</mat-icon>
|
||||
<span>{{ camera.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
|
||||
<!-- No Camera Available -->
|
||||
@if (cameras.length === 0) {
|
||||
<div class="no-device-message">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<span>{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -1,106 +1,51 @@
|
|||
@use '../device-selector-shared' as shared;
|
||||
|
||||
$ov-selection-color-btn: #afafaf;
|
||||
$ov-selection-color: #cccccc;
|
||||
|
||||
:host {
|
||||
.device-container-element {
|
||||
border-radius: var(--ov-surface-radius);
|
||||
border: 1px solid $ov-selection-color-btn;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.video-device-selector {
|
||||
@include shared.device-selector-base();
|
||||
|
||||
// Video-specific overrides for compact mode
|
||||
&.compact {
|
||||
.unified-device-button {
|
||||
.toggle-section {
|
||||
display: flex-end; // Video-specific styling
|
||||
}
|
||||
.device-container-element.mute-btn {
|
||||
border: 1px solid var(--ov-error-color);
|
||||
}
|
||||
#video-devices-form {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#video-devices-not-found {
|
||||
font-size: 13px;
|
||||
// Video-specific overrides for normal mode
|
||||
&:not(.compact) {
|
||||
.normal-device-selector {
|
||||
.device-input-selector {
|
||||
&:not(.disabled) {
|
||||
.selector-button {
|
||||
// Video-specific hover effect with box-shadow
|
||||
&:hover:not([disabled]) {
|
||||
background-color: white !important;
|
||||
border-color: var(--ov-primary-action-color);
|
||||
}
|
||||
|
||||
#camera-button {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper,
|
||||
::ng-deep .mat-mdc-form-field-flex,
|
||||
::ng-deep .mat-mdc-select-trigger {
|
||||
height: 50px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
background-color: $ov-selection-color !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
::ng-deep .mdc-button--unelevated {
|
||||
border-top-left-radius: var(--ov-surface-radius);
|
||||
border-bottom-left-radius: var(--ov-surface-radius);
|
||||
border-bottom-right-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
background-color: $ov-selection-color-btn !important;
|
||||
width: 48px !important;
|
||||
min-width: 48px !important;
|
||||
padding: 0;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-unelevated-button > .mat-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
font-size: 24px !important;
|
||||
margin: auto;
|
||||
}
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
padding: 0px !important;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.selected-text {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
|
||||
border: 0px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-button-touch-target {
|
||||
border-radius: var(--ov-surface-radius) !important;
|
||||
}
|
||||
|
||||
.mute-btn {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--ov-error-color) !important;
|
||||
}
|
||||
}
|
||||
::ng-deep .mat-mdc-select-panel {
|
||||
background-color: var(--ov-surface-color) !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-select-panel {
|
||||
background-color: #e2e2e2 !important;
|
||||
}
|
||||
// Include shared device menu styles
|
||||
@include shared.device-menu-styles();
|
||||
|
||||
::ng-deep .mat-mdc-option {
|
||||
padding: 10px 10px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
|
||||
color: var(--ov-primary-action-color-lighter) !important;
|
||||
}
|
||||
// Video-specific additional styles
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after {
|
||||
border-bottom-color: var(--ov-primary-action-color-lighter) !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-multiple) {
|
||||
background-color: $ov-selection-color !important;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CustomDevice } from '../../../models/device.model';
|
||||
import { DeviceService } from '../../../services/device/device.service';
|
||||
|
@ -16,6 +16,7 @@ import { ParticipantModel } from '../../../models/participant.model';
|
|||
standalone: false
|
||||
})
|
||||
export class VideoDevicesComponent implements OnInit, OnDestroy {
|
||||
@Input() compact: boolean = false;
|
||||
@Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>();
|
||||
@Output() onVideoEnabledChanged = new EventEmitter<boolean>();
|
||||
|
||||
|
|
Loading…
Reference in New Issue