diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.html index f97e969d..827b4723 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.html @@ -1,63 +1,88 @@ -
- -
- - {{ 'PREJOIN.PREPARING' | translate }} +
+ +
+
+ + {{ 'PREJOIN.PREPARING' | translate }} +
-
- - + +
+ +
+ +
-
-
-
+ +
+
+
+ class="video-element" + > + + + +
+
+
+ + +
+ +
+ + +
+
+
+
-
- -
- -
+ +
+ +
+ + +
- -
- -
+ +
+ error_outline + {{ _error }} +
-
- -
- -
- {{ _error }} -
- -
- -
+ +
+
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss index d4621ad6..f504195f 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss @@ -1,159 +1,350 @@ :host { - .container { - height: 100%; - background-color: var(--ov-background-color); + display: block; + width: 100%; + height: 100%; + + .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; - } - } - - #prejoin-card { + bottom: 0; display: flex; - flex-direction: column; align-items: center; justify-content: center; - margin: auto; - 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; - } + background-color: var(--ov-background-color, #f5f5f5); + z-index: 1000; - ::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; - justify-content: center; - align-items: center; - } - - #video-poster { - height: 100%; - width: 100%; - position: relative; - border-radius: var(--ov-surface-radius); - overflow: hidden; - } - - .media-controls-container { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - width: 100%; - margin-top: 15px; - height: auto; - } - - .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 { + .loading-content { + display: flex; flex-direction: column; align-items: center; - height: auto; - } + gap: 16px; - .video-controls-container, - .audio-controls-container { + .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); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.08), + 0 2px 8px rgba(0, 0, 0, 0.04); + overflow: hidden; + display: flex; + 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); + } + } + + 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; + } + } + } + + .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; + gap: 12px; + justify-content: center; + } + } } } - @media (max-width: 800px) and (orientation: landscape) { - .media-controls-container { - flex-direction: row; - justify-content: space-between; + // Configuration Section + .configuration-section { + padding: 0 24px 24px; + display: flex; + flex-direction: column; + gap: 20px; + + .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); + } + } + + .mat-mdc-form-field-subscript-wrapper { + display: none; + } + + 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; + } + } + } + } } - .video-controls-container, - .audio-controls-container { - width: 48%; + .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; + } + + .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; + } + } } } - @media (max-height: 630px) { - .video-container { - height: 30vh; + // Responsive Design + @media (max-width: 640px) { + .prejoin-container { + padding: 16px; + min-height: 100vh; } - .media-controls-container { - height: auto; + .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; } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts index 99eb6803..e48dd8bd 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts @@ -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 { + 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)); + } + } + } + } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.html index ea1b47c2..89a812f4 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.html @@ -1,55 +1,81 @@ -
- - - - - - {{ 'PANEL.SETTINGS.DISABLED_AUDIO' | translate }} - {{ microphoneSelected.label }} - - + + @if (compact) { +
+ + - {{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }} + + + @if (microphones.length > 1 && isMicrophoneEnabled) { + + }
-
+ } @else { + +
+ +
+ + @if (isMicrophoneEnabled) { +
+ +
+ } @else { + +
+
+ mic_off + + {{ !hasAudioDevices ? ('PREJOIN.NO_AUDIO_DEVICE' | translate) : 'Microphone disabled' }} + +
+
+ } +
+
+ } + + + @for (microphone of microphones; track microphone.device) { + + } + + + + @if (microphones.length === 0) { +
+ warning + {{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }} +
+ }
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.scss index cc20fb5f..cc04e7b1 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.scss @@ -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 - } - - ::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; + // 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-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(); diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.ts index 6c482dcd..203f1b27 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.ts @@ -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(); @Output() onAudioEnabledChanged = new EventEmitter(); diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/device-selector-shared.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/device-selector-shared.scss new file mode 100644 index 00000000..928bc499 --- /dev/null +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/device-selector-shared.scss @@ -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; + } + } + } +} diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html index 80fbd5d7..720d19f5 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html @@ -1,18 +1,39 @@ - - - +
+ - + + + + + + + + +
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss index a74aea1f..629ec95b 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss @@ -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; + } + } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.html index 48be7868..1130f2db 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.html @@ -1,20 +1,17 @@ -
- - - - +
+
+ person - +
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.scss index 143d5a18..2f1aba87 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.scss @@ -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); - } + display: block; + width: 100%; - #name-input-container mat-form-field { + .participant-name-input-container { 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 { - 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; - } + .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; + transition: all 0.2s ease; + position: relative; + overflow: hidden; - .error { - ::ng-deep .mdc-button--unelevated { - background-color: var(--ov-error-color) !important; + &:focus-within { + border-color: var(--ov-primary-action-color, #4285f4); + box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1); + } + + .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; + } + } + } + + &.error .input-wrapper { + border-color: var(--ov-error-color, #d32f2f); + box-shadow: 0 0 0 3px rgba(211, 47, 47, 0.1); + + .input-icon { + background: rgba(211, 47, 47, 0.1); + color: var(--ov-error-color, #d32f2f); + } } } - - ::ng-deep .mat-mdc-unelevated-button > .mat-icon { - height: 24px; - width: 24px; - font-size: 24px !important; - margin: auto; - color: #000000 !important; - } - - 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; - } -} - -::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow { - color: var(--ov-primary-action-color) !important; } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.html index fad87637..7bd746f6 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.html @@ -1,40 +1,72 @@ -
- - - - - {{ 'PANEL.SETTINGS.DISABLED_VIDEO' | translate }} - {{ cameraSelected.label }} - - - {{ camera.label }} - - - - -
-
- - {{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }} + + + @if (isCameraEnabled && cameras.length > 1) { + + }
-
+ } @else { + +
+ + @if (isCameraEnabled) { +
+ +
+ } @else { + +
+
+ videocam_off + {{ 'PANEL.SETTINGS.DISABLED_VIDEO' | translate }} +
+
+ } +
+ } + + + + @for (camera of cameras; track camera.device) { + + } + + + + @if (cameras.length === 0) { +
+ warning + {{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }} +
+ }
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.scss index ef3137d9..3a2c47da 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.scss @@ -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; - } - .device-container-element.mute-btn { - border: 1px solid var(--ov-error-color); - } - #video-devices-form { - width: 100%; - height: 50px; - } + display: flex; + align-items: center; - #video-devices-not-found { - font-size: 13px; - } + .video-device-selector { + @include shared.device-selector-base(); - #camera-button { - color: #000000; - } + // Video-specific overrides for compact mode + &.compact { + .unified-device-button { + .toggle-section { + display: flex-end; // Video-specific styling + } + } + } - ::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; + // 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); + } + } + } + } + } + } } } -::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; } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.ts index 3d0bae8f..d4c77118 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/video-devices/video-devices.component.ts @@ -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(); @Output() onVideoEnabledChanged = new EventEmitter();