feat: plugins v1
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user