feat: plugins v1
This commit is contained in:
@@ -250,6 +250,41 @@
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (pluginChannelSections().length > 0 || pluginSidePanels().length > 0) {
|
||||
<section class="border-t border-border px-2 py-3" data-testid="plugin-room-side-panel">
|
||||
<div class="mb-2 px-1">
|
||||
<h4 class="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">Plugins</h4>
|
||||
</div>
|
||||
|
||||
@if (pluginChannelSections().length > 0) {
|
||||
<div class="space-y-1">
|
||||
@for (record of pluginChannelSections(); track record.id) {
|
||||
<button
|
||||
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-foreground/70 transition-colors hover:bg-secondary/60 hover:text-foreground"
|
||||
[title]="record.pluginId">
|
||||
<ng-icon
|
||||
[name]="record.contribution.type === 'video' ? 'lucideVideo' : 'lucideHash'"
|
||||
class="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate">{{ record.contribution.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (pluginSidePanels().length > 0) {
|
||||
<div class="mt-3 space-y-2">
|
||||
@for (record of pluginSidePanels(); track record.id) {
|
||||
<article class="rounded-md border border-border bg-background/40 p-2">
|
||||
<p class="mb-2 truncate text-xs font-medium text-muted-foreground">{{ record.contribution.label }}</p>
|
||||
<app-plugin-render-host [render]="record.contribution.render" />
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ import { VoicePlaybackService } from '../../../domains/voice-connection';
|
||||
import { formatGameActivityElapsed } from '../../../domains/game-activity';
|
||||
import { ExternalLinkService } from '../../../core/platform/external-link.service';
|
||||
import { VoiceControlsComponent } from '../../../domains/voice-session/feature/voice-controls/voice-controls.component';
|
||||
import { PluginRenderHostComponent } from '../../../domains/plugins/feature/plugin-render-host/plugin-render-host.component';
|
||||
import { PluginUiRegistryService } from '../../../domains/plugins';
|
||||
import { isChannelNameTaken, normalizeChannelName } from '../../../store/rooms/room-channels.rules';
|
||||
import {
|
||||
canManageMember,
|
||||
@@ -89,6 +91,7 @@ type PanelMode = 'channels' | 'users';
|
||||
UserVolumeMenuComponent,
|
||||
UserAvatarComponent,
|
||||
ConfirmDialogComponent,
|
||||
PluginRenderHostComponent,
|
||||
ThemeNodeDirective
|
||||
],
|
||||
viewProviders: [
|
||||
@@ -124,6 +127,7 @@ export class RoomsSidePanelComponent implements OnDestroy {
|
||||
private readonly externalLinks = inject(ExternalLinkService);
|
||||
private readonly voiceActivity = inject(VoiceActivityService);
|
||||
private readonly voiceConnectivity = inject(VoiceConnectivityHealthService);
|
||||
private readonly pluginUi = inject(PluginUiRegistryService);
|
||||
private profileCardOpenTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private readonly activityTimer = setInterval(() => this.activityNow.set(Date.now()), 1_000);
|
||||
|
||||
@@ -137,6 +141,8 @@ export class RoomsSidePanelComponent implements OnDestroy {
|
||||
activeChannelId = this.store.selectSignal(selectActiveChannelId);
|
||||
textChannels = this.store.selectSignal(selectTextChannels);
|
||||
voiceChannels = this.store.selectSignal(selectVoiceChannels);
|
||||
pluginChannelSections = this.pluginUi.channelSectionRecords;
|
||||
pluginSidePanels = this.pluginUi.sidePanelRecords;
|
||||
localUserHasDesync = this.voiceConnectivity.localUserHasDesync;
|
||||
roomMembers = computed(() => this.currentRoom()?.members ?? []);
|
||||
roomMemberIdentifiers = computed(() => {
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
@case ('general') {
|
||||
General
|
||||
}
|
||||
@case ('plugins') {
|
||||
Plugins
|
||||
}
|
||||
@case ('network') {
|
||||
Network
|
||||
}
|
||||
@@ -193,6 +196,9 @@
|
||||
@case ('general') {
|
||||
<app-general-settings />
|
||||
}
|
||||
@case ('plugins') {
|
||||
<app-plugin-manager (closed)="navigate('general')" />
|
||||
}
|
||||
@case ('network') {
|
||||
<app-network-settings />
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
lucideGlobe,
|
||||
lucideAudioLines,
|
||||
lucidePalette,
|
||||
lucidePackage,
|
||||
lucideSettings,
|
||||
lucideUsers,
|
||||
lucideBan,
|
||||
@@ -33,6 +34,7 @@ import { selectSavedRooms, selectCurrentRoom } from '../../../store/rooms/rooms.
|
||||
import { selectCurrentUser } from '../../../store/users/users.selectors';
|
||||
import { Room, UserRole } from '../../../shared-kernel';
|
||||
import { NotificationsSettingsComponent } from '../../../domains/notifications';
|
||||
import { PluginManagerComponent } from '../../../domains/plugins/feature/plugin-manager/plugin-manager.component';
|
||||
import { resolveLegacyRole, resolveRoomPermission } from '../../../domains/access-control';
|
||||
|
||||
import { GeneralSettingsComponent } from './general-settings/general-settings.component';
|
||||
@@ -62,6 +64,7 @@ import {
|
||||
GeneralSettingsComponent,
|
||||
NetworkSettingsComponent,
|
||||
NotificationsSettingsComponent,
|
||||
PluginManagerComponent,
|
||||
VoiceSettingsComponent,
|
||||
UpdatesSettingsComponent,
|
||||
DataSettingsComponent,
|
||||
@@ -81,6 +84,7 @@ import {
|
||||
lucideGlobe,
|
||||
lucideAudioLines,
|
||||
lucidePalette,
|
||||
lucidePackage,
|
||||
lucideSettings,
|
||||
lucideUsers,
|
||||
lucideBan,
|
||||
@@ -117,6 +121,7 @@ export class SettingsModalComponent {
|
||||
|
||||
readonly globalPages: { id: SettingsPage; label: string; icon: string }[] = [
|
||||
{ id: 'general', label: 'General', icon: 'lucideSettings' },
|
||||
{ id: 'plugins', label: 'Plugins', icon: 'lucidePackage' },
|
||||
{ id: 'theme', label: 'Theme Studio', icon: 'lucidePalette' },
|
||||
{ id: 'network', label: 'Network', icon: 'lucideGlobe' },
|
||||
{ id: 'notifications', label: 'Notifications', icon: 'lucideBell' },
|
||||
|
||||
@@ -18,6 +18,18 @@
|
||||
<h1 class="text-2xl font-bold text-foreground">Settings</h1>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
(click)="openPluginStore()"
|
||||
class="mb-6 flex items-center gap-2 rounded-lg bg-secondary px-3 py-1.5 text-sm text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucidePackage"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
Plugin Store
|
||||
</button>
|
||||
|
||||
<!-- Server Endpoints Section -->
|
||||
<div class="bg-card border border-border rounded-lg p-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
lucideRefreshCw,
|
||||
lucideGlobe,
|
||||
lucideArrowLeft,
|
||||
lucideAudioLines
|
||||
lucideAudioLines,
|
||||
lucidePackage
|
||||
} from '@ng-icons/lucide';
|
||||
|
||||
import { ServerDirectoryFacade } from '../../domains/server-directory';
|
||||
@@ -47,7 +48,8 @@ import { STORAGE_KEY_CONNECTION_SETTINGS, STORAGE_KEY_VOICE_SETTINGS } from '../
|
||||
lucideRefreshCw,
|
||||
lucideGlobe,
|
||||
lucideArrowLeft,
|
||||
lucideAudioLines
|
||||
lucideAudioLines,
|
||||
lucidePackage
|
||||
})
|
||||
],
|
||||
templateUrl: './settings.component.html'
|
||||
@@ -173,6 +175,12 @@ export class SettingsComponent implements OnInit {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
openPluginStore(): void {
|
||||
const returnUrl = this.router.url.startsWith('/plugin-store') ? '/search' : this.router.url;
|
||||
|
||||
void this.router.navigate(['/plugin-store'], { queryParams: { returnUrl } });
|
||||
}
|
||||
|
||||
/** Load voice settings (noise reduction) from localStorage. */
|
||||
loadVoiceSettings(): void {
|
||||
const settings = localStorage.getItem(STORAGE_KEY_VOICE_SETTINGS);
|
||||
|
||||
@@ -85,6 +85,20 @@
|
||||
Login
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 items-center gap-1.5 rounded-md px-2 text-sm text-foreground transition-colors hover:bg-secondary"
|
||||
[class.hidden]="!isAuthed()"
|
||||
(click)="openPluginStore()"
|
||||
title="Plugin Store"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucidePackage"
|
||||
class="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
Plugins
|
||||
</button>
|
||||
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
@@ -131,6 +145,14 @@
|
||||
{{ inviteStatus() }}
|
||||
</div>
|
||||
<div class="mx-2 my-1 h-px bg-border"></div>
|
||||
<button
|
||||
type="button"
|
||||
(click)="openPluginStore()"
|
||||
class="w-full rounded-md px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
||||
>
|
||||
Plugin Store
|
||||
</button>
|
||||
<div class="mx-2 my-1 h-px bg-border"></div>
|
||||
<button
|
||||
type="button"
|
||||
(click)="logout()"
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
lucideChevronLeft,
|
||||
lucideHash,
|
||||
lucideMenu,
|
||||
lucidePackage,
|
||||
lucideRefreshCw
|
||||
} from '@ng-icons/lucide';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
@@ -59,6 +60,7 @@ import { ThemeNodeDirective } from '../../../domains/theme';
|
||||
lucideChevronLeft,
|
||||
lucideHash,
|
||||
lucideMenu,
|
||||
lucidePackage,
|
||||
lucideRefreshCw })
|
||||
],
|
||||
templateUrl: './title-bar.component.html'
|
||||
@@ -179,6 +181,13 @@ export class TitleBarComponent {
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
openPluginStore(): void {
|
||||
const returnUrl = this.router.url.startsWith('/plugin-store') ? '/search' : this.router.url;
|
||||
|
||||
this._showMenu.set(false);
|
||||
void this.router.navigate(['/plugin-store'], { queryParams: { returnUrl } });
|
||||
}
|
||||
|
||||
/** Open the unified leave-server confirmation dialog. */
|
||||
private openLeaveConfirm() {
|
||||
this._showMenu.set(false);
|
||||
|
||||
Reference in New Issue
Block a user