Add eslint

This commit is contained in:
2026-03-03 22:56:12 +01:00
parent d641229f9d
commit ad0e28bf84
92 changed files with 2656 additions and 1127 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { Component, inject, input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgIcon, provideIcons } from '@ng-icons/core';
@@ -14,10 +15,10 @@ import { selectBannedUsers } from '../../../../store/users/users.selectors';
imports: [CommonModule, NgIcon],
viewProviders: [
provideIcons({
lucideX,
}),
lucideX
})
],
templateUrl: './bans-settings.component.html',
templateUrl: './bans-settings.component.html'
})
export class BansSettingsComponent {
private store = inject(Store);
@@ -35,6 +36,7 @@ export class BansSettingsComponent {
formatExpiry(timestamp: number): string {
const date = new Date(timestamp);
return (
date.toLocaleDateString() +
' ' +

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { Component, inject, input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -18,10 +19,10 @@ import { UserAvatarComponent } from '../../../../shared';
viewProviders: [
provideIcons({
lucideUserX,
lucideBan,
}),
lucideBan
})
],
templateUrl: './members-settings.component.html',
templateUrl: './members-settings.component.html'
})
export class MembersSettingsComponent {
private store = inject(Store);
@@ -37,6 +38,7 @@ export class MembersSettingsComponent {
membersFiltered(): User[] {
const me = this.currentUser();
return this.onlineUsers().filter((user) => user.id !== me?.id && user.oderId !== me?.oderId);
}
@@ -45,7 +47,7 @@ export class MembersSettingsComponent {
this.webrtcService.broadcastMessage({
type: 'role-change',
targetUserId: user.id,
role,
role
});
}
@@ -54,7 +56,7 @@ export class MembersSettingsComponent {
this.webrtcService.broadcastMessage({
type: 'kick',
targetUserId: user.id,
kickedBy: this.currentUser()?.id,
kickedBy: this.currentUser()?.id
});
}
@@ -63,7 +65,7 @@ export class MembersSettingsComponent {
this.webrtcService.broadcastMessage({
type: 'ban',
targetUserId: user.id,
bannedBy: this.currentUser()?.id,
bannedBy: this.currentUser()?.id
});
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -8,7 +9,7 @@ import {
lucideRefreshCw,
lucidePlus,
lucideTrash2,
lucideCheck,
lucideCheck
} from '@ng-icons/lucide';
import { ServerDirectoryService } from '../../../../core/services/server-directory.service';
@@ -25,10 +26,10 @@ import { STORAGE_KEY_CONNECTION_SETTINGS } from '../../../../core/constants';
lucideRefreshCw,
lucidePlus,
lucideTrash2,
lucideCheck,
}),
lucideCheck
})
],
templateUrl: './network-settings.component.html',
templateUrl: './network-settings.component.html'
})
export class NetworkSettingsComponent {
private serverDirectory = inject(ServerDirectoryService);
@@ -47,25 +48,30 @@ export class NetworkSettingsComponent {
addServer(): void {
this.addError.set(null);
try {
new URL(this.newServerUrl);
} catch {
this.addError.set('Please enter a valid URL');
return;
}
if (this.servers().some((s) => s.url === this.newServerUrl)) {
if (this.servers().some((serverEntry) => serverEntry.url === this.newServerUrl)) {
this.addError.set('This server URL already exists');
return;
}
this.serverDirectory.addServer({
name: this.newServerName.trim(),
url: this.newServerUrl.trim().replace(/\/$/, ''),
url: this.newServerUrl.trim().replace(/\/$/, '')
});
this.newServerName = '';
this.newServerUrl = '';
const servers = this.servers();
const newServer = servers[servers.length - 1];
if (newServer) this.serverDirectory.testServer(newServer.id);
if (newServer)
this.serverDirectory.testServer(newServer.id);
}
removeServer(id: string): void {
@@ -84,8 +90,10 @@ export class NetworkSettingsComponent {
loadConnectionSettings(): void {
const raw = localStorage.getItem(STORAGE_KEY_CONNECTION_SETTINGS);
if (raw) {
const parsed = JSON.parse(raw);
this.autoReconnect = parsed.autoReconnect ?? true;
this.searchAllServers = parsed.searchAllServers ?? true;
this.serverDirectory.setSearchAllServers(this.searchAllServers);
@@ -97,8 +105,8 @@ export class NetworkSettingsComponent {
STORAGE_KEY_CONNECTION_SETTINGS,
JSON.stringify({
autoReconnect: this.autoReconnect,
searchAllServers: this.searchAllServers,
}),
searchAllServers: this.searchAllServers
})
);
this.serverDirectory.setSearchAllServers(this.searchAllServers);
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { Component, inject, input, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -14,10 +15,10 @@ import { RoomsActions } from '../../../../store/rooms/rooms.actions';
imports: [CommonModule, FormsModule, NgIcon],
viewProviders: [
provideIcons({
lucideCheck,
}),
lucideCheck
})
],
templateUrl: './permissions-settings.component.html',
templateUrl: './permissions-settings.component.html'
})
export class PermissionsSettingsComponent {
private store = inject(Store);
@@ -42,6 +43,7 @@ export class PermissionsSettingsComponent {
/** Load permissions from the server input. Called by parent via effect or on init. */
loadPermissions(room: Room): void {
const perms = room.permissions || {};
this.allowVoice = perms.allowVoice !== false;
this.allowScreenShare = perms.allowScreenShare !== false;
this.allowFileUploads = perms.allowFileUploads !== false;
@@ -54,7 +56,10 @@ export class PermissionsSettingsComponent {
savePermissions(): void {
const room = this.server();
if (!room) return;
if (!room)
return;
this.store.dispatch(
RoomsActions.updateRoomPermissions({
roomId: room.id,
@@ -66,16 +71,19 @@ export class PermissionsSettingsComponent {
adminsManageRooms: this.adminsManageRooms,
moderatorsManageRooms: this.moderatorsManageRooms,
adminsManageIcon: this.adminsManageIcon,
moderatorsManageIcon: this.moderatorsManageIcon,
},
}),
moderatorsManageIcon: this.moderatorsManageIcon
}
})
);
this.showSaveSuccess('permissions');
}
private showSaveSuccess(key: string): void {
this.saveSuccess.set(key);
if (this.saveTimeout) clearTimeout(this.saveTimeout);
if (this.saveTimeout)
clearTimeout(this.saveTimeout);
this.saveTimeout = setTimeout(() => this.saveSuccess.set(null), 2000);
}
}

View File

@@ -10,22 +10,24 @@
}
<div class="space-y-4">
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1">Room Name</label>
<label for="room-name" class="block text-xs font-medium text-muted-foreground mb-1">Room Name</label>
<input
type="text"
[(ngModel)]="roomName"
[readOnly]="!isAdmin()"
id="room-name"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary"
[class.opacity-60]="!isAdmin()"
[class.cursor-not-allowed]="!isAdmin()"
/>
</div>
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1">Description</label>
<label for="room-description" class="block text-xs font-medium text-muted-foreground mb-1">Description</label>
<textarea
[(ngModel)]="roomDescription"
[readOnly]="!isAdmin()"
rows="3"
id="room-description"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary resize-none"
[class.opacity-60]="!isAdmin()"
[class.cursor-not-allowed]="!isAdmin()"
@@ -39,6 +41,7 @@
</div>
<button
(click)="togglePrivate()"
type="button"
class="p-2 rounded-lg transition-colors"
[class.bg-primary]="isPrivate()"
[class.text-primary-foreground]="isPrivate()"
@@ -62,14 +65,15 @@
</div>
}
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1"
>Max Users (0 = unlimited)</label
>
<label for="room-max-users" class="block text-xs font-medium text-muted-foreground mb-1">
Max Users (0 = unlimited)
</label>
<input
type="number"
[(ngModel)]="maxUsers"
[readOnly]="!isAdmin()"
min="0"
id="room-max-users"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary"
[class.opacity-60]="!isAdmin()"
[class.cursor-not-allowed]="!isAdmin()"
@@ -81,6 +85,7 @@
@if (isAdmin()) {
<button
(click)="saveServerSettings()"
type="button"
class="w-full px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors flex items-center justify-center gap-2 text-sm"
[class.bg-green-600]="saveSuccess() === 'server'"
[class.hover:bg-green-600]="saveSuccess() === 'server'"
@@ -94,6 +99,7 @@
<h4 class="text-sm font-medium text-destructive mb-3">Danger Zone</h4>
<button
(click)="confirmDeleteRoom()"
type="button"
class="w-full px-4 py-2 bg-destructive/10 text-destructive border border-destructive/20 rounded-lg hover:bg-destructive/20 transition-colors flex items-center justify-center gap-2 text-sm"
>
<ng-icon name="lucideTrash2" class="w-4 h-4" />

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { Component, inject, input, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -19,10 +20,10 @@ import { SettingsModalService } from '../../../../core/services/settings-modal.s
lucideCheck,
lucideTrash2,
lucideLock,
lucideUnlock,
}),
lucideUnlock
})
],
templateUrl: './server-settings.component.html',
templateUrl: './server-settings.component.html'
})
export class ServerSettingsComponent {
private store = inject(Store);
@@ -45,22 +46,27 @@ export class ServerSettingsComponent {
/** Reload form fields whenever the server input changes. */
serverData = computed(() => {
const room = this.server();
if (room) {
this.roomName = room.name;
this.roomDescription = room.description || '';
this.isPrivate.set(room.isPrivate);
this.maxUsers = room.maxUsers || 0;
}
return room;
});
togglePrivate(): void {
this.isPrivate.update((v) => !v);
this.isPrivate.update((currentValue) => !currentValue);
}
saveServerSettings(): void {
const room = this.server();
if (!room) return;
if (!room)
return;
this.store.dispatch(
RoomsActions.updateRoom({
roomId: room.id,
@@ -68,9 +74,9 @@ export class ServerSettingsComponent {
name: this.roomName,
description: this.roomDescription,
isPrivate: this.isPrivate(),
maxUsers: this.maxUsers,
},
}),
maxUsers: this.maxUsers
}
})
);
this.showSaveSuccess('server');
}
@@ -81,7 +87,10 @@ export class ServerSettingsComponent {
deleteRoom(): void {
const room = this.server();
if (!room) return;
if (!room)
return;
this.store.dispatch(RoomsActions.deleteRoom({ roomId: room.id }));
this.showDeleteConfirm.set(false);
this.modal.navigate('network');
@@ -89,7 +98,10 @@ export class ServerSettingsComponent {
private showSaveSuccess(key: string): void {
this.saveSuccess.set(key);
if (this.saveTimeout) clearTimeout(this.saveTimeout);
if (this.saveTimeout)
clearTimeout(this.saveTimeout);
this.saveTimeout = setTimeout(() => this.saveSuccess.set(null), 2000);
}
}

View File

@@ -5,6 +5,11 @@
[class.opacity-100]="animating()"
[class.opacity-0]="!animating()"
(click)="onBackdropClick()"
(keydown.enter)="onBackdropClick()"
(keydown.space)="onBackdropClick()"
role="button"
tabindex="0"
aria-label="Close settings"
></div>
<!-- Modal -->
@@ -16,6 +21,11 @@
[class.scale-95]="!animating()"
[class.opacity-0]="!animating()"
(click)="$event.stopPropagation()"
(keydown.enter)="$event.stopPropagation()"
(keydown.space)="$event.stopPropagation()"
role="dialog"
aria-modal="true"
tabindex="-1"
>
<!-- Side Navigation -->
<nav class="w-52 flex-shrink-0 bg-secondary/40 border-r border-border flex flex-col">
@@ -33,6 +43,7 @@
@for (page of globalPages; track page.id) {
<button
(click)="navigate(page.id)"
type="button"
class="w-full flex items-center gap-2.5 px-4 py-2 text-sm transition-colors"
[class.bg-primary/10]="activePage() === page.id"
[class.text-primary]="activePage() === page.id"
@@ -72,6 +83,7 @@
@for (page of serverPages; track page.id) {
<button
(click)="navigate(page.id)"
type="button"
class="w-full flex items-center gap-2.5 px-4 py-2 text-sm transition-colors"
[class.bg-primary/10]="activePage() === page.id"
[class.text-primary]="activePage() === page.id"
@@ -119,6 +131,7 @@
</h3>
<button
(click)="close()"
type="button"
class="p-2 hover:bg-secondary rounded-lg transition-colors text-muted-foreground hover:text-foreground"
>
<ng-icon name="lucideX" class="w-5 h-5" />

View File

@@ -1,13 +1,12 @@
/* eslint-disable @typescript-eslint/member-ordering */
import {
Component,
inject,
signal,
computed,
OnInit,
OnDestroy,
effect,
HostListener,
viewChild,
viewChild
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -20,13 +19,13 @@ import {
lucideSettings,
lucideUsers,
lucideBan,
lucideShield,
lucideShield
} from '@ng-icons/lucide';
import { SettingsModalService, SettingsPage } from '../../../core/services/settings-modal.service';
import { selectSavedRooms, selectCurrentRoom } from '../../../store/rooms/rooms.selectors';
import {
selectCurrentUser,
selectCurrentUser
} from '../../../store/users/users.selectors';
import { Room } from '../../../core/models';
@@ -49,7 +48,7 @@ import { PermissionsSettingsComponent } from './permissions-settings/permissions
ServerSettingsComponent,
MembersSettingsComponent,
BansSettingsComponent,
PermissionsSettingsComponent,
PermissionsSettingsComponent
],
viewProviders: [
provideIcons({
@@ -59,12 +58,12 @@ import { PermissionsSettingsComponent } from './permissions-settings/permissions
lucideSettings,
lucideUsers,
lucideBan,
lucideShield,
}),
lucideShield
})
],
templateUrl: './settings-modal.component.html',
templateUrl: './settings-modal.component.html'
})
export class SettingsModalComponent implements OnInit, OnDestroy {
export class SettingsModalComponent {
readonly modal = inject(SettingsModalService);
private store = inject(Store);
@@ -82,21 +81,24 @@ export class SettingsModalComponent implements OnInit, OnDestroy {
// --- Side-nav items ---
readonly globalPages: { id: SettingsPage; label: string; icon: string }[] = [
{ id: 'network', label: 'Network', icon: 'lucideGlobe' },
{ id: 'voice', label: 'Voice & Audio', icon: 'lucideAudioLines' },
{ id: 'voice', label: 'Voice & Audio', icon: 'lucideAudioLines' }
];
readonly serverPages: { id: SettingsPage; label: string; icon: string }[] = [
{ id: 'server', label: 'Server', icon: 'lucideSettings' },
{ id: 'members', label: 'Members', icon: 'lucideUsers' },
{ id: 'bans', label: 'Bans', icon: 'lucideBan' },
{ id: 'permissions', label: 'Permissions', icon: 'lucideShield' },
{ id: 'permissions', label: 'Permissions', icon: 'lucideShield' }
];
// ===== SERVER SELECTOR =====
selectedServerId = signal<string | null>(null);
selectedServer = computed<Room | null>(() => {
const id = this.selectedServerId();
if (!id) return null;
return this.savedRooms().find((r) => r.id === id) ?? null;
if (!id)
return null;
return this.savedRooms().find((room) => room.id === id) ?? null;
});
/** Whether the user can see server-admin tabs. */
@@ -108,7 +110,10 @@ export class SettingsModalComponent implements OnInit, OnDestroy {
isSelectedServerAdmin = computed(() => {
const server = this.selectedServer();
const user = this.currentUser();
if (!server || !user) return false;
if (!server || !user)
return false;
return server.hostId === user.id || server.hostId === user.oderId;
});
@@ -120,11 +125,17 @@ export class SettingsModalComponent implements OnInit, OnDestroy {
effect(() => {
if (this.isOpen()) {
const targetId = this.modal.targetServerId();
if (targetId) {
this.selectedServerId.set(targetId);
} else if (this.currentRoom()) {
this.selectedServerId.set(this.currentRoom()!.id);
} else {
const currentRoom = this.currentRoom();
if (currentRoom) {
this.selectedServerId.set(currentRoom.id);
}
}
this.animating.set(true);
}
});
@@ -132,19 +143,16 @@ export class SettingsModalComponent implements OnInit, OnDestroy {
// When selected server changes, reload permissions data
effect(() => {
const server = this.selectedServer();
if (server) {
const permsComp = this.permissionsComponent();
if (permsComp) {
permsComp.loadPermissions(server);
}
}
});
}
ngOnInit(): void {}
ngOnDestroy(): void {}
@HostListener('document:keydown.escape')
onEscapeKey(): void {
if (this.isOpen()) {
@@ -168,6 +176,7 @@ export class SettingsModalComponent implements OnInit, OnDestroy {
onServerSelect(event: Event): void {
const select = event.target as HTMLSelectElement;
this.selectedServerId.set(select.value || null);
}
}

View File

@@ -7,9 +7,10 @@
</div>
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1">Microphone</label>
<label for="input-device-select" class="block text-xs font-medium text-muted-foreground mb-1">Microphone</label>
<select
(change)="onInputDeviceChange($event)"
id="input-device-select"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary"
>
@for (device of inputDevices(); track device.deviceId) {
@@ -23,9 +24,10 @@
</select>
</div>
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1">Speaker</label>
<label for="output-device-select" class="block text-xs font-medium text-muted-foreground mb-1">Speaker</label>
<select
(change)="onOutputDeviceChange($event)"
id="output-device-select"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary"
>
@for (device of outputDevices(); track device.deviceId) {
@@ -49,7 +51,7 @@
</div>
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1">
<label for="input-volume-slider" class="block text-xs font-medium text-muted-foreground mb-1">
Input Volume: {{ inputVolume() }}%
</label>
<input
@@ -58,11 +60,12 @@
(input)="onInputVolumeChange($event)"
min="0"
max="100"
id="input-volume-slider"
class="w-full h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
/>
</div>
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1">
<label for="output-volume-slider" class="block text-xs font-medium text-muted-foreground mb-1">
Output Volume: {{ outputVolume() }}%
</label>
<input
@@ -71,11 +74,12 @@
(input)="onOutputVolumeChange($event)"
min="0"
max="100"
id="output-volume-slider"
class="w-full h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
/>
</div>
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1">
<label for="notification-volume-slider" class="block text-xs font-medium text-muted-foreground mb-1">
Notification Volume: {{ audioService.notificationVolume() * 100 | number: '1.0-0' }}%
</label>
<div class="flex items-center gap-2">
@@ -86,10 +90,12 @@
min="0"
max="1"
step="0.01"
id="notification-volume-slider"
class="flex-1 h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
/>
<button
(click)="previewNotificationSound()"
type="button"
class="px-2.5 py-1 text-xs bg-secondary text-foreground rounded-lg hover:bg-secondary/80 transition-colors flex-shrink-0"
title="Preview notification sound"
>
@@ -111,9 +117,10 @@
</div>
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1">Latency Profile</label>
<label for="latency-profile-select" class="block text-xs font-medium text-muted-foreground mb-1">Latency Profile</label>
<select
(change)="onLatencyProfileChange($event)"
id="latency-profile-select"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="low" [selected]="latencyProfile() === 'low'">Low (fast)</option>
@@ -122,7 +129,7 @@
</select>
</div>
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1">
<label for="audio-bitrate-slider" class="block text-xs font-medium text-muted-foreground mb-1">
Audio Bitrate: {{ audioBitrate() }} kbps
</label>
<input
@@ -132,6 +139,7 @@
min="32"
max="256"
step="8"
id="audio-bitrate-slider"
class="w-full h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
/>
</div>
@@ -145,6 +153,8 @@
type="checkbox"
[checked]="noiseReduction()"
(change)="onNoiseReductionChange()"
id="noise-reduction-toggle"
aria-label="Toggle noise reduction"
class="sr-only peer"
/>
<div
@@ -162,6 +172,8 @@
type="checkbox"
[checked]="includeSystemAudio()"
(change)="onIncludeSystemAudioChange($event)"
id="system-audio-toggle"
aria-label="Toggle system audio in screen share"
class="sr-only peer"
/>
<div
@@ -190,6 +202,8 @@
type="checkbox"
[checked]="voiceLeveling.enabled()"
(change)="onVoiceLevelingToggle()"
id="voice-leveling-toggle"
aria-label="Toggle voice leveling"
class="sr-only peer"
/>
<div
@@ -203,7 +217,7 @@
<div class="space-y-3 pl-1 border-l-2 border-primary/20 ml-1">
<!-- Target Loudness -->
<div class="pl-3">
<label class="block text-xs font-medium text-muted-foreground mb-1">
<label for="target-loudness-slider" class="block text-xs font-medium text-muted-foreground mb-1">
Target Loudness: {{ voiceLeveling.targetDbfs() }} dBFS
</label>
<input
@@ -213,6 +227,7 @@
min="-30"
max="-12"
step="1"
id="target-loudness-slider"
class="w-full h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
/>
<div class="flex justify-between text-[10px] text-muted-foreground/60 mt-0.5">
@@ -223,9 +238,10 @@
<!-- AGC Strength -->
<div class="pl-3">
<label class="block text-xs font-medium text-muted-foreground mb-1">AGC Strength</label>
<label for="agc-strength-select" class="block text-xs font-medium text-muted-foreground mb-1">AGC Strength</label>
<select
(change)="onStrengthChange($event)"
id="agc-strength-select"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="low" [selected]="voiceLeveling.strength() === 'low'">
@@ -242,7 +258,7 @@
<!-- Max Gain Boost -->
<div class="pl-3">
<label class="block text-xs font-medium text-muted-foreground mb-1">
<label for="max-gain-slider" class="block text-xs font-medium text-muted-foreground mb-1">
Max Gain Boost: {{ voiceLeveling.maxGainDb() }} dB
</label>
<input
@@ -252,6 +268,7 @@
min="3"
max="20"
step="1"
id="max-gain-slider"
class="w-full h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
/>
<div class="flex justify-between text-[10px] text-muted-foreground/60 mt-0.5">
@@ -262,11 +279,12 @@
<!-- Response Speed -->
<div class="pl-3">
<label class="block text-xs font-medium text-muted-foreground mb-1">
<label for="response-speed-select" class="block text-xs font-medium text-muted-foreground mb-1">
Response Speed
</label>
<select
(change)="onSpeedChange($event)"
id="response-speed-select"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="slow" [selected]="voiceLeveling.speed() === 'slow'">
@@ -290,6 +308,8 @@
type="checkbox"
[checked]="voiceLeveling.noiseGate()"
(change)="onNoiseGateToggle()"
id="noise-gate-toggle"
aria-label="Toggle noise floor gate"
class="sr-only peer"
/>
<div

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -8,7 +9,7 @@ import { WebRTCService } from '../../../../core/services/webrtc.service';
import { VoiceLevelingService } from '../../../../core/services/voice-leveling.service';
import {
NotificationAudioService,
AppSound,
AppSound
} from '../../../../core/services/notification-audio.service';
import { STORAGE_KEY_VOICE_SETTINGS } from '../../../../core/constants';
@@ -26,10 +27,10 @@ interface AudioDevice {
lucideMic,
lucideHeadphones,
lucideAudioLines,
lucideActivity,
}),
lucideActivity
})
],
templateUrl: './voice-settings.component.html',
templateUrl: './voice-settings.component.html'
})
export class VoiceSettingsComponent {
private webrtcService = inject(WebRTCService);
@@ -54,17 +55,20 @@ export class VoiceSettingsComponent {
async loadAudioDevices(): Promise<void> {
try {
if (!navigator.mediaDevices?.enumerateDevices) return;
if (!navigator.mediaDevices?.enumerateDevices)
return;
const devices = await navigator.mediaDevices.enumerateDevices();
this.inputDevices.set(
devices
.filter((d) => d.kind === 'audioinput')
.map((d) => ({ deviceId: d.deviceId, label: d.label })),
.filter((device) => device.kind === 'audioinput')
.map((device) => ({ deviceId: device.deviceId, label: device.label }))
);
this.outputDevices.set(
devices
.filter((d) => d.kind === 'audiooutput')
.map((d) => ({ deviceId: d.deviceId, label: d.label })),
.filter((device) => device.kind === 'audiooutput')
.map((device) => ({ deviceId: device.deviceId, label: device.label }))
);
} catch {}
}
@@ -72,18 +76,37 @@ export class VoiceSettingsComponent {
loadVoiceSettings(): void {
try {
const raw = localStorage.getItem(STORAGE_KEY_VOICE_SETTINGS);
if (!raw) return;
const s = JSON.parse(raw);
if (s.inputDevice) this.selectedInputDevice.set(s.inputDevice);
if (s.outputDevice) this.selectedOutputDevice.set(s.outputDevice);
if (typeof s.inputVolume === 'number') this.inputVolume.set(s.inputVolume);
if (typeof s.outputVolume === 'number') this.outputVolume.set(s.outputVolume);
if (typeof s.audioBitrate === 'number') this.audioBitrate.set(s.audioBitrate);
if (s.latencyProfile) this.latencyProfile.set(s.latencyProfile);
if (typeof s.includeSystemAudio === 'boolean')
this.includeSystemAudio.set(s.includeSystemAudio);
if (typeof s.noiseReduction === 'boolean') this.noiseReduction.set(s.noiseReduction);
if (!raw)
return;
const settings = JSON.parse(raw);
if (settings.inputDevice)
this.selectedInputDevice.set(settings.inputDevice);
if (settings.outputDevice)
this.selectedOutputDevice.set(settings.outputDevice);
if (typeof settings.inputVolume === 'number')
this.inputVolume.set(settings.inputVolume);
if (typeof settings.outputVolume === 'number')
this.outputVolume.set(settings.outputVolume);
if (typeof settings.audioBitrate === 'number')
this.audioBitrate.set(settings.audioBitrate);
if (settings.latencyProfile)
this.latencyProfile.set(settings.latencyProfile);
if (typeof settings.includeSystemAudio === 'boolean')
this.includeSystemAudio.set(settings.includeSystemAudio);
if (typeof settings.noiseReduction === 'boolean')
this.noiseReduction.set(settings.noiseReduction);
} catch {}
if (this.noiseReduction() !== this.webrtcService.isNoiseReductionEnabled()) {
this.webrtcService.toggleNoiseReduction(this.noiseReduction());
}
@@ -101,20 +124,22 @@ export class VoiceSettingsComponent {
audioBitrate: this.audioBitrate(),
latencyProfile: this.latencyProfile(),
includeSystemAudio: this.includeSystemAudio(),
noiseReduction: this.noiseReduction(),
}),
noiseReduction: this.noiseReduction()
})
);
} catch {}
}
onInputDeviceChange(event: Event): void {
const select = event.target as HTMLSelectElement;
this.selectedInputDevice.set(select.value);
this.saveVoiceSettings();
}
onOutputDeviceChange(event: Event): void {
const select = event.target as HTMLSelectElement;
this.selectedOutputDevice.set(select.value);
this.webrtcService.setOutputVolume(this.outputVolume() / 100);
this.saveVoiceSettings();
@@ -122,12 +147,14 @@ export class VoiceSettingsComponent {
onInputVolumeChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.inputVolume.set(parseInt(input.value, 10));
this.saveVoiceSettings();
}
onOutputVolumeChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.outputVolume.set(parseInt(input.value, 10));
this.webrtcService.setOutputVolume(this.outputVolume() / 100);
this.saveVoiceSettings();
@@ -136,6 +163,7 @@ export class VoiceSettingsComponent {
onLatencyProfileChange(event: Event): void {
const select = event.target as HTMLSelectElement;
const profile = select.value as 'low' | 'balanced' | 'high';
this.latencyProfile.set(profile);
this.webrtcService.setLatencyProfile(profile);
this.saveVoiceSettings();
@@ -143,6 +171,7 @@ export class VoiceSettingsComponent {
onAudioBitrateChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.audioBitrate.set(parseInt(input.value, 10));
this.webrtcService.setAudioBitrate(this.audioBitrate());
this.saveVoiceSettings();
@@ -150,12 +179,13 @@ export class VoiceSettingsComponent {
onIncludeSystemAudioChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.includeSystemAudio.set(!!input.checked);
this.saveVoiceSettings();
}
async onNoiseReductionChange(): Promise<void> {
this.noiseReduction.update((v) => !v);
this.noiseReduction.update((currentValue) => !currentValue);
await this.webrtcService.toggleNoiseReduction(this.noiseReduction());
this.saveVoiceSettings();
}
@@ -168,21 +198,25 @@ export class VoiceSettingsComponent {
onTargetDbfsChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.voiceLeveling.setTargetDbfs(parseInt(input.value, 10));
}
onStrengthChange(event: Event): void {
const select = event.target as HTMLSelectElement;
this.voiceLeveling.setStrength(select.value as 'low' | 'medium' | 'high');
}
onMaxGainDbChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.voiceLeveling.setMaxGainDb(parseInt(input.value, 10));
}
onSpeedChange(event: Event): void {
const select = event.target as HTMLSelectElement;
this.voiceLeveling.setSpeed(select.value as 'slow' | 'medium' | 'fast');
}
@@ -192,6 +226,7 @@ export class VoiceSettingsComponent {
onNotificationVolumeChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.audioService.setNotificationVolume(parseFloat(input.value));
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { Component, inject, signal, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -13,7 +14,7 @@ import {
lucideRefreshCw,
lucideGlobe,
lucideArrowLeft,
lucideAudioLines,
lucideAudioLines
} from '@ng-icons/lucide';
import { ServerDirectoryService } from '../../core/services/server-directory.service';
@@ -36,10 +37,10 @@ import { STORAGE_KEY_CONNECTION_SETTINGS, STORAGE_KEY_VOICE_SETTINGS } from '../
lucideRefreshCw,
lucideGlobe,
lucideArrowLeft,
lucideAudioLines,
}),
lucideAudioLines
})
],
templateUrl: './settings.component.html',
templateUrl: './settings.component.html'
})
/**
* Settings page for managing signaling servers and connection preferences.
@@ -86,7 +87,7 @@ export class SettingsComponent implements OnInit {
this.serverDirectory.addServer({
name: this.newServerName.trim(),
url: this.newServerUrl.trim().replace(/\/$/, ''), // Remove trailing slash
url: this.newServerUrl.trim().replace(/\/$/, '') // Remove trailing slash
});
// Clear form
@@ -96,6 +97,7 @@ export class SettingsComponent implements OnInit {
// Test the new server
const servers = this.servers();
const newServer = servers[servers.length - 1];
if (newServer) {
this.serverDirectory.testServer(newServer.id);
}
@@ -121,8 +123,10 @@ export class SettingsComponent implements OnInit {
/** Load connection settings (auto-reconnect, search scope) from localStorage. */
loadConnectionSettings(): void {
const settings = localStorage.getItem(STORAGE_KEY_CONNECTION_SETTINGS);
if (settings) {
const parsed = JSON.parse(settings);
this.autoReconnect = parsed.autoReconnect ?? true;
this.searchAllServers = parsed.searchAllServers ?? true;
this.serverDirectory.setSearchAllServers(this.searchAllServers);
@@ -135,8 +139,8 @@ export class SettingsComponent implements OnInit {
STORAGE_KEY_CONNECTION_SETTINGS,
JSON.stringify({
autoReconnect: this.autoReconnect,
searchAllServers: this.searchAllServers,
}),
searchAllServers: this.searchAllServers
})
);
this.serverDirectory.setSearchAllServers(this.searchAllServers);
}
@@ -149,10 +153,13 @@ export class SettingsComponent implements OnInit {
/** Load voice settings (noise reduction) from localStorage. */
loadVoiceSettings(): void {
const settings = localStorage.getItem(STORAGE_KEY_VOICE_SETTINGS);
if (settings) {
const parsed = JSON.parse(settings);
this.noiseReduction = parsed.noiseReduction ?? false;
}
// Sync the live WebRTC state with the persisted preference
if (this.noiseReduction !== this.webrtcService.isNoiseReductionEnabled()) {
this.webrtcService.toggleNoiseReduction(this.noiseReduction);
@@ -173,13 +180,17 @@ export class SettingsComponent implements OnInit {
async saveVoiceSettings(): Promise<void> {
// Merge into existing voice settings so we don't overwrite device/volume prefs
let existing: Record<string, unknown> = {};
try {
const raw = localStorage.getItem(STORAGE_KEY_VOICE_SETTINGS);
if (raw) existing = JSON.parse(raw);
if (raw)
existing = JSON.parse(raw);
} catch {}
localStorage.setItem(
STORAGE_KEY_VOICE_SETTINGS,
JSON.stringify({ ...existing, noiseReduction: this.noiseReduction }),
JSON.stringify({ ...existing, noiseReduction: this.noiseReduction })
);
await this.webrtcService.toggleNoiseReduction(this.noiseReduction);
}