Refactor template syntax to use @if directives for conditional rendering

- Updated panel.component.html to replace *ngIf with @if for chat, participants, background effects, settings, activities, and external panels.
- Modified participants-panel.component.html to use @if for local and remote participants rendering.
- Changed settings-panel.component.html to utilize @if for menu options based on visibility conditions.
- Refactored pre-join.component.html to implement @if for participant name input and error message display.
- Adjusted session.component.html to use @if for toolbar template rendering.
- Updated audio-devices.component.html and video-devices.component.html to replace *ngIf with @if for dropdown icons.
- Refactored stream.component.html to use @if for participant name and audio wave display.
- Modified toolbar-media-buttons.component.html and toolbar-panel-buttons.component.html to implement @if for button visibility.
- Updated toolbar.component.html to use @if for recording time display.
- Refactored videoconference.component.html to replace *ngIf with @if for pre-join and template rendering.
ov-components_modernization
Carlos Santos 2025-12-11 17:21:28 +01:00
parent 264db1facc
commit 53927b05a7
19 changed files with 606 additions and 506 deletions

View File

@ -1,37 +1,45 @@
<mat-toolbar class="header"> {{ title || ('ADMIN.DASHBOARD_TITLE' | translate) }} </mat-toolbar>
<div class="center-container">
<div *ngIf="loading" class="outer">
<div class="middle">
<div class="inner">
<mat-spinner [diameter]="50"></mat-spinner>
@if (loading) {
<div class="outer">
<div class="middle">
<div class="inner">
<mat-spinner [diameter]="50"></mat-spinner>
</div>
</div>
</div>
</div>
}
<mat-card *ngIf="!loading" class="card-container" appearance="outlined">
<mat-card-content>
<form [formGroup]="loginForm" (ngSubmit)="login()">
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'ADMIN.USERNAME' | translate }}</mat-label>
<input matInput formControlName="username" type="text" name="username" />
<mat-error *ngIf="loginForm.get('username')?.hasError('required')">
{{ 'ADMIN.USERNAME_REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'ADMIN.PASSWORD' | translate }}</mat-label>
<input matInput formControlName="password" type="password" name="password" autocomplete="current-password" />
<mat-error *ngIf="loginForm.get('password')?.hasError('required')">
{{ 'ADMIN.PASSWORD_REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<div class="col-12 d-flex text-center">
<button mat-flat-button disableRipple class="form-btn" type="submit">
{{ 'ADMIN.LOGIN' | translate }}
</button>
</div>
</form>
</mat-card-content>
</mat-card>
@if (!loading) {
<mat-card class="card-container" appearance="outlined">
<mat-card-content>
<form [formGroup]="loginForm" (ngSubmit)="login()">
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'ADMIN.USERNAME' | translate }}</mat-label>
<input matInput formControlName="username" type="text" name="username" />
@if (loginForm.get('username')?.hasError('required')) {
<mat-error>
{{ 'ADMIN.USERNAME_REQUIRED' | translate }}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'ADMIN.PASSWORD' | translate }}</mat-label>
<input matInput formControlName="password" type="password" name="password" autocomplete="current-password" />
@if (loginForm.get('password')?.hasError('required')) {
<mat-error>
{{ 'ADMIN.PASSWORD_REQUIRED' | translate }}
</mat-error>
}
</mat-form-field>
<div class="col-12 d-flex text-center">
<button mat-flat-button disableRipple class="form-btn" type="submit">
{{ 'ADMIN.LOGIN' | translate }}
</button>
</div>
</form>
</mat-card-content>
</mat-card>
}
</div>

View File

@ -1,45 +1,47 @@
<div class="container" [ngClass]="{ withCaptions: captionsEnabled, withMargin: localParticipant.isMinimized }">
<div id="layout" class="layout" #layout>
<div
#localLayoutElement
*ngFor="let track of localParticipant.tracks; trackBy: trackParticipantElement"
[ngClass]="{
local_participant: true,
OV_root: !track.isAudioTrack && !track.isMinimized,
OV_publisher: !track.isAudioTrack && !track.isMinimized,
OV_minimized: track.isMinimized,
OV_big: track.isPinned,
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks,
OV_screen: track.isScreenTrack
}"
[id]="'participant-' + track.participant.identity"
cdkDrag
cdkDragBoundary=".layout"
[cdkDragDisabled]="!track.isMinimized"
[cdkDragFreeDragPosition]="!track.isMinimized ? { x: 0, y: 0 } : null"
>
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
</div>
@for (track of localParticipant.tracks; track track) {
<div
#localLayoutElement
[ngClass]="{
local_participant: true,
OV_root: !track.isAudioTrack && !track.isMinimized,
OV_publisher: !track.isAudioTrack && !track.isMinimized,
OV_minimized: track.isMinimized,
OV_big: track.isPinned,
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks,
OV_screen: track.isScreenTrack
}"
[id]="'participant-' + track.participant.identity"
cdkDrag
cdkDragBoundary=".layout"
[cdkDragDisabled]="!track.isMinimized"
[cdkDragFreeDragPosition]="!track.isMinimized ? { x: 0, y: 0 } : null"
>
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
</div>
}
<!-- Render additional layout elements injected via ovAdditionalLayoutElement -->
@if (layoutAdditionalElementsTemplate) {
<ng-container *ngTemplateOutlet="layoutAdditionalElementsTemplate"></ng-container>
}
<div
*ngFor="let track of remoteParticipants | tracks; trackBy: trackParticipantElement"
class="remote-participant"
[id]="'participant-' + track.participant.identity"
[ngClass]="{
OV_root: !track.isAudioTrack,
OV_publisher: !track.isAudioTrack,
OV_big: track.isPinned,
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks,
OV_screen: track.isScreenTrack
}"
>
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
</div>
@for (track of remoteParticipants | tracks; track track) {
<div
class="remote-participant"
[id]="'participant-' + track.participant.identity"
[ngClass]="{
OV_root: !track.isAudioTrack,
OV_publisher: !track.isAudioTrack,
OV_big: track.isPinned,
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks,
OV_screen: track.isScreenTrack
}"
>
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
</div>
}
</div>
<!-- <ov-captions *ngIf="captionsEnabled" class="OV_ignored"></ov-captions> -->

View File

@ -8,25 +8,27 @@
<div class="activities-body-container">
<mat-accordion [multi]="false">
<ov-recording-activity
*ngIf="showRecordingActivity"
id="recording-activity"
[expanded]="expandedPanel === 'recording'"
(onRecordingStartRequested)="onRecordingStartRequested.emit($event)"
(onRecordingStopRequested)="onRecordingStopRequested.emit($event)"
(onRecordingDeleteRequested)="onRecordingDeleteRequested.emit($event)"
(onRecordingDownloadClicked)="onRecordingDownloadClicked.emit($event)"
(onRecordingPlayClicked)="onRecordingPlayClicked.emit($event)"
(onViewRecordingClicked)="onViewRecordingClicked.emit($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked.emit()"
></ov-recording-activity>
<ov-broadcasting-activity
*ngIf="showBroadcastingActivity"
id="broadcasting-activity"
[expanded]="expandedPanel === 'broadcasting'"
(onBroadcastingStartRequested)="onBroadcastingStartRequested.emit($event)"
(onBroadcastingStopRequested)="onBroadcastingStopRequested.emit($event)"
></ov-broadcasting-activity>
@if (showRecordingActivity) {
<ov-recording-activity
id="recording-activity"
[expanded]="expandedPanel === 'recording'"
(onRecordingStartRequested)="onRecordingStartRequested.emit($event)"
(onRecordingStopRequested)="onRecordingStopRequested.emit($event)"
(onRecordingDeleteRequested)="onRecordingDeleteRequested.emit($event)"
(onRecordingDownloadClicked)="onRecordingDownloadClicked.emit($event)"
(onRecordingPlayClicked)="onRecordingPlayClicked.emit($event)"
(onViewRecordingClicked)="onViewRecordingClicked.emit($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked.emit()"
></ov-recording-activity>
}
@if (showBroadcastingActivity) {
<ov-broadcasting-activity
id="broadcasting-activity"
[expanded]="expandedPanel === 'broadcasting'"
(onBroadcastingStartRequested)="onBroadcastingStartRequested.emit($event)"
(onBroadcastingStopRequested)="onBroadcastingStopRequested.emit($event)"
></ov-broadcasting-activity>
}
</mat-accordion>
</div>
</div>

View File

@ -73,28 +73,30 @@
/>
<button
mat-icon-button
*ngIf="broadcastingStatus !== broadcastingStatusEnum.STARTED"
id="broadcasting-btn"
[disabled]="
!broadcastUrl ||
broadcastingStatus === broadcastingStatusEnum.STARTING ||
broadcastingStatus === broadcastingStatusEnum.STOPPING
"
(click)="startBroadcasting()"
matTooltip="{{ 'PANEL.STREAMING.START' | translate }}"
@if (broadcastingStatus !== broadcastingStatusEnum.STARTED) {
id="broadcasting-btn"
[disabled]="
!broadcastUrl ||
broadcastingStatus === broadcastingStatusEnum.STARTING ||
broadcastingStatus === broadcastingStatusEnum.STOPPING
"
(click)="startBroadcasting()"
matTooltip="{{ 'PANEL.STREAMING.START' | translate }}"
}
>
<mat-icon>play_circle</mat-icon>
</button>
<button
mat-icon-button
*ngIf="broadcastingStatus === broadcastingStatusEnum.STARTED"
id="stop-broadcasting-btn"
(click)="stopBroadcasting()"
matTooltip="{{ 'PANEL.STREAMING.STOP' | translate }}"
>
<mat-icon>stop_circle</mat-icon>
</button>
@if (broadcastingStatus === broadcastingStatusEnum.STARTED) {
<button
mat-icon-button
id="stop-broadcasting-btn"
(click)="stopBroadcasting()"
matTooltip="{{ 'PANEL.STREAMING.STOP' | translate }}"
>
<mat-icon>stop_circle</mat-icon>
</button>
}
</div>
<div>

View File

@ -11,17 +11,23 @@
</div>
<div class="messages-container" #chatScroll>
<div *ngFor="let data of messageList" class="message" [ngClass]="data.isLocal ? 'right' : 'left'">
<div class="msg-detail">
<div class="participant-name-container">
<p *ngIf="data.isLocal">{{ 'PANEL.CHAT.YOU' | translate }}</p>
<p *ngIf="!data.isLocal">{{ data.participantName }}</p>
</div>
<div class="chat-message">
<p [innerHTML]="data.message | linkify"></p>
@for (data of messageList; track data) {
<div class="message" [ngClass]="data.isLocal ? 'right' : 'left'">
<div class="msg-detail">
<div class="participant-name-container">
@if (data.isLocal) {
<p>{{ 'PANEL.CHAT.YOU' | translate }}</p>
}
@if (!data.isLocal) {
<p>{{ data.participantName }}</p>
}
</div>
<div class="chat-message">
<p [innerHTML]="data.message | linkify"></p>
</div>
</div>
</div>
</div>
}
</div>
<div class="input-container">

View File

@ -1,30 +1,29 @@
<!-- CHAT panel -->
<ng-container *ngIf="isChatPanelOpened">
@if (isChatPanelOpened) {
<ng-container *ngTemplateOutlet="chatPanelTemplate"></ng-container>
</ng-container>
}
<!-- PARTICIPANTS panel -->
<ng-container *ngIf="isParticipantsPanelOpened">
@if (isParticipantsPanelOpened) {
<ng-container *ngTemplateOutlet="participantsPanelTemplate"></ng-container>
</ng-container>
}
<!-- Background effects panel -->
<ng-container *ngIf="isBackgroundEffectsPanelOpened">
@if (isBackgroundEffectsPanelOpened) {
<ng-container *ngTemplateOutlet="backgroundEffectsPanelTemplate"></ng-container>
</ng-container>
}
<!-- Settings panel -->
<ng-container *ngIf="isSettingsPanelOpened">
@if (isSettingsPanelOpened) {
<ng-container *ngTemplateOutlet="settingsPanelTemplate"></ng-container>
</ng-container>
}
<!-- Activities panel -->
<ng-container *ngIf="isActivitiesPanelOpened">
@if (isActivitiesPanelOpened) {
<ng-container *ngTemplateOutlet="activitiesPanelTemplate"></ng-container>
</ng-container>
}
<!-- External additional panels -->
<ng-container *ngIf="additionalPanelsTemplate && isExternalPanelOpened">
@if (additionalPanelsTemplate && isExternalPanelOpened) {
<ng-container *ngTemplateOutlet="additionalPanelsTemplate"></ng-container>
</ng-container>
}

View File

@ -7,17 +7,25 @@
</div>
<div class="scrollable">
<div class="local-participant-container" *ngIf="localParticipant">
<ng-container *ngTemplateOutlet="participantPanelItemTemplate; context: { $implicit: localParticipant }"></ng-container>
<mat-divider *ngIf="true"></mat-divider>
</div>
@if (localParticipant) {
<div class="local-participant-container">
<ng-container *ngTemplateOutlet="participantPanelItemTemplate; context: { $implicit: localParticipant }"></ng-container>
@if (true) {
<mat-divider></mat-divider>
}
</div>
}
<ng-container *ngTemplateOutlet="participantPanelAfterLocalParticipantTemplate"></ng-container>
<div class="remote-participants-container" id="remote-participants-container" *ngIf="remoteParticipants.length > 0">
<div *ngFor="let participant of this.remoteParticipants" id="remote-participant-item">
<ng-container *ngTemplateOutlet="participantPanelItemTemplate; context: { $implicit: participant }"></ng-container>
@if (remoteParticipants.length > 0) {
<div class="remote-participants-container" id="remote-participants-container">
@for (participant of this.remoteParticipants; track participant.identity) {
<div id="remote-participant-item">
<ng-container *ngTemplateOutlet="participantPanelItemTemplate; context: { $implicit: participant }"></ng-container>
</div>
}
</div>
</div>
}
</div>
</div>

View File

@ -24,32 +24,40 @@
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>manage_accounts</mat-icon>
<div *ngIf="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
</mat-list-option>
<mat-list-option
*ngIf="showCameraButton"
class="option"
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="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
</mat-list-option>
<mat-list-option
*ngIf="showMicrophoneButton"
class="option"
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="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
@if (!shouldHideMenuText) {
<div class="option-text">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
}
</mat-list-option>
@if (showCameraButton) {
<mat-list-option
class="option"
id="video-opt"
[selected]="selectedOption === settingsOptions.VIDEO"
[value]="settingsOptions.VIDEO"
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.VIDEO' | translate) : '' }}"
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>videocam</mat-icon>
@if (!shouldHideMenuText) {
<div class="option-text">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
}
</mat-list-option>
}
@if (showMicrophoneButton) {
<mat-list-option
class="option"
id="audio-opt"
[selected]="selectedOption === settingsOptions.AUDIO"
[value]="settingsOptions.AUDIO"
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.AUDIO' | translate) : '' }}"
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>mic</mat-icon>
@if (!shouldHideMenuText) {
<div class="option-text">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
}
</mat-list-option>
}
<!-- <mat-list-option
*ngIf="showCaptions"
class="option"
@ -66,52 +74,58 @@
</div>
<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>
@if (selectedOption === settingsOptions.GENERAL) {
<div 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>
<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>
@if (showThemeSelector) {
<div class="theme-section">
<div class="language-section">
<mat-list>
<mat-list-item class="theme-selector">
<mat-icon matListItemIcon class="material-symbols-outlined">routine</mat-icon>
<div matListItemTitle>{{ 'PANEL.SETTINGS.THEME' | translate }}</div>
<ov-theme-selector matListItemMeta></ov-theme-selector>
<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>
}
<!-- Additional elements injected via directive -->
@if (generalAdditionalElementsTemplate) {
<div class="additional-elements-section">
<ng-container *ngTemplateOutlet="generalAdditionalElementsTemplate"></ng-container>
</div>
}
</div>
<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>
@if (showThemeSelector) {
<div class="theme-section">
<mat-list>
<mat-list-item class="theme-selector">
<mat-icon matListItemIcon class="material-symbols-outlined">routine</mat-icon>
<div matListItemTitle>{{ 'PANEL.SETTINGS.THEME' | translate }}</div>
<ov-theme-selector matListItemMeta></ov-theme-selector>
</mat-list-item>
</mat-list>
</div>
}
<!-- Additional elements injected via directive -->
@if (generalAdditionalElementsTemplate) {
<div class="additional-elements-section">
<ng-container *ngTemplateOutlet="generalAdditionalElementsTemplate"></ng-container>
</div>
}
</div>
}
@if (showCameraButton && selectedOption === settingsOptions.VIDEO) {
<div class="video-settings">
<ov-video-devices-select
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
></ov-video-devices-select>
</div>
}
@if (showMicrophoneButton && selectedOption === settingsOptions.AUDIO) {
<div 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> -->

View File

@ -41,27 +41,31 @@
<!-- Video Controls Overlay -->
<div class="video-overlay">
<div class="device-controls">
<div class="control-group" *ngIf="showCameraButton">
<ov-video-devices-select
[compact]="true"
(onVideoDeviceChanged)="videoDeviceChanged($event)"
(onVideoEnabledChanged)="videoEnabledChanged($event)"
(onVideoDevicesLoaded)="onVideoDevicesLoaded($event)"
class="device-selector"
>
</ov-video-devices-select>
</div>
@if (showCameraButton) {
<div class="control-group">
<ov-video-devices-select
[compact]="true"
(onVideoDeviceChanged)="videoDeviceChanged($event)"
(onVideoEnabledChanged)="videoEnabledChanged($event)"
(onVideoDevicesLoaded)="onVideoDevicesLoaded($event)"
class="device-selector"
>
</ov-video-devices-select>
</div>
}
<div class="control-group" *ngIf="showMicrophoneButton">
<ov-audio-devices-select
[compact]="true"
(onAudioDeviceChanged)="audioDeviceChanged($event)"
(onAudioEnabledChanged)="audioEnabledChanged($event)"
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
class="device-selector"
>
</ov-audio-devices-select>
</div>
@if (showMicrophoneButton) {
<div class="control-group">
<ov-audio-devices-select
[compact]="true"
(onAudioDeviceChanged)="audioDeviceChanged($event)"
(onAudioEnabledChanged)="audioEnabledChanged($event)"
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
class="device-selector"
>
</ov-audio-devices-select>
</div>
}
</div>
<!-- Virtual Background Button -->
@ -93,22 +97,26 @@
<!-- Configuration Section -->
<div class="configuration-section">
<!-- Participant Name Input -->
<div class="participant-name-container input-section" *ngIf="showParticipantName">
<ov-participant-name-input
[isPrejoinPage]="true"
[error]="!!_error"
(onNameUpdated)="onParticipantNameChanged($event)"
(onEnterPressed)="onEnterPressed()"
class="name-input"
>
</ov-participant-name-input>
</div>
@if (showParticipantName) {
<div class="participant-name-container input-section">
<ov-participant-name-input
[isPrejoinPage]="true"
[error]="!!_error"
(onNameUpdated)="onParticipantNameChanged($event)"
(onEnterPressed)="onEnterPressed()"
class="name-input"
>
</ov-participant-name-input>
</div>
}
<!-- Error Message -->
<div *ngIf="!!_error" class="error-message" id="token-error">
<mat-icon class="error-icon">error_outline</mat-icon>
<span class="error-text">{{ _error }}</span>
</div>
@if (!!_error) {
<div class="error-message" id="token-error">
<mat-icon class="error-icon">error_outline</mat-icon>
<span class="error-text">{{ _error }}</span>
</div>
}
<!-- Join Button -->
<div class="join-section">

View File

@ -29,8 +29,10 @@
</mat-sidenav-content>
</mat-sidenav-container>
<div id="footer-container" *ngIf="toolbarTemplate">
<ng-container *ngTemplateOutlet="toolbarTemplate"></ng-container>
</div>
@if (toolbarTemplate) {
<div id="footer-container">
<ng-container *ngTemplateOutlet="toolbarTemplate"></ng-container>
</div>
}
</div>
}

View File

@ -56,7 +56,9 @@
>
<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>
@if (microphones.length > 1) {
<mat-icon class="dropdown-icon">expand_more</mat-icon>
}
</button>
</div>
} @else {
@ -91,7 +93,9 @@
(click)="onMicrophoneSelected({ value: microphone })"
[class.selected]="microphone.device === microphoneSelected.device"
>
<mat-icon *ngIf="microphone.device === microphoneSelected.device">check</mat-icon>
@if (microphone.device === microphoneSelected.device) {
<mat-icon>check</mat-icon>
}
<span>{{ microphone.label }}</span>
</button>
}

View File

@ -55,7 +55,9 @@
>
<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>
@if (cameras.length > 1) {
<mat-icon class="dropdown-icon">expand_more</mat-icon>
}
</button>
</div>
} @else {
@ -87,7 +89,9 @@
(click)="onCameraSelected({ value: camera })"
[class.selected]="camera.device === cameraSelected?.device"
>
<mat-icon *ngIf="camera.device === cameraSelected?.device" class="check-icon">check</mat-icon>
@if (camera.device === cameraSelected?.device) {
<mat-icon class="check-icon">check</mat-icon>
}
<span>{{ camera.label }}</span>
</button>
}

View File

@ -1,83 +1,112 @@
<div
*ngIf="_track.participant"
[ngClass]="{
OV_stream: !_track.isAudioTrack || (_track.isAudioTrack && _track.participant.onlyHasAudioTracks),
'no-size': !showVideo,
local: _track.participant.isLocal,
remote: !_track.participant.isLocal,
speaking: !isMinimal && showAudioDetection && _track.participant.isSpeaking && _track.isCameraTrack
}"
[id]="'stream-' + _track.source + '-' + _track.trackSid"
(mousemove)="mouseHover($event)"
#streamContainer
>
@if (_track.participant) {
<div
*ngIf="!isMinimal && showParticipantName && !_track.isAudioTrack || (_track.isAudioTrack && _track.participant.onlyHasAudioTracks)"
id="participant-name-container"
class="participant-name"
[class.fullscreen]="isFullscreen"
[ngClass]="{
OV_stream: !_track.isAudioTrack || (_track.isAudioTrack && _track.participant.onlyHasAudioTracks),
'no-size': !showVideo,
local: _track.participant.isLocal,
remote: !_track.participant.isLocal,
speaking: !isMinimal && showAudioDetection && _track.participant.isSpeaking && _track.isCameraTrack
}"
[id]="'stream-' + _track.source + '-' + _track.trackSid"
(mousemove)="mouseHover($event)"
#streamContainer
>
<div class="participant-name-container">
<span id="participant-name">{{ _track.participant.name }}</span>
<span *ngIf="_track.isScreenTrack" id="participant-name">_SCREEN</span>
</div>
</div>
<div *ngIf="!isMinimal && showAudioDetection && _track.participant.isSpeaking && _track.isCameraTrack" id="audio-wave-container">
<ov-audio-wave></ov-audio-wave>
</div>
<ov-media-element
[track]="_track.track"
[showAvatar]="_track.participant.onlyHasAudioTracks || (_track.isCameraTrack && !_track.participant.isCameraEnabled)"
[avatarColor]="_track.participant.colorProfile"
[avatarName]="_track.participant.name"
[muted]="_track.isMutedForcibly"
[isLocal]="_track.participant.isLocal"
[hasEncryptionError]="_track.participant.hasEncryptionError"
></ov-media-element>
@if (!_track.participant.hasEncryptionError) {
<div class="status-icons">
<mat-icon id="status-mic" fontIcon="mic_off" *ngIf="!_track.participant?.isMicrophoneEnabled"></mat-icon>
<mat-icon id="status-muted-forcibly" fontIcon="volume_off" *ngIf="_track.isMutedForcibly"></mat-icon>
<mat-icon id="status-pinned" fontIcon="push_pin" *ngIf="_track.isPinned"></mat-icon>
</div>
<div class="stream-video-controls" *ngIf="!isMinimal && showVideoControls && mouseHovering">
<div class="flex-container">
<button
mat-icon-button
id="pin-btn"
(click)="toggleVideoPinned()"
[matTooltip]="_track.isPinned ? ('STREAM.UNPIN' | translate) : ('STREAM.PIN' | translate)"
>
<mat-icon *ngIf="_track.isPinned" fontSet="material-symbols-outlined" fontIcon="keep">keep_off</mat-icon>
<mat-icon *ngIf="!_track.isPinned" id="status-pinned" fontIcon="push_pin"></mat-icon>
</button>
<button
*ngIf="!_track.participant.isLocal"
mat-icon-button
id="silence-btn"
(click)="toggleMuteForcibly()"
[class.muted-btn]="_track.isMutedForcibly"
[matTooltip]="_track.isMutedForcibly ? ('STREAM.UNMUTE_SOUND' | translate) : ('STREAM.MUTE_SOUND' | translate)"
>
<mat-icon *ngIf="_track.isMutedForcibly">volume_off</mat-icon>
<mat-icon *ngIf="!_track.isMutedForcibly">volume_up</mat-icon>
</button>
<button
*ngIf="_track.participant.isLocal"
mat-icon-button
id="minimize-btn"
[disabled]="_track.isPinned"
(click)="toggleMinimize()"
[matTooltip]="_track.isMinimized ? ('STREAM.MAXIMIZE' | translate) : ('STREAM.MINIMIZE' | translate)"
>
<mat-icon *ngIf="_track.isMinimized">open_in_full</mat-icon>
<mat-icon *ngIf="!_track.isMinimized">close_fullscreen</mat-icon>
</button>
@if ((!isMinimal && showParticipantName && !_track.isAudioTrack) || (_track.isAudioTrack && _track.participant.onlyHasAudioTracks)) {
<div
id="participant-name-container"
class="participant-name"
[class.fullscreen]="isFullscreen"
>
<div class="participant-name-container">
<span id="participant-name">{{ _track.participant.name }}</span>
@if (_track.isScreenTrack) {
<span id="participant-name">_SCREEN</span>
}
</div>
</div>
</div>
}
</div>
}
@if (!isMinimal && showAudioDetection && _track.participant.isSpeaking && _track.isCameraTrack) {
<div id="audio-wave-container">
<ov-audio-wave></ov-audio-wave>
</div>
}
<ov-media-element
[track]="_track.track"
[showAvatar]="_track.participant.onlyHasAudioTracks || (_track.isCameraTrack && !_track.participant.isCameraEnabled)"
[avatarColor]="_track.participant.colorProfile"
[avatarName]="_track.participant.name"
[muted]="_track.isMutedForcibly"
[isLocal]="_track.participant.isLocal"
[hasEncryptionError]="_track.participant.hasEncryptionError"
></ov-media-element>
@if (!_track.participant.hasEncryptionError) {
<div class="status-icons">
@if (!_track.participant?.isMicrophoneEnabled) {
<mat-icon id="status-mic" fontIcon="mic_off"></mat-icon>
}
@if (_track.isMutedForcibly) {
<mat-icon id="status-muted-forcibly" fontIcon="volume_off"></mat-icon>
}
@if (_track.isPinned) {
<mat-icon id="status-pinned" fontIcon="push_pin"></mat-icon>
}
</div>
}
@if (!isMinimal && showVideoControls && mouseHovering && !_track.participant.hasEncryptionError) {
<div class="stream-video-controls">
<div class="flex-container">
<button
mat-icon-button
id="pin-btn"
(click)="toggleVideoPinned()"
[matTooltip]="_track.isPinned ? ('STREAM.UNPIN' | translate) : ('STREAM.PIN' | translate)"
>
@if (_track.isPinned) {
<mat-icon fontSet="material-symbols-outlined" fontIcon="keep">keep_off</mat-icon>
}
@if (!_track.isPinned) {
<mat-icon id="status-pinned" fontIcon="push_pin"></mat-icon>
}
</button>
@if (!_track.participant.isLocal) {
<button
mat-icon-button
id="silence-btn"
(click)="toggleMuteForcibly()"
[class.muted-btn]="_track.isMutedForcibly"
[matTooltip]="_track.isMutedForcibly ? ('STREAM.UNMUTE_SOUND' | translate) : ('STREAM.MUTE_SOUND' | translate)"
>
@if (_track.isMutedForcibly) {
<mat-icon>volume_off</mat-icon>
}
@if (!_track.isMutedForcibly) {
<mat-icon>volume_up</mat-icon>
}
</button>
}
@if (_track.participant.isLocal) {
<button
mat-icon-button
id="minimize-btn"
[disabled]="_track.isPinned"
(click)="toggleMinimize()"
[matTooltip]="_track.isMinimized ? ('STREAM.MAXIMIZE' | translate) : ('STREAM.MINIMIZE' | translate)"
>
@if (_track.isMinimized) {
<mat-icon>open_in_full</mat-icon>
}
@if (!_track.isMinimized) {
<mat-icon>close_fullscreen</mat-icon>
}
</button>
}
</div>
</div>
}
</div>
}

View File

@ -11,6 +11,7 @@ import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.servic
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { LayoutService } from '../../services/layout/layout.service';
import { ParticipantService } from '../../services/participant/participant.service';
import { TranslatePipe } from '../../pipes/translate.pipe';
/**
* The **StreamComponent** is hosted inside of the {@link LayoutComponent}.
@ -21,7 +22,7 @@ import { ParticipantService } from '../../services/participant/participant.servi
templateUrl: './stream.component.html',
styleUrls: ['./stream.component.scss'],
standalone: true,
imports: [CommonModule, AppMaterialModule, AudioWaveComponent, MediaElementComponent]
imports: [CommonModule, AppMaterialModule, AudioWaveComponent, MediaElementComponent, TranslatePipe]
})
export class StreamComponent implements OnInit, OnDestroy {
/**
@ -193,21 +194,18 @@ export class StreamComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.showParticipantName = value;
// this.cd.markForCheck();
});
this.libService.displayAudioDetection$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.showAudioDetection = value;
// this.cd.markForCheck();
});
this.libService.streamVideoControls$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.showVideoControls = value;
// this.cd.markForCheck();
});
}
}

View File

@ -159,17 +159,22 @@
}
<!-- Captions button -->
<!-- <button
*ngIf="!isMinimal && showCaptionsButton"
[disabled]="isConnectionLost"
mat-menu-item
id="captions-btn"
(click)="onCaptionsToggle()"
>
<mat-icon>closed_caption</mat-icon>
<span *ngIf="captionsEnabled">{{ 'TOOLBAR.DISABLE_CAPTIONS' | translate }}</span>
<span *ngIf="!captionsEnabled">{{ 'TOOLBAR.ENABLE_CAPTIONS' | translate }}</span>
</button> -->
@if (!isMinimal && showCaptionsButton) {
<button
[disabled]="isConnectionLost"
mat-menu-item
id="captions-btn"
(click)="onCaptionsToggle()"
>
<mat-icon>closed_caption</mat-icon>
@if (captionsEnabled) {
<span>{{ 'TOOLBAR.DISABLE_CAPTIONS' | translate }}</span>
}
@if (!captionsEnabled) {
<span>{{ 'TOOLBAR.ENABLE_CAPTIONS' | translate }}</span>
}
</button>
}
<!-- Additional buttons injection inside menu (mobile only) -->
@if (showAdditionalButtonsInsideMenu() && additionalButtonsPosition === 'afterMenu') {

View File

@ -1,128 +1,136 @@
<!-- Responsive container: show individual buttons on larger screens, collapsed menu on smaller screens -->
<ng-container *ngIf="shouldShowCollapsed; else uncollapsedButtons">
<div class="collapsed-menu-container" *ngIf="visibleButtonsCount > 0">
@if (shouldShowCollapsed) {
@if (visibleButtonsCount > 0) {
<div class="collapsed-menu-container">
<button
mat-icon-button
class="fab-menu-trigger"
[matMenuTriggerFor]="panelsMenu"
[disabled]="isConnectionLost"
[class.active-btn]="isAnyPanelOpened"
matTooltip="{{ 'TOOLBAR.PANELS' | translate }}"
#menuTrigger="matMenuTrigger"
>
<mat-icon class="material-symbols-outlined" [class.rotated]="menuTrigger.menuOpen">keyboard_arrow_up</mat-icon>
</button>
<!-- Vertical panels menu -->
<mat-menu #panelsMenu="matMenu" class="panels-menu">
<!-- Activities menu item -->
@if (!isMinimal && showActivitiesPanelButton) {
<button
mat-menu-item
class="panel-menu-item"
(click)="onToggleActivities()"
[disabled]="isConnectionLost"
[class.active-menu-item]="isActivitiesOpened"
>
<mat-icon>category</mat-icon>
</button>
}
<!-- Participants menu item -->
@if (!isMinimal && showParticipantsPanelButton) {
<button
mat-menu-item
class="panel-menu-item"
(click)="onToggleParticipants()"
[disabled]="isConnectionLost"
[class.active-menu-item]="isParticipantsOpened"
>
<mat-icon>people</mat-icon>
</button>
}
<!-- Chat menu item -->
@if (!isMinimal && showChatPanelButton) {
<button
mat-menu-item
class="panel-menu-item"
(click)="onToggleChat()"
[disabled]="isConnectionLost"
[class.active-menu-item]="isChatOpened"
>
<mat-icon
matBadge="{{ unreadMessages }}"
[matBadgeHidden]="unreadMessages === 0"
matBadgePosition="above before"
matBadgeSize="small"
matBadgeColor="accent"
aria-hidden="false"
>
chat
</mat-icon>
</button>
}
<!-- External additional panel buttons in menu -->
@if (toolbarAdditionalPanelButtonsTemplate) {
<ng-container *ngTemplateOutlet="toolbarAdditionalPanelButtonsTemplate"></ng-container>
}
</mat-menu>
</div>
}
} @else {
<!-- Default activities button -->
@if (!isMinimal && showActivitiesPanelButton) {
<button
mat-icon-button
class="fab-menu-trigger"
[matMenuTriggerFor]="panelsMenu"
id="activities-panel-btn"
class="panel-button"
matTooltip="{{ 'TOOLBAR.ACTIVITIES' | translate }}"
(click)="onToggleActivities()"
[disabled]="isConnectionLost"
[class.active-btn]="isAnyPanelOpened"
matTooltip="{{ 'TOOLBAR.PANELS' | translate }}"
#menuTrigger="matMenuTrigger"
[class.active-btn]="isActivitiesOpened"
>
<mat-icon class="material-symbols-outlined" [class.rotated]="menuTrigger.menuOpen">keyboard_arrow_up</mat-icon>
<mat-icon>category</mat-icon>
</button>
}
<!-- Vertical panels menu -->
<mat-menu #panelsMenu="matMenu" class="panels-menu">
<!-- Activities menu item -->
<button
mat-menu-item
class="panel-menu-item"
*ngIf="!isMinimal && showActivitiesPanelButton"
(click)="onToggleActivities()"
[disabled]="isConnectionLost"
[class.active-menu-item]="isActivitiesOpened"
<!-- Default participants button -->
@if (!isMinimal && showParticipantsPanelButton) {
<button
mat-icon-button
class="panel-button"
id="participants-panel-btn"
matTooltip="{{ 'TOOLBAR.PARTICIPANTS' | translate }}"
(click)="onToggleParticipants()"
[disabled]="isConnectionLost"
[class.active-btn]="isParticipantsOpened"
>
<mat-icon>people</mat-icon>
</button>
}
<!-- Default chat button -->
@if (!isMinimal && showChatPanelButton) {
<button
mat-icon-button
class="panel-button"
id="chat-panel-btn"
matTooltip="{{ 'TOOLBAR.CHAT' | translate }}"
(click)="onToggleChat()"
[disabled]="isConnectionLost"
[class.active-btn]="isChatOpened"
>
<mat-icon
matBadge="{{ unreadMessages }}"
[matBadgeHidden]="unreadMessages === 0"
matBadgePosition="above before"
matBadgeSize="small"
matBadgeColor="accent"
aria-hidden="false"
>
<mat-icon>category</mat-icon>
</button>
chat
</mat-icon>
</button>
}
<!-- Participants menu item -->
<button
mat-menu-item
class="panel-menu-item"
*ngIf="!isMinimal && showParticipantsPanelButton"
(click)="onToggleParticipants()"
[disabled]="isConnectionLost"
[class.active-menu-item]="isParticipantsOpened"
>
<mat-icon>people</mat-icon>
</button>
<!-- Chat menu item -->
<button
mat-menu-item
class="panel-menu-item"
*ngIf="!isMinimal && showChatPanelButton"
(click)="onToggleChat()"
[disabled]="isConnectionLost"
[class.active-menu-item]="isChatOpened"
>
<mat-icon
matBadge="{{ unreadMessages }}"
[matBadgeHidden]="unreadMessages === 0"
matBadgePosition="above before"
matBadgeSize="small"
matBadgeColor="accent"
aria-hidden="false"
>
chat
</mat-icon>
</button>
<!-- External additional panel buttons in menu -->
<ng-container *ngIf="toolbarAdditionalPanelButtonsTemplate">
<ng-container *ngTemplateOutlet="toolbarAdditionalPanelButtonsTemplate"></ng-container>
</ng-container>
</mat-menu>
</div>
</ng-container>
<!-- External additional panel buttons -->
@if (toolbarAdditionalPanelButtonsTemplate) {
<ng-container *ngTemplateOutlet="toolbarAdditionalPanelButtonsTemplate"></ng-container>
}
}
<!-- Collapsed menu template for small screens -->
<ng-template #uncollapsedButtons>
<!-- Default activities button -->
<button
mat-icon-button
id="activities-panel-btn"
class="panel-button"
*ngIf="!isMinimal && showActivitiesPanelButton"
matTooltip="{{ 'TOOLBAR.ACTIVITIES' | translate }}"
(click)="onToggleActivities()"
[disabled]="isConnectionLost"
[class.active-btn]="isActivitiesOpened"
>
<mat-icon>category</mat-icon>
</button>
<!-- Default participants button -->
<button
mat-icon-button
class="panel-button"
id="participants-panel-btn"
*ngIf="!isMinimal && showParticipantsPanelButton"
matTooltip="{{ 'TOOLBAR.PARTICIPANTS' | translate }}"
(click)="onToggleParticipants()"
[disabled]="isConnectionLost"
[class.active-btn]="isParticipantsOpened"
>
<mat-icon>people</mat-icon>
</button>
<!-- Default chat button -->
<button
mat-icon-button
class="panel-button"
id="chat-panel-btn"
*ngIf="!isMinimal && showChatPanelButton"
matTooltip="{{ 'TOOLBAR.CHAT' | translate }}"
(click)="onToggleChat()"
[disabled]="isConnectionLost"
[class.active-btn]="isChatOpened"
>
<mat-icon
matBadge="{{ unreadMessages }}"
[matBadgeHidden]="unreadMessages === 0"
matBadgePosition="above before"
matBadgeSize="small"
matBadgeColor="accent"
aria-hidden="false"
>
chat
</mat-icon>
</button>
<!-- External additional panel buttons -->
<ng-container *ngIf="toolbarAdditionalPanelButtonsTemplate">
<ng-container *ngTemplateOutlet="toolbarAdditionalPanelButtonsTemplate"></ng-container>
</ng-container>
</ng-template>

View File

@ -21,7 +21,9 @@
<div id="recording-tag" class="recording-tag" (click)="openRecordingActivityPanel()">
<mat-icon class="blink">radio_button_checked</mat-icon>
<span class="blink">REC</span>
<span *ngIf="recordingTime"> | {{ recordingTime | date: 'H:mm:ss' }}</span>
@if (recordingTime) {
<span> | {{ recordingTime | date: 'H:mm:ss' }}</span>
}
</div>
}

View File

@ -8,10 +8,9 @@
} @else if (componentState.showPrejoin) {
<!-- Prejoin -->
<div [@inOutAnimation] id="pre-join-container">
<ng-container *ngIf="openviduAngularPreJoinTemplate; else defaultPreJoin">
@if (openviduAngularPreJoinTemplate) {
<ng-container *ngTemplateOutlet="openviduAngularPreJoinTemplate"></ng-container>
</ng-container>
<ng-template #defaultPreJoin>
} @else {
<ov-pre-join
[error]="componentState.error?.tokenError"
(onReadyToJoin)="_onReadyToJoin()"
@ -21,7 +20,7 @@
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
(onLangChanged)="onLangChanged.emit($event)"
></ov-pre-join>
</ng-template>
}
</div>
} @else if (componentState.error?.hasError) {
<!-- Error -->
@ -44,21 +43,21 @@
(onParticipantLeft)="_onParticipantLeft($event)"
>
<ng-template #toolbar>
<ng-container *ngIf="openviduAngularToolbarTemplate">
@if (openviduAngularToolbarTemplate) {
<ng-container *ngTemplateOutlet="openviduAngularToolbarTemplate"></ng-container>
</ng-container>
}
</ng-template>
<ng-template #panel>
<ng-container *ngIf="openviduAngularPanelTemplate">
@if (openviduAngularPanelTemplate) {
<ng-container *ngTemplateOutlet="openviduAngularPanelTemplate"></ng-container>
</ng-container>
}
</ng-template>
<ng-template #layout>
<ng-container *ngIf="openviduAngularLayoutTemplate">
@if (openviduAngularLayoutTemplate) {
<ng-container *ngTemplateOutlet="openviduAngularLayoutTemplate"></ng-container>
</ng-container>
}
</ng-template>
</ov-session>
</div>