Files
Toju/src/app/features/voice/floating-voice-controls/floating-voice-controls.component.ts
2026-03-09 23:02:52 +01:00

292 lines
9.1 KiB
TypeScript

/* 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<ScreenShareQuality>('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<void> {
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<void> {
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<void> {
try {
await this.webrtcService.startScreenShare({
includeSystemAudio: this.includeSystemAudio(),
quality
});
this.isScreenSharing.set(true);
} catch (_error) {
// Screen share request was denied or failed
}
}
}