fix: improve plugins functionality with server management

This commit is contained in:
2026-04-29 20:33:54 +02:00
parent b8f6d58d99
commit fa2cca6fa4
82 changed files with 1708 additions and 303 deletions

View File

@@ -98,6 +98,24 @@
/>
</button>
@if (hasServerPlugins()) {
<button
type="button"
class="relative grid h-8 w-8 place-items-center rounded-md text-foreground transition-colors hover:bg-secondary"
(click)="openServerPlugins()"
title="Server plugins"
aria-label="Server plugins"
>
<ng-icon
name="lucideShield"
class="h-4 w-4 text-muted-foreground"
/>
<span class="absolute right-0 top-0 min-w-3 rounded-full bg-primary px-1 text-[9px] font-semibold leading-3 text-primary-foreground">
{{ serverPluginCount() }}
</span>
</button>
}
@if (isElectron()) {
<button
type="button"
@@ -227,6 +245,123 @@
}
</div>
</div>
@if (optionalPluginRequirement(); as requirement) {
<section
class="flex min-h-10 items-center justify-between gap-3 border-b border-border bg-primary/10 px-4 py-2 text-sm text-foreground"
role="status"
aria-live="polite"
style="-webkit-app-region: no-drag"
>
<div class="flex min-w-0 items-center gap-2">
<ng-icon
name="lucidePackage"
class="h-4 w-4 shrink-0 text-primary"
/>
<p class="truncate">
Optional server plugin available:
<span class="font-semibold">{{ requirement.manifest?.title || requirement.pluginId }}</span>
@if (optionalPluginRequirementCount() > 1) {
<span class="text-muted-foreground">+{{ optionalPluginRequirementCount() - 1 }} more</span>
}
</p>
</div>
<div class="flex shrink-0 items-center gap-2">
@if (pluginRequirementError()) {
<span class="max-w-56 truncate text-xs text-destructive">{{ pluginRequirementError() }}</span>
}
<button
type="button"
class="rounded-md border border-border bg-card px-2.5 py-1 text-xs font-semibold transition-colors hover:bg-secondary disabled:opacity-60"
[disabled]="pluginRequirementBusy()"
(click)="rejectOptionalServerPlugin(requirement)"
>
Reject
</button>
<button
type="button"
class="rounded-md border border-border bg-card px-2.5 py-1 text-xs font-semibold transition-colors hover:bg-secondary disabled:opacity-60"
[disabled]="pluginRequirementBusy()"
(click)="hideOptionalServerPlugin(requirement)"
>
Don't show again
</button>
<button
type="button"
class="rounded-md border border-primary bg-primary px-2.5 py-1 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-60"
[disabled]="pluginRequirementBusy()"
(click)="installOptionalServerPlugin(requirement)"
>
{{ pluginRequirementBusy() ? 'Installing' : 'Install' }}
</button>
</div>
</section>
}
@if (requiredPluginRequirements().length > 0 && currentRoom()) {
<div
class="fixed inset-0 z-[80] bg-black/60"
role="presentation"
></div>
<section
class="fixed left-1/2 top-1/2 z-[81] flex max-h-[min(38rem,calc(100vh-2rem))] w-[min(32rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-2xl"
role="dialog"
aria-modal="true"
aria-labelledby="required-server-plugin-title"
style="-webkit-app-region: no-drag"
>
<header class="border-b border-border p-4">
<p class="text-sm text-muted-foreground">Required server plugins</p>
<h2
id="required-server-plugin-title"
class="mt-1 text-lg font-semibold"
>
{{ currentRoom()!.name }} requires a plugin update
</h2>
</header>
<div class="min-h-0 space-y-3 overflow-auto p-4">
<p class="text-sm text-muted-foreground">
An admin added required plugins for this server. Install them to keep using the server, or leave the server.
</p>
@for (requirement of requiredPluginRequirements(); track requirement.pluginId) {
<article class="rounded-lg border border-border bg-background/50 px-3 py-2">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="truncate text-sm font-semibold">{{ requirement.manifest?.title || requirement.pluginId }}</p>
@if (requirement.reason) {
<p class="mt-1 text-xs text-muted-foreground">{{ requirement.reason }}</p>
}
</div>
<span class="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-semibold text-primary">Required</span>
</div>
</article>
}
@if (pluginRequirementError()) {
<p class="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">{{ pluginRequirementError() }}</p>
}
</div>
<footer class="flex justify-end gap-2 border-t border-border p-4">
<button
type="button"
class="rounded-lg border border-border bg-card px-3 py-1.5 text-sm font-semibold transition-colors hover:bg-secondary disabled:opacity-60"
[disabled]="pluginRequirementBusy()"
(click)="confirmLeave({})"
>
Leave server
</button>
<button
type="button"
class="rounded-lg border border-primary bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-60"
[disabled]="pluginRequirementBusy()"
(click)="installRequiredServerPlugins()"
>
{{ pluginRequirementBusy() ? 'Installing' : 'Install plugins' }}
</button>
</footer>
</section>
}
<!-- Click-away overlay to close dropdown -->
@if (showMenu()) {
<div

View File

@@ -18,7 +18,8 @@ import {
lucideHash,
lucideMenu,
lucidePackage,
lucideRefreshCw
lucideRefreshCw,
lucideShield
} from '@ng-icons/lucide';
import { NavigationEnd, Router } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
@@ -42,9 +43,15 @@ import { PlatformService } from '../../../core/platform';
import { clearStoredCurrentUserId } from '../../../core/storage/current-user-storage';
import { SettingsModalService } from '../../../core/services/settings-modal.service';
import { LeaveServerDialogComponent } from '../../../shared';
import { Room } from '../../../shared-kernel';
import { Room, type PluginRequirementSummary } from '../../../shared-kernel';
import { VoiceWorkspaceService } from '../../../domains/voice-session';
import { ThemeNodeDirective } from '../../../domains/theme';
import {
PluginRegistryService,
PluginRequirementStateService,
PluginStoreService
} from '../../../domains/plugins';
import { getPluginInstallScope } from '../../../domains/plugins/domain/logic/plugin-install-scope.logic';
@Component({
selector: 'app-title-bar',
@@ -64,7 +71,8 @@ import { ThemeNodeDirective } from '../../../domains/theme';
lucideHash,
lucideMenu,
lucidePackage,
lucideRefreshCw })
lucideRefreshCw,
lucideShield })
],
templateUrl: './title-bar.component.html'
})
@@ -80,6 +88,9 @@ export class TitleBarComponent {
private platform = inject(PlatformService);
private voiceWorkspace = inject(VoiceWorkspaceService);
private settingsModal = inject(SettingsModalService);
private pluginRegistry = inject(PluginRegistryService);
private pluginRequirements = inject(PluginRequirementStateService);
private pluginStore = inject(PluginStoreService);
private getWindowControlsApi() {
return this.electronBridge.getApi();
@@ -153,11 +164,20 @@ export class TitleBarComponent {
|| this.isReconnecting()
)
);
serverPluginCount = computed(() => this.pluginRegistry.entries()
.filter((entry) => getPluginInstallScope(entry.manifest) === 'server')
.length);
hasServerPlugins = computed(() => this.inRoom() && this.serverPluginCount() > 0);
requiredPluginRequirements = this.pluginRequirements.missingRequiredRequirements;
optionalPluginRequirement = computed(() => this.inRoom() ? this.pluginRequirements.visibleOptionalRequirements()[0] ?? null : null);
optionalPluginRequirementCount = computed(() => this.pluginRequirements.visibleOptionalRequirements().length);
private _showMenu = signal(false);
showMenu = computed(() => this._showMenu());
showLeaveConfirm = signal(false);
inviteStatus = signal<string | null>(null);
creatingInvite = signal(false);
pluginRequirementBusy = signal(false);
pluginRequirementError = signal<string | null>(null);
/** Minimize the Electron window. */
minimize() {
@@ -192,6 +212,17 @@ export class TitleBarComponent {
void this.router.navigate(['/plugin-store'], { queryParams: { returnUrl } });
}
openServerPlugins(): void {
const roomId = this.currentRoom()?.id;
if (!roomId) {
return;
}
this._showMenu.set(false);
this.settingsModal.open('serverPlugins', roomId);
}
openSettings(): void {
this._showMenu.set(false);
this.settingsModal.open('general');
@@ -267,6 +298,24 @@ export class TitleBarComponent {
this.openLeaveConfirm();
}
installRequiredServerPlugins(): void {
void this.installServerRequirements(this.requiredPluginRequirements());
}
installOptionalServerPlugin(requirement: PluginRequirementSummary): void {
void this.installServerRequirements([requirement]);
}
rejectOptionalServerPlugin(requirement: PluginRequirementSummary): void {
this.pluginRequirements.dismissOptionalRequirement(requirement);
this.pluginRequirementError.set(null);
}
hideOptionalServerPlugin(requirement: PluginRequirementSummary): void {
this.pluginRequirements.dismissOptionalRequirement(requirement, { persist: true });
this.pluginRequirementError.set(null);
}
/** Confirm the unified leave action and remove the server locally. */
confirmLeave(result: { nextOwnerKey?: string }) {
const roomId = this.currentRoom()?.id;
@@ -294,6 +343,25 @@ export class TitleBarComponent {
this._showMenu.set(false);
}
private async installServerRequirements(requirements: PluginRequirementSummary[]): Promise<void> {
const room = this.currentRoom();
if (!room || requirements.length === 0 || this.pluginRequirementBusy()) {
return;
}
this.pluginRequirementBusy.set(true);
this.pluginRequirementError.set(null);
try {
await this.pluginStore.installServerRequirementsLocally(room.id, requirements, { activate: true });
} catch (error) {
this.pluginRequirementError.set(error instanceof Error ? error.message : 'Unable to install server plugin');
} finally {
this.pluginRequirementBusy.set(false);
}
}
/** Log out the current user, disconnect from signaling, and navigate to login. */
logout() {
this._showMenu.set(false);