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

View File

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

View File

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

View File

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

View File

@ -61,7 +61,8 @@ import { LangOption } from '../../models/lang.model';
import { import {
LayoutAdditionalElementsDirective, LayoutAdditionalElementsDirective,
ParticipantPanelAfterLocalParticipantDirective, ParticipantPanelAfterLocalParticipantDirective,
PreJoinDirective PreJoinDirective,
LeaveButtonDirective
} from '../../directives/template/internals.directive'; } from '../../directives/template/internals.directive';
/** /**
@ -124,6 +125,24 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
return this._externalToolbarAdditionalButtons; 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; private _externalToolbarAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
/** /**
@ -397,6 +416,12 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal * @internal
*/ */
openviduAngularToolbarAdditionalButtonsTemplate: TemplateRef<any>; openviduAngularToolbarAdditionalButtonsTemplate: TemplateRef<any>;
/**
* @internal
*/
openviduAngularToolbarLeaveButtonTemplate: TemplateRef<any> | undefined;
/** /**
* @internal * @internal
*/ */
@ -744,6 +769,7 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
toolbar: this.externalToolbar, toolbar: this.externalToolbar,
toolbarAdditionalButtons: this.externalToolbarAdditionalButtons, toolbarAdditionalButtons: this.externalToolbarAdditionalButtons,
toolbarAdditionalPanelButtons: this.externalToolbarAdditionalPanelButtons, toolbarAdditionalPanelButtons: this.externalToolbarAdditionalPanelButtons,
toolbarLeaveButton: this.externalToolbarLeaveButton,
additionalPanels: this.externalAdditionalPanels, additionalPanels: this.externalAdditionalPanels,
panel: this.externalPanel, panel: this.externalPanel,
chatPanel: this.externalChatPanel, chatPanel: this.externalChatPanel,
@ -800,6 +826,9 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
if (this.templateConfig.toolbarAdditionalButtonsTemplate) { if (this.templateConfig.toolbarAdditionalButtonsTemplate) {
assignIfChanged('openviduAngularToolbarAdditionalButtonsTemplate', this.templateConfig.toolbarAdditionalButtonsTemplate); assignIfChanged('openviduAngularToolbarAdditionalButtonsTemplate', this.templateConfig.toolbarAdditionalButtonsTemplate);
} }
if (this.templateConfig.toolbarLeaveButtonTemplate) {
assignIfChanged('openviduAngularToolbarLeaveButtonTemplate', this.templateConfig.toolbarLeaveButtonTemplate);
}
if (this.templateConfig.toolbarAdditionalPanelButtonsTemplate) { if (this.templateConfig.toolbarAdditionalPanelButtonsTemplate) {
assignIfChanged( assignIfChanged(
'openviduAngularToolbarAdditionalPanelButtonsTemplate', '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 * The ***ovLayoutAdditionalElements** directive allows you to inject custom HTML or Angular templates
* as additional layout elements within the videoconference UI. * as additional layout elements within the videoconference UI.

View File

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

View File

@ -18,7 +18,8 @@ import {
import { import {
PreJoinDirective, PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective, ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective LayoutAdditionalElementsDirective,
LeaveButtonDirective
} from '../../directives/template/internals.directive'; } from '../../directives/template/internals.directive';
/** /**
@ -29,6 +30,7 @@ export interface TemplateConfiguration {
toolbarTemplate: TemplateRef<any>; toolbarTemplate: TemplateRef<any>;
toolbarAdditionalButtonsTemplate?: TemplateRef<any>; toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>; toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
toolbarLeaveButtonTemplate?: TemplateRef<any>;
// Panel templates // Panel templates
panelTemplate: TemplateRef<any>; panelTemplate: TemplateRef<any>;
@ -69,6 +71,7 @@ export interface PanelTemplateConfiguration {
export interface ToolbarTemplateConfiguration { export interface ToolbarTemplateConfiguration {
toolbarAdditionalButtonsTemplate?: TemplateRef<any>; toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>; toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
toolbarLeaveButtonTemplate?: TemplateRef<any>;
} }
/** /**
@ -110,6 +113,7 @@ export interface ExternalDirectives {
toolbar?: ToolbarDirective; toolbar?: ToolbarDirective;
toolbarAdditionalButtons?: ToolbarAdditionalButtonsDirective; toolbarAdditionalButtons?: ToolbarAdditionalButtonsDirective;
toolbarAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective; toolbarAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
toolbarLeaveButton?: LeaveButtonDirective;
additionalPanels?: AdditionalPanelsDirective; additionalPanels?: AdditionalPanelsDirective;
panel?: PanelDirective; panel?: PanelDirective;
chatPanel?: ChatPanelDirective; chatPanel?: ChatPanelDirective;
@ -179,6 +183,11 @@ export class TemplateManagerService {
this.log.v('Setting EXTERNAL TOOLBAR ADDITIONAL BUTTONS'); 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) { if (externalDirectives.toolbarAdditionalPanelButtons) {
config.toolbarAdditionalPanelButtonsTemplate = externalDirectives.toolbarAdditionalPanelButtons.template; config.toolbarAdditionalPanelButtonsTemplate = externalDirectives.toolbarAdditionalPanelButtons.template;
this.log.v('Setting EXTERNAL TOOLBAR ADDITIONAL PANEL BUTTONS'); this.log.v('Setting EXTERNAL TOOLBAR ADDITIONAL PANEL BUTTONS');
@ -358,13 +367,15 @@ export class TemplateManagerService {
*/ */
setupToolbarTemplates( setupToolbarTemplates(
externalAdditionalButtons?: ToolbarAdditionalButtonsDirective, externalAdditionalButtons?: ToolbarAdditionalButtonsDirective,
externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective,
externalLeaveButton?: LeaveButtonDirective
): ToolbarTemplateConfiguration { ): ToolbarTemplateConfiguration {
this.log.v('Setting up toolbar templates...'); this.log.v('Setting up toolbar templates...');
return { return {
toolbarAdditionalButtonsTemplate: externalAdditionalButtons?.template, 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/components/videoconference/videoconference.component';
export * from './lib/config/openvidu-components-angular.config'; export * from './lib/config/openvidu-components-angular.config';
// Directives // Directives
export * from './lib/directives/template/internals.directive';
export * from './lib/directives/api/activities-panel.directive'; export * from './lib/directives/api/activities-panel.directive';
export * from './lib/directives/api/admin.directive'; export * from './lib/directives/api/admin.directive';
export * from './lib/directives/api/api.directive.module'; 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/stream.directive';
export * from './lib/directives/api/toolbar.directive'; export * from './lib/directives/api/toolbar.directive';
export * from './lib/directives/api/videoconference.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';
export * from './lib/directives/template/openvidu-components-angular.directive.module'; export * from './lib/directives/template/openvidu-components-angular.directive.module';
export * from './lib/models/broadcasting.model';
// Models // Models
export * from './lib/models/broadcasting.model';
export * from './lib/models/panel.model'; export * from './lib/models/panel.model';
export * from './lib/models/participant.model'; export * from './lib/models/participant.model';
export * from './lib/models/recording.model'; export * from './lib/models/recording.model';