ov-components: implement custom leave button directive and enhance toolbar for additional button templates

master
Carlos Santos 2025-09-18 19:40:25 +02:00
parent 2a9f3a62fa
commit 41152de276
11 changed files with 309 additions and 260 deletions

View File

@ -63,9 +63,10 @@
</mat-menu>
}
<ng-container *ngIf="toolbarAdditionalButtonsTemplate && additionalButtonsPosition && additionalButtonsPosition === 'beforeMenu'">
<ng-container *ngTemplateOutlet="toolbarAdditionalButtonsTemplate"></ng-container>
</ng-container>
<!-- Additional buttons injection from parent component (desktop/tablet only) -->
@if (showAdditionalButtonsOutside() && additionalButtonsPosition === 'beforeMenu') {
<ng-container *ngTemplateOutlet="toolbarAdditionalButtonsTemplate; context: { $implicit: additionalButtonsPosition }"></ng-container>
}
<!-- More options button -->
@if (showMoreOptionsButtonDirect()) {
@ -75,35 +76,17 @@
[matMenuTriggerFor]="settingsMenu"
[disabled]="isConnectionLost"
[class.mobile-btn]="isMobileView()"
[matBadge]="hasActiveFeatures() && isMobileView() ? '!' : ''"
matBadgeColor="accent"
matBadgeSize="small"
[matBadgeHidden]="!hasActiveFeatures() || !isMobileView()"
matTooltip="{{ 'TOOLBAR.MORE_OPTIONS' | translate }}"
>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #settingsMenu="matMenu" id="more-options-menu">
<!-- Dynamic mobile buttons -->
@for (button of buttonsInMoreOptions(); track button.key) {
@if (button.show) {
<button
mat-menu-item
[id]="button.key + '-btn'"
[disabled]="button.disabled"
(click)="button.action()"
>
<mat-icon [color]="button.color || ''">{{ button.icon }}</mat-icon>
<span>{{ button.label | translate }}</span>
</button>
}
<!-- Additional buttons injection inside menu (mobile only) -->
@if (showAdditionalButtonsInsideMenu() && additionalButtonsPosition === 'beforeMenu') {
<ng-container *ngTemplateOutlet="toolbarAdditionalButtonsTemplate; context: { $implicit: additionalButtonsPosition }">
</ng-container>
}
<!-- Divider if there are mobile buttons -->
@if (buttonsInMoreOptions().length > 0) {
<mat-divider class="divider"></mat-divider>
}
<!-- Fullscreen button -->
@if (showFullscreenButton) {
<button mat-menu-item id="fullscreen-btn" (click)="onFullscreenToggle()">
@ -177,16 +160,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> -->
*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> -->
<!-- Additional buttons injection inside menu (mobile only) -->
@if (showAdditionalButtonsInsideMenu() && additionalButtonsPosition === 'afterMenu') {
<ng-container *ngTemplateOutlet="toolbarAdditionalButtonsTemplate; context: { $implicit: additionalButtonsPosition }">
</ng-container>
}
<!-- Divider before settings -->
@if (showSettingsButton) {
@ -203,20 +192,24 @@
</mat-menu>
}
<!-- External additional buttons -->
<ng-container *ngIf="toolbarAdditionalButtonsTemplate && additionalButtonsPosition && additionalButtonsPosition === 'afterMenu'">
<!-- Additional buttons injection from parent component (desktop/tablet only) -->
@if (showAdditionalButtonsOutside() && additionalButtonsPosition === 'afterMenu') {
<ng-container *ngTemplateOutlet="toolbarAdditionalButtonsTemplate"></ng-container>
</ng-container>
}
<!-- Leave session button -->
@if (showLeaveButtonDirect()) {
<button
mat-icon-button
(click)="onLeaveClick()"
id="leave-btn"
[class.mobile-btn]="isMobileView()"
matTooltip="{{ 'TOOLBAR.LEAVE' | translate }}"
>
<mat-icon>call_end</mat-icon>
</button>
@if (toolbarLeaveButtonTemplate) {
<ng-container *ngTemplateOutlet="toolbarLeaveButtonTemplate"></ng-container>
} @else {
<button
mat-icon-button
(click)="onLeaveClick()"
id="leave-btn"
[class.mobile-btn]="isMobileView()"
matTooltip="{{ 'TOOLBAR.LEAVE' | translate }}"
>
<mat-icon>call_end</mat-icon>
</button>
}
}

View File

@ -39,10 +39,9 @@
// Mobile responsive styles
&.mobile-btn {
margin: 3px;
padding: 8px 0;
width: 36px;
height: 36px;
text-align: justify;
mat-icon {
font-size: 20px;
@ -59,7 +58,8 @@
background-color: var(--ov-accent-action-color) !important;
}
#leave-btn {
#leave-btn,
::ng-deep #leave-btn {
background-color: var(--ov-error-color) !important;
border-radius: var(--ov-leave-button-radius) !important;
width: 65px !important;
@ -68,7 +68,6 @@
&.mobile-btn {
width: 56px !important;
height: 36px !important;
margin: 3px;
text-align: center;
}
}

View File

@ -4,231 +4,183 @@ import { BroadcastingStatus } from '../../../models/broadcasting.model';
import { ToolbarAdditionalButtonsPosition } from '../../../models/toolbar.model';
import { ViewportService } from '../../../services/viewport/viewport.service';
/**
* @internal
*/
@Component({
selector: 'ov-toolbar-media-buttons',
templateUrl: './toolbar-media-buttons.component.html',
styleUrl: './toolbar-media-buttons.component.scss',
standalone: false
selector: 'ov-toolbar-media-buttons',
templateUrl: './toolbar-media-buttons.component.html',
styleUrl: './toolbar-media-buttons.component.scss',
standalone: false
})
export class ToolbarMediaButtonsComponent {
// Camera related inputs
@Input() showCameraButton: boolean = true;
@Input() isCameraEnabled: boolean = true;
@Input() cameraMuteChanging: boolean = false;
// Camera related inputs
@Input() showCameraButton: boolean = true;
@Input() isCameraEnabled: boolean = true;
@Input() cameraMuteChanging: boolean = false;
// Microphone related inputs
@Input() showMicrophoneButton: boolean = true;
@Input() isMicrophoneEnabled: boolean = true;
@Input() microphoneMuteChanging: boolean = false;
// Microphone related inputs
@Input() showMicrophoneButton: boolean = true;
@Input() isMicrophoneEnabled: boolean = true;
@Input() microphoneMuteChanging: boolean = false;
// Screenshare related inputs
@Input() showScreenshareButton: boolean = true;
@Input() isScreenShareEnabled: boolean = false;
// Screenshare related inputs
@Input() showScreenshareButton: boolean = true;
@Input() isScreenShareEnabled: boolean = false;
// Device availability inputs
@Input() hasVideoDevices: boolean = true;
@Input() hasAudioDevices: boolean = true;
// Device availability inputs
@Input() hasVideoDevices: boolean = true;
@Input() hasAudioDevices: boolean = true;
// Connection state inputs
@Input() isConnectionLost: boolean = false;
// Connection state inputs
@Input() isConnectionLost: boolean = false;
// UI state inputs
@Input() isMinimal: boolean = false;
// UI state inputs
@Input() isMinimal: boolean = false;
// More options menu inputs
@Input() showMoreOptionsButton: boolean = true;
@Input() showFullscreenButton: boolean = true;
@Input() showRecordingButton: boolean = true;
@Input() showViewRecordingsButton: boolean = false;
@Input() showBroadcastingButton: boolean = true;
@Input() showBackgroundEffectsButton: boolean = true;
@Input() showCaptionsButton: boolean = true;
@Input() showSettingsButton: boolean = true;
// More options menu inputs
@Input() showMoreOptionsButton: boolean = true;
@Input() showFullscreenButton: boolean = true;
@Input() showRecordingButton: boolean = true;
@Input() showViewRecordingsButton: boolean = false;
@Input() showBroadcastingButton: boolean = true;
@Input() showBackgroundEffectsButton: boolean = true;
@Input() showCaptionsButton: boolean = true;
@Input() showSettingsButton: boolean = true;
// Fullscreen state
@Input() isFullscreenActive: boolean = false;
// Fullscreen state
@Input() isFullscreenActive: boolean = false;
// Recording related inputs
@Input() recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
@Input() hasRoomTracksPublished: boolean = false;
// Recording related inputs
@Input() recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
@Input() hasRoomTracksPublished: boolean = false;
// Broadcasting related inputs
@Input() broadcastingStatus: BroadcastingStatus = BroadcastingStatus.STOPPED;
// Broadcasting related inputs
@Input() broadcastingStatus: BroadcastingStatus = BroadcastingStatus.STOPPED;
// Captions
@Input() captionsEnabled: boolean = false;
// Captions
@Input() captionsEnabled: boolean = false;
// Leave button
@Input() showLeaveButton: boolean = true;
// Leave button
@Input() showLeaveButton: boolean = true;
// Additional buttons template
@Input() toolbarAdditionalButtonsTemplate: TemplateRef<any> | null = null;
@Input() additionalButtonsPosition: ToolbarAdditionalButtonsPosition | undefined;
// Additional buttons template
@Input() toolbarAdditionalButtonsTemplate: TemplateRef<any> | null = null;
@Input() additionalButtonsPosition: ToolbarAdditionalButtonsPosition | undefined;
// Leave button template
@Input() toolbarLeaveButtonTemplate: TemplateRef<any> | null = null;
// Status enums for template usage
_recordingStatus = RecordingStatus;
_broadcastingStatus = BroadcastingStatus;
// Status enums for template usage
_recordingStatus = RecordingStatus;
_broadcastingStatus = BroadcastingStatus;
// Viewport service for responsive behavior
private viewportService = inject(ViewportService);
// Viewport service for responsive behavior
private viewportService = inject(ViewportService);
// Computed properties for responsive button grouping
readonly isMobileView = computed(() => this.viewportService.isMobile());
readonly isTabletView = computed(() => this.viewportService.isTablet());
readonly isDesktopView = computed(() => this.viewportService.isDesktop());
// Computed properties for responsive button grouping
readonly isMobileView = computed(() => this.viewportService.isMobile());
readonly isTabletView = computed(() => this.viewportService.isTablet());
readonly isDesktopView = computed(() => this.viewportService.isDesktop());
// Essential buttons that always stay visible
readonly showCameraButtonDirect = computed(() =>
this.showCameraButton && !this.isMinimal
);
// Essential buttons that always stay visible
readonly showCameraButtonDirect = computed(() => this.showCameraButton && !this.isMinimal);
readonly showMicrophoneButtonDirect = computed(() =>
this.showMicrophoneButton && !this.isMinimal
);
readonly showMicrophoneButtonDirect = computed(() => this.showMicrophoneButton && !this.isMinimal);
// Screenshare button - visible on tablet+ or when already active
readonly showScreenshareButtonDirect = computed(() =>
this.showScreenshareButton &&
!this.isMinimal &&
(!this.isMobileView() || this.isScreenShareEnabled)
);
// Screenshare button - visible on tablet+ or when already active
readonly showScreenshareButtonDirect = computed(
() => this.showScreenshareButton && !this.isMinimal && (!this.isMobileView() || this.isScreenShareEnabled)
);
// More options button - always visible when not minimal
readonly showMoreOptionsButtonDirect = computed(() =>
this.showMoreOptionsButton && !this.isMinimal
);
// More options button - always visible when not minimal
readonly showMoreOptionsButtonDirect = computed(() => this.showMoreOptionsButton && !this.isMinimal);
// Leave button - always visible
readonly showLeaveButtonDirect = computed(() =>
this.showLeaveButton
);
// Leave button - always visible
readonly showLeaveButtonDirect = computed(() => this.showLeaveButton);
// Buttons that should be moved to "More Options" on mobile
readonly buttonsInMoreOptions = computed(() => {
const buttons: Array<{
key: string;
show: boolean;
label: string;
icon: string;
action: () => void;
disabled?: boolean;
active?: boolean;
color?: string;
}> = [];
// Check if there are active features that should show a badge on More Options
readonly hasActiveFeatures = computed(
() =>
this.isScreenShareEnabled ||
this.recordingStatus === this._recordingStatus.STARTED ||
this.broadcastingStatus === this._broadcastingStatus.STARTED
);
const isMobile = this.isMobileView();
// Check if additional buttons should be shown outside (desktop/tablet) or inside More Options (mobile)
readonly showAdditionalButtonsOutside = computed(() => {
return !this.isMobileView() && this.toolbarAdditionalButtonsTemplate;
});
// On mobile, screenshare goes to more options when not active
if (isMobile && this.showScreenshareButton && !this.isScreenShareEnabled) {
buttons.push({
key: 'screenshare',
show: true,
label: 'TOOLBAR.ENABLE_SCREEN',
icon: 'screen_share',
action: () => this.onScreenShareToggle(),
disabled: this.isConnectionLost
});
}
// Check if additional buttons should be shown inside More Options menu (mobile only)
readonly showAdditionalButtonsInsideMenu = computed(() => {
return this.isMobileView() && this.toolbarAdditionalButtonsTemplate;
});
// Replace screenshare option when active on mobile
if (isMobile && this.showScreenshareButton && this.isScreenShareEnabled) {
buttons.push({
key: 'screenshare-replace',
show: true,
label: 'STREAM.REPLACE_SCREEN',
icon: 'picture_in_picture',
action: () => this.onScreenTrackReplace(),
disabled: this.isConnectionLost
});
// Media button outputs
@Output() cameraToggled = new EventEmitter<void>();
@Output() microphoneToggled = new EventEmitter<void>();
@Output() screenShareToggled = new EventEmitter<void>();
@Output() screenTrackReplaced = new EventEmitter<void>();
buttons.push({
key: 'screenshare-stop',
show: true,
label: 'TOOLBAR.DISABLE_SCREEN',
icon: 'stop_screen_share',
action: () => this.onScreenShareToggle(),
disabled: this.isConnectionLost,
color: 'warn'
});
}
// More options menu outputs
@Output() fullscreenToggled = new EventEmitter<void>();
@Output() recordingToggled = new EventEmitter<void>();
@Output() viewRecordingsClicked = new EventEmitter<void>();
@Output() broadcastingToggled = new EventEmitter<void>();
@Output() backgroundEffectsToggled = new EventEmitter<void>();
@Output() captionsToggled = new EventEmitter<void>();
@Output() settingsToggled = new EventEmitter<void>();
return buttons;
});
// Leave button output
@Output() leaveClicked = new EventEmitter<void>();
// Check if there are active features that should show a badge on More Options
readonly hasActiveFeatures = computed(() =>
this.isScreenShareEnabled ||
this.recordingStatus === this._recordingStatus.STARTED ||
this.broadcastingStatus === this._broadcastingStatus.STARTED
);
// Event handler methods
onCameraToggle(): void {
this.cameraToggled.emit();
}
// Media button outputs
@Output() cameraToggled = new EventEmitter<void>();
@Output() microphoneToggled = new EventEmitter<void>();
@Output() screenShareToggled = new EventEmitter<void>();
@Output() screenTrackReplaced = new EventEmitter<void>();
onMicrophoneToggle(): void {
this.microphoneToggled.emit();
}
// More options menu outputs
@Output() fullscreenToggled = new EventEmitter<void>();
@Output() recordingToggled = new EventEmitter<void>();
@Output() viewRecordingsClicked = new EventEmitter<void>();
@Output() broadcastingToggled = new EventEmitter<void>();
@Output() backgroundEffectsToggled = new EventEmitter<void>();
@Output() captionsToggled = new EventEmitter<void>();
@Output() settingsToggled = new EventEmitter<void>();
onScreenShareToggle(): void {
this.screenShareToggled.emit();
}
// Leave button output
@Output() leaveClicked = new EventEmitter<void>();
onScreenTrackReplace(): void {
this.screenTrackReplaced.emit();
}
// Event handler methods
onCameraToggle(): void {
this.cameraToggled.emit();
}
onFullscreenToggle(): void {
this.fullscreenToggled.emit();
}
onMicrophoneToggle(): void {
this.microphoneToggled.emit();
}
onRecordingToggle(): void {
this.recordingToggled.emit();
}
onScreenShareToggle(): void {
this.screenShareToggled.emit();
}
onViewRecordingsClick(): void {
this.viewRecordingsClicked.emit();
}
onScreenTrackReplace(): void {
this.screenTrackReplaced.emit();
}
onBroadcastingToggle(): void {
this.broadcastingToggled.emit();
}
onFullscreenToggle(): void {
this.fullscreenToggled.emit();
}
onBackgroundEffectsToggle(): void {
this.backgroundEffectsToggled.emit();
}
onRecordingToggle(): void {
this.recordingToggled.emit();
}
onViewRecordingsClick(): void {
this.viewRecordingsClicked.emit();
}
onBroadcastingToggle(): void {
this.broadcastingToggled.emit();
}
onBackgroundEffectsToggle(): void {
this.backgroundEffectsToggled.emit();
}
onCaptionsToggle(): void {
this.captionsToggled.emit();
}
onSettingsToggle(): void {
this.settingsToggled.emit();
}
onLeaveClick(): void {
this.leaveClicked.emit();
}
onCaptionsToggle(): void {
this.captionsToggled.emit();
}
onSettingsToggle(): void {
this.settingsToggled.emit();
}
onLeaveClick(): void {
this.leaveClicked.emit();
}
}

View File

@ -69,6 +69,7 @@
[showLeaveButton]="showLeaveButton"
[toolbarAdditionalButtonsTemplate]="toolbarAdditionalButtonsTemplate"
[additionalButtonsPosition]="additionalButtonsPosition"
[toolbarLeaveButtonTemplate]="toolbarLeaveButtonTemplate"
(cameraToggled)="toggleCamera()"
(microphoneToggled)="toggleMicrophone()"
(screenShareToggled)="toggleScreenShare()"

View File

@ -50,6 +50,7 @@ import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.servic
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import { Room, RoomEvent } from 'livekit-client';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
import { LeaveButtonDirective } from '../../directives/template/internals.directive';
/**
* The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}.
@ -66,12 +67,18 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/**
* @ignore
*/
@ContentChild('toolbarAdditionalButtons', { read: TemplateRef }) toolbarAdditionalButtonsTemplate: TemplateRef<any>;
@ContentChild('toolbarAdditionalButtons', { read: TemplateRef }) toolbarAdditionalButtonsTemplate: TemplateRef<any> | undefined;
/**
* @ignore
*/
@ContentChild('toolbarAdditionalPanelButtons', { read: TemplateRef }) toolbarAdditionalPanelButtonsTemplate: TemplateRef<any>;
@ContentChild('toolbarLeaveButton', { read: TemplateRef }) toolbarLeaveButtonTemplate: TemplateRef<any> | undefined;
/**
* @ignore
*/
@ContentChild('toolbarAdditionalPanelButtons', { read: TemplateRef }) toolbarAdditionalPanelButtonsTemplate:
| TemplateRef<any>
| undefined;
/**
* @ignore
@ -84,6 +91,17 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
}
/**
* @ignore
*/
@ContentChild(LeaveButtonDirective)
set externalLeaveButton(externalLeaveButton: LeaveButtonDirective) {
this._externalLeaveButton = externalLeaveButton;
if (externalLeaveButton) {
this.updateTemplatesAndMarkForCheck();
}
}
/**
* @ignore
*/
@ -153,12 +171,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/**
* @ignore
*/
@ViewChild(MatMenuTrigger) public menuTrigger: MatMenuTrigger;
@ViewChild(MatMenuTrigger) public menuTrigger: MatMenuTrigger | undefined;
/**
* @ignore
*/
room: Room;
room!: Room;
/**
* @ignore
*/
@ -186,11 +204,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/**
* @ignore
*/
hasVideoDevices: boolean;
hasVideoDevices: boolean = true;
/**
* @ignore
*/
hasAudioDevices: boolean;
hasAudioDevices: boolean = true;
/**
* @ignore
*/
@ -305,7 +323,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/**
* @ignore
*/
additionalButtonsPosition: ToolbarAdditionalButtonsPosition;
additionalButtonsPosition: ToolbarAdditionalButtonsPosition = ToolbarAdditionalButtonsPosition.BEFORE_MENU;
/**
* @ignore
@ -357,7 +375,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/**
* @ignore
*/
recordingTime: Date;
recordingTime: Date | undefined;
/**
* @internal
@ -367,6 +385,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
// Store directive references for template setup
private _externalAdditionalButtons?: ToolbarAdditionalButtonsDirective;
private _externalLeaveButton?: LeaveButtonDirective;
private _externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
private log: ILogger;
@ -416,7 +435,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
* @ignore
*/
@HostListener('window:resize', ['$event'])
sizeChange(event) {
sizeChange(_: Event) {
if (this.currentWindowHeight >= window.innerHeight) {
// The user has exit the fullscreen mode
this.currentWindowHeight = window.innerHeight;
@ -474,7 +493,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
private setupTemplates(): void {
this.templateConfig = this.templateManagerService.setupToolbarTemplates(
this._externalAdditionalButtons,
this._externalAdditionalPanelButtons
this._externalAdditionalPanelButtons,
this._externalLeaveButton
);
// Apply templates to component properties for backward compatibility
@ -492,6 +512,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
if (this.templateConfig.toolbarAdditionalPanelButtonsTemplate) {
this.toolbarAdditionalPanelButtonsTemplate = this.templateConfig.toolbarAdditionalPanelButtonsTemplate;
}
if (this.templateConfig.toolbarLeaveButtonTemplate) {
this.toolbarLeaveButtonTemplate = this.templateConfig.toolbarLeaveButtonTemplate;
}
}
/**
@ -518,11 +541,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.microphoneMuteChanging = false;
const isMicrophoneEnabled = this.participantService.isMyMicrophoneEnabled();
await this.participantService.setMicrophoneEnabled(!isMicrophoneEnabled);
} catch (error) {
this.log.e('There was an error toggling microphone:', error.code, error.message);
} catch (error: unknown) {
this.log.e('There was an error toggling microphone:', (error as any).code, (error as any).message);
this.actionService.openDialog(
this.translateService.translate('ERRORS.TOGGLE_MICROPHONE'),
error?.error || error?.message || error
(error as any)?.error || (error as any)?.message || error
);
} finally {
this.microphoneMuteChanging = false;
@ -541,8 +564,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
await this.participantService.setCameraEnabled(!isCameraEnabled);
} catch (error) {
this.log.e('There was an error toggling camera:', error.code, error.message);
this.actionService.openDialog(this.translateService.translate('ERRORS.TOGGLE_CAMERA'), error?.error || error?.message || error);
this.log.e('There was an error toggling camera:', (error as any).code, (error as any).message);
this.actionService.openDialog(
this.translateService.translate('ERRORS.TOGGLE_CAMERA'),
(error as any)?.error || (error as any)?.message || error
);
} finally {
this.cameraMuteChanging = false;
}
@ -579,8 +605,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onRoomDisconnected.emit();
}, false);
} catch (error) {
this.log.e('There was an error disconnecting:', error.code, error.message);
this.actionService.openDialog(this.translateService.translate('ERRORS.DISCONNECT'), error?.error || error?.message || error);
this.log.e('There was an error disconnecting:', (error as any).code, (error as any).message);
this.actionService.openDialog(
this.translateService.translate('ERRORS.DISCONNECT'),
(error as any)?.error || (error as any)?.message || error
);
}
}

View File

@ -92,6 +92,10 @@
<ng-template #toolbarAdditionalPanelButtons>
<ng-container *ngTemplateOutlet="openviduAngularToolbarAdditionalPanelButtonsTemplate"></ng-container>
</ng-template>
<ng-template #toolbarLeaveButton>
<ng-container *ngTemplateOutlet="openviduAngularToolbarLeaveButtonTemplate"></ng-container>
</ng-template>
</ov-toolbar>
</ng-template>

View File

@ -61,7 +61,8 @@ import { LangOption } from '../../models/lang.model';
import {
LayoutAdditionalElementsDirective,
ParticipantPanelAfterLocalParticipantDirective,
PreJoinDirective
PreJoinDirective,
LeaveButtonDirective
} from '../../directives/template/internals.directive';
/**
@ -124,6 +125,24 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
return this._externalToolbarAdditionalButtons;
}
private _externalToolbarLeaveButton?: LeaveButtonDirective;
/**
* @internal
*/
@ContentChild(LeaveButtonDirective)
set externalToolbarLeaveButton(value: LeaveButtonDirective) {
this._externalToolbarLeaveButton = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalToolbarLeaveButton(): LeaveButtonDirective | undefined {
return this._externalToolbarLeaveButton;
}
private _externalToolbarAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
/**
@ -397,6 +416,12 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal
*/
openviduAngularToolbarAdditionalButtonsTemplate: TemplateRef<any>;
/**
* @internal
*/
openviduAngularToolbarLeaveButtonTemplate: TemplateRef<any> | undefined;
/**
* @internal
*/
@ -744,6 +769,7 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
toolbar: this.externalToolbar,
toolbarAdditionalButtons: this.externalToolbarAdditionalButtons,
toolbarAdditionalPanelButtons: this.externalToolbarAdditionalPanelButtons,
toolbarLeaveButton: this.externalToolbarLeaveButton,
additionalPanels: this.externalAdditionalPanels,
panel: this.externalPanel,
chatPanel: this.externalChatPanel,
@ -800,6 +826,9 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
if (this.templateConfig.toolbarAdditionalButtonsTemplate) {
assignIfChanged('openviduAngularToolbarAdditionalButtonsTemplate', this.templateConfig.toolbarAdditionalButtonsTemplate);
}
if (this.templateConfig.toolbarLeaveButtonTemplate) {
assignIfChanged('openviduAngularToolbarLeaveButtonTemplate', this.templateConfig.toolbarLeaveButtonTemplate);
}
if (this.templateConfig.toolbarAdditionalPanelButtonsTemplate) {
assignIfChanged(
'openviduAngularToolbarAdditionalPanelButtonsTemplate',

View File

@ -222,6 +222,33 @@ export class ParticipantPanelAfterLocalParticipantDirective {
) {}
}
/**
* The ***ovLeaveButton** directive allows you to replace the default leave button with a custom template.
* Use this directive to provide your own button, confirm dialogs, or any custom leave logic while keeping
* the internal leave flow intact.
*
* Usage example:
* ```html
* <ov-videoconference>
* <ng-container *ovLeaveButton>
* <button class="my-leave-button" (click)="customLeave()">
* Leave meeting
* </button>
* </ng-container>
* </ov-videoconference>
* ```
*/
@Directive({
selector: '[ovToolbarLeaveButton]',
standalone: false
})
export class LeaveButtonDirective {
constructor(
public template: TemplateRef<any>,
public container: ViewContainerRef
) {}
}
/**
* The ***ovLayoutAdditionalElements** directive allows you to inject custom HTML or Angular templates
* as additional layout elements within the videoconference UI.

View File

@ -18,7 +18,8 @@ import {
LayoutAdditionalElementsDirective,
ParticipantPanelAfterLocalParticipantDirective,
ParticipantPanelParticipantBadgeDirective,
PreJoinDirective
PreJoinDirective,
LeaveButtonDirective
} from './internals.directive';
@NgModule({
@ -32,6 +33,7 @@ import {
StreamDirective,
ToolbarDirective,
ToolbarAdditionalButtonsDirective,
LeaveButtonDirective,
ToolbarAdditionalPanelButtonsDirective,
ParticipantPanelItemElementsDirective,
ActivitiesPanelDirective,
@ -51,6 +53,7 @@ import {
StreamDirective,
ToolbarDirective,
ToolbarAdditionalButtonsDirective,
LeaveButtonDirective,
ToolbarAdditionalPanelButtonsDirective,
ParticipantPanelItemElementsDirective,
ActivitiesPanelDirective,

View File

@ -18,7 +18,8 @@ import {
import {
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective
LayoutAdditionalElementsDirective,
LeaveButtonDirective
} from '../../directives/template/internals.directive';
/**
@ -29,6 +30,7 @@ export interface TemplateConfiguration {
toolbarTemplate: TemplateRef<any>;
toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
toolbarLeaveButtonTemplate?: TemplateRef<any>;
// Panel templates
panelTemplate: TemplateRef<any>;
@ -69,6 +71,7 @@ export interface PanelTemplateConfiguration {
export interface ToolbarTemplateConfiguration {
toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
toolbarLeaveButtonTemplate?: TemplateRef<any>;
}
/**
@ -110,6 +113,7 @@ export interface ExternalDirectives {
toolbar?: ToolbarDirective;
toolbarAdditionalButtons?: ToolbarAdditionalButtonsDirective;
toolbarAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
toolbarLeaveButton?: LeaveButtonDirective;
additionalPanels?: AdditionalPanelsDirective;
panel?: PanelDirective;
chatPanel?: ChatPanelDirective;
@ -179,6 +183,11 @@ export class TemplateManagerService {
this.log.v('Setting EXTERNAL TOOLBAR ADDITIONAL BUTTONS');
}
if (externalDirectives.toolbarLeaveButton) {
config.toolbarLeaveButtonTemplate = externalDirectives.toolbarLeaveButton.template;
this.log.v('Setting EXTERNAL TOOLBAR LEAVE BUTTON');
}
if (externalDirectives.toolbarAdditionalPanelButtons) {
config.toolbarAdditionalPanelButtonsTemplate = externalDirectives.toolbarAdditionalPanelButtons.template;
this.log.v('Setting EXTERNAL TOOLBAR ADDITIONAL PANEL BUTTONS');
@ -358,13 +367,15 @@ export class TemplateManagerService {
*/
setupToolbarTemplates(
externalAdditionalButtons?: ToolbarAdditionalButtonsDirective,
externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective
externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective,
externalLeaveButton?: LeaveButtonDirective
): ToolbarTemplateConfiguration {
this.log.v('Setting up toolbar templates...');
return {
toolbarAdditionalButtonsTemplate: externalAdditionalButtons?.template,
toolbarAdditionalPanelButtonsTemplate: externalAdditionalPanelButtons?.template
toolbarAdditionalPanelButtonsTemplate: externalAdditionalPanelButtons?.template,
toolbarLeaveButtonTemplate: externalLeaveButton?.template
};
}

View File

@ -19,7 +19,6 @@ export * from './lib/components/toolbar/toolbar-media-buttons/toolbar-media-butt
export * from './lib/components/videoconference/videoconference.component';
export * from './lib/config/openvidu-components-angular.config';
// Directives
export * from './lib/directives/template/internals.directive';
export * from './lib/directives/api/activities-panel.directive';
export * from './lib/directives/api/admin.directive';
export * from './lib/directives/api/api.directive.module';
@ -28,10 +27,12 @@ export * from './lib/directives/api/participant-panel-item.directive';
export * from './lib/directives/api/stream.directive';
export * from './lib/directives/api/toolbar.directive';
export * from './lib/directives/api/videoconference.directive';
export * from './lib/directives/template/internals.directive';
export * from './lib/directives/template/openvidu-components-angular.directive';
export * from './lib/directives/template/openvidu-components-angular.directive.module';
export * from './lib/models/broadcasting.model';
// Models
export * from './lib/models/broadcasting.model';
export * from './lib/models/panel.model';
export * from './lib/models/participant.model';
export * from './lib/models/recording.model';