Files
Toju/toju-app/src/app/domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component.ts

297 lines
9.8 KiB
TypeScript

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