feat: plugins v1

This commit is contained in:
2026-04-29 01:14:14 +02:00
parent ec3802ade6
commit 6920f93b41
86 changed files with 9036 additions and 14 deletions

View File

@@ -0,0 +1,208 @@
import { CommonModule } from '@angular/common';
import {
Component,
EventEmitter,
Output,
computed,
inject,
signal
} from '@angular/core';
import { Router } from '@angular/router';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideArrowLeft,
lucideBug,
lucideCheck,
lucidePackage,
lucidePlay,
lucideRefreshCw,
lucideSettings,
lucideShield,
lucideStore,
lucideX
} from '@ng-icons/lucide';
import type { PluginCapabilityId } from '../../../../shared-kernel';
import { PluginCapabilityService } from '../../application/services/plugin-capability.service';
import { PluginHostService } from '../../application/services/plugin-host.service';
import { PluginLoggerService } from '../../application/services/plugin-logger.service';
import { PluginRegistryService } from '../../application/services/plugin-registry.service';
import { PluginRequirementStateService } from '../../application/services/plugin-requirement-state.service';
import { PluginUiRegistryService } from '../../application/services/plugin-ui-registry.service';
import type { RegisteredPlugin } from '../../domain/models/plugin-runtime.models';
import { PluginRenderHostComponent } from '../plugin-render-host/plugin-render-host.component';
type PluginManagerTab = 'docs' | 'extensions' | 'installed' | 'logs' | 'requirements' | 'settings';
@Component({
selector: 'app-plugin-manager',
standalone: true,
imports: [
CommonModule,
NgIcon,
PluginRenderHostComponent
],
templateUrl: './plugin-manager.component.html',
viewProviders: [
provideIcons({
lucideArrowLeft,
lucideBug,
lucideCheck,
lucidePackage,
lucidePlay,
lucideRefreshCw,
lucideSettings,
lucideShield,
lucideStore,
lucideX
})
]
})
export class PluginManagerComponent {
@Output() readonly closed = new EventEmitter<void>();
readonly capabilities = inject(PluginCapabilityService);
readonly host = inject(PluginHostService);
readonly logger = inject(PluginLoggerService);
readonly registry = inject(PluginRegistryService);
readonly requirementState = inject(PluginRequirementStateService);
readonly router = inject(Router);
readonly uiRegistry = inject(PluginUiRegistryService);
readonly activeTab = signal<PluginManagerTab>('installed');
readonly busyPluginId = signal<string | null>(null);
readonly busyAll = signal(false);
readonly selectedPluginId = signal<string | null>(null);
readonly entries = this.registry.entries;
readonly selectedPlugin = computed(() => {
const selectedPluginId = this.selectedPluginId();
return this.entries().find((entry) => entry.manifest.id === selectedPluginId) ?? this.entries()[0] ?? null;
});
readonly missingCapabilities = computed(() => {
const selectedPlugin = this.selectedPlugin();
return selectedPlugin ? this.capabilities.missing(selectedPlugin.manifest) : [];
});
readonly selectedLogs = computed(() => {
const selectedPlugin = this.selectedPlugin();
return selectedPlugin ? this.logger.entries().filter((entry) => entry.pluginId === selectedPlugin.manifest.id)
.slice(-20) : [];
});
readonly extensionCounts = computed(() => ({
appPages: this.uiRegistry.appPages().length,
channelSections: this.uiRegistry.channelSections().length,
composerActions: this.uiRegistry.composerActions().length,
embeds: this.uiRegistry.embeds().length,
profileActions: this.uiRegistry.profileActions().length,
settingsPages: this.uiRegistry.settingsPages().length,
sidePanels: this.uiRegistry.sidePanels().length,
toolbarActions: this.uiRegistry.toolbarActions().length
}));
readonly requirementComparisons = this.requirementState.comparisons;
readonly uiConflicts = this.uiRegistry.conflicts;
readonly selectedRequirement = computed(() => {
const selectedPlugin = this.selectedPlugin();
return selectedPlugin ? this.requirementState.comparisonFor(selectedPlugin.manifest.id) : null;
});
readonly selectedSettingsSchema = computed(() => this.selectedPlugin()?.manifest.settings ?? null);
readonly selectedSettingsPages = computed(() => {
const selectedPlugin = this.selectedPlugin();
return selectedPlugin
? this.uiRegistry.settingsPageRecords().filter((record) => record.pluginId === selectedPlugin.manifest.id)
: [];
});
readonly selectedDocs = computed(() => {
const manifest = this.selectedPlugin()?.manifest;
if (!manifest) {
return [];
}
return [
{ label: 'Readme', url: manifest.readme },
{ label: 'Homepage', url: manifest.homepage },
{ label: 'Changelog', url: manifest.changelog },
{ label: 'Support', url: manifest.bugs }
].filter((item): item is { label: string; url: string } => typeof item.url === 'string' && item.url.length > 0);
});
setTab(tab: PluginManagerTab): void {
this.activeTab.set(tab);
}
openStore(): void {
const returnUrl = this.router.url.startsWith('/plugin-store') ? '/search' : this.router.url;
this.closed.emit();
void this.router.navigate(['/plugin-store'], { queryParams: { returnUrl } });
}
selectPlugin(pluginId: string): void {
this.selectedPluginId.set(pluginId);
}
grantAll(entry: RegisteredPlugin): void {
this.capabilities.grantAll(entry.manifest);
}
toggleCapability(entry: RegisteredPlugin, capability: PluginCapabilityId): void {
if (this.capabilities.has(entry.manifest.id, capability)) {
this.capabilities.revoke(entry.manifest.id, capability);
return;
}
this.capabilities.grant(entry.manifest.id, capability);
}
async activateAll(): Promise<void> {
this.busyAll.set(true);
try {
await this.host.activateReadyPlugins();
} finally {
this.busyAll.set(false);
}
}
async reload(entry: RegisteredPlugin): Promise<void> {
this.busyPluginId.set(entry.manifest.id);
try {
await this.host.reloadPlugin(entry.manifest.id);
} finally {
this.busyPluginId.set(null);
}
}
async unload(entry: RegisteredPlugin): Promise<void> {
this.busyPluginId.set(entry.manifest.id);
try {
await this.host.deactivatePlugin(entry.manifest.id);
} finally {
this.busyPluginId.set(null);
}
}
setEnabled(entry: RegisteredPlugin, enabled: boolean): void {
this.registry.setEnabled(entry.manifest.id, enabled);
}
isSelected(entry: RegisteredPlugin): boolean {
return this.selectedPlugin()?.manifest.id === entry.manifest.id;
}
close(): void {
this.closed.emit();
}
trackEntry(index: number, entry: RegisteredPlugin): string {
return entry.manifest.id;
}
trackCapability(index: number, capability: PluginCapabilityId): string {
return capability;
}
}