ov-components: enhance settings panel for responsive design and improve layout handling

master
Carlos Santos 2025-09-19 18:31:10 +02:00
parent b35f959394
commit d48e44ea55
3 changed files with 429 additions and 53 deletions

View File

@ -1,4 +1,4 @@
<div class="panel-container" id="settings-container">
<div class="panel-container" id="settings-container" [class.vertical-layout]="isVerticalLayout" [class.compact-view]="isCompactView">
<div class="panel-header-container">
<h3 class="panel-title">{{ 'PANEL.SETTINGS.TITLE' | translate }}</h3>
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
@ -6,8 +6,8 @@
</button>
</div>
<div class="settings-container">
<div class="item-menu" [ngClass]="{ mobile: isMobile }">
<div class="settings-container" [class.vertical-layout]="isVerticalLayout">
<div class="item-menu" [class.compact]="isCompactView" [class.icons-only]="shouldHideMenuText">
<mat-selection-list
#optionList
(selectionChange)="onSelectionChanged(optionList.selectedOptions.selected[0]?.value)"
@ -20,9 +20,11 @@
id="general-opt"
[selected]="selectedOption === settingsOptions.GENERAL"
[value]="settingsOptions.GENERAL"
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.GENERAL' | translate) : '' }}"
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>manage_accounts</mat-icon>
<div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
<div *ngIf="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
</mat-list-option>
<mat-list-option
*ngIf="showCameraButton"
@ -30,9 +32,11 @@
id="video-opt"
[selected]="selectedOption === settingsOptions.VIDEO"
[value]="settingsOptions.VIDEO"
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.VIDEO' | translate) : '' }}"
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>videocam</mat-icon>
<div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
<div *ngIf="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
</mat-list-option>
<mat-list-option
*ngIf="showMicrophoneButton"
@ -40,9 +44,11 @@
id="audio-opt"
[selected]="selectedOption === settingsOptions.AUDIO"
[value]="settingsOptions.AUDIO"
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.AUDIO' | translate) : '' }}"
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>mic</mat-icon>
<div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
<div *ngIf="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
</mat-list-option>
<!-- <mat-list-option
*ngIf="showCaptions"
@ -50,36 +56,48 @@
[selected]="selectedOption === settingsOptions.CAPTIONS"
[value]="settingsOptions.CAPTIONS"
id="captions-opt"
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.CAPTIONS' | translate) : '' }}"
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>closed_caption</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.CAPTIONS' | translate }}</div>
<div mat-line *ngIf="!shouldHideMenuText">{{ 'PANEL.SETTINGS.CAPTIONS' | translate }}</div>
</mat-list-option> -->
</mat-selection-list>
</div>
<div class="item-content">
<div *ngIf="selectedOption === settingsOptions.GENERAL">
<mat-label class="input-label">{{ 'PREJOIN.NICKNAME' | translate }}</mat-label>
<ov-participant-name-input></ov-participant-name-input>
<mat-list>
<mat-list-item class="lang-selector">
<mat-icon matListItemIcon>translate</mat-icon>
<div matListItemTitle>{{ 'PANEL.SETTINGS.LANGUAGE' | translate }}</div>
<ov-lang-selector matListItemMeta (onLangChanged)="onLangChanged.emit($event)"></ov-lang-selector>
</mat-list-item>
</mat-list>
<div class="item-content" [class.full-width]="isVerticalLayout">
<div *ngIf="selectedOption === settingsOptions.GENERAL" class="general-settings">
<div class="nickname-section">
<mat-label class="input-label">{{ 'PREJOIN.NICKNAME' | translate }}</mat-label>
<div class="nickname-input-container">
<ov-participant-name-input></ov-participant-name-input>
</div>
</div>
<div class="language-section">
<mat-list>
<mat-list-item class="lang-selector">
<mat-icon matListItemIcon>translate</mat-icon>
<div matListItemTitle>{{ 'PANEL.SETTINGS.LANGUAGE' | translate }}</div>
<ov-lang-selector matListItemMeta (onLangChanged)="onLangChanged.emit($event)"></ov-lang-selector>
</mat-list-item>
</mat-list>
</div>
</div>
<ov-video-devices-select
*ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO"
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
></ov-video-devices-select>
<ov-audio-devices-select
*ngIf="showMicrophoneButton && selectedOption === settingsOptions.AUDIO"
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
></ov-audio-devices-select>
<!-- <ov-captions-settings *ngIf="selectedOption === settingsOptions.CAPTIONS && showCaptions"></ov-captions-settings> -->
<div *ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO" class="video-settings">
<ov-video-devices-select
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
></ov-video-devices-select>
</div>
<div *ngIf="showMicrophoneButton && selectedOption === settingsOptions.AUDIO" class="audio-settings">
<ov-audio-devices-select
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
></ov-audio-devices-select>
</div>
<!-- <div *ngIf="selectedOption === settingsOptions.CAPTIONS && showCaptions" class="captions-settings">
<ov-captions-settings></ov-captions-settings>
</div> -->
</div>
</div>
</div>

View File

@ -1,36 +1,189 @@
#settings-container {
display: flex !important;
flex-direction: column;
height: 100%;
min-height: 0;
// Base layout - horizontal (desktop/tablet)
.settings-container {
display: flex;
padding: 10px;
flex: 1;
width: auto;
justify-content: space-between;
align-items: stretch;
}
.item-menu {
padding-right: 5px;
border-right: 1px solid var(--ov-border-color);
width: 170px;
}
.item-menu.mobile {
width: 50px !important;
}
.item-content {
min-height: 0;
padding: 16px;
flex-grow: 1;
width: min-content;
gap: 16px;
align-items: stretch;
box-sizing: border-box;
}
.lang-container button {
// Vertical layout for mobile
&.vertical-layout .settings-container {
flex-direction: column;
gap: 16px;
padding: 12px;
}
// Menu styling - Desktop/Tablet default
.item-menu {
flex-shrink: 0;
border-right: 1px solid var(--ov-border-color);
width: 180px;
min-width: 180px;
padding-right: 16px;
// Compact view (tablet)
&.compact {
width: 140px;
min-width: 140px;
padding-right: 12px;
.option {
display: grid;
justify-content: center;
}
}
// Icons only (mobile/small tablets) - ahora con mejor padding
&.icons-only {
width: 80px;
min-width: 80px;
padding-right: 8px;
}
}
// Mobile vertical layout - no side border, full width menu
&.vertical-layout .item-menu {
width: 100%;
min-width: auto;
border-right: none;
border-bottom: 1px solid var(--ov-border-color);
padding-right: 0;
padding-bottom: 16px;
margin-bottom: 4px;
// Make menu horizontal on mobile with better spacing
mat-selection-list {
display: flex;
flex-direction: row;
justify-content: space-around;
gap: 8px;
padding: 0 8px;
}
mat-list-option {
flex: 1;
min-width: auto;
min-height: 60px;
border-radius: var(--ov-surface-radius);
// Estructura vertical: icono arriba, texto abajo
::ng-deep .mdc-list-item__content {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
gap: 4px;
}
// Resetear el margin del icono para layout vertical
::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start {
margin-right: 0 !important;
}
// Mejor presentación en mobile con layout vertical
.option-text {
font-size: 11px;
text-align: center;
line-height: 1.1;
font-weight: 500;
white-space: nowrap;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
}
}
// Content area styling
.item-content {
flex: 1;
min-width: 0;
padding: 8px 16px;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
&.full-width {
padding: 8px 12px;
}
}
// General settings specific styling
.general-settings {
display: flex;
flex-direction: column;
gap: 24px;
padding: 8px 0;
.nickname-section {
.input-label {
display: block;
margin-bottom: 12px;
font-weight: 500;
color: var(--ov-text-surface-color);
font-size: 14px;
}
.nickname-input-container {
width: 100%;
max-width: 100%;
box-sizing: border-box;
// Ensure the input takes full width of its container
::ng-deep ov-participant-name-input {
width: 100%;
.participant-name-input-container {
width: 100%;
box-sizing: border-box;
.participant-name-input {
width: 100% !important;
box-sizing: border-box;
}
}
}
}
}
.language-section {
box-sizing: border-box;
mat-list {
padding: 0;
}
}
}
// Video and Audio settings containers
.video-settings,
.audio-settings,
.captions-settings {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
// List option styling
mat-list-option[aria-selected='true'] {
background: var(--ov-accent-action-color) !important;
border-radius: var(--ov-surface-radius);
::ng-deep .mat-mdc-list-item-unscoped-content,
mat-icon {
color: var(--ov-secondary-action-color) !important;
@ -42,23 +195,212 @@
mat-icon {
color: var(--ov-text-surface-color) !important;
}
&:hover {
background-color: rgba(var(--ov-accent-action-color-rgb), 0.1) !important;
}
}
// Icon spacing
::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start {
margin-right: 15px !important;
}
// Remove focus state layer
.mat-mdc-list-base {
--mdc-list-list-item-focus-state-layer-color: transparent !important;
}
::ng-deep .lang-selector .expand-more-icon,
::ng-deep .lang-selector mat-icon {
color: var(--ov-text-surface-color) !important;
// Language selector styling
::ng-deep .lang-selector {
.expand-more-icon,
mat-icon {
color: var(--ov-text-surface-color) !important;
}
div {
color: var(--ov-text-surface-color) !important;
}
}
::ng-deep .lang-selector div,
.input-label {
color: var(--ov-text-surface-color) !important;
}
// Icons-only mode styling for compact menu items
&.compact-view .item-menu.icons-only {
mat-list-option {
min-height: 52px;
padding: 8px;
justify-content: center;
border-radius: var(--ov-surface-radius);
::ng-deep .mdc-list-item__content {
justify-content: center;
flex-direction: column;
align-items: center;
}
::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start {
margin-right: 0 !important;
margin-bottom: 4px;
}
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
}
}
// Mejor transición para cambios de estado
mat-list-option {
transition: all 0.2s ease-in-out;
&:hover:not([aria-selected='true']) {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
// Mejora en la tipografía y espaciado
.option-text {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
// Responsive breakpoints optimizados
@media (max-width: 1024px) {
#settings-container {
.settings-container {
padding: 14px;
gap: 14px;
}
}
}
@media (max-width: 768px) {
#settings-container {
.settings-container {
padding: 12px;
}
.item-content {
padding: 8px 14px;
}
.general-settings {
gap: 20px;
.nickname-input-container {
max-width: none;
}
}
// Ajustes para tablet en modo portrait
.item-menu {
width: 140px;
min-width: 140px;
padding-right: 10px;
}
}
}
@media (max-width: 640px) {
#settings-container {
.settings-container {
padding: 10px;
}
.item-content {
padding: 8px 10px;
}
.general-settings {
gap: 18px;
}
}
}
@media (max-width: 480px) {
#settings-container {
.settings-container {
padding: 8px;
}
.item-content {
padding: 6px 8px;
}
.general-settings {
gap: 16px;
padding: 4px 0;
}
// Mobile horizontal menu adjustments mejorados
&.vertical-layout .item-menu {
mat-selection-list {
gap: 6px;
padding: 0 6px;
}
mat-list-option {
min-height: 56px;
padding: 6px 4px;
.option-text {
font-size: 10px;
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
}
}
}
// High DPI / small screens optimization
@media (max-width: 360px) {
#settings-container {
.settings-container {
padding: 6px;
}
.item-content {
padding: 4px 6px;
}
.general-settings {
gap: 14px;
.nickname-section .input-label {
font-size: 13px;
margin-bottom: 10px;
}
}
&.vertical-layout .item-menu {
mat-list-option {
min-height: 52px;
padding: 4px 2px;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.option-text {
font-size: 11px;
}
}
}
}
}

View File

@ -4,6 +4,7 @@ import { PanelStatusInfo, PanelSettingsOptions, PanelType } from '../../../model
import { OpenViduComponentsConfigService } from '../../../services/config/directive-config.service';
import { PanelService } from '../../../services/panel/panel.service';
import { PlatformService } from '../../../services/platform/platform.service';
import { ViewportService } from '../../../services/viewport/viewport.service';
import { CustomDevice } from '../../../models/device.model';
import { LangOption } from '../../../models/lang.model';
@ -29,11 +30,26 @@ export class SettingsPanelComponent implements OnInit {
showCaptions: boolean = true;
isMobile: boolean = false;
private destroy$ = new Subject<void>();
constructor(
private panelService: PanelService,
private platformService: PlatformService,
private libService: OpenViduComponentsConfigService
private libService: OpenViduComponentsConfigService,
public viewportService: ViewportService
) {}
// Computed properties for responsive behavior
get isCompactView(): boolean {
return this.viewportService.isMobileView() || this.viewportService.isTabletDown();
}
get isVerticalLayout(): boolean {
return this.viewportService.isMobileView();
}
get shouldHideMenuText(): boolean {
return !this.viewportService.isMobileView() && this.viewportService.isTablet();
}
ngOnInit() {
this.isMobile = this.platformService.isMobile();
this.subscribeToPanelToggling();