/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-unused-vars */ import { Component, inject, signal, computed, OnInit } from '@angular/core'; import { CommonModule, NgOptimizedImage } from '@angular/common'; import { Store } from '@ngrx/store'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { lucideMic, lucideMicOff, lucideMonitor, lucideMonitorOff, lucidePhoneOff, lucideHeadphones, lucideArrowLeft } from '@ng-icons/lucide'; import { VoiceSessionFacade } from '../../application/facades/voice-session.facade'; import { loadVoiceSettingsFromStorage, saveVoiceSettingsToStorage } from '../../infrastructure/util/voice-settings-storage.util'; import { VoiceConnectionFacade } from '../../../../domains/voice-connection'; import { VoicePlaybackService } from '../../../../domains/voice-connection'; import { ScreenShareFacade, ScreenShareQuality } from '../../../../domains/screen-share'; import { UsersActions } from '../../../../store/users/users.actions'; import { selectCurrentUser } from '../../../../store/users/users.selectors'; import { DebugConsoleComponent, ScreenShareQualityDialogComponent } from '../../../../shared'; import { ThemeNodeDirective } from '../../../../domains/theme'; @Component({ selector: 'app-floating-voice-controls', standalone: true, imports: [ CommonModule, NgOptimizedImage, NgIcon, DebugConsoleComponent, ScreenShareQualityDialogComponent, ThemeNodeDirective ], viewProviders: [ provideIcons({ lucideMic, lucideMicOff, lucideMonitor, lucideMonitorOff, lucidePhoneOff, lucideHeadphones, lucideArrowLeft }) ], templateUrl: './floating-voice-controls.component.html' }) /** * Floating voice controls displayed when the user navigates away from the voice-connected server. * Provides mute, deafen, screen-share, and disconnect actions in a compact overlay. */ export class FloatingVoiceControlsComponent implements OnInit { private readonly webrtcService = inject(VoiceConnectionFacade); private readonly screenShareService = inject(ScreenShareFacade); private readonly voiceSessionService = inject(VoiceSessionFacade); private readonly voicePlayback = inject(VoicePlaybackService); private readonly store = inject(Store); currentUser = this.store.selectSignal(selectCurrentUser); // Voice state from services showFloatingControls = this.voiceSessionService.showFloatingControls; voiceSession = this.voiceSessionService.voiceSession; isConnected = computed(() => this.webrtcService.isVoiceConnected()); isMuted = signal(false); isDeafened = signal(false); isScreenSharing = this.screenShareService.isScreenSharing; includeSystemAudio = signal(false); screenShareQuality = signal('balanced'); askScreenShareQuality = signal(true); showScreenShareQualityDialog = signal(false); /** Sync local mute/deafen state from the WebRTC service on init. */ ngOnInit(): void { // Sync mute/deafen state from webrtc service this.isMuted.set(this.webrtcService.isMuted()); this.isDeafened.set(this.webrtcService.isDeafened()); this.syncScreenShareSettings(); const settings = loadVoiceSettingsFromStorage(); this.voicePlayback.updateOutputVolume(settings.outputVolume / 100); this.voicePlayback.updateDeafened(this.isDeafened()); if (settings.outputDevice) { this.voicePlayback.applyOutputDevice(settings.outputDevice); } } /** Navigate back to the voice-connected server. */ navigateToServer(): void { this.voiceSessionService.navigateToVoiceServer(); } /** Toggle microphone mute and broadcast the updated voice state. */ toggleMute(): void { this.isMuted.update((current) => !current); this.webrtcService.toggleMute(this.isMuted()); // Broadcast mute state change this.webrtcService.broadcastMessage({ type: 'voice-state', oderId: this.currentUser()?.oderId || this.currentUser()?.id, displayName: this.currentUser()?.displayName || 'User', voiceState: { isConnected: this.isConnected(), isMuted: this.isMuted(), isDeafened: this.isDeafened() } }); } /** Toggle deafen state (muting audio output) and broadcast the updated voice state. */ toggleDeafen(): void { this.isDeafened.update((current) => !current); this.webrtcService.toggleDeafen(this.isDeafened()); this.voicePlayback.updateDeafened(this.isDeafened()); // When deafening, also mute if (this.isDeafened() && !this.isMuted()) { this.isMuted.set(true); this.webrtcService.toggleMute(true); } // Broadcast deafen state change this.webrtcService.broadcastMessage({ type: 'voice-state', oderId: this.currentUser()?.oderId || this.currentUser()?.id, displayName: this.currentUser()?.displayName || 'User', voiceState: { isConnected: this.isConnected(), isMuted: this.isMuted(), isDeafened: this.isDeafened() } }); } /** Toggle screen sharing on or off. */ async toggleScreenShare(): Promise { if (this.isScreenSharing()) { this.screenShareService.stopScreenShare(); } else { this.syncScreenShareSettings(); if (this.askScreenShareQuality()) { this.showScreenShareQualityDialog.set(true); return; } await this.startScreenShareWithOptions(this.screenShareQuality()); } } onScreenShareQualityCancelled(): void { this.showScreenShareQualityDialog.set(false); } async onScreenShareQualityConfirmed(quality: ScreenShareQuality): Promise { this.showScreenShareQualityDialog.set(false); this.screenShareQuality.set(quality); saveVoiceSettingsToStorage({ screenShareQuality: quality }); await this.startScreenShareWithOptions(quality); } /** Disconnect from the voice session entirely, cleaning up all voice state. */ disconnect(): void { // Stop voice heartbeat this.webrtcService.stopVoiceHeartbeat(); // Broadcast voice disconnect this.webrtcService.broadcastMessage({ type: 'voice-state', oderId: this.currentUser()?.oderId || this.currentUser()?.id, displayName: this.currentUser()?.displayName || 'User', voiceState: { isConnected: false, isMuted: false, isDeafened: false } }); // Stop screen sharing if active if (this.isScreenSharing()) { this.screenShareService.stopScreenShare(); } // Disable voice this.webrtcService.disableVoice(); this.voicePlayback.teardownAll(); this.voicePlayback.updateDeafened(false); // Update user voice state in store const user = this.currentUser(); if (user?.id) { this.store.dispatch(UsersActions.updateVoiceState({ userId: user.id, voiceState: { isConnected: false, isMuted: false, isDeafened: false, roomId: undefined, serverId: undefined } })); } // End voice session this.voiceSessionService.endSession(); // Reset local state this.isMuted.set(false); this.isDeafened.set(false); } /** Return the CSS classes for the compact control button based on active state. */ getCompactButtonClass(isActive: boolean): string { const base = 'inline-flex h-7 w-7 items-center justify-center rounded-md border transition-colors'; if (isActive) { return base + ' border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15'; } return base + ' border-border bg-card text-foreground hover:bg-secondary/70'; } /** Return the CSS classes for the compact screen-share button. */ getCompactScreenShareClass(): string { const base = 'inline-flex h-7 w-7 items-center justify-center rounded-md border transition-colors'; if (this.isScreenSharing()) { return base + ' border-primary/20 bg-primary/10 text-primary hover:bg-primary/15'; } return base + ' border-border bg-card text-foreground hover:bg-secondary/70'; } /** Return the CSS classes for the mute toggle button. */ getMuteButtonClass(): string { const base = 'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors'; if (this.isMuted()) { return base + ' border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15'; } return base + ' border-border bg-card text-foreground hover:bg-secondary/70'; } /** Return the CSS classes for the deafen toggle button. */ getDeafenButtonClass(): string { const base = 'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors'; if (this.isDeafened()) { return base + ' border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15'; } return base + ' border-border bg-card text-foreground hover:bg-secondary/70'; } /** Return the CSS classes for the screen-share toggle button. */ getScreenShareButtonClass(): string { const base = 'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors'; if (this.isScreenSharing()) { return base + ' border-primary/20 bg-primary/10 text-primary hover:bg-primary/15'; } return base + ' border-border bg-card text-foreground hover:bg-secondary/70'; } private syncScreenShareSettings(): void { const settings = loadVoiceSettingsFromStorage(); this.includeSystemAudio.set(settings.includeSystemAudio); this.screenShareQuality.set(settings.screenShareQuality); this.askScreenShareQuality.set(settings.askScreenShareQuality); } private async startScreenShareWithOptions(quality: ScreenShareQuality): Promise { try { await this.screenShareService.startScreenShare({ includeSystemAudio: this.includeSystemAudio(), quality }); } catch (_error) { // Screen share request was denied or failed } } }