/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-unused-vars */ import { Component, inject, signal, computed, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Store } from '@ngrx/store'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { Subscription } from 'rxjs'; import { lucideMic, lucideMicOff, lucideMonitor, lucideMonitorOff, lucidePhoneOff, lucideHeadphones, lucideArrowLeft } from '@ng-icons/lucide'; import { WebRTCService } from '../../../core/services/webrtc.service'; import { VoiceSessionService } from '../../../core/services/voice-session.service'; import { loadVoiceSettingsFromStorage, saveVoiceSettingsToStorage } from '../../../core/services/voice-settings.storage'; import { ScreenShareQuality } from '../../../core/services/webrtc'; import { UsersActions } from '../../../store/users/users.actions'; import { selectCurrentUser } from '../../../store/users/users.selectors'; import { DebugConsoleComponent, ScreenShareQualityDialogComponent } from '../../../shared'; @Component({ selector: 'app-floating-voice-controls', standalone: true, imports: [ CommonModule, NgIcon, DebugConsoleComponent, ScreenShareQualityDialogComponent ], 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, OnDestroy { private webrtcService = inject(WebRTCService); private voiceSessionService = inject(VoiceSessionService); private 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 = signal(false); includeSystemAudio = signal(false); screenShareQuality = signal('balanced'); askScreenShareQuality = signal(true); showScreenShareQualityDialog = signal(false); private stateSubscription: Subscription | null = null; /** Sync local mute/deafen/screen-share 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.isScreenSharing.set(this.webrtcService.isScreenSharing()); this.syncScreenShareSettings(); } ngOnDestroy(): void { this.stateSubscription?.unsubscribe(); } /** 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()); // 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.webrtcService.stopScreenShare(); this.isScreenSharing.set(false); } 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.webrtcService.stopScreenShare(); } // Disable voice this.webrtcService.disableVoice(); // 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.isScreenSharing.set(false); 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 = 'w-7 h-7 inline-flex items-center justify-center rounded-lg transition-colors'; if (isActive) { return base + ' bg-destructive/20 text-destructive hover:bg-destructive/30'; } return base + ' bg-secondary text-foreground hover:bg-secondary/80'; } /** Return the CSS classes for the compact screen-share button. */ getCompactScreenShareClass(): string { const base = 'w-7 h-7 inline-flex items-center justify-center rounded-lg transition-colors'; if (this.isScreenSharing()) { return base + ' bg-primary/20 text-primary hover:bg-primary/30'; } return base + ' bg-secondary text-foreground hover:bg-secondary/80'; } /** Return the CSS classes for the mute toggle button. */ getMuteButtonClass(): string { const base = 'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors'; if (this.isMuted()) { return base + ' bg-destructive/20 text-destructive hover:bg-destructive/30'; } return base + ' bg-secondary text-foreground hover:bg-secondary/80'; } /** Return the CSS classes for the deafen toggle button. */ getDeafenButtonClass(): string { const base = 'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors'; if (this.isDeafened()) { return base + ' bg-destructive/20 text-destructive hover:bg-destructive/30'; } return base + ' bg-secondary text-foreground hover:bg-secondary/80'; } /** Return the CSS classes for the screen-share toggle button. */ getScreenShareButtonClass(): string { const base = 'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors'; if (this.isScreenSharing()) { return base + ' bg-primary/20 text-primary hover:bg-primary/30'; } return base + ' bg-secondary text-foreground hover:bg-secondary/80'; } 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.webrtcService.startScreenShare({ includeSystemAudio: this.includeSystemAudio(), quality }); this.isScreenSharing.set(true); } catch (_error) { // Screen share request was denied or failed } } }