Add eslint
This commit is contained in:
@@ -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() +
|
||||
' ' +
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user