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 class="prejoin-container" id="prejoin-container">
|
||||||
|
<!-- Loading State -->
|
||||||
<div *ngIf="isLoading" id="loading-container">
|
<div *ngIf="isLoading" class="loading-overlay">
|
||||||
<mat-spinner [diameter]="50"></mat-spinner>
|
<div class="loading-content">
|
||||||
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
|
<mat-spinner [diameter]="40"></mat-spinner>
|
||||||
|
<span class="loading-text">{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="!isLoading" id="prejoin-card">
|
<!-- Main Content -->
|
||||||
<ov-lang-selector *ngIf="!isMinimal" [compact]="true" class="lang-btn" (onLangChanged)="onLangChanged.emit($event)">
|
<div *ngIf="!isLoading" class="prejoin-main">
|
||||||
</ov-lang-selector>
|
<!-- 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>
|
<!-- Video Preview Section -->
|
||||||
<div class="video-container">
|
<div class="video-preview-section">
|
||||||
<div id="video-poster">
|
<div class="video-preview-container">
|
||||||
|
<div class="video-frame">
|
||||||
<ov-media-element
|
<ov-media-element
|
||||||
[track]="videoTrack"
|
[track]="videoTrack"
|
||||||
[showAvatar]="!videoTrack || videoTrack.isMuted"
|
[showAvatar]="!videoTrack || videoTrack.isMuted"
|
||||||
[avatarName]="participantName"
|
[avatarName]="participantName"
|
||||||
[avatarColor]="'hsl(48, 100%, 50%)'"
|
[avatarColor]="'hsl(48, 100%, 50%)'"
|
||||||
[isLocal]="true"
|
[isLocal]="true"
|
||||||
></ov-media-element>
|
class="video-element"
|
||||||
</div>
|
>
|
||||||
</div>
|
</ov-media-element>
|
||||||
|
|
||||||
<div class="media-controls-container">
|
<!-- Video Controls Overlay -->
|
||||||
<!-- Camera -->
|
<div class="video-overlay">
|
||||||
<div class="video-controls-container" *ngIf="showCameraButton">
|
<div class="device-controls">
|
||||||
|
<div class="control-group" *ngIf="showCameraButton">
|
||||||
<ov-video-devices-select
|
<ov-video-devices-select
|
||||||
|
[compact]="true"
|
||||||
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
||||||
(onVideoEnabledChanged)="videoEnabledChanged($event)"
|
(onVideoEnabledChanged)="videoEnabledChanged($event)"
|
||||||
></ov-video-devices-select>
|
class="device-selector"
|
||||||
|
>
|
||||||
|
</ov-video-devices-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Microphone -->
|
<div class="control-group" *ngIf="showMicrophoneButton">
|
||||||
<div class="audio-controls-container" *ngIf="showMicrophoneButton">
|
|
||||||
<ov-audio-devices-select
|
<ov-audio-devices-select
|
||||||
|
[compact]="true"
|
||||||
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
||||||
(onAudioEnabledChanged)="audioEnabledChanged($event)"
|
(onAudioEnabledChanged)="audioEnabledChanged($event)"
|
||||||
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
|
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
|
||||||
></ov-audio-devices-select>
|
class="device-selector"
|
||||||
|
>
|
||||||
|
</ov-audio-devices-select>
|
||||||
|
</div>
|
||||||
|
</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
|
<ov-participant-name-input
|
||||||
[isPrejoinPage]="true"
|
[isPrejoinPage]="true"
|
||||||
[error]="!!_error"
|
[error]="!!_error"
|
||||||
(onNameUpdated)="onParticipantNameChanged($event)"
|
(onNameUpdated)="onParticipantNameChanged($event)"
|
||||||
(onEnterPressed)="onEnterPressed()"
|
(onEnterPressed)="onEnterPressed()"
|
||||||
></ov-participant-name-input>
|
class="name-input"
|
||||||
|
>
|
||||||
|
</ov-participant-name-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="!!_error" id="token-error">
|
<!-- Error Message -->
|
||||||
<span class="error">{{ _error }}</span>
|
<div *ngIf="!!_error" class="error-message">
|
||||||
|
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||||
|
<span class="error-text">{{ _error }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="join-btn-container">
|
<!-- Join Button -->
|
||||||
<button mat-flat-button (click)="join()" id="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 }}
|
{{ 'PREJOIN.JOIN' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,159 +1,350 @@
|
||||||
:host {
|
:host {
|
||||||
.container {
|
display: block;
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--ov-background-color);
|
|
||||||
|
.prejoin-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--ov-background-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#loading-container {
|
// Loading State
|
||||||
|
.loading-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 40%;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
text-align: center;
|
bottom: 0;
|
||||||
color: var(--ov-text-primary-color);
|
display: flex;
|
||||||
.mat-mdc-progress-spinner {
|
align-items: center;
|
||||||
margin: auto;
|
justify-content: center;
|
||||||
}
|
background-color: var(--ov-background-color, #f5f5f5);
|
||||||
}
|
z-index: 1000;
|
||||||
|
|
||||||
#prejoin-card {
|
.loading-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 16px;
|
||||||
margin: auto;
|
|
||||||
|
.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);
|
border-radius: var(--ov-surface-radius);
|
||||||
width: 90%;
|
box-shadow:
|
||||||
max-width: 370px;
|
0 8px 32px rgba(0, 0, 0, 0.08),
|
||||||
// max-height: 650px;
|
0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
height: min-content;
|
overflow: hidden;
|
||||||
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%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
align-items: center;
|
}
|
||||||
|
.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 {
|
mat-icon {
|
||||||
height: 100%;
|
color: var(--ov-text-secondary-color, #666) !important;
|
||||||
width: 100%;
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video Preview Section
|
||||||
|
.video-preview-section {
|
||||||
|
padding: 24px 24px 20px;
|
||||||
|
|
||||||
|
.video-preview-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
border-radius: var(--ov-surface-radius);
|
border-radius: var(--ov-surface-radius);
|
||||||
overflow: hidden;
|
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;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
gap: 12px;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
width: 100%;
|
}
|
||||||
margin-top: 15px;
|
}
|
||||||
height: auto;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.participant-name-container {
|
// Configuration Section
|
||||||
display: block !important;
|
.configuration-section {
|
||||||
width: 100%;
|
padding: 0 24px 24px;
|
||||||
margin: 10px 0;
|
display: flex;
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 20px;
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-controls-container,
|
.input-section {
|
||||||
.audio-controls-container {
|
::ng-deep .name-input {
|
||||||
|
.mat-mdc-form-field {
|
||||||
width: 100%;
|
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) {
|
.mat-mdc-form-field-subscript-wrapper {
|
||||||
.media-controls-container {
|
display: none;
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-controls-container,
|
input {
|
||||||
.audio-controls-container {
|
font-size: 16px;
|
||||||
width: 48%;
|
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) {
|
.error-message {
|
||||||
.video-container {
|
display: flex;
|
||||||
height: 30vh;
|
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 {
|
.error-text {
|
||||||
height: auto;
|
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 { LocalTrack } from 'livekit-client';
|
||||||
import { CustomDevice } from '../../models/device.model';
|
import { CustomDevice } from '../../models/device.model';
|
||||||
import { LangOption } from '../../models/lang.model';
|
import { LangOption } from '../../models/lang.model';
|
||||||
|
import { VirtualBackgroundService } from '../../services/virtual-background/virtual-background.service';
|
||||||
|
import { BackgroundEffect } from '../../models/background-effect.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
@ -56,6 +58,11 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
||||||
showLogo: boolean = true;
|
showLogo: boolean = true;
|
||||||
showParticipantName: boolean = true;
|
showParticipantName: boolean = true;
|
||||||
|
|
||||||
|
// Future feature preparation
|
||||||
|
backgroundEffectEnabled: boolean = false;
|
||||||
|
availableBackgroundEffects: BackgroundEffect[] = [];
|
||||||
|
selectedBackgroundEffect: BackgroundEffect | undefined;
|
||||||
|
|
||||||
videoTrack: LocalTrack | undefined;
|
videoTrack: LocalTrack | undefined;
|
||||||
audioTrack: LocalTrack | undefined;
|
audioTrack: LocalTrack | undefined;
|
||||||
private tracks: LocalTrack[];
|
private tracks: LocalTrack[];
|
||||||
|
@ -74,9 +81,11 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
||||||
private cdkSrv: CdkOverlayService,
|
private cdkSrv: CdkOverlayService,
|
||||||
private openviduService: OpenViduService,
|
private openviduService: OpenViduService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
|
private virtualBackgroundService: VirtualBackgroundService,
|
||||||
private changeDetector: ChangeDetectorRef
|
private changeDetector: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
this.log = this.loggerSrv.get('PreJoinComponent');
|
this.log = this.loggerSrv.get('PreJoinComponent');
|
||||||
|
this.availableBackgroundEffects = this.virtualBackgroundService.getBackgrounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
@ -105,14 +114,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initializeDevices() {
|
private async initializeDevices() {
|
||||||
try {
|
await this.initializeDevicesWithRetry();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDeviceSelectorClicked() {
|
onDeviceSelectorClicked() {
|
||||||
|
@ -122,24 +124,27 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
join() {
|
join() {
|
||||||
if (this.showParticipantName && !this.participantName) {
|
if (this.showParticipantName && !this.participantName?.trim()) {
|
||||||
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
|
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear any previous errors
|
||||||
|
this._error = undefined;
|
||||||
|
|
||||||
// Mark tracks as permanent for avoiding to be removed in ngOnDestroy
|
// Mark tracks as permanent for avoiding to be removed in ngOnDestroy
|
||||||
this.shouldRemoveTracksWhenComponentIsDestroyed = false;
|
this.shouldRemoveTracksWhenComponentIsDestroyed = false;
|
||||||
|
|
||||||
// Assign participant name to the observable if it is defined
|
// Assign participant name to the observable if it is defined
|
||||||
if (this.participantName) {
|
if (this.participantName?.trim()) {
|
||||||
this.libService.updateGeneralConfig({ participantName: this.participantName });
|
this.libService.updateGeneralConfig({ participantName: this.participantName.trim() });
|
||||||
|
|
||||||
// Wait for the next tick to ensure the participant name propagates
|
// Wait for the next tick to ensure the participant name propagates
|
||||||
// through the observable before emitting onReadyToJoin
|
// through the observable before emitting onReadyToJoin
|
||||||
this.libService.participantName$
|
this.libService.participantName$
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
filter((name) => name === this.participantName),
|
filter((name) => name === this.participantName?.trim()),
|
||||||
tap(() => this.onReadyToJoin.emit())
|
tap(() => this.onReadyToJoin.emit())
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
@ -150,7 +155,11 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
onParticipantNameChanged(name: string) {
|
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() {
|
onEnterPressed() {
|
||||||
|
@ -210,4 +219,48 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
this.onAudioEnabledChanged.emit(enabled);
|
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">
|
<div class="audio-device-selector" [class.compact]="compact">
|
||||||
<!-- <button mat-stroked-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" id="audio-devices-menu">
|
<!-- Unified Device Button (Compact Mode) -->
|
||||||
<mat-icon class="audio-icon">mic</mat-icon>
|
@if (compact) {
|
||||||
<span class="device-label"> {{ microphoneSelected.label }} </span>
|
<div class="unified-device-button">
|
||||||
<mat-icon iconPositionEnd class="chevron-icon">
|
<!-- Main toggle button -->
|
||||||
{{ 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>
|
|
||||||
<button
|
<button
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
id="microphone-button"
|
class="toggle-section"
|
||||||
[disableRipple]="true"
|
|
||||||
[disabled]="!hasAudioDevices || microphoneStatusChanging"
|
[disabled]="!hasAudioDevices || microphoneStatusChanging"
|
||||||
[class.mute-btn]="!isMicrophoneEnabled"
|
[class.device-enabled]="isMicrophoneEnabled"
|
||||||
|
[class.device-disabled]="!isMicrophoneEnabled"
|
||||||
(click)="toggleMic($event)"
|
(click)="toggleMic($event)"
|
||||||
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
|
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
|
||||||
[matTooltipDisabled]="!hasAudioDevices"
|
[matTooltipDisabled]="!hasAudioDevices"
|
||||||
>
|
>
|
||||||
<mat-icon *ngIf="isMicrophoneEnabled" id="mic"> mic </mat-icon>
|
<mat-icon>{{ isMicrophoneEnabled ? 'mic' : 'mic_off' }}</mat-icon>
|
||||||
<mat-icon *ngIf="!isMicrophoneEnabled" id="mic_off"> mic_off </mat-icon>
|
|
||||||
</button>
|
</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">
|
<!-- Dropdown section -->
|
||||||
<div id="mat-select-trigger">
|
@if (microphones.length > 1 && isMicrophoneEnabled) {
|
||||||
<button mat-icon-button id="microphone-button" class="mute-btn" [disabled]="true">
|
<button mat-flat-button class="dropdown-section" [matMenuTriggerFor]="microphoneMenu" [disabled]="microphoneStatusChanging">
|
||||||
<mat-icon id="mic_off"> mic_off </mat-icon>
|
<mat-icon>expand_more</mat-icon>
|
||||||
</button>
|
</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>
|
||||||
|
}
|
||||||
|
</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>
|
</div>
|
||||||
|
|
|
@ -1,103 +1,29 @@
|
||||||
$ov-selection-color-btn: #afafaf;
|
@use '../device-selector-shared' as shared;
|
||||||
$ov-selection-color: #cccccc;
|
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
.device-container-element {
|
display: flex;
|
||||||
border-radius: var(--ov-surface-radius);
|
align-items: center;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
#audio-devices-not-found {
|
.audio-device-selector {
|
||||||
font-size: 13px;
|
@include shared.device-selector-base();
|
||||||
}
|
|
||||||
|
|
||||||
#microphone-button {
|
// Audio-specific overrides for normal mode
|
||||||
color:#000000
|
&: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 {
|
// Include shared device menu styles
|
||||||
padding: 10px 10px !important;
|
@include shared.device-menu-styles();
|
||||||
}
|
|
||||||
|
|
||||||
::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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 { Subscription } from 'rxjs';
|
||||||
import { CustomDevice } from '../../../models/device.model';
|
import { CustomDevice } from '../../../models/device.model';
|
||||||
import { DeviceService } from '../../../services/device/device.service';
|
import { DeviceService } from '../../../services/device/device.service';
|
||||||
|
@ -16,6 +16,7 @@ import { ParticipantModel } from '../../../models/participant.model';
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AudioDevicesComponent implements OnInit, OnDestroy {
|
export class AudioDevicesComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() compact: boolean = false;
|
||||||
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
|
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
|
||||||
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
|
@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>
|
<mat-icon>translate</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="!compact" mat-flat-button [matMenuTriggerFor]="menu" class="lang-button" id="lang-btn">
|
|
||||||
<span id="lang-selected-name">{{ langSelected?.name }}</span>
|
<!-- Full version (with text) -->
|
||||||
<mat-icon class="expand-more-icon">expand_more</mat-icon>
|
<button
|
||||||
</button>
|
*ngIf="!compact"
|
||||||
<mat-menu #menu="matMenu">
|
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
|
<button
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
*ngFor="let lang of languages"
|
*ngFor="let lang of languages"
|
||||||
(click)="onLangSelected(lang.lang)"
|
(click)="onLangSelected(lang.lang)"
|
||||||
[attr.id]="'lang-opt-' + 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>
|
</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 {
|
.language-selector-container {
|
||||||
background-color: var(--ov-primary-action-color) !important;
|
.compact-lang-button {
|
||||||
color: var(--ov-secondary-action-color) !important;
|
width: 40px;
|
||||||
}
|
height: 40px;
|
||||||
.lang-button .mat-icon {
|
background: rgba(255, 255, 255, 0.9);
|
||||||
color: var(--ov-secondary-action-color);
|
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);
|
||||||
|
|
||||||
}
|
&:hover {
|
||||||
::ng-deep .mat-mdc-menu-panel {
|
background: rgba(255, 255, 255, 1);
|
||||||
border-radius: var(--ov-surface-radius) !important;
|
border-color: var(--ov-primary-action-color, #4285f4);
|
||||||
background-color: $ov-surface-color-lighter !important;
|
transform: scale(1.02);
|
||||||
box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2) !important;
|
}
|
||||||
|
|
||||||
|
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,
|
::ng-deep .language-menu {
|
||||||
.mat-mdc-menu-item:visited,
|
.mat-mdc-menu-panel {
|
||||||
.mat-mdc-menu-item:link {
|
border-radius: 12px;
|
||||||
color: var(--ov-text-surface-color) !important;
|
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 }">
|
<div class="participant-name-input-container" [class.error]="error">
|
||||||
<mat-form-field id="name-form" [ngClass]="{ error: error }">
|
<div class="input-wrapper">
|
||||||
<mat-select-trigger>
|
<mat-icon class="input-icon">person</mat-icon>
|
||||||
<button mat-flat-button disabled>
|
|
||||||
<mat-icon>person</mat-icon>
|
|
||||||
</button>
|
|
||||||
</mat-select-trigger>
|
|
||||||
<input
|
<input
|
||||||
id="name-input"
|
id="name-input"
|
||||||
matInput
|
|
||||||
(change)="updateName()"
|
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
[(ngModel)]="name"
|
[(ngModel)]="name"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
[disabled]="!isPrejoinPage"
|
[disabled]="!isPrejoinPage"
|
||||||
|
(change)="updateName()"
|
||||||
(keypress)="eventKeyPress($event)"
|
(keypress)="eventKeyPress($event)"
|
||||||
|
[placeholder]="'PREJOIN.NICKNAME' | translate"
|
||||||
|
class="name-input-field"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,67 +1,71 @@
|
||||||
$ov-selection-color-btn: #afafaf;
|
|
||||||
$ov-selection-color: #cccccc;
|
|
||||||
:host {
|
:host {
|
||||||
#name-input-container {
|
display: block;
|
||||||
height: 70px;
|
|
||||||
border-radius: var(--ov-surface-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
#name-input-container mat-form-field {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--ov-secondary-action-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .mat-mdc-form-field-infix {
|
.participant-name-input-container {
|
||||||
display: inline-flex;
|
width: 100%;
|
||||||
padding: 0px !important;
|
|
||||||
}
|
.input-wrapper {
|
||||||
::ng-deep .mat-mdc-text-field-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;
|
padding: 0;
|
||||||
height: 70px;
|
transition: all 0.2s ease;
|
||||||
background-color: $ov-selection-color !important;
|
position: relative;
|
||||||
border-radius: var(--ov-surface-radius);
|
overflow: hidden;
|
||||||
}
|
|
||||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
|
&:focus-within {
|
||||||
border: 0px !important;
|
border-color: var(--ov-primary-action-color, #4285f4);
|
||||||
}
|
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
|
||||||
::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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.input-icon {
|
||||||
::ng-deep .mdc-button--unelevated {
|
display: flex;
|
||||||
background-color: var(--ov-error-color) !important;
|
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 {
|
&.error .input-wrapper {
|
||||||
height: 24px;
|
border-color: var(--ov-error-color, #d32f2f);
|
||||||
width: 24px;
|
box-shadow: 0 0 0 3px rgba(211, 47, 47, 0.1);
|
||||||
font-size: 24px !important;
|
|
||||||
margin: auto;
|
|
||||||
color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
.input-icon {
|
||||||
padding-left: 10px !important;
|
background: rgba(211, 47, 47, 0.1);
|
||||||
border-top-right-radius: var(--ov-surface-radius) !important;
|
color: var(--ov-error-color, #d32f2f);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::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">
|
<div class="video-device-selector" [class.compact]="compact">
|
||||||
<mat-form-field id="video-devices-form" *ngIf="cameras.length > 0">
|
<!-- Unified Device Button (Compact Mode) -->
|
||||||
<mat-select
|
@if (compact) {
|
||||||
[disabled]="!hasVideoDevices"
|
<div class="unified-device-button">
|
||||||
[compareWith]="compareObjectDevices"
|
<!-- Main toggle button -->
|
||||||
[value]="cameraSelected"
|
|
||||||
(selectionChange)="onCameraSelected($event)"
|
|
||||||
>
|
|
||||||
<mat-select-trigger id="mat-select-trigger">
|
|
||||||
<button
|
<button
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
id="camera-button"
|
class="toggle-section"
|
||||||
[disableRipple]="true"
|
|
||||||
[disabled]="!hasVideoDevices || cameraStatusChanging"
|
[disabled]="!hasVideoDevices || cameraStatusChanging"
|
||||||
[class.mute-btn]="!isCameraEnabled"
|
[class.device-enabled]="isCameraEnabled"
|
||||||
|
[class.device-disabled]="!isCameraEnabled"
|
||||||
(click)="toggleCam($event)"
|
(click)="toggleCam($event)"
|
||||||
[matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)"
|
[matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)"
|
||||||
[matTooltipDisabled]="!hasVideoDevices"
|
[matTooltipDisabled]="!hasVideoDevices"
|
||||||
>
|
>
|
||||||
<mat-icon *ngIf="isCameraEnabled" id="videocam"> videocam </mat-icon>
|
<mat-icon>{{ isCameraEnabled ? 'videocam' : 'videocam_off' }}</mat-icon>
|
||||||
<mat-icon *ngIf="!isCameraEnabled" id="videocam_off"> videocam_off </mat-icon>
|
|
||||||
</button>
|
</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">
|
<!-- Dropdown section -->
|
||||||
<div id="mat-select-trigger">
|
@if (isCameraEnabled && cameras.length > 1) {
|
||||||
<button mat-icon-button id="camera-button" class="mute-btn" [disabled]="true">
|
<button mat-flat-button class="dropdown-section" [matMenuTriggerFor]="cameraMenu" [disabled]="cameraStatusChanging">
|
||||||
<mat-icon id="videocam_off"> videocam_off </mat-icon>
|
<mat-icon>expand_more</mat-icon>
|
||||||
</button>
|
</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>
|
</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>
|
</div>
|
||||||
|
|
|
@ -1,106 +1,51 @@
|
||||||
|
@use '../device-selector-shared' as shared;
|
||||||
|
|
||||||
$ov-selection-color-btn: #afafaf;
|
$ov-selection-color-btn: #afafaf;
|
||||||
$ov-selection-color: #cccccc;
|
$ov-selection-color: #cccccc;
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
.device-container-element {
|
display: flex;
|
||||||
border-radius: var(--ov-surface-radius);
|
align-items: center;
|
||||||
border: 1px solid $ov-selection-color-btn;
|
|
||||||
|
.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 {
|
// Video-specific overrides for normal mode
|
||||||
font-size: 13px;
|
&: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 {
|
// Include shared device menu styles
|
||||||
background-color: #e2e2e2 !important;
|
@include shared.device-menu-styles();
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .mat-mdc-option {
|
// Video-specific additional styles
|
||||||
padding: 10px 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
|
|
||||||
color: var(--ov-primary-action-color-lighter) !important;
|
|
||||||
}
|
|
||||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after {
|
::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;
|
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) {
|
::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;
|
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 { Subscription } from 'rxjs';
|
||||||
import { CustomDevice } from '../../../models/device.model';
|
import { CustomDevice } from '../../../models/device.model';
|
||||||
import { DeviceService } from '../../../services/device/device.service';
|
import { DeviceService } from '../../../services/device/device.service';
|
||||||
|
@ -16,6 +16,7 @@ import { ParticipantModel } from '../../../models/participant.model';
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
export class VideoDevicesComponent implements OnInit, OnDestroy {
|
export class VideoDevicesComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() compact: boolean = false;
|
||||||
@Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>();
|
@Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>();
|
||||||
@Output() onVideoEnabledChanged = new EventEmitter<boolean>();
|
@Output() onVideoEnabledChanged = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue